diff --git a/server/accounts.go b/server/accounts.go new file mode 100644 index 0000000..4664107 --- /dev/null +++ b/server/accounts.go @@ -0,0 +1,4902 @@ +// Copyright 2018-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 ( + "bytes" + "cmp" + "encoding/hex" + "errors" + "fmt" + "io" + "io/fs" + "math" + "math/rand/v2" + "net/http" + "net/textproto" + "reflect" + "slices" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/nats-io/jwt/v2" + "github.com/nats-io/nkeys" + "github.com/nats-io/nuid" +) + +// For backwards compatibility with NATS < 2.0, users who are not explicitly defined into an +// account will be grouped in the default global account. +const globalAccountName = DEFAULT_GLOBAL_ACCOUNT + +const defaultMaxSubLimitReportThreshold = int64(2 * time.Second) + +var maxSubLimitReportThreshold = defaultMaxSubLimitReportThreshold + +// Account are subject namespace definitions. By default no messages are shared between accounts. +// You can share via Exports and Imports of Streams and Services. +type Account struct { + // Total stats for the account. + stats struct { + sync.Mutex + stats // Totals + gw stats // Gateways + rt stats // Routes + ln stats // Leafnodes + } + + gwReplyMapping + Name string + Nkey string + Issuer string + claimJWT string + updated time.Time + mu sync.RWMutex + smu sync.Mutex // serializes route interest updates + sl *Sublist + ic *client + sq *sendq + isid uint64 + etmr *time.Timer + ctmr *time.Timer + strack map[string]sconns + nrclients int32 + sysclients int32 + nleafs int32 + nrleafs int32 + clients map[*client]struct{} + rm map[string]int32 + lws map[string]int32 // per key, last rm[key] sent to routes; used to dedup sends + usersRevoked map[string]int64 + mappings []*mapping + hasMapped atomic.Bool + lmu sync.RWMutex + lleafs []*client + leafClusters map[string]uint64 + imports importMap + exports exportMap + js *jsAccount + jsLimits map[string]JetStreamAccountLimits + nrgAccount string + limits + expired atomic.Bool + incomplete bool + signingKeys map[string]jwt.Scope + extAuth *jwt.ExternalAuthorization + srv *Server // server this account is registered with (possibly nil) + lds string // loop detection subject for leaf nodes + siReply []byte // service reply prefix, will form wildcard subscription. + eventIds *nuid.NUID + eventIdsMu sync.Mutex + defaultPerms *Permissions + tags jwt.TagList + nameTag string + lastLimErr int64 + routePoolIdx int + // If the trace destination is specified and a message with a traceParentHdr + // is received, and has the least significant bit of the last token set to 1, + // then if traceDestSampling is > 0 and < 100, a random value will be selected + // and if it falls between 0 and that value, message tracing will be triggered. + traceDest string + traceDestSampling int + // Guarantee that only one goroutine can be running either checkJetStreamMigrate + // or clearObserverState at a given time for this account to prevent interleaving. + jscmMu sync.Mutex +} + +const ( + accDedicatedRoute = -1 + accTransitioningToDedicatedRoute = -2 +) + +// Account based limits. +type limits struct { + mpay int32 + msubs int32 + mconns int32 + mleafs int32 + disallowBearer bool +} + +// Used to track remote clients and leafnodes per remote server. +type sconns struct { + conns int32 + leafs int32 +} + +// clampInt64ToInt32 safely converts an int64 limit to int32, +// clamping values to the [math.MinInt32, math.MaxInt32] range. +func clampInt64ToInt32(v int64) int32 { + return int32(max(math.MinInt32, min(math.MaxInt32, v))) +} + +// Import stream mapping struct +type streamImport struct { + acc *Account + from string + to string + tr *subjectTransform + rtr *subjectTransform + claim *jwt.Import + usePub bool + invalid bool + // This is `allow_trace` and when true and message tracing is happening, + // we will trace egresses past the account boundary, if `false`, we stop + // at the account boundary. + atrc bool +} + +const ClientInfoHdr = "Nats-Request-Info" + +// Import service mapping struct +type serviceImport struct { + acc *Account + claim *jwt.Import + se *serviceExport + sid []byte + from string + to string + tr *subjectTransform + ts int64 + rt ServiceRespType + latency *serviceLatency + m1 *ServiceLatency + rc *client + mt *msgTrace + usePub bool + response bool + invalid bool + share bool + tracking bool + didDeliver bool + atrc bool // allow trace (got from service export) + trackingHdr http.Header // header from request +} + +// This is used to record when we create a mapping for implicit service +// imports. We use this to clean up entries that are not singletons when +// we detect that interest is no longer present. The key to the map will +// be the actual interest. We record the mapped subject and the account. +type serviceRespEntry struct { + acc *Account + msub string +} + +// rrIndexThreshold is the number of outstanding responses sharing a single +// reply subject above which we build an index for constant time removal. +// Reply subjects are normally unique per request, so the overwhelming majority +// of entries stay a one element list and never allocate the index. +const rrIndexThreshold = 16 + +// respEntries holds the outstanding response entries for one reply subject. +// The list is authoritative and unordered; idx, when present, maps a response +// subject to its position in the list. Once allocated the index is kept for as +// long as the reply subject has any entry left, since a subject that has been +// shared once tends to be shared again. +// +// This is held by value in importMap.rrMap, so mutating it means writing it +// back under its key. Add and remove entries through importMap.addRespEntry +// and importMap.removeRespEntry, which keep that write back in one place, +// rather than calling the methods below directly. +type respEntries struct { + list []*serviceRespEntry + idx map[string]int +} + +// add appends an entry, building or maintaining the index as needed. +func (re *respEntries) add(sre *serviceRespEntry) { + re.list = append(re.list, sre) + if re.idx != nil { + re.idx[sre.msub] = len(re.list) - 1 + return + } + if len(re.list) > rrIndexThreshold { + re.idx = make(map[string]int, len(re.list)) + for i, e := range re.list { + re.idx[e.msub] = i + } + } +} + +// remove drops the entry for msub, in constant time once indexed. +func (re *respEntries) remove(msub string) { + i := -1 + if re.idx != nil { + var ok bool + if i, ok = re.idx[msub]; !ok { + return + } + delete(re.idx, msub) + } else { + for j, e := range re.list { + if e.msub == msub { + i = j + break + } + } + if i < 0 { + return + } + } + // Swap with the last entry so removal does not shift the whole list. + last := len(re.list) - 1 + if i != last { + moved := re.list[last] + re.list[i] = moved + if re.idx != nil { + re.idx[moved.msub] = i + } + } + re.list[last] = nil + re.list = re.list[:last] +} + +// ServiceRespType represents the types of service request response types. +type ServiceRespType uint8 + +// Service response types. Defaults to a singleton. +const ( + Singleton ServiceRespType = iota + Streamed + Chunked +) + +// String helper. +func (rt ServiceRespType) String() string { + switch rt { + case Singleton: + return "Singleton" + case Streamed: + return "Streamed" + case Chunked: + return "Chunked" + } + return "Unknown ServiceResType" +} + +// exportAuth holds configured approvals or boolean indicating an +// auth token is required for import. +type exportAuth struct { + tokenReq bool + accountPos uint + approved map[string]*Account + actsRevoked map[string]int64 +} + +// streamExport +type streamExport struct { + exportAuth +} + +// serviceExport holds additional information for exported services. +type serviceExport struct { + exportAuth + acc *Account + respType ServiceRespType + latency *serviceLatency + rtmr *time.Timer + respThresh time.Duration + // This is `allow_trace` and when true and message tracing is happening, + // when processing a service import we will go through account boundary + // and trace egresses on that other account. If `false`, we stop at the + // account boundary. + atrc bool +} + +// Used to track service latency. +type serviceLatency struct { + sampling int8 // percentage from 1-100 or 0 to indicate triggered by header + subject string +} + +// exportMap tracks the exported streams and services. +type exportMap struct { + streams map[string]*streamExport + services map[string]*serviceExport + responses map[string]*serviceImport +} + +// importMap tracks the imported streams and services. +// For services we will also track the response mappings as well. +type importMap struct { + streams []*streamImport + services map[string][]*serviceImport + rrMap map[string]respEntries +} + +// addRespEntry records an outstanding response entry under reply. +// Lock should be held on the owning account. +func (im *importMap) addRespEntry(reply string, sre *serviceRespEntry) { + if im.rrMap == nil { + im.rrMap = make(map[string]respEntries) + } + re := im.rrMap[reply] + re.add(sre) + im.rrMap[reply] = re +} + +// removeRespEntry drops the outstanding response entry for msub under reply, +// dropping the reply subject itself once its last entry goes. +// Lock should be held on the owning account. +func (im *importMap) removeRespEntry(reply, msub string) { + re, ok := im.rrMap[reply] + if !ok { + return + } + re.remove(msub) + if len(re.list) == 0 { + delete(im.rrMap, reply) + } else { + im.rrMap[reply] = re + } +} + +// NewAccount creates a new unlimited account with the given name. +func NewAccount(name string) *Account { + a := &Account{ + Name: name, + limits: limits{-1, -1, -1, -1, false}, + eventIds: nuid.New(), + } + return a +} + +func (a *Account) String() string { + return a.Name +} + +func (a *Account) setTraceDest(dest string) { + a.mu.Lock() + a.traceDest = dest + a.mu.Unlock() +} + +func (a *Account) getTraceDestAndSampling() (string, int) { + a.mu.RLock() + dest := a.traceDest + sampling := a.traceDestSampling + a.mu.RUnlock() + return dest, sampling +} + +// Used to create shallow copies of accounts for transfer +// from opts to real accounts in server struct. +// Account `na` write lock is expected to be held on entry +// while account `a` is the one from the Options struct +// being loaded/reloaded and do not need locking. +func (a *Account) shallowCopy(na *Account) { + na.Nkey = a.Nkey + na.Issuer = a.Issuer + na.traceDest, na.traceDestSampling = a.traceDest, a.traceDestSampling + na.nrgAccount = a.nrgAccount + + if a.imports.streams != nil { + na.imports.streams = make([]*streamImport, 0, len(a.imports.streams)) + for _, v := range a.imports.streams { + si := *v + na.imports.streams = append(na.imports.streams, &si) + } + } + if a.imports.services != nil { + na.imports.services = make(map[string][]*serviceImport) + for k, v := range a.imports.services { + sis := make([]*serviceImport, 0, len(v)) + for _, si := range v { + csi := *si + sis = append(sis, &csi) + } + na.imports.services[k] = sis + } + } + if a.exports.streams != nil { + na.exports.streams = make(map[string]*streamExport) + for k, v := range a.exports.streams { + if v != nil { + se := *v + na.exports.streams[k] = &se + } else { + na.exports.streams[k] = nil + } + } + } + if a.exports.services != nil { + na.exports.services = make(map[string]*serviceExport) + for k, v := range a.exports.services { + if v != nil { + se := *v + na.exports.services[k] = &se + } else { + na.exports.services[k] = nil + } + } + } + na.mappings = a.mappings + na.hasMapped.Store(len(na.mappings) > 0) + + // JetStream + na.jsLimits = a.jsLimits + // Server config account limits. + na.limits = a.limits +} + +// nextEventID uses its own lock for better concurrency. +func (a *Account) nextEventID() string { + a.eventIdsMu.Lock() + id := a.eventIds.Next() + a.eventIdsMu.Unlock() + return id +} + +// Returns a slice of clients stored in the account, or nil if none is present. +// Lock is held on entry. +func (a *Account) getClientsLocked() []*client { + if len(a.clients) == 0 { + return nil + } + clients := make([]*client, 0, len(a.clients)) + for c := range a.clients { + clients = append(clients, c) + } + return clients +} + +// Returns a slice of clients stored in the account, or nil if none is present. +func (a *Account) getClients() []*client { + a.mu.RLock() + clients := a.getClientsLocked() + a.mu.RUnlock() + return clients +} + +// Returns a slice of external (non-internal) clients stored in the account, or nil if none is present. +// Lock is held on entry. +func (a *Account) getExternalClientsLocked() []*client { + if len(a.clients) == 0 { + return nil + } + var clients []*client + for c := range a.clients { + if !isInternalClient(c.kind) { + clients = append(clients, c) + } + } + return clients +} + +// Called to track a remote server and connections and leafnodes it +// has for this account. +func (a *Account) updateRemoteServer(m *AccountNumConns) []*client { + a.mu.Lock() + if a.strack == nil { + a.strack = make(map[string]sconns) + } + // This does not depend on receiving all updates since each one is idempotent. + // FIXME(dlc) - We should cleanup when these both go to zero. + prev := a.strack[m.Server.ID] + a.strack[m.Server.ID] = sconns{conns: int32(m.Conns), leafs: int32(m.LeafNodes)} + a.nrclients += int32(m.Conns) - prev.conns + a.nrleafs += int32(m.LeafNodes) - prev.leafs + + mtce := a.mconns != jwt.NoLimit && (len(a.clients)-int(a.sysclients)+int(a.nrclients) > int(a.mconns)) + // If we are over here some have snuck in and we need to rebalance. + // All others will probably be doing the same thing but better to be + // conservative and bit harsh here. Clients will reconnect if we over compensate. + var clients []*client + if mtce { + clients = a.getExternalClientsLocked() + + // Sort in reverse chronological. + slices.SortFunc(clients, func(i, j *client) int { return -i.start.Compare(j.start) }) + over := (len(a.clients) - int(a.sysclients) + int(a.nrclients)) - int(a.mconns) + if over < len(clients) { + clients = clients[:over] + } + } + // Now check leafnodes. + mtlce := a.mleafs != jwt.NoLimit && (a.nleafs+a.nrleafs > a.mleafs) + if mtlce { + // Take ones from the end. + a.lmu.RLock() + leafs := a.lleafs + over := int(a.nleafs + a.nrleafs - a.mleafs) + if over < len(leafs) { + leafs = leafs[len(leafs)-over:] + } + clients = append(clients, leafs...) + a.lmu.RUnlock() + } + a.mu.Unlock() + + // If we have exceeded our max clients this will be populated. + return clients +} + +// Removes tracking for a remote server that has shutdown. +func (a *Account) removeRemoteServer(sid string) { + a.mu.Lock() + if a.strack != nil { + prev := a.strack[sid] + delete(a.strack, sid) + a.nrclients -= prev.conns + a.nrleafs -= prev.leafs + } + a.mu.Unlock() +} + +// When querying for subject interest this is the number of +// expected responses. We need to actually check that the entry +// has active connections. +func (a *Account) expectedRemoteResponses() (expected int32) { + a.mu.RLock() + for _, sc := range a.strack { + if sc.conns > 0 || sc.leafs > 0 { + expected++ + } + } + a.mu.RUnlock() + return +} + +// Clears eventing and tracking for this account. +func (a *Account) clearEventing() { + a.mu.Lock() + a.nrclients = 0 + // Now clear state + clearTimer(&a.etmr) + clearTimer(&a.ctmr) + a.clients = nil + a.strack = nil + a.mu.Unlock() +} + +// GetName will return the accounts name. +func (a *Account) GetName() string { + if a == nil { + return "n/a" + } + a.mu.RLock() + name := a.Name + a.mu.RUnlock() + return name +} + +// getNameTag will return the name tag or the account name if not set. +func (a *Account) getNameTag() string { + if a == nil { + return _EMPTY_ + } + a.mu.RLock() + defer a.mu.RUnlock() + return a.getNameTagLocked() +} + +// getNameTagLocked will return the name tag or the account name if not set. +// Lock should be held. +func (a *Account) getNameTagLocked() string { + if a == nil { + return _EMPTY_ + } + nameTag := a.nameTag + if nameTag == _EMPTY_ { + nameTag = a.Name + } + return nameTag +} + +// NumConnections returns active number of clients for this account for +// all known servers. +func (a *Account) NumConnections() int { + a.mu.RLock() + nc := len(a.clients) - int(a.sysclients) + int(a.nrclients) + a.mu.RUnlock() + return nc +} + +// NumRemoteConnections returns the number of client or leaf connections that +// are not on this server. +func (a *Account) NumRemoteConnections() int { + a.mu.RLock() + nc := int(a.nrclients + a.nrleafs) + a.mu.RUnlock() + return nc +} + +// NumLocalConnections returns active number of clients for this account +// on this server. +func (a *Account) NumLocalConnections() int { + a.mu.RLock() + nlc := a.numLocalConnections() + a.mu.RUnlock() + return nlc +} + +// Do not account for the system accounts. +func (a *Account) numLocalConnections() int { + return len(a.clients) - int(a.sysclients) - int(a.nleafs) +} + +// This is for extended local interest. +// Lock should not be held. +func (a *Account) numLocalAndLeafConnections() int { + a.mu.RLock() + nlc := len(a.clients) - int(a.sysclients) + a.mu.RUnlock() + return nlc +} + +func (a *Account) numLocalLeafNodes() int { + return int(a.nleafs) +} + +// MaxTotalConnectionsReached returns if we have reached our limit for number of connections. +func (a *Account) MaxTotalConnectionsReached() bool { + var mtce bool + a.mu.RLock() + if a.mconns != jwt.NoLimit { + mtce = len(a.clients)-int(a.sysclients)+int(a.nrclients) >= int(a.mconns) + } + a.mu.RUnlock() + return mtce +} + +// MaxActiveConnections return the set limit for the account system +// wide for total number of active connections. +func (a *Account) MaxActiveConnections() int { + a.mu.RLock() + mconns := int(a.mconns) + a.mu.RUnlock() + return mconns +} + +// MaxTotalLeafNodesReached returns if we have reached our limit for number of leafnodes. +func (a *Account) MaxTotalLeafNodesReached() bool { + a.mu.RLock() + mtc := a.maxTotalLeafNodesReached() + a.mu.RUnlock() + return mtc +} + +func (a *Account) maxTotalLeafNodesReached() bool { + if a.mleafs != jwt.NoLimit { + return a.nleafs+a.nrleafs >= a.mleafs + } + return false +} + +// NumLeafNodes returns the active number of local and remote +// leaf node connections. +func (a *Account) NumLeafNodes() int { + a.mu.RLock() + nln := int(a.nleafs + a.nrleafs) + a.mu.RUnlock() + return nln +} + +// NumRemoteLeafNodes returns the active number of remote +// leaf node connections. +func (a *Account) NumRemoteLeafNodes() int { + a.mu.RLock() + nrn := int(a.nrleafs) + a.mu.RUnlock() + return nrn +} + +// MaxActiveLeafNodes return the set limit for the account system +// wide for total number of leavenode connections. +// NOTE: these are tracked separately. +func (a *Account) MaxActiveLeafNodes() int { + a.mu.RLock() + mleafs := int(a.mleafs) + a.mu.RUnlock() + return mleafs +} + +// RoutedSubs returns how many subjects we would send across a route when first +// connected or expressing interest. Local client subs. +func (a *Account) RoutedSubs() int { + a.mu.RLock() + defer a.mu.RUnlock() + return len(a.rm) +} + +// TotalSubs returns total number of Subscriptions for this account. +func (a *Account) TotalSubs() int { + a.mu.RLock() + defer a.mu.RUnlock() + if a.sl == nil { + return 0 + } + return int(a.sl.Count()) +} + +func (a *Account) shouldLogMaxSubErr() bool { + if a == nil { + return true + } + a.mu.RLock() + last := a.lastLimErr + a.mu.RUnlock() + if now := time.Now().UnixNano(); now-last >= maxSubLimitReportThreshold { + a.mu.Lock() + a.lastLimErr = now + a.mu.Unlock() + return true + } + return false +} + +// MapDest is for mapping published subjects for clients. +type MapDest struct { + Subject string `json:"subject"` + Weight uint8 `json:"weight"` + Cluster string `json:"cluster,omitempty"` +} + +func NewMapDest(subject string, weight uint8) *MapDest { + return &MapDest{subject, weight, _EMPTY_} +} + +// destination is for internal representation for a weighted mapped destination. +type destination struct { + tr *subjectTransform + weight uint8 +} + +// mapping is an internal entry for mapping subjects. +type mapping struct { + src string + wc bool + dests []*destination + cdests map[string][]*destination +} + +// AddMapping adds in a simple route mapping from src subject to dest subject +// for inbound client messages. +func (a *Account) AddMapping(src, dest string) error { + return a.AddWeightedMappings(src, NewMapDest(dest, 100)) +} + +// AddWeightedMappings will add in a weighted mappings for the destinations. +func (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error { + a.mu.Lock() + defer a.mu.Unlock() + + if !IsValidSubject(src) { + return ErrBadSubject + } + + m := &mapping{src: src, wc: subjectHasWildcard(src), dests: make([]*destination, 0, len(dests)+1)} + seen := make(map[string]struct{}) + + tw := make(map[string]uint8) + for _, d := range dests { + if _, ok := seen[d.Subject]; ok { + return fmt.Errorf("duplicate entry for %q", d.Subject) + } + seen[d.Subject] = struct{}{} + if d.Weight > 100 { + return fmt.Errorf("individual weights need to be <= 100") + } + tw[d.Cluster] += d.Weight + if tw[d.Cluster] > 100 { + return fmt.Errorf("total weight needs to be <= 100") + } + err := ValidateMapping(src, d.Subject) + if err != nil { + return err + } + tr, err := NewSubjectTransform(src, d.Subject) + if err != nil { + return err + } + if d.Cluster == _EMPTY_ { + m.dests = append(m.dests, &destination{tr, d.Weight}) + } else { + // We have a cluster scoped filter. + if m.cdests == nil { + m.cdests = make(map[string][]*destination) + } + ad := m.cdests[d.Cluster] + ad = append(ad, &destination{tr, d.Weight}) + m.cdests[d.Cluster] = ad + } + } + + processDestinations := func(dests []*destination) ([]*destination, error) { + var ltw uint8 + for _, d := range dests { + ltw += d.weight + } + // Auto add in original at weight difference if all entries weight does not total to 100. + // Iff the src was not already added in explicitly, meaning they want loss. + _, haveSrc := seen[src] + if ltw != 100 && !haveSrc { + dest := src + if m.wc { + // We need to make the appropriate markers for the wildcards etc. + dest = transformTokenize(dest) + } + tr, err := NewSubjectTransform(src, dest) + if err != nil { + return nil, err + } + aw := 100 - ltw + if len(dests) == 0 { + aw = 100 + } + dests = append(dests, &destination{tr, aw}) + } + slices.SortFunc(dests, func(i, j *destination) int { return cmp.Compare(i.weight, j.weight) }) + + var lw uint8 + for _, d := range dests { + d.weight += lw + lw = d.weight + } + return dests, nil + } + + var err error + if m.dests, err = processDestinations(m.dests); err != nil { + return err + } + + // Option cluster scoped destinations + for cluster, dests := range m.cdests { + if dests, err = processDestinations(dests); err != nil { + return err + } + m.cdests[cluster] = dests + } + + // Replace an old one if it exists. + for i, em := range a.mappings { + if em.src == src { + a.mappings[i] = m + return nil + } + } + // If we did not replace add to the end. + a.mappings = append(a.mappings, m) + a.hasMapped.Store(len(a.mappings) > 0) + + // If we have connected leafnodes make sure to update. + if a.nleafs > 0 { + // Need to release because lock ordering is client -> account + a.mu.Unlock() + // Now grab the leaf list lock. We can hold client lock under this one. + a.lmu.RLock() + for _, lc := range a.lleafs { + lc.forceAddToSmap(src) + } + a.lmu.RUnlock() + a.mu.Lock() + } + return nil +} + +// RemoveMapping will remove an existing mapping. +func (a *Account) RemoveMapping(src string) bool { + a.mu.Lock() + defer a.mu.Unlock() + for i, m := range a.mappings { + if m.src == src { + // Swap last one into this spot. Its ok to change order. + a.mappings[i] = a.mappings[len(a.mappings)-1] + a.mappings[len(a.mappings)-1] = nil // gc + a.mappings = a.mappings[:len(a.mappings)-1] + a.hasMapped.Store(len(a.mappings) > 0) + // If we have connected leafnodes make sure to update. + if a.nleafs > 0 { + // Need to release because lock ordering is client -> account + a.mu.Unlock() + // Now grab the leaf list lock. We can hold client lock under this one. + a.lmu.RLock() + for _, lc := range a.lleafs { + lc.forceRemoveFromSmap(src) + } + a.lmu.RUnlock() + a.mu.Lock() + } + return true + } + } + return false +} + +// Indicates we have mapping entries. +func (a *Account) hasMappings() bool { + if a == nil { + return false + } + return a.hasMapped.Load() +} + +// This performs the logic to map to a new dest subject based on mappings. +// Should only be called from processInboundClientMsg or service import processing. +func (a *Account) selectMappedSubject(dest string) (string, bool) { + if !a.hasMappings() { + return dest, false + } + + a.mu.RLock() + // In case we have to tokenize for subset matching. + tsa := [32]string{} + tts := tsa[:0] + + var m *mapping + for _, rm := range a.mappings { + if !rm.wc && rm.src == dest { + m = rm + break + } else { + // tokenize and reuse for subset matching. + if len(tts) == 0 { + start := 0 + subject := dest + for i := 0; i < len(subject); i++ { + if subject[i] == btsep { + tts = append(tts, subject[start:i]) + start = i + 1 + } + } + tts = append(tts, subject[start:]) + } + if isSubsetMatch(tts, rm.src) { + m = rm + break + } + } + } + + if m == nil { + a.mu.RUnlock() + return dest, false + } + + // The selected destination for the mapping. + var d *destination + var ndest string + + dests := m.dests + if len(m.cdests) > 0 { + cn := a.srv.cachedClusterName() + dests = m.cdests[cn] + if dests == nil { + // Fallback to main if we do not match the cluster. + dests = m.dests + } + } + + // Optimize for single entry case. + if len(dests) == 1 && dests[0].weight == 100 { + d = dests[0] + } else { + w := uint8(rand.Uint32N(100)) + for _, rm := range dests { + if w < rm.weight { + d = rm + break + } + } + } + + if d != nil { + if len(d.tr.dtokmftokindexesargs) == 0 { + ndest = d.tr.dest + } else { + ndest = d.tr.TransformTokenizedSubject(tts) + } + } + + a.mu.RUnlock() + return ndest, true +} + +// SubscriptionInterest returns true if this account has a matching subscription +// for the given `subject`. +func (a *Account) SubscriptionInterest(subject string) bool { + return a.Interest(subject) > 0 +} + +// Interest returns the number of subscriptions for a given subject that match. +func (a *Account) Interest(subject string) int { + var nms int + a.mu.RLock() + if a.sl != nil { + np, nq := a.sl.NumInterest(subject) + nms = np + nq + } + a.mu.RUnlock() + return nms +} + +// addClient keeps our accounting of local active clients or leafnodes updated. +// Returns previous total. +func (a *Account) addClient(c *client) int { + a.mu.Lock() + n := len(a.clients) + + // Could come here earlier than the account is registered with the server. + // Make sure we can still track clients. + if a.clients == nil { + a.clients = make(map[*client]struct{}) + } + a.clients[c] = struct{}{} + + // If we did not add it, we are done + if n == len(a.clients) { + a.mu.Unlock() + return n + } + if c.kind != CLIENT && c.kind != LEAF { + a.sysclients++ + } else if c.kind == LEAF { + a.nleafs++ + } + isGlobal := a.Name == globalAccountName + a.mu.Unlock() + + // If we added a new leaf use the list lock and add it to the list. + if c.kind == LEAF { + a.lmu.Lock() + a.lleafs = append(a.lleafs, c) + a.lmu.Unlock() + } + + if !isGlobal && c != nil && c.srv != nil { + c.srv.accConnsUpdate(a) + } + + return n +} + +// For registering clusters for remote leafnodes. +// We only register as the hub. +func (a *Account) registerLeafNodeCluster(cluster string) { + a.mu.Lock() + defer a.mu.Unlock() + if a.leafClusters == nil { + a.leafClusters = make(map[string]uint64) + } + a.leafClusters[cluster]++ +} + +// Check to see if we already have this cluster registered. +func (a *Account) hasLeafNodeCluster(cluster string) bool { + a.mu.RLock() + defer a.mu.RUnlock() + return a.leafClusters[cluster] > 0 +} + +// Check to see if this cluster is isolated, meaning the only one. +// Read Lock should be held. +func (a *Account) isLeafNodeClusterIsolated(cluster string) bool { + if cluster == _EMPTY_ { + return false + } + if len(a.leafClusters) > 1 { + return false + } + return a.leafClusters[cluster] == uint64(a.nleafs) +} + +// Helper function to remove leaf nodes. If number of leafnodes gets large +// this may need to be optimized out of linear search but believe number +// of active leafnodes per account scope to be small and therefore cache friendly. +// Lock should not be held on general account lock. +func (a *Account) removeLeafNode(c *client) { + // Make sure we hold the list lock as well. + a.lmu.Lock() + defer a.lmu.Unlock() + + ll := len(a.lleafs) + for i, l := range a.lleafs { + if l == c { + a.lleafs[i] = a.lleafs[ll-1] + if ll == 1 { + a.lleafs = nil + } else { + a.lleafs = a.lleafs[:ll-1] + } + return + } + } +} + +// removeClient keeps our accounting of local active clients updated. +func (a *Account) removeClient(c *client) int { + a.mu.Lock() + n := len(a.clients) + delete(a.clients, c) + // If we did not actually remove it, we are done. + if n == len(a.clients) { + a.mu.Unlock() + return n + } + if c.kind != CLIENT && c.kind != LEAF { + a.sysclients-- + } else if c.kind == LEAF { + a.nleafs-- + // Need to do cluster accounting here. + // Do cluster accounting if we are a hub. + if c.isHubLeafNode() { + cluster := c.remoteCluster() + if count := a.leafClusters[cluster]; count > 1 { + a.leafClusters[cluster]-- + } else if count == 1 { + delete(a.leafClusters, cluster) + } + } + } + isGlobal := a.Name == globalAccountName + a.mu.Unlock() + + if c.kind == LEAF { + a.removeLeafNode(c) + } + + if !isGlobal && c != nil && c.srv != nil { + c.srv.accConnsUpdate(a) + } + + return n +} + +func setExportAuth(ea *exportAuth, subject string, accounts []*Account, accountPos uint) error { + if accountPos > 0 { + token := strings.Split(subject, tsep) + if len(token) < int(accountPos) || token[accountPos-1] != "*" { + return ErrInvalidSubject + } + } + ea.accountPos = accountPos + // empty means auth required but will be import token. + if accounts == nil { + return nil + } + if len(accounts) == 0 { + ea.tokenReq = true + return nil + } + if ea.approved == nil { + ea.approved = make(map[string]*Account, len(accounts)) + } + for _, acc := range accounts { + ea.approved[acc.Name] = acc + } + return nil +} + +// AddServiceExport will configure the account with the defined export. +func (a *Account) AddServiceExport(subject string, accounts []*Account) error { + return a.addServiceExportWithResponseAndAccountPos(subject, Singleton, accounts, 0) +} + +// AddServiceExport will configure the account with the defined export. +func (a *Account) addServiceExportWithAccountPos(subject string, accounts []*Account, accountPos uint) error { + return a.addServiceExportWithResponseAndAccountPos(subject, Singleton, accounts, accountPos) +} + +// AddServiceExportWithResponse will configure the account with the defined export and response type. +func (a *Account) AddServiceExportWithResponse(subject string, respType ServiceRespType, accounts []*Account) error { + return a.addServiceExportWithResponseAndAccountPos(subject, respType, accounts, 0) +} + +// AddServiceExportWithresponse will configure the account with the defined export and response type. +func (a *Account) addServiceExportWithResponseAndAccountPos( + subject string, respType ServiceRespType, accounts []*Account, accountPos uint, +) error { + if a == nil { + return ErrMissingAccount + } + + a.mu.Lock() + if a.exports.services == nil { + a.exports.services = make(map[string]*serviceExport) + } + + se := a.exports.services[subject] + // Always create a service export + if se == nil { + se = &serviceExport{} + } + + if respType != Singleton { + se.respType = respType + } + + if accounts != nil || accountPos > 0 { + if err := setExportAuth(&se.exportAuth, subject, accounts, accountPos); err != nil { + a.mu.Unlock() + return err + } + } + lrt := a.lowestServiceExportResponseTime() + se.acc = a + se.respThresh = DEFAULT_SERVICE_EXPORT_RESPONSE_THRESHOLD + a.exports.services[subject] = se + + var clients []*client + nlrt := a.lowestServiceExportResponseTime() + if nlrt != lrt && len(a.clients) > 0 { + clients = a.getClientsLocked() + } + // Need to release because lock ordering is client -> Account + a.mu.Unlock() + if len(clients) > 0 { + updateAllClientsServiceExportResponseTime(clients, nlrt) + } + return nil +} + +// TrackServiceExport will enable latency tracking of the named service. +// Results will be published in this account to the given results subject. +func (a *Account) TrackServiceExport(service, results string) error { + return a.TrackServiceExportWithSampling(service, results, DEFAULT_SERVICE_LATENCY_SAMPLING) +} + +// TrackServiceExportWithSampling will enable latency tracking of the named service for the given +// sampling rate (1-100). Results will be published in this account to the given results subject. +func (a *Account) TrackServiceExportWithSampling(service, results string, sampling int) error { + if a == nil { + return ErrMissingAccount + } + + if sampling != 0 { // 0 means triggered by header + if sampling < 1 || sampling > 100 { + return ErrBadSampling + } + } + if !IsValidPublishSubject(results) { + return ErrBadPublishSubject + } + // Don't loop back on outselves. + if a.IsExportService(results) { + return ErrBadPublishSubject + } + + if a.srv != nil && !a.srv.EventsEnabled() { + return ErrNoSysAccount + } + + a.mu.Lock() + if a.exports.services == nil { + a.mu.Unlock() + return ErrMissingService + } + ea, ok := a.exports.services[service] + if !ok { + a.mu.Unlock() + return ErrMissingService + } + if ea == nil { + ea = &serviceExport{} + a.exports.services[service] = ea + } else if ea.respType != Singleton { + a.mu.Unlock() + return ErrBadServiceType + } + ea.latency = &serviceLatency{ + sampling: int8(sampling), + subject: results, + } + s := a.srv + a.mu.Unlock() + + if s == nil { + return nil + } + + // Now track down the imports and add in latency as needed to enable. + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + acc.mu.Lock() + for _, ims := range acc.imports.services { + for _, im := range ims { + if im != nil && im.acc.Name == a.Name && subjectIsSubsetMatch(im.to, service) { + im.latency = ea.latency + } + } + } + acc.mu.Unlock() + return true + }) + + return nil +} + +// UnTrackServiceExport will disable latency tracking of the named service. +func (a *Account) UnTrackServiceExport(service string) { + if a == nil || (a.srv != nil && !a.srv.EventsEnabled()) { + return + } + + a.mu.Lock() + if a.exports.services == nil { + a.mu.Unlock() + return + } + ea, ok := a.exports.services[service] + if !ok || ea == nil || ea.latency == nil { + a.mu.Unlock() + return + } + // We have latency here. + ea.latency = nil + s := a.srv + a.mu.Unlock() + + if s == nil { + return + } + + // Now track down the imports and clean them up. + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + acc.mu.Lock() + for _, ims := range acc.imports.services { + for _, im := range ims { + if im != nil && im.acc.Name == a.Name { + if subjectIsSubsetMatch(im.to, service) { + im.latency, im.m1 = nil, nil + } + } + } + } + acc.mu.Unlock() + return true + }) +} + +// IsExportService will indicate if this service exists. Will check wildcard scenarios. +func (a *Account) IsExportService(service string) bool { + a.mu.RLock() + defer a.mu.RUnlock() + _, ok := a.exports.services[service] + if ok { + return true + } + tokens := strings.Split(service, tsep) + for subj := range a.exports.services { + if isSubsetMatch(tokens, subj) { + return true + } + } + return false +} + +// IsExportServiceTracking will indicate if given publish subject is an export service with tracking enabled. +func (a *Account) IsExportServiceTracking(service string) bool { + a.mu.RLock() + ea, ok := a.exports.services[service] + if ok && ea == nil { + a.mu.RUnlock() + return false + } + if ok && ea != nil && ea.latency != nil { + a.mu.RUnlock() + return true + } + // FIXME(dlc) - Might want to cache this is in the hot path checking for latency tracking. + tokens := strings.Split(service, tsep) + for subj, ea := range a.exports.services { + if isSubsetMatch(tokens, subj) && ea != nil && ea.latency != nil { + a.mu.RUnlock() + return true + } + } + a.mu.RUnlock() + return false +} + +// ServiceLatency is the JSON message sent out in response to latency tracking for +// an accounts exported services. Additional client info is available in requestor +// and responder. Note that for a requestor, the only information shared by default +// is the RTT used to calculate the total latency. The requestor's account can +// designate to share the additional information in the service import. +type ServiceLatency struct { + TypedEvent + Status int `json:"status"` + Error string `json:"description,omitempty"` + Requestor *ClientInfo `json:"requestor,omitempty"` + Responder *ClientInfo `json:"responder,omitempty"` + RequestHeader http.Header `json:"header,omitempty"` // only contains header(s) triggering the measurement + RequestStart time.Time `json:"start"` + ServiceLatency time.Duration `json:"service"` + SystemLatency time.Duration `json:"system"` + TotalLatency time.Duration `json:"total"` +} + +// ServiceLatencyType is the NATS Event Type for ServiceLatency +const ServiceLatencyType = "io.nats.server.metric.v1.service_latency" + +// NATSTotalTime is a helper function that totals the NATS latencies. +func (m1 *ServiceLatency) NATSTotalTime() time.Duration { + return m1.Requestor.RTT + m1.Responder.RTT + m1.SystemLatency +} + +// Merge function to merge m1 and m2 (requestor and responder) measurements +// when there are two samples. This happens when the requestor and responder +// are on different servers. +// +// m2 ServiceLatency is correct, so use that. +// m1 TotalLatency is correct, so use that. +// Will use those to back into NATS latency. +func (m1 *ServiceLatency) merge(m2 *ServiceLatency) { + rtt := time.Duration(0) + if m2.Responder != nil { + rtt = m2.Responder.RTT + } + m1.SystemLatency = m1.ServiceLatency - (m2.ServiceLatency + rtt) + m1.ServiceLatency = m2.ServiceLatency + m1.Responder = m2.Responder + sanitizeLatencyMetric(m1) +} + +// sanitizeLatencyMetric adjusts latency metric values that could go +// negative in some edge conditions since we estimate client RTT +// for both requestor and responder. +// These numbers are never meant to be negative, it just could be +// how we back into the values based on estimated RTT. +func sanitizeLatencyMetric(sl *ServiceLatency) { + if sl.ServiceLatency < 0 { + sl.ServiceLatency = 0 + } + if sl.SystemLatency < 0 { + sl.SystemLatency = 0 + } +} + +// Used for transporting remote latency measurements. +type remoteLatency struct { + Account string `json:"account"` + ReqId string `json:"req_id"` + M2 ServiceLatency `json:"m2"` + respThresh time.Duration +} + +// sendLatencyResult will send a latency result and clear the si of the requestor(rc). +func (a *Account) sendLatencyResult(si *serviceImport, sl *ServiceLatency) { + sl.Type = ServiceLatencyType + sl.ID = a.nextEventID() + sl.Time = time.Now().UTC() + a.mu.Lock() + lsubj := si.latency.subject + si.rc = nil + a.mu.Unlock() + + a.srv.sendInternalAccountMsg(a, lsubj, sl) +} + +// Used to send a bad request metric when we do not have a reply subject +func (a *Account) sendBadRequestTrackingLatency(si *serviceImport, requestor *client, header http.Header) { + sl := &ServiceLatency{ + Status: 400, + Error: "Bad Request", + Requestor: requestor.getClientInfo(si.share), + } + sl.RequestHeader = header + sl.RequestStart = time.Now().Add(-sl.Requestor.RTT).UTC() + a.sendLatencyResult(si, sl) +} + +// Used to send a latency result when the requestor interest was lost before the +// response could be delivered. +func (a *Account) sendReplyInterestLostTrackLatency(si *serviceImport) { + sl := &ServiceLatency{ + Status: 408, + Error: "Request Timeout", + } + a.mu.RLock() + rc, share, ts := si.rc, si.share, si.ts + sl.RequestHeader = si.trackingHdr + a.mu.RUnlock() + if rc != nil { + sl.Requestor = rc.getClientInfo(share) + } + var reqRTT time.Duration + if sl.Requestor != nil { + reqRTT = sl.Requestor.RTT + } + sl.RequestStart = time.Unix(0, ts-int64(reqRTT)).UTC() + a.sendLatencyResult(si, sl) +} + +func (a *Account) sendBackendErrorTrackingLatency(si *serviceImport, reason rsiReason) { + sl := &ServiceLatency{} + a.mu.RLock() + rc, share, ts := si.rc, si.share, si.ts + sl.RequestHeader = si.trackingHdr + a.mu.RUnlock() + if rc != nil { + sl.Requestor = rc.getClientInfo(share) + } + var reqRTT time.Duration + if sl.Requestor != nil { + reqRTT = sl.Requestor.RTT + } + sl.RequestStart = time.Unix(0, ts-int64(reqRTT)).UTC() + if reason == rsiNoDelivery { + sl.Status = 503 + sl.Error = "Service Unavailable" + } else if reason == rsiTimeout { + sl.Status = 504 + sl.Error = "Service Timeout" + } + a.sendLatencyResult(si, sl) +} + +// sendTrackingLatency will send out the appropriate tracking information for the +// service request/response latency. This is called when the requestor's server has +// received the response. +// TODO(dlc) - holding locks for RTTs may be too much long term. Should revisit. +func (a *Account) sendTrackingLatency(si *serviceImport, responder *client) bool { + a.mu.RLock() + rc, share, siTs := si.rc, si.share, si.ts + a.mu.RUnlock() + if rc == nil { + return true + } + + ts := time.Now() + serviceRTT := time.Duration(ts.UnixNano() - siTs) + requestor := rc + + sl := &ServiceLatency{ + Status: 200, + Requestor: requestor.getClientInfo(share), + Responder: responder.getClientInfo(true), + } + var respRTT, reqRTT time.Duration + if sl.Responder != nil { + respRTT = sl.Responder.RTT + } + if sl.Requestor != nil { + reqRTT = sl.Requestor.RTT + } + sl.RequestStart = time.Unix(0, siTs-int64(reqRTT)).UTC() + sl.ServiceLatency = serviceRTT - respRTT + sl.TotalLatency = reqRTT + serviceRTT + if respRTT > 0 { + sl.SystemLatency = time.Since(ts) + sl.TotalLatency += sl.SystemLatency + } + sl.RequestHeader = si.trackingHdr + sanitizeLatencyMetric(sl) + + sl.Type = ServiceLatencyType + sl.ID = a.nextEventID() + sl.Time = time.Now().UTC() + + // If we are expecting a remote measurement, store our sl here. + // We need to account for the race between this and us receiving the + // remote measurement. + // FIXME(dlc) - We need to clean these up but this should happen + // already with the auto-expire logic. + if responder != nil && responder.kind != CLIENT { + si.acc.mu.Lock() + if si.m1 != nil { + m1, m2 := sl, si.m1 + m1.merge(m2) + si.acc.mu.Unlock() + a.srv.sendInternalAccountMsg(a, si.latency.subject, m1) + a.mu.Lock() + si.rc = nil + a.mu.Unlock() + return true + } + si.m1 = sl + si.acc.mu.Unlock() + return false + } else { + a.srv.sendInternalAccountMsg(a, si.latency.subject, sl) + a.mu.Lock() + si.rc = nil + a.mu.Unlock() + } + return true +} + +// This will check to make sure our response lower threshold is set +// properly in any clients doing rrTracking. +func updateAllClientsServiceExportResponseTime(clients []*client, lrt time.Duration) { + for _, c := range clients { + c.mu.Lock() + if c.rrTracking != nil && lrt != c.rrTracking.lrt { + c.rrTracking.lrt = lrt + if c.rrTracking.ptmr.Stop() { + c.rrTracking.ptmr.Reset(lrt) + } + } + c.mu.Unlock() + } +} + +// Will select the lowest respThresh from all service exports. +// Read lock should be held. +func (a *Account) lowestServiceExportResponseTime() time.Duration { + // Lowest we will allow is 5 minutes. Its an upper bound for this function. + lrt := 5 * time.Minute + for _, se := range a.exports.services { + if se.respThresh < lrt { + lrt = se.respThresh + } + } + return lrt +} + +// AddServiceImportWithClaim will add in the service import via the jwt claim. +func (a *Account) AddServiceImportWithClaim(destination *Account, from, to string, imClaim *jwt.Import) error { + return a.addServiceImportWithClaim(destination, from, to, imClaim, false) +} + +// addServiceImportWithClaim will add in the service import via the jwt claim. +// It will also skip the authorization check in cases where internal is true +func (a *Account) addServiceImportWithClaim(destination *Account, from, to string, imClaim *jwt.Import, internal bool) error { + if destination == nil { + return ErrMissingAccount + } + // Empty means use from. + if to == _EMPTY_ { + to = from + } + if !IsValidSubject(from) || !IsValidSubject(to) { + return ErrInvalidSubject + } + + // First check to see if the account has authorized us to route to the "to" subject. + if !internal && !destination.checkServiceImportAuthorized(a, to, imClaim) { + return ErrServiceImportAuthorization + } + + // Check if this introduces a cycle before proceeding. + // From will be the mapped subject. + // If the 'to' has a wildcard make sure we pre-transform the 'from' before we check for cycles, e.g. '$1' + fromT := from + if subjectHasWildcard(to) { + fromT, _ = transformUntokenize(from) + } + if err := a.serviceImportFormsCycle(destination, fromT); err != nil { + return err + } + + _, err := a.addServiceImport(destination, from, to, imClaim) + + return err +} + +const MaxAccountCycleSearchDepth = 1024 + +func (a *Account) serviceImportFormsCycle(dest *Account, from string) error { + return dest.checkServiceImportsForCycles(from, map[string]bool{a.Name: true}) +} + +func (a *Account) checkServiceImportsForCycles(from string, visited map[string]bool) error { + if len(visited) >= MaxAccountCycleSearchDepth { + return ErrCycleSearchDepth + } + a.mu.RLock() + for _, sis := range a.imports.services { + for _, si := range sis { + if SubjectsCollide(from, si.to) { + a.mu.RUnlock() + if visited[si.acc.Name] { + return ErrImportFormsCycle + } + // Push ourselves and check si.acc + visited[a.Name] = true + // Make a copy to not overwrite the passed value. + f := from + if subjectIsSubsetMatch(si.from, f) { + f = si.from + } + if err := si.acc.checkServiceImportsForCycles(f, visited); err != nil { + return err + } + a.mu.RLock() + } + } + } + a.mu.RUnlock() + return nil +} + +func (a *Account) streamImportFormsCycle(dest *Account, to string) error { + return dest.checkStreamImportsForCycles(to, map[string]bool{a.Name: true}) +} + +// Lock should be held. +func (a *Account) hasServiceExportMatching(to string) bool { + for subj := range a.exports.services { + if subjectIsSubsetMatch(to, subj) { + return true + } + } + return false +} + +// Lock should be held. +func (a *Account) hasStreamExportMatching(to string) bool { + for subj := range a.exports.streams { + if subjectIsSubsetMatch(to, subj) { + return true + } + } + return false +} + +func (a *Account) checkStreamImportsForCycles(to string, visited map[string]bool) error { + if len(visited) >= MaxAccountCycleSearchDepth { + return ErrCycleSearchDepth + } + + a.mu.RLock() + + if !a.hasStreamExportMatching(to) { + a.mu.RUnlock() + return nil + } + + for _, si := range a.imports.streams { + if SubjectsCollide(to, si.to) { + a.mu.RUnlock() + if visited[si.acc.Name] { + return ErrImportFormsCycle + } + // Push ourselves and check si.acc + visited[a.Name] = true + // Make a copy to not overwrite the passed value. + t := to + if subjectIsSubsetMatch(si.to, t) { + t = si.to + } + if err := si.acc.checkStreamImportsForCycles(t, visited); err != nil { + return err + } + a.mu.RLock() + } + } + a.mu.RUnlock() + return nil +} + +// SetServiceImportSharing will allow sharing of information about requests with the export account. +// Used for service latency tracking at the moment. +func (a *Account) SetServiceImportSharing(destination *Account, to string, allow bool) error { + return a.setServiceImportSharing(destination, to, true, allow) +} + +// setServiceImportSharing will allow sharing of information about requests with the export account. +func (a *Account) setServiceImportSharing(destination *Account, to string, check, allow bool) error { + a.mu.Lock() + defer a.mu.Unlock() + if check && a.isClaimAccount() { + return fmt.Errorf("claim based accounts can not be updated directly") + } + // We can't use getServiceImportForAccountLocked() here since we are looking + // for the service import with the si.to == to, which may not be the key + // for the service import in the map. + for _, sis := range a.imports.services { + for _, si := range sis { + if si.acc.Name == destination.Name && si.to == to { + si.share = allow + return nil + } + } + } + return fmt.Errorf("service import not found") +} + +// AddServiceImport will add a route to an account to send published messages / requests +// to the destination account. From is the local subject to map, To is the +// subject that will appear on the destination account. Destination will need +// to have an import rule to allow access via addService. +func (a *Account) AddServiceImport(destination *Account, from, to string) error { + return a.AddServiceImportWithClaim(destination, from, to, nil) +} + +// NumPendingReverseResponses returns the number of response mappings we have for all outstanding +// requests for service imports. +func (a *Account) NumPendingReverseResponses() int { + a.mu.RLock() + defer a.mu.RUnlock() + return len(a.imports.rrMap) +} + +// NumPendingAllResponses return the number of all responses outstanding for service exports. +func (a *Account) NumPendingAllResponses() int { + return a.NumPendingResponses(_EMPTY_) +} + +// NumPendingResponses returns the number of responses outstanding for service exports +// on this account. An empty filter string returns all responses regardless of which export. +// If you specify the filter we will only return ones that are for that export. +// NOTE this is only for what this server is tracking. +func (a *Account) NumPendingResponses(filter string) int { + a.mu.RLock() + defer a.mu.RUnlock() + if filter == _EMPTY_ { + return len(a.exports.responses) + } + se := a.getServiceExport(filter) + if se == nil { + return 0 + } + var nre int + for _, si := range a.exports.responses { + if si.se == se { + nre++ + } + } + return nre +} + +// NumServiceImports returns the number of service imports we have configured. +func (a *Account) NumServiceImports() int { + a.mu.RLock() + defer a.mu.RUnlock() + return len(a.imports.services) +} + +// Reason why we are removing this response serviceImport. +type rsiReason int + +const ( + rsiOk = rsiReason(iota) + rsiNoDelivery + rsiTimeout +) + +// removeRespServiceImport removes a response si mapping and the reverse entries for interest detection. +func (a *Account) removeRespServiceImport(si *serviceImport, reason rsiReason) { + if si == nil { + return + } + + a.mu.Lock() + c := a.ic + delete(a.exports.responses, si.from) + dest, to, tracking, rc, didDeliver := si.acc, si.to, si.tracking, si.rc, si.didDeliver + a.mu.Unlock() + + // If we have a sid make sure to unsub. + if len(si.sid) > 0 && c != nil { + c.processUnsub(si.sid) + } + + if tracking && rc != nil && !didDeliver { + a.sendBackendErrorTrackingLatency(si, reason) + } + + dest.checkForReverseEntry(to, si, false) +} + +func (a *Account) getServiceImportForAccountLocked(dstAccName, subject string) *serviceImport { + sis, ok := a.imports.services[subject] + if !ok { + return nil + } + if len(sis) == 1 && sis[0].acc.Name == dstAccName { + return sis[0] + } + for _, si := range sis { + if si.acc.Name == dstAccName { + return si + } + } + return nil +} + +// removeServiceImport will remove the route by subject. +func (a *Account) removeServiceImport(dstAccName, subject string) { + a.mu.Lock() + sis, ok := a.imports.services[subject] + if !ok { + a.mu.Unlock() + return + } + var si *serviceImport + if len(sis) == 1 { + si = sis[0] + if si.acc.Name != dstAccName { + si = nil + } else { + delete(a.imports.services, subject) + } + } else { + for i, esi := range sis { + if esi.acc.Name == dstAccName { + si = esi + last := len(sis) - 1 + if i != last { + sis[i] = sis[last] + } + sis = sis[:last] + a.imports.services[subject] = sis + break + } + } + } + if si == nil { + a.mu.Unlock() + return + } + var sid []byte + c := a.ic + if c != nil && si.sid != nil { + sid = si.sid + } + a.mu.Unlock() + + if sid != nil { + c.processUnsub(sid) + } +} + +// This tracks responses to service requests mappings. This is used for cleanup. +func (a *Account) addReverseRespMapEntry(acc *Account, reply, from string) { + a.mu.Lock() + a.imports.addRespEntry(reply, &serviceRespEntry{acc, from}) + a.mu.Unlock() +} + +// checkForReverseEntries is for when we are trying to match reverse entries to a wildcard. +// This will be called from checkForReverseEntry when the reply arg is a wildcard subject. +// This will usually be called in a go routine since we need to walk all the entries. +func (a *Account) checkForReverseEntries(reply string, checkInterest, recursed bool) { + if subjectIsLiteral(reply) { + a._checkForReverseEntry(reply, nil, checkInterest, recursed) + return + } + + a.mu.RLock() + if len(a.imports.rrMap) == 0 { + a.mu.RUnlock() + return + } + + var _rs [64]string + rs := _rs[:0] + if n := len(a.imports.rrMap); n > cap(rs) { + rs = make([]string, 0, n) + } + + for k := range a.imports.rrMap { + rs = append(rs, k) + } + a.mu.RUnlock() + + tsa := [32]string{} + tts := tokenizeSubjectIntoSlice(tsa[:0], reply) + + rsa := [32]string{} + for _, r := range rs { + rts := tokenizeSubjectIntoSlice(rsa[:0], r) + // isSubsetMatchTokenized is heavy so make sure we do this without the lock. + if isSubsetMatchTokenized(rts, tts) { + a._checkForReverseEntry(r, nil, checkInterest, recursed) + } + } +} + +// This checks for any response map entries. If you specify an si we will only match and +// clean up for that one, otherwise we remove them all. +func (a *Account) checkForReverseEntry(reply string, si *serviceImport, checkInterest bool) { + a._checkForReverseEntry(reply, si, checkInterest, false) +} + +// Callers should use checkForReverseEntry instead. This function exists to help prevent +// infinite recursion. +func (a *Account) _checkForReverseEntry(reply string, si *serviceImport, checkInterest, recursed bool) { + a.mu.RLock() + if len(a.imports.rrMap) == 0 { + a.mu.RUnlock() + return + } + + if subjectHasWildcard(reply) { + if recursed { + // If we have reached this condition then it is because the reverse entries also + // contain wildcards (that shouldn't happen but a client *could* provide an inbox + // prefix that is illegal because it ends in a wildcard character), at which point + // we will end up with infinite recursion between this func and checkForReverseEntries. + // To avoid a stack overflow panic, we'll give up instead. + a.mu.RUnlock() + return + } + + doInline := len(a.imports.rrMap) <= 64 + a.mu.RUnlock() + + if doInline { + a.checkForReverseEntries(reply, checkInterest, true) + } else { + go a.checkForReverseEntries(reply, checkInterest, true) + } + return + } + + if _, ok := a.imports.rrMap[reply]; !ok { + a.mu.RUnlock() + return + } + + // If we are here we have an entry we should check. + // If requested we will first check if there is any + // interest for this subject for the entire account. + // If there is we can not delete any entries yet. + // Note that if we are here reply has to be a literal subject. + if checkInterest { + // If interest still exists we can not clean these up yet. + if a.sl.HasInterest(reply) { + a.mu.RUnlock() + return + } + } + a.mu.RUnlock() + + // Delete the appropriate entries here based on optional si. + a.mu.Lock() + // We need a new lookup here because we have released the lock. + var sres []*serviceRespEntry + if si == nil { + sres = a.imports.rrMap[reply].list + delete(a.imports.rrMap, reply) + } else { + // Constant time once indexed, instead of a linear scan across every + // outstanding response that happens to share this reply subject. + // This also keys the write back off reply rather than si.to. Every + // caller reaches here with si.to equal to reply, so behavior is + // unchanged, but the lookup and the write back can no longer drift + // apart if that ever stops holding. + a.imports.removeRespEntry(reply, si.from) + } + a.mu.Unlock() + + // If we are here we no longer have interest and we have + // response entries that we should clean up. + if si == nil { + // sres is now known to have been removed from a.imports.rrMap, so we + // can safely (data race wise) iterate through. + for _, sre := range sres { + acc := sre.acc + var trackingCleanup bool + var rsi *serviceImport + acc.mu.Lock() + c := acc.ic + if rsi = acc.exports.responses[sre.msub]; rsi != nil && !rsi.didDeliver { + delete(acc.exports.responses, rsi.from) + trackingCleanup = rsi.tracking && rsi.rc != nil + } + acc.mu.Unlock() + // If we are doing explicit subs for all responses (e.g. bound to leafnode) + // we will have a non-empty sid here. + if rsi != nil && len(rsi.sid) > 0 && c != nil { + c.processUnsub(rsi.sid) + } + if trackingCleanup { + acc.sendReplyInterestLostTrackLatency(rsi) + } + } + } +} + +// Checks to see if a potential service import subject is already overshadowed. +func (a *Account) serviceImportShadowed(from string) bool { + a.mu.RLock() + defer a.mu.RUnlock() + if a.imports.services[from] != nil { + return true + } + // We did not find a direct match, so check individually. + for subj := range a.imports.services { + if subjectIsSubsetMatch(from, subj) { + return true + } + } + return false +} + +// Internal check to see if a service import exists. +func (a *Account) serviceImportExists(dstAccName, from string) bool { + a.mu.RLock() + dup := a.getServiceImportForAccountLocked(dstAccName, from) + a.mu.RUnlock() + return dup != nil +} + +// Add a service import. +// This does no checks and should only be called by the msg processing code. +// Use AddServiceImport from above if responding to user input or config changes, etc. +func (a *Account) addServiceImport(dest *Account, from, to string, claim *jwt.Import) (*serviceImport, error) { + rt := Singleton + var lat *serviceLatency + + if dest == nil { + return nil, ErrMissingAccount + } + + var atrc bool + dest.mu.RLock() + se := dest.getServiceExport(to) + if se != nil { + rt = se.respType + lat = se.latency + atrc = se.atrc + } + destAccName := dest.Name + dest.mu.RUnlock() + + a.mu.Lock() + if a.imports.services == nil { + a.imports.services = make(map[string][]*serviceImport) + } else if dup := a.getServiceImportForAccountLocked(destAccName, from); dup != nil { + a.mu.Unlock() + return nil, fmt.Errorf("duplicate service import subject %q, previously used in import for account %q, subject %q", + from, dup.acc.Name, dup.to) + } + + if to == _EMPTY_ { + to = from + } + // Check to see if we have a wildcard + var ( + usePub bool + tr *subjectTransform + err error + ) + + if subjectHasWildcard(to) { + // If to and from match, then we use the published subject. + if to == from { + usePub = true + } else { + to, _ = transformUntokenize(to) + // Create a transform. Do so in reverse such that $ symbols only exist in to + if tr, err = NewSubjectTransformStrict(to, transformTokenize(from)); err != nil { + a.mu.Unlock() + return nil, fmt.Errorf("failed to create mapping transform for service import subject from %q to %q: %v", + from, to, err) + } else { + // un-tokenize and reverse transform so we get the transform needed + from, _ = transformUntokenize(from) + tr = tr.reverse() + } + } + } + var share bool + if claim != nil { + share = claim.Share + } + si := &serviceImport{dest, claim, se, nil, from, to, tr, 0, rt, lat, nil, nil, nil, usePub, false, false, share, false, false, atrc, nil} + sis := a.imports.services[from] + sis = append(sis, si) + a.imports.services[from] = sis + a.mu.Unlock() + + if err := a.addServiceImportSub(si); err != nil { + a.removeServiceImport(destAccName, si.from) + return nil, err + } + return si, nil +} + +// Returns the internal client, will create one if not present. +// Lock should be held. +func (a *Account) internalClient() *client { + if a.ic == nil && a.srv != nil { + a.ic = a.srv.createInternalAccountClient() + a.ic.acc = a + } + return a.ic +} + +// Internal account scoped subscriptions. +func (a *Account) subscribeInternal(subject string, cb msgHandler) (*subscription, error) { + return a.subscribeInternalEx(subject, cb, false) +} + +// Unsubscribe from an internal account subscription. +func (a *Account) unsubscribeInternal(sub *subscription) { + if ic := a.internalClient(); ic != nil { + ic.processUnsub(sub.sid) + } +} + +// Creates internal subscription for service import responses. +func (a *Account) subscribeServiceImportResponse(subject string) (*subscription, error) { + return a.subscribeInternalEx(subject, a.processServiceImportResponse, true) +} + +func (a *Account) subscribeInternalEx(subject string, cb msgHandler, ri bool) (*subscription, error) { + a.mu.Lock() + a.isid++ + c, sid := a.internalClient(), strconv.FormatUint(a.isid, 10) + a.mu.Unlock() + + // This will happen in parsing when the account has not been properly setup. + if c == nil { + return nil, fmt.Errorf("no internal account client") + } + + return c.processSubEx([]byte(subject), nil, []byte(sid), cb, false, false, ri) +} + +// This will add an account subscription that matches the "from" from a service import entry. +func (a *Account) addServiceImportSub(si *serviceImport) error { + a.mu.Lock() + c := a.internalClient() + // This will happen in parsing when the account has not been properly setup. + if c == nil { + a.mu.Unlock() + return nil + } + if si.sid != nil { + a.mu.Unlock() + return fmt.Errorf("duplicate call to create subscription for service import") + } + a.isid++ + sid := strconv.FormatUint(a.isid, 10) + si.sid = []byte(sid) + subject := si.from + a.mu.Unlock() + + cb := func(sub *subscription, c *client, acc *Account, subject, reply string, msg []byte) { + c.pa.delivered = c.processServiceImport(si, acc, msg) + } + sub, err := c.processSubEx([]byte(subject), nil, []byte(sid), cb, true, true, false) + if err != nil { + return err + } + // Leafnodes introduce a new way to introduce messages into the system. Therefore forward import subscription + // This is similar to what initLeafNodeSmapAndSendSubs does + // TODO we need to consider performing this update as we get client subscriptions. + // This behavior would result in subscription propagation only where actually used. + a.updateLeafNodes(sub, 1) + return nil +} + +// Remove all the subscriptions associated with service imports. +func (a *Account) removeAllServiceImportSubs() { + a.mu.Lock() + var sids [][]byte + for _, sis := range a.imports.services { + for _, si := range sis { + if si.sid != nil { + sids = append(sids, si.sid) + si.sid = nil + } + } + } + c := a.ic + a.ic = nil + a.mu.Unlock() + + if c == nil { + return + } + for _, sid := range sids { + c.processUnsub(sid) + } + c.closeConnection(InternalClient) +} + +// Add in subscriptions for all registered service imports. +func (a *Account) addAllServiceImportSubs() { + var sis [32]*serviceImport + serviceImports := sis[:0] + a.mu.RLock() + for _, sis := range a.imports.services { + serviceImports = append(serviceImports, sis...) + } + a.mu.RUnlock() + for _, si := range serviceImports { + a.addServiceImportSub(si) + } +} + +var ( + // header where all information is encoded in one value. + trcUber = textproto.CanonicalMIMEHeaderKey("Uber-Trace-Id") + trcCtx = textproto.CanonicalMIMEHeaderKey("Traceparent") + trcB3 = textproto.CanonicalMIMEHeaderKey("B3") + // openzipkin header to check + trcB3Sm = textproto.CanonicalMIMEHeaderKey("X-B3-Sampled") + trcB3Id = textproto.CanonicalMIMEHeaderKey("X-B3-TraceId") + // additional header needed to include when present + trcB3PSId = textproto.CanonicalMIMEHeaderKey("X-B3-ParentSpanId") + trcB3SId = textproto.CanonicalMIMEHeaderKey("X-B3-SpanId") + trcCtxSt = textproto.CanonicalMIMEHeaderKey("Tracestate") + trcUberCtxPrefix = textproto.CanonicalMIMEHeaderKey("Uberctx-") +) + +func newB3Header(h http.Header) http.Header { + retHdr := http.Header{} + if v, ok := h[trcB3Sm]; ok { + retHdr[trcB3Sm] = v + } + if v, ok := h[trcB3Id]; ok { + retHdr[trcB3Id] = v + } + if v, ok := h[trcB3PSId]; ok { + retHdr[trcB3PSId] = v + } + if v, ok := h[trcB3SId]; ok { + retHdr[trcB3SId] = v + } + return retHdr +} + +func newUberHeader(h http.Header, tId []string) http.Header { + retHdr := http.Header{trcUber: tId} + for k, v := range h { + if strings.HasPrefix(k, trcUberCtxPrefix) { + retHdr[k] = v + } + } + return retHdr +} + +func newTraceCtxHeader(h http.Header, tId []string) http.Header { + retHdr := http.Header{trcCtx: tId} + if v, ok := h[trcCtxSt]; ok { + retHdr[trcCtxSt] = v + } + return retHdr +} + +// Helper to determine when to sample. When header has a value, sampling is driven by header +func shouldSample(l *serviceLatency, c *client) (bool, http.Header) { + if l == nil { + return false, nil + } + if l.sampling < 0 { + return false, nil + } + if l.sampling >= 100 { + return true, nil + } + if l.sampling > 0 && rand.Int32N(100) <= int32(l.sampling) { + return true, nil + } + h := c.parseState.getHeader() + if len(h) == 0 { + return false, nil + } + if tId := h[trcUber]; len(tId) != 0 { + // sample 479fefe9525eddb:5adb976bfc1f95c1:479fefe9525eddb:1 + tk := strings.Split(tId[0], ":") + if len(tk) == 4 && len(tk[3]) > 0 && len(tk[3]) <= 2 { + dst := [2]byte{} + src := [2]byte{'0', tk[3][0]} + if len(tk[3]) == 2 { + src[1] = tk[3][1] + } + if _, err := hex.Decode(dst[:], src[:]); err == nil && dst[0]&1 == 1 { + return true, newUberHeader(h, tId) + } + } + return false, nil + } else if sampled := h[trcB3Sm]; len(sampled) != 0 && sampled[0] == "1" { + return true, newB3Header(h) // allowed + } else if len(sampled) != 0 && sampled[0] == "0" { + return false, nil // denied + } else if _, ok := h[trcB3Id]; ok { + // sample 80f198ee56343ba864fe8b2a57d3eff7 + // presence (with X-B3-Sampled not being 0) means sampling left to recipient + return true, newB3Header(h) + } else if b3 := h[trcB3]; len(b3) != 0 { + // sample 80f198ee56343ba864fe8b2a57d3eff7-e457b5a2e4d86bd1-1-05e3ac9a4f6e3b90 + // sample 0 + tk := strings.Split(b3[0], "-") + if len(tk) > 2 && tk[2] == "0" { + return false, nil // denied + } else if len(tk) == 1 && tk[0] == "0" { + return false, nil // denied + } + return true, http.Header{trcB3: b3} // sampling allowed or left to recipient of header + } else if tId := h[trcCtx]; len(tId) != 0 { + var sample bool + // sample 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 + tk := strings.Split(tId[0], "-") + if len(tk) == 4 && len([]byte(tk[3])) == 2 { + if hexVal, err := strconv.ParseInt(tk[3], 16, 8); err == nil { + sample = hexVal&0x1 == 0x1 + } + } + if sample { + return true, newTraceCtxHeader(h, tId) + } else { + return false, nil + } + } + return false, nil +} + +// Used to mimic client like replies. +const ( + replyPrefix = "_R_." + replyPrefixLen = len(replyPrefix) + baseServerLen = 10 + replyLen = 6 + minReplyLen = 15 + digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + base = 62 +) + +// This is where all service export responses are handled. +func (a *Account) processServiceImportResponse(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + a.mu.RLock() + if a.expired.Load() || len(a.exports.responses) == 0 { + a.mu.RUnlock() + return + } + si := a.exports.responses[subject] + + if si == nil || si.invalid { + a.mu.RUnlock() + return + } + a.mu.RUnlock() + + // Send for normal processing. + c.processServiceImport(si, a, msg) +} + +// Will create the response prefix for fast generation of responses. +// A wildcard subscription may be used handle interest graph propagation +// for all service replies, unless we are bound to a leafnode. +// Lock should be held. +func (a *Account) createRespWildcard() { + b := [baseServerLen]byte{'_', 'R', '_', '.'} + rn := rand.Uint64() + for i, l := replyPrefixLen, rn; i < len(b); i++ { + b[i] = digits[l%base] + l /= base + } + a.siReply = append(b[:], '.') +} + +// Test whether this is a tracked reply. +func isTrackedReply(reply []byte) bool { + lreply := len(reply) - 1 + return lreply > 3 && reply[lreply-1] == '.' && reply[lreply] == 'T' +} + +// Generate a new service reply from the wildcard prefix. +// FIXME(dlc) - probably do not have to use rand here. about 25ns per. +func (a *Account) newServiceReply(tracking bool) []byte { + a.mu.Lock() + s := a.srv + rn := rand.Uint64() + + // Check if we need to create the reply here. + var createdSiReply bool + if a.siReply == nil { + a.createRespWildcard() + createdSiReply = true + } + replyPre := a.siReply + a.mu.Unlock() + + // If we created the siReply and we are not bound to a leafnode + // we need to do the wildcard subscription. + if createdSiReply { + a.subscribeServiceImportResponse(string(append(replyPre, '>'))) + } + + var b [replyLen]byte + for i, l := 0, rn; i < len(b); i++ { + b[i] = digits[l%base] + l /= base + } + // Make sure to copy. + reply := make([]byte, 0, len(replyPre)+len(b)) + reply = append(reply, replyPre...) + reply = append(reply, b[:]...) + + if tracking && s.sys != nil { + // Add in our tracking identifier. This allows the metrics to get back to only + // this server without needless SUBS/UNSUBS. + reply = append(reply, '.') + reply = append(reply, s.sys.shash...) + reply = append(reply, '.', 'T') + } + + return reply +} + +// Checks if a serviceImport was created to map responses. +func (si *serviceImport) isRespServiceImport() bool { + return si != nil && si.response +} + +// Sets the response threshold timer for a service export. +// Account lock should be held +func (se *serviceExport) setResponseThresholdTimer() { + if se.rtmr != nil { + return // Already set + } + se.rtmr = time.AfterFunc(se.respThresh, se.checkExpiredResponses) +} + +// Account lock should be held +func (se *serviceExport) clearResponseThresholdTimer() bool { + if se.rtmr == nil { + return true + } + stopped := se.rtmr.Stop() + se.rtmr = nil + return stopped +} + +// checkExpiredResponses will check for any pending responses that need to +// be cleaned up. +func (se *serviceExport) checkExpiredResponses() { + acc := se.acc + if acc == nil { + se.clearResponseThresholdTimer() + return + } + + var expired []*serviceImport + mints := time.Now().UnixNano() - int64(se.respThresh) + + // TODO(dlc) - Should we release lock while doing this? Or only do these in batches? + // Should we break this up for responses only from this service export? + // Responses live on acc directly for fast inbound processsing for the _R_ wildcard. + // We could do another indirection at this level but just to get to the service export? + var totalResponses int + acc.mu.RLock() + for _, si := range acc.exports.responses { + if si.se == se { + totalResponses++ + if si.ts <= mints { + expired = append(expired, si) + } + } + } + acc.mu.RUnlock() + + for _, si := range expired { + acc.removeRespServiceImport(si, rsiTimeout) + } + + // Pull out expired to determine if we have any left for timer. + totalResponses -= len(expired) + + // Redo timer as needed. + acc.mu.Lock() + if totalResponses > 0 && se.rtmr != nil { + se.rtmr.Stop() + se.rtmr.Reset(se.respThresh) + } else { + se.clearResponseThresholdTimer() + } + acc.mu.Unlock() +} + +// ServiceExportResponseThreshold returns the current threshold. +func (a *Account) ServiceExportResponseThreshold(export string) (time.Duration, error) { + a.mu.Lock() + defer a.mu.Unlock() + se := a.getServiceExport(export) + if se == nil { + return 0, fmt.Errorf("no export defined for %q", export) + } + return se.respThresh, nil +} + +// SetServiceExportResponseThreshold sets the maximum time the system will a response to be delivered +// from a service export responder. +func (a *Account) SetServiceExportResponseThreshold(export string, maxTime time.Duration) error { + a.mu.Lock() + if a.isClaimAccount() { + a.mu.Unlock() + return fmt.Errorf("claim based accounts can not be updated directly") + } + lrt := a.lowestServiceExportResponseTime() + se := a.getServiceExport(export) + if se == nil { + a.mu.Unlock() + return fmt.Errorf("no export defined for %q", export) + } + se.respThresh = maxTime + + var clients []*client + nlrt := a.lowestServiceExportResponseTime() + if nlrt != lrt && len(a.clients) > 0 { + clients = a.getClientsLocked() + } + // Need to release because lock ordering is client -> Account + a.mu.Unlock() + if len(clients) > 0 { + updateAllClientsServiceExportResponseTime(clients, nlrt) + } + return nil +} + +func (a *Account) SetServiceExportAllowTrace(export string, allowTrace bool) error { + a.mu.Lock() + se := a.getServiceExport(export) + if se == nil { + a.mu.Unlock() + return fmt.Errorf("no export defined for %q", export) + } + se.atrc = allowTrace + a.mu.Unlock() + return nil +} + +// This is for internal service import responses. +func (a *Account) addRespServiceImport(dest *Account, to string, osi *serviceImport, tracking bool, header http.Header, mt *msgTrace) *serviceImport { + nrr := string(osi.acc.newServiceReply(tracking)) + + a.mu.Lock() + rt := osi.rt + + // dest is the requestor's account. a is the service responder with the export. + // Marked as internal here, that is how we distinguish. + si := &serviceImport{dest, nil, osi.se, nil, nrr, to, nil, 0, rt, nil, nil, nil, mt, false, true, false, osi.share, false, false, false, nil} + + if a.exports.responses == nil { + a.exports.responses = make(map[string]*serviceImport) + } + a.exports.responses[nrr] = si + + // Always grab time and make sure response threshold timer is running. + si.ts = time.Now().UnixNano() + if osi.se != nil { + osi.se.setResponseThresholdTimer() + } + + if rt == Singleton && tracking { + si.latency = osi.latency + si.tracking = true + si.trackingHdr = header + } + a.mu.Unlock() + + // We do add in the reverse map such that we can detect loss of interest and do proper + // cleanup of this si as interest goes away. + dest.addReverseRespMapEntry(a, to, nrr) + + return si +} + +// AddStreamImportWithClaim will add in the stream import from a specific account with optional token. +func (a *Account) AddStreamImportWithClaim(account *Account, from, prefix string, imClaim *jwt.Import) error { + return a.addStreamImportWithClaim(account, from, prefix, false, imClaim) +} + +func (a *Account) addStreamImportWithClaim(account *Account, from, prefix string, allowTrace bool, imClaim *jwt.Import) error { + if account == nil { + return ErrMissingAccount + } + + // First check to see if the account has authorized export of the subject. + if !account.checkStreamImportAuthorized(a, from, imClaim) { + return ErrStreamImportAuthorization + } + + // Check prefix if it exists and make sure its a literal. + // Append token separator if not already present. + if prefix != _EMPTY_ { + // Make sure there are no wildcards here, this prefix needs to be a literal + // since it will be prepended to a publish subject. + if !subjectIsLiteral(prefix) { + return ErrStreamImportBadPrefix + } + if prefix[len(prefix)-1] != btsep { + prefix = prefix + string(btsep) + } + } + + return a.addMappedStreamImportWithClaim(account, from, prefix+from, allowTrace, imClaim) +} + +// AddMappedStreamImport helper for AddMappedStreamImportWithClaim +func (a *Account) AddMappedStreamImport(account *Account, from, to string) error { + return a.AddMappedStreamImportWithClaim(account, from, to, nil) +} + +// AddMappedStreamImportWithClaim will add in the stream import from a specific account with optional token. +func (a *Account) AddMappedStreamImportWithClaim(account *Account, from, to string, imClaim *jwt.Import) error { + return a.addMappedStreamImportWithClaim(account, from, to, false, imClaim) +} + +func (a *Account) addMappedStreamImportWithClaim(account *Account, from, to string, allowTrace bool, imClaim *jwt.Import) error { + if account == nil { + return ErrMissingAccount + } + + // First check to see if the account has authorized export of the subject. + if !account.checkStreamImportAuthorized(a, from, imClaim) { + return ErrStreamImportAuthorization + } + + if to == _EMPTY_ { + to = from + } + + // Check if this forms a cycle. + if err := a.streamImportFormsCycle(account, to); err != nil { + return err + } + + if err := a.streamImportFormsCycle(account, from); err != nil { + return err + } + + var ( + usePub bool + tr *subjectTransform + err error + ) + if subjectHasWildcard(from) { + if to == from { + usePub = true + } else { + // Create a transform + if tr, err = NewSubjectTransformStrict(from, transformTokenize(to)); err != nil { + return fmt.Errorf("failed to create mapping transform for stream import subject from %q to %q: %v", + from, to, err) + } + to, _ = transformUntokenize(to) + } + } + + a.mu.Lock() + if a.isStreamImportDuplicate(account, from) { + a.mu.Unlock() + return ErrStreamImportDuplicate + } + if imClaim != nil { + allowTrace = imClaim.AllowTrace + } + a.imports.streams = append(a.imports.streams, &streamImport{account, from, to, tr, nil, imClaim, usePub, false, allowTrace}) + a.mu.Unlock() + return nil +} + +// isStreamImportDuplicate checks for duplicate. +// Lock should be held. +func (a *Account) isStreamImportDuplicate(acc *Account, from string) bool { + for _, si := range a.imports.streams { + if si.acc == acc && si.from == from { + return true + } + } + return false +} + +// AddStreamImport will add in the stream import from a specific account. +func (a *Account) AddStreamImport(account *Account, from, prefix string) error { + return a.addStreamImportWithClaim(account, from, prefix, false, nil) +} + +// IsPublicExport is a placeholder to denote a public export. +var IsPublicExport = []*Account(nil) + +// AddStreamExport will add an export to the account. If accounts is nil +// it will signify a public export, meaning anyone can import. +func (a *Account) AddStreamExport(subject string, accounts []*Account) error { + return a.addStreamExportWithAccountPos(subject, accounts, 0) +} + +// AddStreamExport will add an export to the account. If accounts is nil +// it will signify a public export, meaning anyone can import. +// if accountPos is > 0, all imports will be granted where the following holds: +// strings.Split(subject, tsep)[accountPos] == account id will be granted. +func (a *Account) addStreamExportWithAccountPos(subject string, accounts []*Account, accountPos uint) error { + if a == nil { + return ErrMissingAccount + } + + a.mu.Lock() + defer a.mu.Unlock() + + if a.exports.streams == nil { + a.exports.streams = make(map[string]*streamExport) + } + ea := a.exports.streams[subject] + if accounts != nil || accountPos > 0 { + if ea == nil { + ea = &streamExport{} + } + if err := setExportAuth(&ea.exportAuth, subject, accounts, accountPos); err != nil { + return err + } + } + a.exports.streams[subject] = ea + return nil +} + +// Check if another account is authorized to import from us. +func (a *Account) checkStreamImportAuthorized(account *Account, subject string, imClaim *jwt.Import) bool { + // Find the subject in the exports list. + a.mu.RLock() + auth := a.checkStreamImportAuthorizedNoLock(account, subject, imClaim) + a.mu.RUnlock() + return auth +} + +func (a *Account) checkStreamImportAuthorizedNoLock(account *Account, subject string, imClaim *jwt.Import) bool { + if a.exports.streams == nil || !IsValidSubject(subject) { + return false + } + return a.checkStreamExportApproved(account, subject, imClaim) +} + +func (a *Account) checkAuth(ea *exportAuth, account *Account, imClaim *jwt.Import, tokens []string) bool { + // if ea is nil or ea.approved is nil, that denotes a public export + if ea == nil || (len(ea.approved) == 0 && !ea.tokenReq && ea.accountPos == 0) { + return true + } + // Check if the export is protected and enforces presence of importing account identity + if ea.accountPos > 0 { + return ea.accountPos <= uint(len(tokens)) && tokens[ea.accountPos-1] == account.Name + } + // Check if token required + if ea.tokenReq { + return a.checkActivation(account, imClaim, ea, true) + } + if ea.approved == nil { + return false + } + // If we have a matching account we are authorized + _, ok := ea.approved[account.Name] + return ok +} + +func (a *Account) checkStreamExportApproved(account *Account, subject string, imClaim *jwt.Import) bool { + // Check direct match of subject first + ea, ok := a.exports.streams[subject] + if ok { + // if ea is nil or eq.approved is nil, that denotes a public export + if ea == nil { + return true + } + return a.checkAuth(&ea.exportAuth, account, imClaim, nil) + } + + // ok if we are here we did not match directly so we need to test each one. + // The import subject arg has to take precedence, meaning the export + // has to be a true subset of the import claim. We already checked for + // exact matches above. + tokens := strings.Split(subject, tsep) + for subj, ea := range a.exports.streams { + if isSubsetMatch(tokens, subj) { + if ea == nil { + return true + } + return a.checkAuth(&ea.exportAuth, account, imClaim, tokens) + } + } + return false +} + +func (a *Account) checkServiceExportApproved(account *Account, subject string, imClaim *jwt.Import) bool { + // Check direct match of subject first + se, ok := a.exports.services[subject] + if ok { + // if se is nil or eq.approved is nil, that denotes a public export + if se == nil { + return true + } + return a.checkAuth(&se.exportAuth, account, imClaim, nil) + } + // ok if we are here we did not match directly so we need to test each one. + // The import subject arg has to take precedence, meaning the export + // has to be a true subset of the import claim. We already checked for + // exact matches above. + tokens := strings.Split(subject, tsep) + for subj, se := range a.exports.services { + if isSubsetMatch(tokens, subj) { + if se == nil { + return true + } + return a.checkAuth(&se.exportAuth, account, imClaim, tokens) + } + } + return false +} + +// Helper function to get a serviceExport. +// Lock should be held on entry. +func (a *Account) getServiceExport(subj string) *serviceExport { + se, ok := a.exports.services[subj] + // The export probably has a wildcard, so lookup that up. + if !ok { + se = a.getWildcardServiceExport(subj) + } + return se +} + +// This helper is used when trying to match a serviceExport record that is +// represented by a wildcard. +// Lock should be held on entry. +func (a *Account) getWildcardServiceExport(from string) *serviceExport { + tokens := strings.Split(from, tsep) + for subj, se := range a.exports.services { + if isSubsetMatch(tokens, subj) { + return se + } + } + return nil +} + +// These are import stream specific versions for when an activation expires. +func (a *Account) streamActivationExpired(exportAcc *Account, subject string) { + a.mu.RLock() + if a.expired.Load() || a.imports.streams == nil { + a.mu.RUnlock() + return + } + var si *streamImport + for _, im := range a.imports.streams { + if im.acc == exportAcc && im.from == subject { + si = im + break + } + } + + if si == nil || si.invalid { + a.mu.RUnlock() + return + } + a.mu.RUnlock() + + if si.acc.checkActivation(a, si.claim, nil, false) { + // The token has been updated most likely and we are good to go. + return + } + + a.mu.Lock() + si.invalid = true + clients := a.getClientsLocked() + awcsti := map[string]struct{}{a.Name: {}} + a.mu.Unlock() + for _, c := range clients { + c.processSubsOnConfigReload(awcsti) + } +} + +// These are import service specific versions for when an activation expires. +func (a *Account) serviceActivationExpired(dstAcc *Account, subject string) { + a.mu.RLock() + if a.expired.Load() || a.imports.services == nil { + a.mu.RUnlock() + return + } + si := a.getServiceImportForAccountLocked(dstAcc.Name, subject) + if si == nil || si.invalid { + a.mu.RUnlock() + return + } + a.mu.RUnlock() + + if si.acc.checkActivation(a, si.claim, nil, false) { + // The token has been updated most likely and we are good to go. + return + } + + a.mu.Lock() + si.invalid = true + a.mu.Unlock() +} + +// Fires for expired activation tokens. We could track this with timers etc. +// Instead we just re-analyze where we are and if we need to act. +func (a *Account) activationExpired(exportAcc *Account, subject string, kind jwt.ExportType) { + switch kind { + case jwt.Stream: + a.streamActivationExpired(exportAcc, subject) + case jwt.Service: + a.serviceActivationExpired(exportAcc, subject) + } +} + +func isRevoked(revocations map[string]int64, subject string, issuedAt int64) bool { + if len(revocations) == 0 { + return false + } + if t, ok := revocations[subject]; !ok || t < issuedAt { + if t, ok := revocations[jwt.All]; !ok || t < issuedAt { + return false + } + } + return true +} + +// checkActivation will check the activation token for validity. +// ea may only be nil in cases where revocation may not be checked, say triggered by expiration timer. +func (a *Account) checkActivation(importAcc *Account, claim *jwt.Import, ea *exportAuth, expTimer bool) bool { + if claim == nil || claim.Token == _EMPTY_ { + return false + } + // Create a quick clone so we can inline Token JWT. + clone := *claim + + vr := jwt.CreateValidationResults() + clone.Validate(importAcc.Name, vr) + if vr.IsBlocking(true) { + return false + } + act, err := jwt.DecodeActivationClaims(clone.Token) + if err != nil { + return false + } + if !a.isIssuerClaimTrusted(act) { + return false + } + vr = jwt.CreateValidationResults() + act.Validate(vr) + if vr.IsBlocking(true) { + return false + } + if act.Expires != 0 { + tn := time.Now().Unix() + if act.Expires <= tn { + return false + } + if expTimer { + expiresAt := time.Duration(act.Expires - tn) + time.AfterFunc(expiresAt*time.Second, func() { + importAcc.activationExpired(a, string(act.ImportSubject), claim.Type) + }) + } + } + if ea == nil { + return true + } + // Check for token revocation.. + return !isRevoked(ea.actsRevoked, act.Subject, act.IssuedAt) +} + +// Returns true if the activation claim is trusted. That is the issuer matches +// the account or is an entry in the signing keys. +func (a *Account) isIssuerClaimTrusted(claims *jwt.ActivationClaims) bool { + // if no issuer account, issuer is the account + if claims.IssuerAccount == _EMPTY_ { + return true + } + // If the IssuerAccount is not us, then this is considered an error. + if a.Name != claims.IssuerAccount { + if a.srv != nil { + a.srv.Errorf("Invalid issuer account %q in activation claim (subject: %q - type: %q) for account %q", + claims.IssuerAccount, claims.Activation.ImportSubject, claims.Activation.ImportType, a.Name) + } + return false + } + _, ok := a.hasIssuerNoLock(claims.Issuer) + return ok +} + +// Returns true if `a` and `b` stream imports are the same. Note that the +// check is done with the account's name, not the pointer. This is used +// during config reload where we are comparing current and new config +// in which pointers are different. +// Acquires `a` read lock, but `b` is assumed to not be accessed +// by anyone but the caller (`b` is not registered anywhere). +func (a *Account) checkStreamImportsEqual(b *Account) bool { + a.mu.RLock() + defer a.mu.RUnlock() + + if len(a.imports.streams) != len(b.imports.streams) { + return false + } + // Load the b imports into a map index by what we are looking for. + bm := make(map[string]*streamImport, len(b.imports.streams)) + for _, bim := range b.imports.streams { + bm[bim.acc.Name+bim.from+bim.to] = bim + } + for _, aim := range a.imports.streams { + if bim, ok := bm[aim.acc.Name+aim.from+aim.to]; !ok { + return false + } else if aim.atrc != bim.atrc { + return false + } + } + return true +} + +// Returns true if `a` and `b` stream exports are the same. +// Acquires `a` read lock, but `b` is assumed to not be accessed +// by anyone but the caller (`b` is not registered anywhere). +func (a *Account) checkStreamExportsEqual(b *Account) bool { + a.mu.RLock() + defer a.mu.RUnlock() + if len(a.exports.streams) != len(b.exports.streams) { + return false + } + for subj, aea := range a.exports.streams { + bea, ok := b.exports.streams[subj] + if !ok { + return false + } + if !isStreamExportEqual(aea, bea) { + return false + } + } + return true +} + +func isStreamExportEqual(a, b *streamExport) bool { + if a == nil && b == nil { + return true + } + if (a == nil && b != nil) || (a != nil && b == nil) { + return false + } + return isExportAuthEqual(&a.exportAuth, &b.exportAuth) +} + +// Returns true if `a` and `b` service exports are the same. +// Acquires `a` read lock, but `b` is assumed to not be accessed +// by anyone but the caller (`b` is not registered anywhere). +func (a *Account) checkServiceExportsEqual(b *Account) bool { + a.mu.RLock() + defer a.mu.RUnlock() + if len(a.exports.services) != len(b.exports.services) { + return false + } + for subj, aea := range a.exports.services { + bea, ok := b.exports.services[subj] + if !ok { + return false + } + if !isServiceExportEqual(aea, bea) { + return false + } + } + return true +} + +func isServiceExportEqual(a, b *serviceExport) bool { + if a == nil && b == nil { + return true + } + if (a == nil && b != nil) || (a != nil && b == nil) { + return false + } + if !isExportAuthEqual(&a.exportAuth, &b.exportAuth) { + return false + } + if a.acc.Name != b.acc.Name { + return false + } + if a.respType != b.respType { + return false + } + if a.latency != nil || b.latency != nil { + if (a.latency != nil && b.latency == nil) || (a.latency == nil && b.latency != nil) { + return false + } + if a.latency.sampling != b.latency.sampling { + return false + } + if a.latency.subject != b.latency.subject { + return false + } + } + if a.atrc != b.atrc { + return false + } + return true +} + +// Returns true if `a` and `b` exportAuth structures are equal. +// Both `a` and `b` are guaranteed to be non-nil. +// Locking is handled by the caller. +func isExportAuthEqual(a, b *exportAuth) bool { + if a.tokenReq != b.tokenReq { + return false + } + if a.accountPos != b.accountPos { + return false + } + if len(a.approved) != len(b.approved) { + return false + } + for ak, av := range a.approved { + if bv, ok := b.approved[ak]; !ok || av.Name != bv.Name { + return false + } + } + if len(a.actsRevoked) != len(b.actsRevoked) { + return false + } + for ak, av := range a.actsRevoked { + if bv, ok := b.actsRevoked[ak]; !ok || av != bv { + return false + } + } + return true +} + +// Check if another account is authorized to route requests to this service. +func (a *Account) checkServiceImportAuthorized(account *Account, subject string, imClaim *jwt.Import) bool { + a.mu.RLock() + authorized := a.checkServiceImportAuthorizedNoLock(account, subject, imClaim) + a.mu.RUnlock() + return authorized +} + +// Check if another account is authorized to route requests to this service. +func (a *Account) checkServiceImportAuthorizedNoLock(account *Account, subject string, imClaim *jwt.Import) bool { + // Find the subject in the services list. + if a.exports.services == nil { + return false + } + return a.checkServiceExportApproved(account, subject, imClaim) +} + +// IsExpired returns expiration status. +func (a *Account) IsExpired() bool { + return a.expired.Load() +} + +// Called when an account has expired. +func (a *Account) expiredTimeout() { + // Mark expired first. + a.expired.Store(true) + + // Collect the clients and expire them. + cs := a.getClients() + for _, c := range cs { + if !isInternalClient(c.kind) { + c.accountAuthExpired() + } + } +} + +// Sets the expiration timer for an account JWT that has it set. +func (a *Account) setExpirationTimer(d time.Duration) { + a.etmr = time.AfterFunc(d, a.expiredTimeout) +} + +// Lock should be held +func (a *Account) clearExpirationTimer() bool { + if a.etmr == nil { + return true + } + stopped := a.etmr.Stop() + a.etmr = nil + return stopped +} + +// checkUserRevoked will check if a user has been revoked. +func (a *Account) checkUserRevoked(nkey string, issuedAt int64) bool { + a.mu.RLock() + defer a.mu.RUnlock() + return isRevoked(a.usersRevoked, nkey, issuedAt) +} + +// failBearer will return if bearer token are allowed (false) or disallowed (true) +func (a *Account) failBearer() bool { + a.mu.RLock() + defer a.mu.RUnlock() + return a.disallowBearer +} + +// Check expiration and set the proper state as needed. +func (a *Account) checkExpiration(claims *jwt.ClaimsData) { + a.mu.Lock() + defer a.mu.Unlock() + + a.clearExpirationTimer() + if claims.Expires == 0 { + a.expired.Store(false) + return + } + tn := time.Now().Unix() + if claims.Expires <= tn { + a.expired.Store(true) + return + } + expiresAt := time.Duration(claims.Expires - tn) + a.setExpirationTimer(expiresAt * time.Second) + a.expired.Store(false) +} + +// hasIssuer returns true if the issuer matches the account +// If the issuer is a scoped signing key, the scope will be returned as well +// issuer or it is a signing key for the account. +func (a *Account) hasIssuer(issuer string) (jwt.Scope, bool) { + a.mu.RLock() + scope, ok := a.hasIssuerNoLock(issuer) + a.mu.RUnlock() + return scope, ok +} + +// hasIssuerNoLock is the unlocked version of hasIssuer +func (a *Account) hasIssuerNoLock(issuer string) (jwt.Scope, bool) { + scope, ok := a.signingKeys[issuer] + return scope, ok +} + +// Returns the loop detection subject used for leafnodes +func (a *Account) getLDSubject() string { + a.mu.RLock() + lds := a.lds + a.mu.RUnlock() + return lds +} + +// Placeholder for signaling token auth required. +var tokenAuthReq = []*Account{} + +func authAccounts(tokenReq bool) []*Account { + if tokenReq { + return tokenAuthReq + } + return nil +} + +// SetAccountResolver will assign the account resolver. +func (s *Server) SetAccountResolver(ar AccountResolver) { + s.mu.Lock() + s.accResolver = ar + s.mu.Unlock() +} + +// AccountResolver returns the registered account resolver. +func (s *Server) AccountResolver() AccountResolver { + s.mu.RLock() + ar := s.accResolver + s.mu.RUnlock() + return ar +} + +// isClaimAccount returns if this account is backed by a JWT claim. +// Lock should be held. +func (a *Account) isClaimAccount() bool { + return a.claimJWT != _EMPTY_ +} + +// UpdateAccountClaims will update an existing account with new claims. +// This will replace any exports or imports previously defined. +// Lock MUST NOT be held upon entry. +func (s *Server) UpdateAccountClaims(a *Account, ac *jwt.AccountClaims) { + s.updateAccountClaimsWithRefresh(a, ac, true) +} + +func (a *Account) traceLabel() string { + if a == nil { + return _EMPTY_ + } + if a.nameTag != _EMPTY_ { + return fmt.Sprintf("%s/%s", a.Name, a.nameTag) + } + return a.Name +} + +// Check if an account has external auth set. +// Operator/Account Resolver only. +func (a *Account) hasExternalAuth() bool { + if a == nil { + return false + } + a.mu.RLock() + defer a.mu.RUnlock() + return a.extAuth != nil +} + +// Deterimine if this is an external auth user. +func (a *Account) isExternalAuthUser(userID string) bool { + if a == nil { + return false + } + a.mu.RLock() + defer a.mu.RUnlock() + if a.extAuth != nil { + for _, u := range a.extAuth.AuthUsers { + if userID == u { + return true + } + } + } + return false +} + +// Return the external authorization xkey if external authorization is enabled and the xkey is set. +// Operator/Account Resolver only. +func (a *Account) externalAuthXKey() string { + if a == nil { + return _EMPTY_ + } + a.mu.RLock() + defer a.mu.RUnlock() + if a.extAuth != nil && a.extAuth.XKey != _EMPTY_ { + return a.extAuth.XKey + } + return _EMPTY_ +} + +// Check if an account switch for external authorization is allowed. +func (a *Account) isAllowedAcount(acc string) bool { + if a == nil { + return false + } + a.mu.RLock() + defer a.mu.RUnlock() + if a.extAuth != nil { + // if we have a single allowed account, and we have a wildcard + // we accept it + if len(a.extAuth.AllowedAccounts) == 1 && + a.extAuth.AllowedAccounts[0] == jwt.AnyAccount { + return true + } + // otherwise must match exactly + for _, a := range a.extAuth.AllowedAccounts { + if a == acc { + return true + } + } + } + return false +} + +// updateAccountClaimsWithRefresh will update an existing account with new claims. +// If refreshImportingAccounts is true it will also update incomplete dependent accounts +// This will replace any exports or imports previously defined. +// Lock MUST NOT be held upon entry. +func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaims, refreshImportingAccounts bool) { + if a == nil { + return + } + s.Debugf("Updating account claims: %s/%s", a.Name, ac.Name) + a.checkExpiration(ac.Claims()) + + a.mu.Lock() + // Clone to update, only select certain fields. + old := &Account{ + Name: a.Name, + exports: a.exports, + limits: a.limits, + signingKeys: a.signingKeys, + defaultPerms: a.defaultPerms.clone(), + } + + // overwrite claim meta data + a.nameTag = ac.Name + a.tags = ac.Tags + + // Grab trace label under lock. + tl := a.traceLabel() + + var td string + var tds int + if ac.Trace != nil { + // Update trace destination and sampling + td, tds = string(ac.Trace.Destination), ac.Trace.Sampling + if !IsValidPublishSubject(td) { + td, tds = _EMPTY_, 0 + } else if tds <= 0 || tds > 100 { + tds = 100 + } + } + a.traceDest, a.traceDestSampling = td, tds + + // Check for external authorization. + if ac.HasExternalAuthorization() { + a.extAuth = &jwt.ExternalAuthorization{} + a.extAuth.AuthUsers.Add(ac.Authorization.AuthUsers...) + a.extAuth.AllowedAccounts.Add(ac.Authorization.AllowedAccounts...) + a.extAuth.XKey = ac.Authorization.XKey + } else { + a.extAuth = nil + } + + // Reset exports and imports here. + + // Exports is creating a whole new map. + a.exports = exportMap{} + + // Imports are checked unlocked in processInbound, so we can't change out the struct here. Need to process inline. + if a.imports.streams != nil { + old.imports.streams = a.imports.streams + a.imports.streams = nil + } + if a.imports.services != nil { + old.imports.services = make(map[string][]*serviceImport, len(a.imports.services)) + for k, v := range a.imports.services { + sis := append([]*serviceImport(nil), v...) + old.imports.services[k] = sis + delete(a.imports.services, k) + } + } + + alteredScope := map[string]struct{}{} + + // update account signing keys + a.signingKeys = nil + _, strict := s.strictSigningKeyUsage[a.Issuer] + if len(ac.SigningKeys) > 0 || !strict { + a.signingKeys = make(map[string]jwt.Scope) + } + signersChanged := false + for k, scope := range ac.SigningKeys { + a.signingKeys[k] = scope + } + if !strict { + a.signingKeys[a.Name] = nil + } + if len(a.signingKeys) != len(old.signingKeys) { + signersChanged = true + } + for k, scope := range a.signingKeys { + if oldScope, ok := old.signingKeys[k]; !ok { + signersChanged = true + } else if !reflect.DeepEqual(scope, oldScope) { + signersChanged = true + alteredScope[k] = struct{}{} + } + } + // collect mappings that need to be removed + removeList := []string{} + for _, m := range a.mappings { + if _, ok := ac.Mappings[jwt.Subject(m.src)]; !ok { + removeList = append(removeList, m.src) + } + } + a.mu.Unlock() + + for sub, wm := range ac.Mappings { + mappings := make([]*MapDest, len(wm)) + for i, m := range wm { + mappings[i] = &MapDest{ + Subject: string(m.Subject), + Weight: m.GetWeight(), + Cluster: m.Cluster, + } + } + // This will overwrite existing entries + a.AddWeightedMappings(string(sub), mappings...) + } + // remove mappings + for _, rmMapping := range removeList { + a.RemoveMapping(rmMapping) + } + + // Re-register system exports/imports. + if a == s.SystemAccount() { + s.addSystemAccountExports(a) + } else { + s.registerSystemImports(a) + } + + jsEnabled := s.JetStreamEnabled() + + streamTokenExpirationChanged := false + serviceTokenExpirationChanged := false + + for _, e := range ac.Exports { + switch e.Type { + case jwt.Stream: + s.Debugf("Adding stream export %q for %s", e.Subject, tl) + if err := a.addStreamExportWithAccountPos( + string(e.Subject), authAccounts(e.TokenReq), e.AccountTokenPosition, + ); err != nil { + s.Debugf("Error adding stream export to account [%s]: %v", tl, err.Error()) + } + case jwt.Service: + s.Debugf("Adding service export %q for %s", e.Subject, tl) + rt := Singleton + switch e.ResponseType { + case jwt.ResponseTypeStream: + rt = Streamed + case jwt.ResponseTypeChunked: + rt = Chunked + } + if err := a.addServiceExportWithResponseAndAccountPos( + string(e.Subject), rt, authAccounts(e.TokenReq), e.AccountTokenPosition, + ); err != nil { + s.Debugf("Error adding service export to account [%s]: %v", tl, err) + continue + } + sub := string(e.Subject) + if e.Latency != nil { + if err := a.TrackServiceExportWithSampling(sub, string(e.Latency.Results), int(e.Latency.Sampling)); err != nil { + hdrNote := _EMPTY_ + if e.Latency.Sampling == jwt.Headers { + hdrNote = " (using headers)" + } + s.Debugf("Error adding latency tracking%s for service export to account [%s]: %v", hdrNote, tl, err) + } + } + if e.ResponseThreshold != 0 { + // Response threshold was set in options. + if err := a.SetServiceExportResponseThreshold(sub, e.ResponseThreshold); err != nil { + s.Debugf("Error adding service export response threshold for [%s]: %v", tl, err) + } + } + if err := a.SetServiceExportAllowTrace(sub, e.AllowTrace); err != nil { + s.Debugf("Error adding allow_trace for %q: %v", sub, err) + } + } + + var revocationChanged *bool + var ea *exportAuth + + a.mu.Lock() + switch e.Type { + case jwt.Stream: + revocationChanged = &streamTokenExpirationChanged + if se, ok := a.exports.streams[string(e.Subject)]; ok && se != nil { + ea = &se.exportAuth + } + case jwt.Service: + revocationChanged = &serviceTokenExpirationChanged + if se, ok := a.exports.services[string(e.Subject)]; ok && se != nil { + ea = &se.exportAuth + } + } + if ea != nil { + oldRevocations := ea.actsRevoked + if len(e.Revocations) == 0 { + // remove all, no need to evaluate existing imports + ea.actsRevoked = nil + } else if len(oldRevocations) == 0 { + // add all, existing imports need to be re evaluated + ea.actsRevoked = e.Revocations + *revocationChanged = true + } else { + ea.actsRevoked = e.Revocations + // diff, existing imports need to be conditionally re evaluated, depending on: + // if a key was added, or it's timestamp increased + for k, t := range e.Revocations { + if tOld, ok := oldRevocations[k]; !ok || tOld < t { + *revocationChanged = true + } + } + } + } + a.mu.Unlock() + } + var incompleteImports []*jwt.Import + for _, i := range ac.Imports { + acc, err := s.lookupAccount(i.Account) + if acc == nil || err != nil { + s.Errorf("Can't locate account [%s] for import of [%v] %s (err=%v)", i.Account, i.Subject, i.Type, err) + incompleteImports = append(incompleteImports, i) + continue + } + // Capture trace labels. + acc.mu.RLock() + atl := acc.traceLabel() + acc.mu.RUnlock() + // Grab from and to + from, to := string(i.Subject), i.GetTo() + switch i.Type { + case jwt.Stream: + if i.LocalSubject != _EMPTY_ { + // set local subject implies to is empty + to = string(i.LocalSubject) + s.Debugf("Adding stream import %s:%q for %s:%q", atl, from, tl, to) + err = a.AddMappedStreamImportWithClaim(acc, from, to, i) + } else { + s.Debugf("Adding stream import %s:%q for %s:%q", atl, from, tl, to) + err = a.AddStreamImportWithClaim(acc, from, to, i) + } + if err != nil { + s.Debugf("Error adding stream import to account [%s]: %v", tl, err.Error()) + incompleteImports = append(incompleteImports, i) + } + case jwt.Service: + if i.LocalSubject != _EMPTY_ { + from = string(i.LocalSubject) + to = string(i.Subject) + } + s.Debugf("Adding service import %s:%q for %s:%q", atl, from, tl, to) + if err := a.AddServiceImportWithClaim(acc, from, to, i); err != nil { + s.Debugf("Error adding service import to account [%s]: %v", tl, err.Error()) + incompleteImports = append(incompleteImports, i) + } + } + } + // Now let's apply any needed changes from import/export changes. + if !a.checkStreamImportsEqual(old) { + awcsti := map[string]struct{}{a.Name: {}} + for _, c := range a.getClients() { + c.processSubsOnConfigReload(awcsti) + } + } + // Now check if stream exports have changed. + if !a.checkStreamExportsEqual(old) || signersChanged || streamTokenExpirationChanged { + clients := map[*client]struct{}{} + // We need to check all accounts that have an import claim from this account. + awcsti := map[string]struct{}{} + + // We must only allow one goroutine to go through here, otherwise we could deadlock + // due to locking two accounts in succession. + s.mu.Lock() + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + // Move to the next if this account is actually account "a". + if acc.Name == a.Name { + return true + } + acc.mu.Lock() + for _, im := range acc.imports.streams { + if im != nil && im.acc.Name == a.Name { + // Check for if we are still authorized for an import. + im.invalid = !a.checkStreamImportAuthorized(acc, im.from, im.claim) + awcsti[acc.Name] = struct{}{} + for c := range acc.clients { + clients[c] = struct{}{} + } + } + } + acc.mu.Unlock() + return true + }) + s.mu.Unlock() + // Now walk clients. + for c := range clients { + c.processSubsOnConfigReload(awcsti) + } + } + // Now check if service exports have changed. + if !a.checkServiceExportsEqual(old) || signersChanged || serviceTokenExpirationChanged { + // We must only allow one goroutine to go through here, otherwise we could deadlock + // due to locking two accounts in succession. + s.mu.Lock() + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + // Move to the next if this account is actually account "a". + if acc.Name == a.Name { + return true + } + acc.mu.Lock() + for _, sis := range acc.imports.services { + for _, si := range sis { + if si != nil && si.acc.Name == a.Name { + // Check for if we are still authorized for an import. + si.invalid = !a.checkServiceImportAuthorized(acc, si.to, si.claim) + // Make sure we should still be tracking latency and if we + // are allowed to trace. + if !si.response { + a.mu.RLock() + if se := a.getServiceExport(si.to); se != nil { + if si.latency != nil { + si.latency = se.latency + } + // Update allow trace. + si.atrc = se.atrc + } + a.mu.RUnlock() + } + } + } + } + acc.mu.Unlock() + return true + }) + s.mu.Unlock() + } + + // Now make sure we shutdown the old service import subscriptions. + var sids [][]byte + a.mu.RLock() + c := a.ic + if c != nil { + for _, sis := range old.imports.services { + for _, si := range sis { + if si.sid != nil { + sids = append(sids, si.sid) + } + } + } + } + a.mu.RUnlock() + for _, sid := range sids { + c.processUnsub(sid) + } + + // Now do limits if they are present. + a.mu.Lock() + a.msubs = clampInt64ToInt32(ac.Limits.Subs) + a.mpay = clampInt64ToInt32(ac.Limits.Payload) + a.mconns = clampInt64ToInt32(ac.Limits.Conn) + a.mleafs = clampInt64ToInt32(ac.Limits.LeafNodeConn) + a.disallowBearer = ac.Limits.DisallowBearer + // Check for any revocations + if len(ac.Revocations) > 0 { + // We will always replace whatever we had with most current, so no + // need to look at what we have. + a.usersRevoked = make(map[string]int64, len(ac.Revocations)) + for pk, t := range ac.Revocations { + a.usersRevoked[pk] = t + } + } else { + a.usersRevoked = nil + } + a.defaultPerms = buildPermissionsFromJwt(&ac.DefaultPermissions) + a.incomplete = len(incompleteImports) != 0 + for _, i := range incompleteImports { + s.incompleteAccExporterMap.Store(i.Account, struct{}{}) + } + if a.srv == nil { + a.srv = s + } + + if ac.Limits.IsJSEnabled() { + toUnlimited := func(value int64) int64 { + if value > 0 { + return value + } + return -1 + } + if ac.Limits.JetStreamLimits.DiskStorage != 0 || ac.Limits.JetStreamLimits.MemoryStorage != 0 { + // JetStreamAccountLimits and jwt.JetStreamLimits use same value for unlimited + a.jsLimits = map[string]JetStreamAccountLimits{ + _EMPTY_: { + MaxMemory: ac.Limits.JetStreamLimits.MemoryStorage, + MaxStore: ac.Limits.JetStreamLimits.DiskStorage, + MaxStreams: int(ac.Limits.JetStreamLimits.Streams), + MaxConsumers: int(ac.Limits.JetStreamLimits.Consumer), + MemoryMaxStreamBytes: toUnlimited(ac.Limits.JetStreamLimits.MemoryMaxStreamBytes), + StoreMaxStreamBytes: toUnlimited(ac.Limits.JetStreamLimits.DiskMaxStreamBytes), + MaxBytesRequired: ac.Limits.JetStreamLimits.MaxBytesRequired, + MaxAckPending: int(toUnlimited(ac.Limits.JetStreamLimits.MaxAckPending)), + }, + } + } else { + a.jsLimits = map[string]JetStreamAccountLimits{} + for t, l := range ac.Limits.JetStreamTieredLimits { + a.jsLimits[t] = JetStreamAccountLimits{ + MaxMemory: l.MemoryStorage, + MaxStore: l.DiskStorage, + MaxStreams: int(l.Streams), + MaxConsumers: int(l.Consumer), + MemoryMaxStreamBytes: toUnlimited(l.MemoryMaxStreamBytes), + StoreMaxStreamBytes: toUnlimited(l.DiskMaxStreamBytes), + MaxBytesRequired: l.MaxBytesRequired, + MaxAckPending: int(toUnlimited(l.MaxAckPending)), + } + } + } + } else if a.jsLimits != nil { + // covers failed update followed by disable + a.jsLimits = nil + } + + defaultPerms := a.defaultPerms + defaultPermsChanged := !reflect.DeepEqual(old.defaultPerms, defaultPerms) + a.updated = time.Now() + clients := a.getClientsLocked() + ajs := a.js + hasJsLimits := a.jsLimits != nil + a.mu.Unlock() + + // Sort in chronological order so that most recent connections over the limit are pruned. + if a.MaxTotalConnectionsReached() { + slices.SortFunc(clients, func(i, j *client) int { return i.start.Compare(j.start) }) + } + + // If JetStream is enabled for this server we will call into configJetStream for the account + // regardless of enabled or disabled. It handles both cases. + if jsEnabled { + if err := s.configJetStream(a, nil); err != nil { + s.Errorf("Error configuring jetstream for account [%s]: %v", tl, err.Error()) + a.mu.Lock() + // Absent reload of js server cfg, this is going to be broken until js is disabled + a.incomplete = true + a.mu.Unlock() + } else { + a.mu.Lock() + // Refresh reference, we've just enabled JetStream, so it would have been nil before. + ajs = a.js + a.mu.Unlock() + } + } else if hasJsLimits { + // We do not have JS enabled for this server, but the account has it enabled so setup + // our imports properly. This allows this server to proxy JS traffic correctly. + s.checkJetStreamExports() + a.enableAllJetStreamServiceImportsAndMappings() + } + + if ajs != nil { + // Check whether the account NRG status changed. If it has then we need to notify the + // Raft groups running on the system so that they can move their subs if needed. + a.mu.Lock() + previous := a.nrgAccount + switch ac.ClusterTraffic { + case "system", _EMPTY_: + a.nrgAccount = _EMPTY_ + case "owner": + a.nrgAccount = a.Name + default: + s.Errorf("Account claim for %q has invalid value %q for cluster traffic account", a.Name, ac.ClusterTraffic) + } + changed := a.nrgAccount != previous + a.mu.Unlock() + if changed { + s.updateNRGAccountStatus() + } + } + + // client list is in chronological order (older cids at the beginning of the list). + count := 0 + for _, c := range clients { + a.mu.RLock() + exceeded := a.mconns != jwt.NoLimit && count >= int(a.mconns) + a.mu.RUnlock() + // Only kick non-internal clients. + if !isInternalClient(c.kind) { + if exceeded { + c.maxAccountConnExceeded() + continue + } + count++ + } + c.mu.Lock() + c.applyAccountLimits() + // if we have an nkey user we are a callout user - save + // the issuedAt, and nkey user id to honor revocations + var nkeyUserID string + var issuedAt int64 + if c.user != nil { + issuedAt = c.user.Issued + nkeyUserID = c.user.Nkey + } + theJWT := c.opts.JWT + c.mu.Unlock() + if defaultPermsChanged && c.updateDefaultPermissions(defaultPerms) && defaultPerms != nil { + c.processSubsOnConfigReload(nil) + } + // Check for being revoked here. We use ac one to avoid the account lock. + if (ac.Revocations != nil || ac.Limits.DisallowBearer) && theJWT != _EMPTY_ { + if juc, err := jwt.DecodeUserClaims(theJWT); err != nil { + c.Debugf("User JWT not valid: %v", err) + c.authViolation() + continue + } else if juc.BearerToken && ac.Limits.DisallowBearer { + c.Debugf("Bearer User JWT not allowed") + c.authViolation() + continue + } else if ok := ac.IsClaimRevoked(juc); ok { + c.sendErrAndDebug("User Authentication Revoked") + c.closeConnection(Revocation) + continue + } + } + + // if we extracted nkeyUserID and issuedAt we are a callout type + // calloutIAT should only be set if we are in callout scenario as + // the user JWT is _NOT_ associated with the client for callouts, + // so we rely on the calloutIAT to know when the JWT was issued + // revocations simply state that JWT issued before or by that date + // are not valid + if ac.Revocations != nil && nkeyUserID != _EMPTY_ && issuedAt > 0 { + seconds, ok := ac.Revocations[jwt.All] + if ok && seconds >= issuedAt { + c.sendErrAndDebug("User Authentication Revoked") + c.closeConnection(Revocation) + continue + } + seconds, ok = ac.Revocations[nkeyUserID] + if ok && seconds >= issuedAt { + c.sendErrAndDebug("User Authentication Revoked") + c.closeConnection(Revocation) + continue + } + } + } + + // Check if the signing keys changed, might have to evict + if signersChanged { + for _, c := range clients { + c.mu.Lock() + if c.user == nil { + c.mu.Unlock() + continue + } + sk := c.user.SigningKey + c.mu.Unlock() + if sk == _EMPTY_ { + continue + } + if _, ok := alteredScope[sk]; ok { + c.closeConnection(AuthenticationViolation) + } else if _, ok := a.hasIssuer(sk); !ok { + c.closeConnection(AuthenticationViolation) + } + } + } + + if _, ok := s.incompleteAccExporterMap.Load(old.Name); ok && refreshImportingAccounts { + s.incompleteAccExporterMap.Delete(old.Name) + s.accounts.Range(func(key, value any) bool { + acc := value.(*Account) + acc.mu.RLock() + incomplete := acc.incomplete + name := acc.Name + label := acc.traceLabel() + // Must use jwt in account or risk failing on fetch + // This jwt may not be the same that caused exportingAcc to be in incompleteAccExporterMap + claimJWT := acc.claimJWT + acc.mu.RUnlock() + if incomplete && name != old.Name { + if accClaims, _, err := s.verifyAccountClaims(claimJWT); err == nil { + // Since claimJWT has not changed, acc can become complete + // but it won't alter incomplete for it's dependents accounts. + s.updateAccountClaimsWithRefresh(acc, accClaims, false) + // old.Name was deleted before ranging over accounts + // If it exists again, UpdateAccountClaims set it for failed imports of acc. + // So there was one import of acc that imported this account and failed again. + // Since this account just got updated, the import itself may be in error. So trace that. + if _, ok := s.incompleteAccExporterMap.Load(old.Name); ok { + s.incompleteAccExporterMap.Delete(old.Name) + s.Errorf("Account %s has issues importing account %s", label, old.Name) + } + } + } + return true + }) + } +} + +// Helper to build an internal account structure from a jwt.AccountClaims. +// Lock MUST NOT be held upon entry. +func (s *Server) buildInternalAccount(ac *jwt.AccountClaims) *Account { + acc := NewAccount(ac.Subject) + acc.Issuer = ac.Issuer + // Set this here since we are placing in s.tmpAccounts below and may be + // referenced by an route RS+, etc. + s.setAccountSublist(acc) + + // We don't want to register an account that is in the process of + // being built, however, to solve circular import dependencies, we + // need to store it here. + if v, loaded := s.tmpAccounts.LoadOrStore(ac.Subject, acc); loaded { + return v.(*Account) + } + + // Update based on claims. + s.UpdateAccountClaims(acc, ac) + + return acc +} + +// Helper to build Permissions from jwt.Permissions +// or return nil if none were specified +func buildPermissionsFromJwt(uc *jwt.Permissions) *Permissions { + if uc == nil { + return nil + } + var p *Permissions + if len(uc.Pub.Allow) > 0 || len(uc.Pub.Deny) > 0 { + p = &Permissions{} + p.Publish = &SubjectPermission{} + p.Publish.Allow = uc.Pub.Allow + p.Publish.Deny = uc.Pub.Deny + } + if len(uc.Sub.Allow) > 0 || len(uc.Sub.Deny) > 0 { + if p == nil { + p = &Permissions{} + } + p.Subscribe = &SubjectPermission{} + p.Subscribe.Allow = uc.Sub.Allow + p.Subscribe.Deny = uc.Sub.Deny + } + if uc.Resp != nil { + if p == nil { + p = &Permissions{} + } + p.Response = &ResponsePermission{ + MaxMsgs: uc.Resp.MaxMsgs, + Expires: uc.Resp.Expires, + } + validateResponsePermissions(p) + } + return p +} + +// Helper to build internal NKeyUser. +func buildInternalNkeyUser(uc *jwt.UserClaims, acts map[string]struct{}, acc *Account) *NkeyUser { + nu := &NkeyUser{Nkey: uc.Subject, Account: acc, AllowedConnectionTypes: acts, Issued: uc.IssuedAt} + if uc.IssuerAccount != _EMPTY_ { + nu.SigningKey = uc.Issuer + } + + // Now check for permissions. + p := buildPermissionsFromJwt(&uc.Permissions) + if p == nil { + nu.defaultPerms = true + acc.mu.RLock() + if acc.defaultPerms != nil { + p = acc.defaultPerms.clone() + } + acc.mu.RUnlock() + } + nu.Permissions = p + return nu +} + +func fetchAccount(res AccountResolver, name string) (string, error) { + if !nkeys.IsValidPublicAccountKey(name) { + return _EMPTY_, fmt.Errorf("will only fetch valid account keys") + } + return res.Fetch(copyString(name)) +} + +// AccountResolver interface. This is to fetch Account JWTs by public nkeys +type AccountResolver interface { + Fetch(name string) (string, error) + Store(name, jwt string) error + IsReadOnly() bool + Start(server *Server) error + IsTrackingUpdate() bool + Reload() error + Close() +} + +// Default implementations of IsReadOnly/Start so only need to be written when changed +type resolverDefaultsOpsImpl struct{} + +func (*resolverDefaultsOpsImpl) IsReadOnly() bool { + return true +} + +func (*resolverDefaultsOpsImpl) IsTrackingUpdate() bool { + return false +} + +func (*resolverDefaultsOpsImpl) Start(*Server) error { + return nil +} + +func (*resolverDefaultsOpsImpl) Reload() error { + return nil +} + +func (*resolverDefaultsOpsImpl) Close() { +} + +func (*resolverDefaultsOpsImpl) Store(_, _ string) error { + return fmt.Errorf("store operation not supported for URL Resolver") +} + +// MemAccResolver is a memory only resolver. +// Mostly for testing. +type MemAccResolver struct { + sm sync.Map + resolverDefaultsOpsImpl +} + +// Fetch will fetch the account jwt claims from the internal sync.Map. +func (m *MemAccResolver) Fetch(name string) (string, error) { + if j, ok := m.sm.Load(name); ok { + return j.(string), nil + } + return _EMPTY_, ErrMissingAccount +} + +// Store will store the account jwt claims in the internal sync.Map. +func (m *MemAccResolver) Store(name, jwt string) error { + m.sm.Store(name, jwt) + return nil +} + +func (m *MemAccResolver) IsReadOnly() bool { + return false +} + +// URLAccResolver implements an http fetcher. +type URLAccResolver struct { + url string + c *http.Client + resolverDefaultsOpsImpl +} + +// NewURLAccResolver returns a new resolver for the given base URL. +func NewURLAccResolver(url string) (*URLAccResolver, error) { + if !strings.HasSuffix(url, "/") { + url += "/" + } + // FIXME(dlc) - Make timeout and others configurable. + // We create our own transport to amortize TLS. + tr := &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + } + ur := &URLAccResolver{ + url: url, + c: &http.Client{Timeout: DEFAULT_ACCOUNT_FETCH_TIMEOUT, Transport: tr}, + } + return ur, nil +} + +// Fetch will fetch the account jwt claims from the base url, appending the +// account name onto the end. +func (ur *URLAccResolver) Fetch(name string) (string, error) { + url := ur.url + name + resp, err := ur.c.Get(url) + if err != nil { + return _EMPTY_, fmt.Errorf("could not fetch <%q>: %v", redactURLString(url), err) + } else if resp == nil { + return _EMPTY_, fmt.Errorf("could not fetch <%q>: no response", redactURLString(url)) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return _EMPTY_, fmt.Errorf("could not fetch <%q>: %v", redactURLString(url), resp.Status) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return _EMPTY_, err + } + return string(body), nil +} + +// Resolver based on nats for synchronization and backing directory for storage. +type DirAccResolver struct { + *DirJWTStore + *Server + syncInterval time.Duration + fetchTimeout time.Duration +} + +func (dr *DirAccResolver) IsTrackingUpdate() bool { + return true +} + +func (dr *DirAccResolver) Reload() error { + return dr.DirJWTStore.Reload() +} + +// ServerAPIClaimUpdateResponse is the response to $SYS.REQ.ACCOUNT..CLAIMS.UPDATE and $SYS.REQ.CLAIMS.UPDATE +type ServerAPIClaimUpdateResponse struct { + Server *ServerInfo `json:"server"` + Data *ClaimUpdateStatus `json:"data,omitempty"` + Error *ClaimUpdateError `json:"error,omitempty"` +} + +type ClaimUpdateError struct { + Account string `json:"account,omitempty"` + Code int `json:"code"` + Description string `json:"description,omitempty"` +} + +type ClaimUpdateStatus struct { + Account string `json:"account,omitempty"` + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +func respondToUpdate(s *Server, respSubj string, acc string, message string, err error) { + if err == nil { + if acc == _EMPTY_ { + s.Debugf("%s", message) + } else { + s.Debugf("%s - %s", message, acc) + } + } else { + if acc == _EMPTY_ { + s.Errorf("%s - %s", message, err) + } else { + s.Errorf("%s - %s - %s", message, acc, err) + } + } + if respSubj == _EMPTY_ { + return + } + + response := ServerAPIClaimUpdateResponse{ + Server: &ServerInfo{}, + } + + if err == nil { + response.Data = &ClaimUpdateStatus{ + Account: acc, + Code: http.StatusOK, + Message: message, + } + } else { + response.Error = &ClaimUpdateError{ + Account: acc, + Code: http.StatusInternalServerError, + Description: fmt.Sprintf("%s - %v", message, err), + } + } + + s.sendInternalMsgLocked(respSubj, _EMPTY_, response.Server, response) +} + +func handleListRequest(store *DirJWTStore, s *Server, reply string) { + if reply == _EMPTY_ { + return + } + accIds := make([]string, 0, 1024) + if err := store.PackWalk(1, func(partialPackMsg string) { + if tk := strings.Split(partialPackMsg, "|"); len(tk) == 2 { + accIds = append(accIds, tk[0]) + } + }); err != nil { + // let them timeout + s.Errorf("list request error: %v", err) + } else { + s.Debugf("list request responded with %d account ids", len(accIds)) + server := &ServerInfo{} + response := map[string]any{"server": server, "data": accIds} + s.sendInternalMsgLocked(reply, _EMPTY_, server, response) + } +} + +func handleDeleteRequest(store *DirJWTStore, s *Server, msg []byte, reply string) { + var accIds []any + var subj, sysAccName string + if sysAcc := s.SystemAccount(); sysAcc != nil { + sysAccName = sysAcc.GetName() + } + // Only operator and operator signing key are allowed to delete + gk, err := jwt.DecodeGeneric(string(msg)) + if err == nil { + subj = gk.Subject + if store.deleteType == NoDelete { + err = fmt.Errorf("delete must be enabled in server config") + } else if subj != gk.Issuer { + err = fmt.Errorf("not self signed") + } else if _, ok := store.operator[gk.Issuer]; !ok { + err = fmt.Errorf("not trusted") + } else if list, ok := gk.Data["accounts"]; !ok { + err = fmt.Errorf("malformed request") + } else if accIds, ok = list.([]any); !ok { + err = fmt.Errorf("malformed request") + } else { + for _, entry := range accIds { + if acc, ok := entry.(string); !ok || + acc == _EMPTY_ || !nkeys.IsValidPublicAccountKey(acc) { + err = fmt.Errorf("malformed request") + break + } else if acc == sysAccName { + err = fmt.Errorf("not allowed to delete system account") + break + } + } + } + } + if err != nil { + respondToUpdate(s, reply, _EMPTY_, fmt.Sprintf("delete accounts request by %s failed", subj), err) + return + } + errs := []string{} + passCnt := 0 + for _, acc := range accIds { + if err := store.delete(acc.(string)); err != nil { + errs = append(errs, err.Error()) + } else { + passCnt++ + } + } + if len(errs) == 0 { + respondToUpdate(s, reply, _EMPTY_, fmt.Sprintf("deleted %d accounts", passCnt), nil) + } else { + respondToUpdate(s, reply, _EMPTY_, fmt.Sprintf("deleted %d accounts, failed for %d", passCnt, len(errs)), + errors.New(strings.Join(errs, "\n"))) + } +} + +func getOperatorKeys(s *Server) (string, map[string]struct{}, bool, error) { + var op string + var strict bool + keys := make(map[string]struct{}) + if opts := s.getOpts(); opts != nil && len(opts.TrustedOperators) > 0 { + op = opts.TrustedOperators[0].Subject + strict = opts.TrustedOperators[0].StrictSigningKeyUsage + if !strict { + keys[opts.TrustedOperators[0].Subject] = struct{}{} + } + for _, key := range opts.TrustedOperators[0].SigningKeys { + keys[key] = struct{}{} + } + } + if len(keys) == 0 { + return _EMPTY_, nil, false, fmt.Errorf("no operator key found") + } + return op, keys, strict, nil +} + +func claimValidate(claim *jwt.AccountClaims) error { + vr := &jwt.ValidationResults{} + claim.Validate(vr) + if vr.IsBlocking(false) { + return fmt.Errorf("validation errors: %v", vr.Errors()) + } + return nil +} + +func removeCb(s *Server, pubKey string) { + v, ok := s.accounts.Load(pubKey) + if !ok { + return + } + a := v.(*Account) + s.Debugf("Disable account %s due to remove", pubKey) + a.mu.Lock() + // lock out new clients + a.msubs = 0 + a.mpay = 0 + a.mconns = 0 + a.mleafs = 0 + a.updated = time.Now() + jsa := a.js + a.mu.Unlock() + // set the account to be expired and disconnect clients + a.expiredTimeout() + // For JS, we need also to disable it. + if js := s.getJetStream(); js != nil && jsa != nil { + js.disableJetStream(jsa) + // Remove JetStream state in memory, this will be reset + // on the changed callback from the account in case it is + // enabled again. + a.mu.Lock() + a.js = nil + a.mu.Unlock() + } + // We also need to remove all ServerImport subscriptions + a.removeAllServiceImportSubs() + a.mu.Lock() + a.clearExpirationTimer() + a.mu.Unlock() +} + +func (dr *DirAccResolver) Start(s *Server) error { + op, opKeys, strict, err := getOperatorKeys(s) + if err != nil { + return err + } + dr.Lock() + defer dr.Unlock() + dr.Server = s + dr.operator = opKeys + dr.DirJWTStore.changed = func(pubKey string) { + if v, ok := s.accounts.Load(pubKey); ok { + if theJwt, err := dr.LoadAcc(pubKey); err != nil { + s.Errorf("DirResolver - Update got error on load: %v", err) + } else { + acc := v.(*Account) + if err = s.updateAccountWithClaimJWT(acc, theJwt); err != nil { + s.Errorf("DirResolver - Update for account %q resulted in error %v", pubKey, err) + } else { + if _, jsa, err := acc.checkForJetStream(); err != nil { + if !IsNatsErr(err, JSNotEnabledForAccountErr) { + s.Warnf("DirResolver - Error checking for JetStream support for account %q: %v", pubKey, err) + } + } else if jsa == nil { + if err = s.configJetStream(acc, nil); err != nil { + s.Errorf("DirResolver - Error configuring JetStream for account %q: %v", pubKey, err) + } + } + } + } + } + } + dr.DirJWTStore.deleted = func(pubKey string) { + removeCb(s, pubKey) + } + packRespIb := s.newRespInbox() + for _, reqSub := range []string{accUpdateEventSubjOld, accUpdateEventSubjNew} { + // subscribe to account jwt update requests + if _, err := s.sysSubscribe(fmt.Sprintf(reqSub, "*"), func(_ *subscription, _ *client, _ *Account, subj, resp string, msg []byte) { + var pubKey string + tk := strings.Split(subj, tsep) + if len(tk) == accUpdateTokensNew { + pubKey = tk[accReqAccIndex] + } else if len(tk) == accUpdateTokensOld { + pubKey = tk[accUpdateAccIdxOld] + } else { + s.Debugf("DirResolver - jwt update skipped due to bad subject %q", subj) + return + } + if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil { + respondToUpdate(s, resp, "n/a", "jwt update resulted in error", err) + } else if err := claimValidate(claim); err != nil { + respondToUpdate(s, resp, claim.Subject, "jwt validation failed", err) + } else if claim.Subject != pubKey { + err := errors.New("subject does not match jwt content") + respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err) + } else if claim.Issuer == op && strict { + err := errors.New("operator requires issuer to be a signing key") + respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err) + } else if err := dr.save(pubKey, string(msg)); err != nil { + respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err) + } else { + respondToUpdate(s, resp, pubKey, "jwt updated", nil) + } + }); err != nil { + return fmt.Errorf("error setting up update handling: %v", err) + } + } + if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, c *client, _ *Account, _, resp string, msg []byte) { + // As this is a raw message, we need to extract payload and only decode claims from it, + // in case request is sent with headers. + _, msg = c.msgParts(msg) + if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil { + respondToUpdate(s, resp, "n/a", "jwt update resulted in error", err) + } else if claim.Issuer == op && strict { + err := errors.New("operator requires issuer to be a signing key") + respondToUpdate(s, resp, claim.Subject, "jwt update resulted in error", err) + } else if err := claimValidate(claim); err != nil { + respondToUpdate(s, resp, claim.Subject, "jwt validation failed", err) + } else if err := dr.save(claim.Subject, string(msg)); err != nil { + respondToUpdate(s, resp, claim.Subject, "jwt update resulted in error", err) + } else { + respondToUpdate(s, resp, claim.Subject, "jwt updated", nil) + } + }); err != nil { + return fmt.Errorf("error setting up update handling: %v", err) + } + // respond to lookups with our version + if _, err := s.sysSubscribe(fmt.Sprintf(accLookupReqSubj, "*"), func(_ *subscription, _ *client, _ *Account, subj, reply string, msg []byte) { + if reply == _EMPTY_ { + return + } + tk := strings.Split(subj, tsep) + if len(tk) != accLookupReqTokens { + return + } + accName := tk[accReqAccIndex] + if theJWT, err := dr.DirJWTStore.LoadAcc(accName); err != nil { + if errors.Is(err, fs.ErrNotExist) { + s.Debugf("DirResolver - Could not find account %q", accName) + // Reply with empty response to signal absence of JWT to others. + s.sendInternalMsgLocked(reply, _EMPTY_, nil, nil) + } else { + s.Errorf("DirResolver - Error looking up account %q: %v", accName, err) + } + } else { + s.sendInternalMsgLocked(reply, _EMPTY_, nil, []byte(theJWT)) + } + }); err != nil { + return fmt.Errorf("error setting up lookup request handling: %v", err) + } + // respond to pack requests with one or more pack messages + // an empty message signifies the end of the response responder. + if _, err := s.sysSubscribeQ(accPackReqSubj, "responder", func(_ *subscription, _ *client, _ *Account, _, reply string, theirHash []byte) { + if reply == _EMPTY_ { + return + } + ourHash := dr.DirJWTStore.Hash() + if bytes.Equal(theirHash, ourHash[:]) { + s.sendInternalMsgLocked(reply, _EMPTY_, nil, []byte{}) + s.Debugf("DirResolver - Pack request matches hash %x", ourHash[:]) + } else if err := dr.DirJWTStore.PackWalk(1, func(partialPackMsg string) { + s.sendInternalMsgLocked(reply, _EMPTY_, nil, []byte(partialPackMsg)) + }); err != nil { + // let them timeout + s.Errorf("DirResolver - Pack request error: %v", err) + } else { + s.Debugf("DirResolver - Pack request hash %x - finished responding with hash %x", theirHash, ourHash) + s.sendInternalMsgLocked(reply, _EMPTY_, nil, []byte{}) + } + }); err != nil { + return fmt.Errorf("error setting up pack request handling: %v", err) + } + // respond to list requests with one message containing all account ids + if _, err := s.sysSubscribe(accListReqSubj, func(_ *subscription, _ *client, _ *Account, _, reply string, _ []byte) { + handleListRequest(dr.DirJWTStore, s, reply) + }); err != nil { + return fmt.Errorf("error setting up list request handling: %v", err) + } + if _, err := s.sysSubscribe(accDeleteReqSubj, func(_ *subscription, c *client, _ *Account, _, reply string, msg []byte) { + // As this is a raw message, we need to extract payload and only decode claims from it, + // in case request is sent with headers. + _, msg = c.msgParts(msg) + handleDeleteRequest(dr.DirJWTStore, s, msg, reply) + }); err != nil { + return fmt.Errorf("error setting up delete request handling: %v", err) + } + // embed pack responses into store + if _, err := s.sysSubscribe(packRespIb, func(_ *subscription, _ *client, _ *Account, _, _ string, msg []byte) { + hash := dr.DirJWTStore.Hash() + if len(msg) == 0 { // end of response stream + s.Debugf("DirResolver - Merging finished and resulting in: %x", dr.DirJWTStore.Hash()) + return + } else if err := dr.DirJWTStore.Merge(string(msg)); err != nil { + s.Errorf("DirResolver - Merging resulted in error: %v", err) + } else { + s.Debugf("DirResolver - Merging succeeded and changed %x to %x", hash, dr.DirJWTStore.Hash()) + } + }); err != nil { + return fmt.Errorf("error setting up pack response handling: %v", err) + } + // periodically send out pack message + quit := s.quitCh + s.startGoRoutine(func() { + defer s.grWG.Done() + ticker := time.NewTicker(dr.syncInterval) + for { + select { + case <-quit: + ticker.Stop() + return + case <-ticker.C: + } + ourHash := dr.DirJWTStore.Hash() + s.Debugf("DirResolver - Checking store state: %x", ourHash) + s.sendInternalMsgLocked(accPackReqSubj, packRespIb, nil, ourHash[:]) + } + }) + s.Noticef("Managing all jwt in exclusive directory %s", dr.directory) + return nil +} + +func (dr *DirAccResolver) Fetch(name string) (string, error) { + if theJWT, err := dr.LoadAcc(name); theJWT != _EMPTY_ { + return theJWT, nil + } else { + dr.Lock() + srv := dr.Server + to := dr.fetchTimeout + dr.Unlock() + if srv == nil { + return _EMPTY_, err + } + return srv.fetch(dr, name, to) // lookup from other server + } +} + +func (dr *DirAccResolver) Store(name, jwt string) error { + return dr.saveIfNewer(name, jwt) +} + +type DirResOption func(s *DirAccResolver) error + +// limits the amount of time spent waiting for an account fetch to complete +func FetchTimeout(to time.Duration) DirResOption { + return func(r *DirAccResolver) error { + if to <= time.Duration(0) { + return fmt.Errorf("Fetch timeout %v is too smal", to) + } + r.fetchTimeout = to + return nil + } +} + +func (dr *DirAccResolver) apply(opts ...DirResOption) error { + for _, o := range opts { + if err := o(dr); err != nil { + return err + } + } + return nil +} + +func NewDirAccResolver(path string, limit int64, syncInterval time.Duration, delete deleteType, opts ...DirResOption) (*DirAccResolver, error) { + if limit == 0 { + limit = math.MaxInt64 + } + if syncInterval <= 0 { + syncInterval = time.Minute + } + store, err := NewExpiringDirJWTStore(path, false, true, delete, 0, limit, false, 0, nil) + if err != nil { + return nil, err + } + + res := &DirAccResolver{store, nil, syncInterval, DEFAULT_ACCOUNT_FETCH_TIMEOUT} + if err := res.apply(opts...); err != nil { + return nil, err + } + return res, nil +} + +// Caching resolver using nats for lookups and making use of a directory for storage +type CacheDirAccResolver struct { + DirAccResolver + ttl time.Duration +} + +func (s *Server) fetch(res AccountResolver, name string, timeout time.Duration) (string, error) { + if s == nil { + return _EMPTY_, ErrNoAccountResolver + } + respC := make(chan []byte, 1) + accountLookupRequest := fmt.Sprintf(accLookupReqSubj, name) + s.mu.Lock() + if s.sys == nil || s.sys.replies == nil { + s.mu.Unlock() + return _EMPTY_, fmt.Errorf("eventing shut down") + } + // Resolver will wait for detected active servers to reply + // before serving an error in case there weren't any found. + expectedServers := len(s.sys.servers) + replySubj := s.newRespInbox() + replies := s.sys.replies + + // Store our handler. + replies[replySubj] = func(sub *subscription, _ *client, _ *Account, subject, _ string, msg []byte) { + var clone []byte + isEmpty := len(msg) == 0 + if !isEmpty { + clone = make([]byte, len(msg)) + copy(clone, msg) + } + s.mu.Lock() + defer s.mu.Unlock() + expectedServers-- + // Skip empty responses until getting all the available servers. + if isEmpty && expectedServers > 0 { + return + } + // Use the first valid response if there is still interest or + // one of the empty responses to signal that it was not found. + if _, ok := replies[replySubj]; ok { + select { + case respC <- clone: + default: + } + } + } + s.sendInternalMsg(accountLookupRequest, replySubj, nil, []byte{}) + quit := s.quitCh + s.mu.Unlock() + var err error + var theJWT string + select { + case <-quit: + err = errors.New("fetching jwt failed due to shutdown") + case <-time.After(timeout): + err = errors.New("fetching jwt timed out") + case m := <-respC: + if len(m) == 0 { + err = errors.New("account jwt not found") + } else if err = res.Store(name, string(m)); err == nil { + theJWT = string(m) + } + } + s.mu.Lock() + delete(replies, replySubj) + s.mu.Unlock() + close(respC) + return theJWT, err +} + +func NewCacheDirAccResolver(path string, limit int64, ttl time.Duration, opts ...DirResOption) (*CacheDirAccResolver, error) { + if limit <= 0 { + limit = 1_000 + } + store, err := NewExpiringDirJWTStore(path, false, true, HardDelete, 0, limit, true, ttl, nil) + if err != nil { + return nil, err + } + res := &CacheDirAccResolver{DirAccResolver{store, nil, 0, DEFAULT_ACCOUNT_FETCH_TIMEOUT}, ttl} + if err := res.apply(opts...); err != nil { + return nil, err + } + return res, nil +} + +func (dr *CacheDirAccResolver) Start(s *Server) error { + op, opKeys, strict, err := getOperatorKeys(s) + if err != nil { + return err + } + dr.Lock() + defer dr.Unlock() + dr.Server = s + dr.operator = opKeys + dr.DirJWTStore.changed = func(pubKey string) { + if v, ok := s.accounts.Load(pubKey); !ok { + } else if theJwt, err := dr.LoadAcc(pubKey); err != nil { + s.Errorf("DirResolver - Update got error on load: %v", err) + } else if err := s.updateAccountWithClaimJWT(v.(*Account), theJwt); err != nil { + s.Errorf("DirResolver - Update resulted in error %v", err) + } + } + dr.DirJWTStore.deleted = func(pubKey string) { + removeCb(s, pubKey) + } + for _, reqSub := range []string{accUpdateEventSubjOld, accUpdateEventSubjNew} { + // subscribe to account jwt update requests + if _, err := s.sysSubscribe(fmt.Sprintf(reqSub, "*"), func(_ *subscription, _ *client, _ *Account, subj, resp string, msg []byte) { + var pubKey string + tk := strings.Split(subj, tsep) + if len(tk) == accUpdateTokensNew { + pubKey = tk[accReqAccIndex] + } else if len(tk) == accUpdateTokensOld { + pubKey = tk[accUpdateAccIdxOld] + } else { + s.Debugf("DirResolver - jwt update cache skipped due to bad subject %q", subj) + return + } + if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil { + respondToUpdate(s, resp, pubKey, "jwt update cache resulted in error", err) + } else if claim.Subject != pubKey { + err := errors.New("subject does not match jwt content") + respondToUpdate(s, resp, pubKey, "jwt update cache resulted in error", err) + } else if claim.Issuer == op && strict { + err := errors.New("operator requires issuer to be a signing key") + respondToUpdate(s, resp, pubKey, "jwt update cache resulted in error", err) + } else if _, ok := s.accounts.Load(pubKey); !ok { + respondToUpdate(s, resp, pubKey, "jwt update cache skipped", nil) + } else if err := claimValidate(claim); err != nil { + respondToUpdate(s, resp, claim.Subject, "jwt update cache validation failed", err) + } else if err := dr.save(pubKey, string(msg)); err != nil { + respondToUpdate(s, resp, pubKey, "jwt update cache resulted in error", err) + } else { + respondToUpdate(s, resp, pubKey, "jwt updated cache", nil) + } + }); err != nil { + return fmt.Errorf("error setting up update handling: %v", err) + } + } + if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, c *client, _ *Account, _, resp string, msg []byte) { + // As this is a raw message, we need to extract payload and only decode claims from it, + // in case request is sent with headers. + _, msg = c.msgParts(msg) + if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil { + respondToUpdate(s, resp, "n/a", "jwt update cache resulted in error", err) + } else if claim.Issuer == op && strict { + err := errors.New("operator requires issuer to be a signing key") + respondToUpdate(s, resp, claim.Subject, "jwt update cache resulted in error", err) + } else if _, ok := s.accounts.Load(claim.Subject); !ok { + respondToUpdate(s, resp, claim.Subject, "jwt update cache skipped", nil) + } else if err := claimValidate(claim); err != nil { + respondToUpdate(s, resp, claim.Subject, "jwt update cache validation failed", err) + } else if err := dr.save(claim.Subject, string(msg)); err != nil { + respondToUpdate(s, resp, claim.Subject, "jwt update cache resulted in error", err) + } else { + respondToUpdate(s, resp, claim.Subject, "jwt updated cache", nil) + } + }); err != nil { + return fmt.Errorf("error setting up update handling: %v", err) + } + // respond to list requests with one message containing all account ids + if _, err := s.sysSubscribe(accListReqSubj, func(_ *subscription, _ *client, _ *Account, _, reply string, _ []byte) { + handleListRequest(dr.DirJWTStore, s, reply) + }); err != nil { + return fmt.Errorf("error setting up list request handling: %v", err) + } + if _, err := s.sysSubscribe(accDeleteReqSubj, func(_ *subscription, c *client, _ *Account, _, reply string, msg []byte) { + // As this is a raw message, we need to extract payload and only decode claims from it, + // in case request is sent with headers. + _, msg = c.msgParts(msg) + handleDeleteRequest(dr.DirJWTStore, s, msg, reply) + }); err != nil { + return fmt.Errorf("error setting up list request handling: %v", err) + } + s.Noticef("Managing some jwt in exclusive directory %s", dr.directory) + return nil +} + +func (dr *CacheDirAccResolver) Reload() error { + return dr.DirAccResolver.Reload() +} diff --git a/server/client.go b/server/client.go new file mode 100644 index 0000000..f6e6280 --- /dev/null +++ b/server/client.go @@ -0,0 +1,7065 @@ +// Copyright 2012-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 ( + "bytes" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "math/rand/v2" + "net" + "net/http" + "net/url" + "regexp" + "runtime" + "slices" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/klauspost/compress/s2" + "github.com/nats-io/jwt/v2" +) + +// Type of client connection. +const ( + // CLIENT is an end user. + CLIENT = iota + // ROUTER represents another server in the cluster. + ROUTER + // GATEWAY is a link between 2 clusters. + GATEWAY + // SYSTEM is an internal system client. + SYSTEM + // LEAF is for leaf node connections. + LEAF + // JETSTREAM is an internal jetstream client. + JETSTREAM + // ACCOUNT is for the internal client for accounts. + ACCOUNT +) + +// Internal clients. kind should be SYSTEM, JETSTREAM or ACCOUNT +func isInternalClient(kind int) bool { + return kind == SYSTEM || kind == JETSTREAM || kind == ACCOUNT +} + +// Extended type of a CLIENT connection. This is returned by c.clientType() +// and indicate what type of client connection we are dealing with. +// If invoked on a non CLIENT connection, NON_CLIENT type is returned. +const ( + // If the connection is not a CLIENT connection. + NON_CLIENT = iota + // Regular NATS client. + NATS + // MQTT client. + MQTT + // Websocket client. + WS +) + +const ( + // ClientProtoZero is the original Client protocol from 2009. + // http://nats.io/documentation/internals/nats-protocol/ + ClientProtoZero = iota + // ClientProtoInfo signals a client can receive more then the original INFO block. + // This can be used to update clients on other cluster members, etc. + ClientProtoInfo +) + +const ( + pingProto = "PING" + _CRLF_ + pongProto = "PONG" + _CRLF_ + errProto = "-ERR '%s'" + _CRLF_ + okProto = "+OK" + _CRLF_ +) + +// TLS Hanshake client types +const ( + tlsHandshakeLeaf = "leafnode" + tlsHandshakeMQTT = "mqtt" +) + +const ( + // Scratch buffer size for the processMsg() calls. + msgScratchSize = 1024 + msgHeadProto = "RMSG " + msgHeadProtoLen = len(msgHeadProto) + + // For controlling dynamic buffer sizes. + startBufSize = 512 // For INFO/CONNECT block + minBufSize = 64 // Smallest to shrink to for PING/PONG + maxBufSize = 65536 // 64k + shortsToShrink = 2 // Trigger to shrink dynamic buffers + maxFlushPending = 10 // Max fsps to have in order to wait for writeLoop + readLoopReport = 2 * time.Second + + // Server should not send a PING (for RTT) before the first PONG has + // been sent to the client. However, in case some client libs don't + // send CONNECT+PING, cap the maximum time before server can send + // the RTT PING. + maxNoRTTPingBeforeFirstPong = 2 * time.Second + + // For stalling fast producers + stallClientMinDuration = 2 * time.Millisecond + stallClientMaxDuration = 5 * time.Millisecond + stallTotalAllowed = 10 * time.Millisecond +) + +var readLoopReportThreshold = readLoopReport + +// Represent client booleans with a bitmask +type clientFlag uint16 + +const ( + hdrLine = "NATS/1.0\r\n" + emptyHdrLine = "NATS/1.0\r\n\r\n" +) + +// Some client state represented as flags +const ( + connectReceived clientFlag = 1 << iota // The CONNECT proto has been received + infoReceived // The INFO protocol has been received + firstPongSent // The first PONG has been sent + handshakeComplete // For TLS clients, indicate that the handshake is complete + flushOutbound // Marks client as having a flushOutbound call in progress. + noReconnect // Indicate that on close, this connection should not attempt a reconnect + closeConnection // Marks that closeConnection has already been called. + connMarkedClosed // Marks that markConnAsClosed has already been called. + writeLoopStarted // Marks that the writeLoop has been started. + skipFlushOnClose // Marks that flushOutbound() should not be called on connection close. + expectConnect // Marks if this connection is expected to send a CONNECT + connectProcessFinished // Marks if this connection has finished the connect process. + compressionNegotiated // Marks if this connection has negotiated compression level with remote. + didTLSFirst // Marks if this connection requested and was accepted doing the TLS handshake first (prior to INFO). + isSlowConsumer // Marks connection as a slow consumer. +) + +// set the flag (would be equivalent to set the boolean to true) +func (cf *clientFlag) set(c clientFlag) { + *cf |= c +} + +// clear the flag (would be equivalent to set the boolean to false) +func (cf *clientFlag) clear(c clientFlag) { + *cf &= ^c +} + +// isSet returns true if the flag is set, false otherwise +func (cf clientFlag) isSet(c clientFlag) bool { + return cf&c != 0 +} + +// setIfNotSet will set the flag `c` only if that flag was not already +// set and return true to indicate that the flag has been set. Returns +// false otherwise. +func (cf *clientFlag) setIfNotSet(c clientFlag) bool { + if *cf&c == 0 { + *cf |= c + return true + } + return false +} + +// ClosedState is the reason client was closed. This will +// be passed into calls to clearConnection, but will only +// be stored in ConnInfo for monitoring. +type ClosedState int + +const ( + ClientClosed = ClosedState(iota + 1) + AuthenticationTimeout + AuthenticationViolation + TLSHandshakeError + SlowConsumerPendingBytes + SlowConsumerWriteDeadline + WriteError + ReadError + ParseError + StaleConnection + ProtocolViolation + BadClientProtocolVersion + WrongPort + MaxAccountConnectionsExceeded + MaxConnectionsExceeded + MaxPayloadExceeded + MaxControlLineExceeded + MaxSubscriptionsExceeded + DuplicateRoute + RouteRemoved + ServerShutdown + AuthenticationExpired + WrongGateway + MissingAccount + Revocation + InternalClient + MsgHeaderViolation + NoRespondersRequiresHeaders + ClusterNameConflict + DuplicateRemoteLeafnodeConnection + DuplicateClientID + DuplicateServerName + MinimumVersionRequired + ClusterNamesIdentical + Kicked + ProxyNotTrusted + ProxyRequired +) + +// Some flags passed to processMsgResults +const pmrNoFlag int = 0 + +const ( + pmrCollectQueueNames int = 1 << iota + pmrIgnoreEmptyQueueFilter + pmrAllowSendFromRouteToRoute + pmrMsgImportedFromService +) + +type WriteTimeoutPolicy uint8 + +const ( + WriteTimeoutPolicyDefault = iota + WriteTimeoutPolicyClose + WriteTimeoutPolicyRetry +) + +// String returns a human-friendly value. Only used in varz. +func (p WriteTimeoutPolicy) String() string { + switch p { + case WriteTimeoutPolicyClose: + return "close" + case WriteTimeoutPolicyRetry: + return "retry" + default: + return _EMPTY_ + } +} + +type client struct { + // Here first because of use of atomics, and memory alignment. + stats + gwReplyMapping + kind int + srv *Server + acc *Account + perms *permissions + in readCache + parseState + opts ClientOpts + rrTracking *rrTracking + mpay int32 + msubs int32 + mcl int32 + mu sync.RWMutex + cid uint64 + start time.Time + nonce []byte + pubKey string + nc net.Conn + ncs atomic.Value + ncsAcc atomic.Value + ncsUser atomic.Value + out outbound + user *NkeyUser + host string + port uint16 + subs map[string]*subscription + replies map[string]*resp + mperms *msgDeny + darray []*subscription // Parsed subscribe deny entries used to build the delivery-time filter. + pcd map[*client]struct{} + atmr *time.Timer + expires time.Time + ping pinfo + msgb [msgScratchSize]byte + last time.Time + lastIn time.Time + proxyKey string + + repliesSincePrune uint16 + lastReplyPrune time.Time + + headers bool + + rtt time.Duration + rttStart time.Time + + route *route + gw *gateway + leaf *leaf + ws *websocket + mqtt *mqtt + + flags clientFlag // Compact booleans into a single field. Size will be increased when needed. + + rref byte + + trace bool + echo bool + noIcb bool + iproc bool // In-Process connection, set at creation and immutable. + + tags jwt.TagList + nameTag string + + tlsTo *time.Timer + + // Authentication error override. This is used because the authentication + // stack is simply returning a boolean, and the only authentication error + // reported is the generic `ErrAuthentication`. In the authentication code, + // if we want to report a different error, we can now set this field + // and `authViolation()` will use that one. + authErr error +} + +type rrTracking struct { + rmap map[string]*remoteLatency + ptmr *time.Timer + lrt time.Duration +} + +// Struct for PING initiation from the server. +type pinfo struct { + tmr *time.Timer + out int +} + +// outbound holds pending data for a socket. +type outbound struct { + nb net.Buffers // Pending buffers for send, each has fixed capacity as per nbPool below. + wnb net.Buffers // Working copy of "nb", reused on each flushOutbound call, partial writes may leave entries here for next iteration. + pb int64 // Total pending/queued bytes. + fsp int32 // Flush signals that are pending per producer from readLoop's pcd. + wtp WriteTimeoutPolicy // What do we do on a write timeout? + sg *sync.Cond // To signal writeLoop that there is data to flush. + wdl time.Duration // Snapshot of write deadline. + mp int64 // Snapshot of max pending for client. + lft time.Duration // Last flush time for Write. + stc chan struct{} // Stall chan we create to slow down producers on overrun, e.g. fan-in. + cw *s2.Writer +} + +const nbMaxVectorSize = 1024 // == IOV_MAX on Linux/Darwin and most other Unices (except Solaris/AIX) + +const ( + nbPoolSizeSmall = 512 // Underlying array size of small buffer + nbPoolSizeMedium = 4096 // Underlying array size of medium buffer + nbPoolSizeLarge = 65536 // Underlying array size of large buffer +) + +var nbPoolSmall = &sync.Pool{ + New: func() any { + b := [nbPoolSizeSmall]byte{} + return &b + }, +} + +var nbPoolMedium = &sync.Pool{ + New: func() any { + b := [nbPoolSizeMedium]byte{} + return &b + }, +} + +var nbPoolLarge = &sync.Pool{ + New: func() any { + b := [nbPoolSizeLarge]byte{} + return &b + }, +} + +// nbPoolGet returns a frame that is a best-effort match for the given size. +// Once a pooled frame is no longer needed, it should be recycled by passing +// it to nbPoolPut. +func nbPoolGet(sz int) []byte { + switch { + case sz <= nbPoolSizeSmall: + return nbPoolSmall.Get().(*[nbPoolSizeSmall]byte)[:0] + case sz <= nbPoolSizeMedium: + return nbPoolMedium.Get().(*[nbPoolSizeMedium]byte)[:0] + default: + return nbPoolLarge.Get().(*[nbPoolSizeLarge]byte)[:0] + } +} + +// nbPoolPut recycles a frame that was retrieved from nbPoolGet. It is not +// safe to return multiple slices referring to chunks of the same underlying +// array as this may create overlaps when the buffers are returned to their +// original size, resulting in race conditions. +func nbPoolPut(b []byte) { + switch cap(b) { + case nbPoolSizeSmall: + b := (*[nbPoolSizeSmall]byte)(b[0:nbPoolSizeSmall]) + nbPoolSmall.Put(b) + case nbPoolSizeMedium: + b := (*[nbPoolSizeMedium]byte)(b[0:nbPoolSizeMedium]) + nbPoolMedium.Put(b) + case nbPoolSizeLarge: + b := (*[nbPoolSizeLarge]byte)(b[0:nbPoolSizeLarge]) + nbPoolLarge.Put(b) + default: + // Ignore frames that are the wrong size, this might happen + // with WebSocket/MQTT messages as they are framed + } +} + +type perm struct { + allow *Sublist + deny *Sublist +} + +type permissions struct { + // Have these 2 first for memory alignment due to the use of atomic. + pcsz int32 + prun int32 + sub perm + pub perm + resp *ResponsePermission + pcache sync.Map +} + +// This is used to dynamically track responses and reply subjects +// for dynamic permissioning. +type resp struct { + t time.Time + n int +} + +// msgDeny is used when a user permission for subscriptions has a deny +// clause but a subscription could be made that is of broader scope. +// e.g. deny = "foo", but user subscribes to "*". That subscription should +// succeed but no message sent on foo should be delivered. +type msgDeny struct { + deny *Sublist + dcache map[msgDenyKey]bool +} + +type msgDenyKey struct { + subject string + queue string +} + +// routeTarget collects information regarding routes and queue groups for +// sending information to a remote. +type routeTarget struct { + sub *subscription + qs []byte + qsubs []*subscription + _qs [32]byte + _qsubs [4]*subscription +} + +const ( + maxResultCacheSize = 512 + maxDenyPermCacheSize = 256 + maxPermCacheSize = 128 + pruneSize = 32 + routeTargetInit = 8 + replyPermLimit = 4096 + replyPruneTime = time.Second +) + +// Represent read cache booleans with a bitmask +type readCacheFlag uint16 + +const ( + hasMappings readCacheFlag = 1 << iota // For account subject mappings. + switchToCompression readCacheFlag = 1 << 1 +) + +const sysGroup = "_sys_" + +// Used in readloop to cache hot subject lookups and group statistics. +type readCache struct { + // These are for clients who are bound to a single account. + genid uint64 + results map[string]*SublistResult + + // This is for routes and gateways to have their own L1 as well that is account aware. + pacache map[string]*perAccountCache + + // This is for when we deliver messages across a route. We use this structure + // to make sure to only send one message and properly scope to queues as needed. + rts []routeTarget + + // These are all temporary totals for an invocation of a read in readloop. + msgs int32 + bytes int32 + subs int32 + + rsz int32 // Read buffer size + srs int32 // Short reads, used for dynamic buffer resizing. + + // These are for readcache flags to avoid locks. + flags readCacheFlag + + // Capture the time we started processing our readLoop. + start time.Time + + // Total time stalled so far for readLoop processing. + tst time.Duration +} + +// set the flag (would be equivalent to set the boolean to true) +func (rcf *readCacheFlag) set(c readCacheFlag) { + *rcf |= c +} + +// clear the flag (would be equivalent to set the boolean to false) +func (rcf *readCacheFlag) clear(c readCacheFlag) { + *rcf &= ^c +} + +// isSet returns true if the flag is set, false otherwise +func (rcf readCacheFlag) isSet(c readCacheFlag) bool { + return rcf&c != 0 +} + +const ( + defaultMaxPerAccountCacheSize = 8192 + defaultClosedSubsCheckInterval = 5 * time.Minute +) + +var ( + maxPerAccountCacheSize = defaultMaxPerAccountCacheSize + closedSubsCheckInterval = defaultClosedSubsCheckInterval +) + +// perAccountCache is for L1 semantics for inbound messages from a route or gateway to mimic the performance of clients. +type perAccountCache struct { + acc *Account + results *SublistResult + genid uint64 +} + +func (c *client) String() (id string) { + loaded := c.ncs.Load() + if loaded != nil { + return loaded.(string) + } + + return _EMPTY_ +} + +// GetNonce returns the nonce that was presented to the user on connection +func (c *client) GetNonce() []byte { + c.mu.Lock() + defer c.mu.Unlock() + + return c.nonce +} + +// GetID returns the client ID +func (c *client) GetID() uint64 { + c.mu.Lock() + defer c.mu.Unlock() + return c.cid +} + +// GetName returns the application supplied name for the connection. +func (c *client) GetName() string { + c.mu.Lock() + name := c.opts.Name + c.mu.Unlock() + return name +} + +// GetOpts returns the client options provided by the application. +func (c *client) GetOpts() *ClientOpts { + return &c.opts +} + +// GetTLSConnectionState returns the TLS ConnectionState if TLS is enabled, nil +// otherwise. Implements the ClientAuth interface. +func (c *client) GetTLSConnectionState() *tls.ConnectionState { + c.mu.Lock() + defer c.mu.Unlock() + if c.nc == nil { + return nil + } + tc, ok := c.nc.(*tls.Conn) + if !ok { + return nil + } + state := tc.ConnectionState() + return &state +} + +// For CLIENT connections, this function returns the client type, that is, +// NATS (for regular clients), MQTT or WS for websocket. +// If this is invoked for a non CLIENT connection, NON_CLIENT is returned. +// +// This function does not lock the client and accesses fields that are supposed +// to be immutable and therefore it can be invoked outside of the client's lock. +func (c *client) clientType() int { + switch c.kind { + case CLIENT: + if c.isMqtt() { + return MQTT + } else if c.isWebsocket() { + return WS + } + return NATS + default: + return NON_CLIENT + } +} + +var clientTypeStringMap = map[int]string{ + NON_CLIENT: _EMPTY_, + NATS: "nats", + WS: "websocket", + MQTT: "mqtt", +} + +func (c *client) clientTypeString() string { + if typeStringVal, ok := clientTypeStringMap[c.clientType()]; ok { + return typeStringVal + } + return _EMPTY_ +} + +// This is the main subscription struct that indicates +// interest in published messages. +// FIXME(dlc) - This is getting bloated for normal subs, need +// to optionally have an opts section for non-normal stuff. +type subscription struct { + client *client + im *streamImport // This is for import stream support. + rsi bool + si bool + leaf bool // Subscription interest originated from a leaf node. + shadow []*subscription // This is to track shadowed accounts. + icb msgHandler + subject []byte + queue []byte + sid []byte + origin []byte + nm int64 + max int64 + qw int32 + closed int32 + mqtt *mqttSub +} + +// Indicate that this subscription is closed. +// This is used in pruning of route and gateway cache items. +func (s *subscription) close() { + atomic.StoreInt32(&s.closed, 1) +} + +// Return true if this subscription was unsubscribed +// or its connection has been closed. +func (s *subscription) isClosed() bool { + return atomic.LoadInt32(&s.closed) == 1 +} + +type ClientOpts struct { + Echo bool `json:"echo"` + Verbose bool `json:"verbose"` + Pedantic bool `json:"pedantic"` + TLSRequired bool `json:"tls_required"` + Nkey string `json:"nkey,omitempty"` + JWT string `json:"jwt,omitempty"` + Sig string `json:"sig,omitempty"` + Token string `json:"auth_token,omitempty"` + Username string `json:"user,omitempty"` + Password string `json:"pass,omitempty"` + Name string `json:"name"` + Lang string `json:"lang"` + Version string `json:"version"` + Protocol int `json:"protocol"` + Account string `json:"account,omitempty"` + AccountNew bool `json:"new_account,omitempty"` + Headers bool `json:"headers,omitempty"` + NoResponders bool `json:"no_responders,omitempty"` + + // Routes and Leafnodes only + Import *SubjectPermission `json:"import,omitempty"` + Export *SubjectPermission `json:"export,omitempty"` + + // Leafnodes + RemoteAccount string `json:"remote_account,omitempty"` + + // Proxy would include its own nonce signature. + ProxySig string `json:"proxy_sig,omitempty"` +} + +var ( + defaultOpts = ClientOpts{Verbose: true, Pedantic: true, Echo: true} + internalOpts = ClientOpts{Verbose: false, Pedantic: false, Echo: false} +) + +func (c *client) setTraceLevel() { + if c.kind == SYSTEM && !(atomic.LoadInt32(&c.srv.logging.traceSysAcc) != 0) { + c.trace = false + } else { + c.trace = (atomic.LoadInt32(&c.srv.logging.trace) != 0) + } +} + +// Lock should be held +func (c *client) initClient() { + s := c.srv + c.cid = atomic.AddUint64(&s.gcid, 1) + + // Outbound data structure setup + c.out.sg = sync.NewCond(&c.mu) + opts := s.getOpts() + // Snapshots to avoid mutex access in fast paths. + c.out.wdl = opts.WriteDeadline + switch { + case c.kind == ROUTER && opts.Cluster.WriteDeadline > 0: + c.out.wdl = opts.Cluster.WriteDeadline + case c.kind == GATEWAY && opts.Gateway.WriteDeadline > 0: + c.out.wdl = opts.Gateway.WriteDeadline + case c.kind == LEAF && opts.LeafNode.WriteDeadline > 0: + c.out.wdl = opts.LeafNode.WriteDeadline + } + switch c.kind { + case ROUTER: + if c.out.wtp = opts.Cluster.WriteTimeout; c.out.wtp == WriteTimeoutPolicyDefault { + c.out.wtp = WriteTimeoutPolicyRetry + } + case LEAF: + if c.out.wtp = opts.LeafNode.WriteTimeout; c.out.wtp == WriteTimeoutPolicyDefault { + c.out.wtp = WriteTimeoutPolicyRetry + } + case GATEWAY: + if c.out.wtp = opts.Gateway.WriteTimeout; c.out.wtp == WriteTimeoutPolicyDefault { + c.out.wtp = WriteTimeoutPolicyRetry + } + default: + if c.out.wtp = opts.WriteTimeout; c.out.wtp == WriteTimeoutPolicyDefault { + c.out.wtp = WriteTimeoutPolicyClose + } + } + c.out.mp = opts.MaxPending + // Snapshot max control line since currently can not be changed on reload and we + // were checking it on each call to parse. If this changes and we allow MaxControlLine + // to be reloaded without restart, this code will need to change. + c.mcl = int32(opts.MaxControlLine) + if c.mcl == 0 { + c.mcl = MAX_CONTROL_LINE_SIZE + } + + c.subs = make(map[string]*subscription) + c.echo = true + + c.setTraceLevel() + + // This is a scratch buffer used for processMsg() + // The msg header starts with "RMSG ", which can be used + // for both local and routes. + // in bytes that is [82 77 83 71 32]. + c.msgb = [msgScratchSize]byte{82, 77, 83, 71, 32} + + // This is to track pending clients that have data to be flushed + // after we process inbound msgs from our own connection. + c.pcd = make(map[*client]struct{}) + + // snapshot the string version of the connection + var conn string + if c.nc != nil { + if addr := c.nc.RemoteAddr(); addr != nil { + if conn = addr.String(); conn != _EMPTY_ { + host, port, _ := net.SplitHostPort(conn) + iPort, _ := strconv.ParseUint(port, 10, 16) + c.host, c.port = host, uint16(iPort) + if c.isWebsocket() && c.ws.clientIP != _EMPTY_ { + cip := c.ws.clientIP + // Surround IPv6 addresses with square brackets, as + // net.JoinHostPort would do... + if strings.Contains(cip, ":") { + cip = "[" + cip + "]" + } + conn = fmt.Sprintf("%s/%s", cip, conn) + } + // Now that we have extracted host and port, escape + // the string because it is going to be used in Sprintf + conn = strings.ReplaceAll(conn, "%", "%%") + } + } + } + + switch c.kind { + case CLIENT: + switch c.clientType() { + case NATS: + c.ncs.Store(fmt.Sprintf("%s - cid:%d", conn, c.cid)) + case WS: + c.ncs.Store(fmt.Sprintf("%s - wid:%d", conn, c.cid)) + case MQTT: + var ws string + if c.isWebsocket() { + ws = "_ws" + } + c.ncs.Store(fmt.Sprintf("%s - mid%s:%d", conn, ws, c.cid)) + } + case ROUTER: + c.ncs.Store(fmt.Sprintf("%s - rid:%d", conn, c.cid)) + case GATEWAY: + c.ncs.Store(fmt.Sprintf("%s - gid:%d", conn, c.cid)) + case LEAF: + var ws string + if c.isWebsocket() { + ws = "_ws" + } + c.ncs.Store(fmt.Sprintf("%s - lid%s:%d", conn, ws, c.cid)) + case SYSTEM: + c.ncs.Store("SYSTEM") + case JETSTREAM: + c.ncs.Store("JETSTREAM") + case ACCOUNT: + c.ncs.Store("ACCOUNT") + } +} + +// RemoteAddress expose the Address of the client connection, +// nil when not connected or unknown +func (c *client) RemoteAddress() net.Addr { + c.mu.Lock() + defer c.mu.Unlock() + + if c.nc == nil { + return nil + } + + return c.nc.RemoteAddr() +} + +// Helper function to report errors. +func (c *client) reportErrRegisterAccount(acc *Account, err error) { + if err == ErrTooManyAccountConnections { + c.maxAccountConnExceeded() + return + } + c.Errorf("Problem registering with account %q: %s", acc.Name, err) + c.sendErr("Failed Account Registration") +} + +// Kind returns the client kind and will be one of the defined constants like CLIENT, ROUTER, GATEWAY, LEAF +func (c *client) Kind() int { + c.mu.Lock() + kind := c.kind + c.mu.Unlock() + + return kind +} + +// registerWithAccount will register the given user with a specific +// account. This will change the subject namespace. +func (c *client) registerWithAccount(acc *Account) error { + if acc == nil { + return ErrBadAccount + } + acc.mu.RLock() + bad := acc.sl == nil + acc.mu.RUnlock() + if bad { + return ErrBadAccount + } + // If we were previously registered, usually to $G, do accounting here to remove. + c.mu.Lock() + prevAcc := c.acc + c.mu.Unlock() + if prevAcc != nil { + if prev := prevAcc.removeClient(c); prev == 1 && c.srv != nil { + c.srv.decActiveAccounts() + } + } + + c.mu.Lock() + // This check does not apply to SYSTEM or JETSTREAM or ACCOUNT clients (because they don't have a `nc`...) + if c.isClosed() && !isInternalClient(c.kind) { + c.mu.Unlock() + return ErrConnectionClosed + } + kind := c.kind + srv := c.srv + c.acc = acc + c.applyAccountLimits() + c.mu.Unlock() + + // Check if we have a max connections violation + if kind == CLIENT && acc.MaxTotalConnectionsReached() { + return ErrTooManyAccountConnections + } else if kind == LEAF { + // Check if we are already connected to this cluster. + if rc := c.remoteCluster(); rc != _EMPTY_ && acc.hasLeafNodeCluster(rc) { + return ErrLeafNodeLoop + } + if acc.MaxTotalLeafNodesReached() { + return ErrTooManyAccountConnections + } + } + + // Add in new one. + if prev := acc.addClient(c); prev == 0 && srv != nil { + srv.incActiveAccounts() + } + + return nil +} + +// Helper to determine if we have met or exceeded max subs. +func (c *client) subsAtLimit() bool { + return c.msubs != jwt.NoLimit && len(c.subs) >= int(c.msubs) +} + +func minLimit(value *int32, limit int32) bool { + v := atomic.LoadInt32(value) + if v != jwt.NoLimit { + if limit != jwt.NoLimit { + if limit < v { + atomic.StoreInt32(value, limit) + return true + } + } + } else if limit != jwt.NoLimit { + atomic.StoreInt32(value, limit) + return true + } + return false +} + +// Apply account limits +// Lock is held on entry. +// FIXME(dlc) - Should server be able to override here? +func (c *client) applyAccountLimits() { + if c.acc == nil || (c.kind != CLIENT && c.kind != LEAF) { + return + } + atomic.StoreInt32(&c.mpay, jwt.NoLimit) + c.msubs = jwt.NoLimit + if c.opts.JWT != _EMPTY_ { // user jwt implies account + if uc, _ := jwt.DecodeUserClaims(c.opts.JWT); uc != nil { + atomic.StoreInt32(&c.mpay, clampInt64ToInt32(uc.Limits.Payload)) + c.msubs = clampInt64ToInt32(uc.Limits.Subs) + if uc.IssuerAccount != _EMPTY_ && uc.IssuerAccount != uc.Issuer { + if scope, ok := c.acc.hasIssuer(uc.Issuer); ok { + if userScope, ok := scope.(*jwt.UserScope); ok { + // if signing key disappeared or changed and we don't get here, the client will be disconnected + atomic.StoreInt32(&c.mpay, clampInt64ToInt32(userScope.Template.Limits.Payload)) + c.msubs = clampInt64ToInt32(userScope.Template.Limits.Subs) + } + } + } + } + } + + c.acc.mu.RLock() + minLimit(&c.mpay, c.acc.mpay) + minLimit(&c.msubs, c.acc.msubs) + c.acc.mu.RUnlock() + + s := c.srv + opts := s.getOpts() + mPay := opts.MaxPayload + // options encode unlimited differently + if mPay == 0 { + mPay = jwt.NoLimit + } + mSubs := int32(opts.MaxSubs) + if mSubs == 0 { + mSubs = jwt.NoLimit + } + wasUnlimited := atomic.LoadInt32(&c.mpay) == jwt.NoLimit + if minLimit(&c.mpay, mPay) && !wasUnlimited { + c.Debugf("Max Payload set to %d from server overrides account or user config", opts.MaxPayload) + } + wasUnlimited = c.msubs == jwt.NoLimit + if minLimit(&c.msubs, mSubs) && !wasUnlimited { + c.Debugf("Max Subscriptions set to %d from server overrides account or user config", opts.MaxSubs) + } + if c.subsAtLimit() { + go func() { + c.maxSubsExceeded() + time.Sleep(20 * time.Millisecond) + c.closeConnection(MaxSubscriptionsExceeded) + }() + } +} + +// RegisterUser allows auth to call back into a new client +// with the authenticated user. This is used to map +// any permissions into the client and setup accounts. +func (c *client) RegisterUser(user *User) { + // Register with proper account and sublist. + if user.Account != nil { + if err := c.registerWithAccount(user.Account); err != nil { + c.reportErrRegisterAccount(user.Account, err) + return + } + } + + c.mu.Lock() + + // Assign permissions. + if user.Permissions == nil { + // Reset perms to nil in case client previously had them. + c.perms = nil + c.mperms = nil + c.darray = nil + } else { + c.setPermissions(user.Permissions) + } + + // allows custom authenticators to set a username to be reported in + // server events and more + if user.Username != _EMPTY_ { + c.opts.Username = user.Username + } + + // if a deadline time stamp is set we start a timer to disconnect the user at that time + if !user.ConnectionDeadline.IsZero() { + c.setExpirationTimerUnlocked(time.Until(user.ConnectionDeadline)) + } + + c.mu.Unlock() +} + +// RegisterNkeyUser allows auth to call back into a new nkey +// client with the authenticated user. This is used to map +// any permissions into the client and setup accounts. +func (c *client) RegisterNkeyUser(user *NkeyUser) error { + // Register with proper account and sublist. + if user.Account != nil { + if err := c.registerWithAccount(user.Account); err != nil { + c.reportErrRegisterAccount(user.Account, err) + return err + } + } + + c.mu.Lock() + c.user = user + // Assign permissions. + if user.Permissions == nil { + // Reset perms to nil in case client previously had them. + c.perms = nil + c.mperms = nil + c.darray = nil + } else { + c.setPermissions(user.Permissions) + } + c.mu.Unlock() + return nil +} + +func (c *client) updateDefaultPermissions(perms *Permissions) bool { + c.mu.Lock() + defer c.mu.Unlock() + if c.user == nil || !c.user.defaultPerms { + return false + } + if perms == nil { + c.user.Permissions = nil + c.perms = nil + c.mperms = nil + c.darray = nil + return true + } + c.user.Permissions = perms.clone() + c.setPermissions(c.user.Permissions) + return true +} + +func splitSubjectQueue(sq string) ([]byte, []byte, error) { + vals := strings.Fields(strings.TrimSpace(sq)) + if len(vals) == 0 { + return nil, nil, fmt.Errorf("invalid subject-queue %q", sq) + } + s := []byte(vals[0]) + var q []byte + if len(vals) == 2 { + q = []byte(vals[1]) + } else if len(vals) > 2 { + return nil, nil, fmt.Errorf("invalid subject-queue %q", sq) + } + if !IsValidSubject(vals[0]) || (len(q) > 0 && !IsValidSubject(vals[1])) { + return nil, nil, fmt.Errorf("invalid subject-queue %q", sq) + } + return s, q, nil +} + +// Initializes client.perms structure. +// Lock is held on entry. +func (c *client) setPermissions(perms *Permissions) { + if perms == nil { + return + } + c.perms = &permissions{} + c.mperms = nil + c.darray = nil + slcache := c.srv != nil && !c.srv.getOpts().NoSublistCache + + // Loop over publish permissions + if perms.Publish != nil { + if perms.Publish.Allow != nil { + c.perms.pub.allow = NewSublist(slcache) + } + for _, pubSubject := range perms.Publish.Allow { + if !IsValidSubject(pubSubject) { + c.Errorf("invalid publish subject %q", pubSubject) + continue + } + sub := &subscription{subject: []byte(pubSubject)} + if err := c.perms.pub.allow.Insert(sub); err != nil { + c.Errorf("invalid publish subject %q", pubSubject) + } + } + if len(perms.Publish.Deny) > 0 { + c.perms.pub.deny = NewSublist(slcache) + } + for _, pubSubject := range perms.Publish.Deny { + subject := []byte(pubSubject) + if !IsValidSubject(pubSubject) { + var err error + subject, _, err = splitSubjectQueue(pubSubject) + if err != nil { + c.Errorf("invalid publish deny subject %q", pubSubject) + continue + } + // Queue qualifiers have no meaning for publish permissions. If + // one reaches this defensive path, retain the deny's subject + // scope instead of silently creating an unreachable trie node. + c.Errorf("queue qualifier is not valid for publish deny subject %q", pubSubject) + } + if err := c.perms.pub.deny.Insert(&subscription{subject: subject}); err != nil { + c.Errorf("invalid publish deny subject %q", pubSubject) + } + } + } + + // Check if we are allowed to send responses. + if perms.Response != nil { + rp := *perms.Response + c.perms.resp = &rp + c.replies = make(map[string]*resp) + } + + // Loop over subscribe permissions + if perms.Subscribe != nil { + var err error + if len(perms.Subscribe.Allow) > 0 { + c.perms.sub.allow = NewSublistNoCache() + } + for _, subSubject := range perms.Subscribe.Allow { + sub := &subscription{} + sub.subject, sub.queue, err = splitSubjectQueue(subSubject) + if err != nil { + c.Errorf("%s", err.Error()) + continue + } + if err := c.perms.sub.allow.Insert(sub); err != nil { + c.Errorf("invalid subscribe allow subject %q", subSubject) + } + } + if len(perms.Subscribe.Deny) > 0 { + c.perms.sub.deny = NewSublistNoCache() + } + for _, subSubject := range perms.Subscribe.Deny { + sub := &subscription{} + sub.subject, sub.queue, err = splitSubjectQueue(subSubject) + if err != nil { + c.Errorf("%s", err.Error()) + continue + } + if err := c.perms.sub.deny.Insert(sub); err != nil { + c.Errorf("invalid subscribe deny subject %q", subSubject) + continue + } + // Retain the parsed representation for delivery-time filtering. + c.darray = append(c.darray, sub) + } + } + + // If we are a leafnode and we are the hub copy the extracted perms + // to resend back to soliciting server. These are reversed from the + // way routes interpret them since this is how the soliciting server + // will receive these back in an update INFO. + if c.isHubLeafNode() { + c.opts.Import = perms.Subscribe + c.opts.Export = perms.Publish + } +} + +// Build public permissions from internal ones. +// Used for user info requests. +func (c *client) publicPermissions() *Permissions { + c.mu.Lock() + defer c.mu.Unlock() + + if c.perms == nil { + return nil + } + perms := &Permissions{ + Publish: &SubjectPermission{}, + Subscribe: &SubjectPermission{}, + } + + _subs := [32]*subscription{} + + // Publish + if c.perms.pub.allow != nil { + subs := _subs[:0] + c.perms.pub.allow.All(&subs) + for _, sub := range subs { + perms.Publish.Allow = append(perms.Publish.Allow, string(sub.subject)) + } + } + if c.perms.pub.deny != nil { + subs := _subs[:0] + c.perms.pub.deny.All(&subs) + for _, sub := range subs { + perms.Publish.Deny = append(perms.Publish.Deny, string(sub.subject)) + } + } + // Subsribe + if c.perms.sub.allow != nil { + subs := _subs[:0] + c.perms.sub.allow.All(&subs) + for _, sub := range subs { + perms.Subscribe.Allow = append(perms.Subscribe.Allow, subjectQueueString(sub)) + } + } + if c.perms.sub.deny != nil { + subs := _subs[:0] + c.perms.sub.deny.All(&subs) + for _, sub := range subs { + perms.Subscribe.Deny = append(perms.Subscribe.Deny, subjectQueueString(sub)) + } + } + // Responses. + if c.perms.resp != nil { + rp := *c.perms.resp + perms.Response = &rp + } + + return perms +} + +func subjectQueueString(sub *subscription) string { + if len(sub.queue) == 0 { + return string(sub.subject) + } + return string(sub.subject) + " " + string(sub.queue) +} + +type denyType int + +const ( + pub = denyType(iota + 1) + sub + both +) + +// Merge client.perms structure with additional pub deny permissions +// Lock is held on entry. +func (c *client) mergeDenyPermissions(what denyType, denyPubs []string) { + if len(denyPubs) == 0 { + return + } + if c.perms == nil { + c.perms = &permissions{} + } + if what == pub || what == both { + if c.perms.pub.deny == nil { + c.perms.pub.deny = NewSublistForServer(c.srv) + } + mergeDenyPerm(&c.perms.pub, denyPubs, false) + } + if what == sub || what == both { + if c.perms.sub.deny == nil { + // Avoid sublist cache contention in canSubscribe. + c.perms.sub.deny = NewSublistNoCache() + } + c.darray = append(c.darray, mergeDenyPerm(&c.perms.sub, denyPubs, true)...) + } +} + +// mergeDenyPerm inserts new deny permissions, skipping subjects that already exist. +func mergeDenyPerm(p *perm, denyPubs []string, allowQueue bool) []*subscription { + var inserted []*subscription +FOR_DENY: + for _, deny := range denyPubs { + subject, queue, err := splitSubjectQueue(deny) + if err != nil { + continue + } + if !allowQueue { + queue = nil + } + r := p.deny.Match(string(subject)) + for _, v := range r.qsubs { + for _, s := range v { + if bytes.Equal(s.subject, subject) && bytes.Equal(s.queue, queue) { + continue FOR_DENY + } + } + } + for _, s := range r.psubs { + if bytes.Equal(s.subject, subject) && len(queue) == 0 { + continue FOR_DENY + } + } + sub := &subscription{subject: subject, queue: queue} + if p.deny.Insert(sub) == nil { + inserted = append(inserted, sub) + } + } + return inserted +} + +// Merge client.perms structure with additional pub deny permissions +// Client lock must not be held on entry +func (c *client) mergeDenyPermissionsLocked(what denyType, denyPubs []string) { + c.mu.Lock() + c.mergeDenyPermissions(what, denyPubs) + c.mu.Unlock() +} + +// Check to see if we have an expiration for the user JWT via base claims. +// FIXME(dlc) - Clear on connect with new JWT. +func (c *client) setExpiration(claims *jwt.ClaimsData, validFor time.Duration) { + if claims.Expires == 0 { + if validFor != 0 { + c.setExpirationTimer(validFor) + } + return + } + expiresAt := time.Duration(0) + tn := time.Now().Unix() + if claims.Expires > tn { + expiresAt = time.Duration(claims.Expires-tn) * time.Second + } + if validFor != 0 && validFor < expiresAt { + c.setExpirationTimer(validFor) + } else { + c.setExpirationTimer(expiresAt) + } +} + +// This will load up the deny structure used for filtering delivered +// messages based on a deny clause for subscriptions. +// Lock should be held. +func (c *client) loadMsgDenyFilter() { + c.mperms = &msgDeny{NewSublistWithCache(), make(map[msgDenyKey]bool)} + for _, sub := range c.darray { + c.mperms.deny.Insert(&subscription{subject: sub.subject, queue: sub.queue}) + } +} + +// writeLoop is the main socket write functionality. +// Runs in its own Go routine. +func (c *client) writeLoop() { + defer c.srv.grWG.Done() + c.mu.Lock() + if c.isClosed() { + c.mu.Unlock() + return + } + c.flags.set(writeLoopStarted) + c.mu.Unlock() + + // Used to check that we did flush from last wake up. + waitOk := true + var closed bool + + // Main loop. Will wait to be signaled and then will use + // buffered outbound structure for efficient writev to the underlying socket. + for { + c.mu.Lock() + if closed = c.isClosed(); !closed { + owtf := c.out.fsp > 0 && c.out.pb < maxBufSize && c.out.fsp < maxFlushPending + if waitOk && (c.out.pb == 0 || owtf) { + c.out.sg.Wait() + // Check that connection has not been closed while lock was released + // in the conditional wait. + closed = c.isClosed() + } + } + if closed { + c.flushAndClose(false) + c.mu.Unlock() + + // We should always call closeConnection() to ensure that state is + // properly cleaned-up. It will be a no-op if already done. + c.closeConnection(WriteError) + + // Now explicitly call reconnect(). Thanks to ref counting, we know + // that the reconnect will execute only after connection has been + // removed from the server state. + c.reconnect() + return + } + // Flush data + waitOk = c.flushOutbound() + c.mu.Unlock() + } +} + +// flushClients will make sure to flush any clients we may have +// sent to during processing. We pass in a budget as a time.Duration +// for how much time to spend in place flushing for this client. +func (c *client) flushClients(budget time.Duration) time.Time { + last := time.Now() + + // Check pending clients for flush. + for cp := range c.pcd { + // TODO(dlc) - Wonder if it makes more sense to create a new map? + delete(c.pcd, cp) + + // Queue up a flush for those in the set + cp.mu.Lock() + // Update last activity for message delivery + cp.last = last + // Remove ourselves from the pending list. + cp.out.fsp-- + + // Just ignore if this was closed. + if cp.isClosed() { + cp.mu.Unlock() + continue + } + + if budget > 0 && cp.out.lft < 2*budget && cp.flushOutbound() { + budget -= cp.out.lft + } else { + cp.flushSignal() + } + + cp.mu.Unlock() + } + return last +} + +func (c *client) resetReadLoopStallTime() { + if c.in.tst >= stallClientMaxDuration { + c.rateLimitFormatWarnf("Producer was stalled for a total of %v", c.in.tst.Round(time.Millisecond)) + } + c.in.tst = 0 +} + +// readLoop is the main socket read functionality. +// Runs in its own Go routine. +func (c *client) readLoop(pre []byte) { + // Grab the connection off the client, it will be cleared on a close. + // We check for that after the loop, but want to avoid a nil dereference + c.mu.Lock() + s := c.srv + defer s.grWG.Done() + if c.isClosed() { + c.mu.Unlock() + return + } + nc := c.nc + ws := c.isWebsocket() + if c.isMqtt() { + c.mqtt.r = &mqttReader{reader: nc} + } + c.in.rsz = startBufSize + + // Check the per-account-cache for closed subscriptions + cpacc := c.kind == ROUTER || c.kind == GATEWAY + // Last per-account-cache check for closed subscriptions + lpacc := time.Now() + acc := c.acc + var masking bool + if ws { + masking = c.ws.maskread + } + checkCompress := c.kind == ROUTER || c.kind == LEAF + c.mu.Unlock() + + defer func() { + if c.isMqtt() { + s.mqttHandleClosedClient(c) + } + // These are used only in the readloop, so we can set them to nil + // on exit of the readLoop. + c.in.results, c.in.pacache = nil, nil + }() + + // Start read buffer. + b := make([]byte, c.in.rsz) + + // Websocket clients will return several slices if there are multiple + // websocket frames in the blind read. For non WS clients though, we + // will always have 1 slice per loop iteration. So we define this here + // so non WS clients will use bufs[0] = b[:n]. + var _bufs [1][]byte + bufs := _bufs[:1] + + var wsr *wsReadInfo + if ws { + wsr = &wsReadInfo{mask: masking} + wsr.init() + } + + var decompress bool + var reader io.Reader + reader = nc + + for { + var n int + var err error + + // If we have a pre buffer parse that first. + if len(pre) > 0 { + b = pre + n = len(pre) + pre = nil + } else { + n, err = reader.Read(b) + // If we have any data we will try to parse and exit at the end. + if n == 0 && err != nil { + c.closeConnection(closedStateForErr(err)) + return + } + } + // Check if the account has mappings and if so set the local readcache flag. + // We check here to make sure any changes such as config reload are reflected here. + if c.kind == CLIENT || c.kind == LEAF { + if acc.hasMappings() { + c.in.flags.set(hasMappings) + } else { + c.in.flags.clear(hasMappings) + } + } + + c.in.start = time.Now() + + // Clear inbound stats cache + c.in.msgs = 0 + c.in.bytes = 0 + c.in.subs = 0 + + if ws { + err = c.wsReadAndParse(wsr, reader, b[:n]) + if err != nil { + // Match the normal parse path: any already-buffered deliveries + // need their pending flush signals drained before we close. + c.flushClients(0) + if err != io.EOF { + c.Errorf("read error: %v", err) + } + c.closeConnection(closedStateForErr(err)) + return + } + c.resetReadLoopStallTime() + goto postParse + } else { + bufs[0] = b[:n] + } + + // Main call into parser for inbound data. This will generate callouts + // to process messages, etc. + for i := 0; i < len(bufs); i++ { + if err := c.parse(bufs[i]); err != nil { + if err == ErrMinimumVersionRequired { + // Special case here, currently only for leaf node connections. + // processLeafConnect() already sent the rejection and closed + // the connection, so there is nothing else to do here. + return + } + if dur := time.Since(c.in.start); dur >= readLoopReportThreshold { + c.Warnf("Readloop processing time: %v", dur) + } + // Need to call flushClients because some of the clients have been + // assigned messages and their "fsp" incremented, and need now to be + // decremented and their writeLoop signaled. + c.flushClients(0) + // handled inline + if err != ErrMaxPayload && err != ErrAuthentication { + c.Error(err) + c.closeConnection(ProtocolViolation) + } + return + } + c.resetReadLoopStallTime() + } + + postParse: + // If we are a ROUTER/LEAF and have processed an INFO, it is possible that + // we are asked to switch to compression now. + if checkCompress && c.in.flags.isSet(switchToCompression) { + c.in.flags.clear(switchToCompression) + // For now we support only s2 compression... + reader = s2.NewReader(nc) + decompress = true + } + + // Updates stats for client and server that were collected + // from parsing through the buffer. + if c.in.msgs > 0 { + inMsgs := int64(c.in.msgs) + inBytes := int64(c.in.bytes) + + atomic.AddInt64(&c.inMsgs, inMsgs) + atomic.AddInt64(&c.inBytes, inBytes) + + if acc != nil { + acc.stats.Lock() + acc.stats.inMsgs += inMsgs + acc.stats.inBytes += inBytes + if c.kind == LEAF { + acc.stats.ln.inMsgs += int64(inMsgs) + acc.stats.ln.inBytes += int64(inBytes) + } + acc.stats.Unlock() + } + + if c.kind == CLIENT { + atomic.AddInt64(&s.inClientMsgs, inMsgs) + atomic.AddInt64(&s.inClientBytes, inBytes) + } + + atomic.AddInt64(&s.inMsgs, inMsgs) + atomic.AddInt64(&s.inBytes, inBytes) + } + + // Signal to writeLoop to flush to socket. + last := c.flushClients(0) + + // Update activity, check read buffer size. + c.mu.Lock() + + // Activity based on interest changes or data/msgs. + // Also update last receive activity for ping sender + if c.in.msgs > 0 || c.in.subs > 0 { + c.last = last + c.lastIn = last + } + + if n >= cap(b) { + c.in.srs = 0 + } else if n < cap(b)/2 { // divide by 2 b/c we want less than what we would shrink to. + c.in.srs++ + } + + // Update read buffer size as/if needed. + if n >= cap(b) && cap(b) < maxBufSize { + // Grow + c.in.rsz = int32(cap(b) * 2) + b = make([]byte, c.in.rsz) + } else if n < cap(b) && cap(b) > minBufSize && c.in.srs > shortsToShrink { + // Shrink, for now don't accelerate, ping/pong will eventually sort it out. + c.in.rsz = int32(cap(b) / 2) + b = make([]byte, c.in.rsz) + } + // re-snapshot the account since it can change during reload, etc. + acc = c.acc + // Refresh nc because in some cases, we have upgraded c.nc to TLS. + if nc != c.nc { + nc = c.nc + if decompress && nc != nil { + // For now we support only s2 compression... + reader.(*s2.Reader).Reset(nc) + } else if !decompress { + reader = nc + } + } + c.mu.Unlock() + + // Connection was closed + if nc == nil { + return + } + + if dur := time.Since(c.in.start); dur >= readLoopReportThreshold { + c.Warnf("Readloop processing time: %v", dur) + } + + // We could have had a read error from above but still read some data. + // If so do the close here unconditionally. + if err != nil { + c.closeConnection(closedStateForErr(err)) + return + } + + if cpacc && c.in.start.Sub(lpacc) >= closedSubsCheckInterval { + c.pruneClosedSubFromPerAccountCache() + lpacc = time.Now() + } + } +} + +// Returns the appropriate closed state for a given read error. +func closedStateForErr(err error) ClosedState { + if err == io.EOF { + return ClientClosed + } + return ReadError +} + +// collapsePtoNB will either returned framed WebSocket buffers or it will +// return a reference to c.out.nb. +func (c *client) collapsePtoNB() (net.Buffers, int64) { + if c.isWebsocket() { + return c.wsCollapsePtoNB() + } + return c.out.nb, c.out.pb +} + +// flushOutbound will flush outbound buffer to a client. +// Will return true if data was attempted to be written. +// Lock must be held +func (c *client) flushOutbound() bool { + if c.flags.isSet(flushOutbound) { + // For CLIENT connections, it is possible that the readLoop calls + // flushOutbound(). If writeLoop and readLoop compete and we are + // here we should release the lock to reduce the risk of spinning. + c.mu.Unlock() + runtime.Gosched() + c.mu.Lock() + return false + } + c.flags.set(flushOutbound) + defer func() { + // Check flushAndClose() for explanation on why we do this. + if c.isClosed() { + for i := range c.out.wnb { + nbPoolPut(c.out.wnb[i]) + } + c.out.wnb = nil + } + c.flags.clear(flushOutbound) + }() + + // Check for nothing to do. + if c.nc == nil || c.srv == nil || c.out.pb == 0 { + return true // true because no need to queue a signal. + } + + // In the case of a normal socket connection, "collapsed" is just a ref + // to "nb". In the case of WebSockets, additional framing is added to + // anything that is waiting in "nb". Also keep a note of how many bytes + // were queued before we release the mutex. + collapsed, attempted := c.collapsePtoNB() + + // Frustratingly, (net.Buffers).WriteTo() modifies the receiver so we + // can't work on "nb" directly — while the mutex is unlocked during IO, + // something else might call queueOutbound and modify it. So instead we + // need a working copy — we'll operate on "wnb" instead. Note that in + // the case of a partial write, "wnb" may have remaining data from the + // previous write, and in the case of WebSockets, that data may already + // be framed, so we are careful not to re-frame "wnb" here. Instead we + // will just frame up "nb" and append it onto whatever is left on "wnb". + // "nb" will be set to nil so that we can manipulate "collapsed" outside + // of the client's lock, which is interesting in case of compression. + c.out.nb = nil + + // In case it goes away after releasing the lock. + nc := c.nc + + // Capture this (we change the value in some tests) + wdl := c.out.wdl + + // Check for compression + cw := c.out.cw + if cw != nil { + // We will have to adjust once we have compressed, so remove for now. + c.out.pb -= attempted + if c.isWebsocket() { + c.ws.fs -= attempted + } + } + + // Do NOT hold lock during actual IO. + c.mu.Unlock() + + // Compress outside of the lock + if cw != nil { + var err error + bb := bytes.Buffer{} + + cw.Reset(&bb) + for _, buf := range collapsed { + if err == nil { + _, err = cw.Write(buf) + } + // Return always after consumed or error. + nbPoolPut(buf) + } + if err == nil { + err = cw.Close() + } + if err != nil { + c.Errorf("Error compressing data: %v", err) + // We need to grab the lock now before marking as closed and exiting + c.mu.Lock() + c.markConnAsClosed(WriteError) + return false + } + collapsed = append(net.Buffers(nil), bb.Bytes()) + attempted = int64(len(collapsed[0])) + } + + // This is safe to do outside of the lock since "collapsed" is no longer + // referenced in c.out.nb (which can be modified in queueOutbound() while + // the lock is released). + c.out.wnb = append(c.out.wnb, collapsed...) + var _orig [nbMaxVectorSize][]byte + orig := append(_orig[:0], c.out.wnb...) + + // Since WriteTo is lopping things off the beginning, we need to remember + // the start position of the underlying array so that we can get back to it. + // Otherwise we'll always "slide forward" and that will result in reallocs. + startOfWnb := c.out.wnb[0:] + + // flush here + start := time.Now() + + var n int64 // Total bytes written + var wn int64 // Bytes written per loop + var err error // Error from last write, if any + for len(c.out.wnb) > 0 { + // Limit the number of vectors to no more than nbMaxVectorSize, + // which if 1024, will mean a maximum of 64MB in one go. + wnb := c.out.wnb + if len(wnb) > nbMaxVectorSize { + wnb = wnb[:nbMaxVectorSize] + } + consumed := len(wnb) + + // Actual write to the socket. The deadline applies to each batch + // rather than the total write, such that the configured deadline + // can be tuned to a known maximum quantity (64MB). + nc.SetWriteDeadline(time.Now().Add(wdl)) + wn, err = wnb.WriteTo(nc) + + // Update accounting, move wnb slice onwards if needed, or stop + // if a write error was reported that wasn't a short write. + n += wn + c.out.wnb = c.out.wnb[consumed-len(wnb):] + if err != nil && err != io.ErrShortWrite { + break + } + } + + // The deadline is overwritten at the start of each iteration anyway, + // so only needed to clear the write deadline once after the loop. + nc.SetWriteDeadline(time.Time{}) + + lft := time.Since(start) + + // Re-acquire client lock. + c.mu.Lock() + + // Adjust if we were compressing. + if cw != nil { + c.out.pb += attempted + if c.isWebsocket() { + c.ws.fs += attempted + } + } + + // In case of a partial write, WriteTo will have resliced the buffer + // to drop the written bytes. As a side effect this will also change + // its capacity so it can't be recycled by nbPoolPut as originally intended + // and may either leak or land in the wrong pool. + // Prevent this by putting the remaining bytes back to the start of the slice + // so that it keeps its full capacity and is recycled once fully written or + // when the connection is closed. + if rem := len(c.out.wnb); rem > 0 && rem <= len(orig) { + if k := len(orig) - rem; len(c.out.wnb[0]) < len(orig[k]) { + n := copy(orig[k], c.out.wnb[0]) + c.out.wnb[0] = orig[k][:n] + } + } + + // At this point, "wnb" has been mutated by WriteTo and any consumed + // buffers have been lopped off the beginning, so in order to return + // them to the pool, we need to look at the difference between "orig" + // and "wnb". + for i := 0; i < len(orig)-len(c.out.wnb); i++ { + nbPoolPut(orig[i]) + } + + // At this point it's possible that "nb" has been modified by another + // call to queueOutbound while the lock was released, so we'll leave + // those for the next iteration. Meanwhile it's possible that we only + // managed a partial write of "wnb", so we'll shift anything that + // remains up to the beginning of the array to prevent reallocating. + // Anything left in "wnb" has already been framed for WebSocket conns + // so leave them alone for the next call to flushOutbound. + c.out.wnb = append(startOfWnb[:0], c.out.wnb...) + + // If we've written everything but the underlying array of our working + // buffer has grown excessively then free it — the GC will tidy it up + // and we can allocate a new one next time. + if len(c.out.wnb) == 0 && cap(c.out.wnb) > nbMaxVectorSize { + c.out.wnb = nil + } + + // Ignore ErrShortWrite errors, they will be handled as partials. + var gotWriteTimeout bool + if err != nil && err != io.ErrShortWrite { + // Handle timeout error (slow consumer) differently + if ne, ok := err.(net.Error); ok && ne.Timeout() { + gotWriteTimeout = true + if closed := c.handleWriteTimeout(n, attempted, len(orig)); closed { + return true + } + } else { + // Other errors will cause connection to be closed. + // For clients, report as debug but for others report as error. + report := c.Debugf + if c.kind != CLIENT { + report = c.Errorf + } + report("Error flushing: %v", err) + c.markConnAsClosed(WriteError) + return true + } + } + + // Update flush time statistics. + c.out.lft = lft + + // Subtract from pending bytes and messages. + c.out.pb -= n + if c.isWebsocket() { + c.ws.fs -= n + } + + // Check that if there is still data to send and writeLoop is in wait, + // then we need to signal. + if c.out.pb > 0 { + c.flushSignal() + } + + // Check if we have a stalled gate and if so and we are recovering release + // any stalled producers. Only kind==CLIENT will stall. + if c.out.stc != nil && (n == attempted || c.out.pb < c.out.mp/4*3) { + close(c.out.stc) + c.out.stc = nil + } + // Check if the connection is recovering from being a slow consumer. + if !gotWriteTimeout && c.flags.isSet(isSlowConsumer) { + c.Noticef("Slow Consumer Recovered: Flush took %.3fs with %d chunks of %d total bytes.", time.Since(start).Seconds(), len(orig), attempted) + c.flags.clear(isSlowConsumer) + } + + return true +} + +// This is invoked from flushOutbound() for io/timeout error (slow consumer). +// Returns a boolean to indicate if the connection has been closed or not. +// Lock is held on entry. +func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) bool { + if tlsConn, ok := c.nc.(*tls.Conn); ok { + if !tlsConn.ConnectionState().HandshakeComplete { + // Likely a TLSTimeout error instead... + c.markConnAsClosed(TLSHandshakeError) + // Would need to coordinate with tlstimeout() + // to avoid double logging, so skip logging + // here, and don't report a slow consumer error. + return true + } + } else if c.flags.isSet(expectConnect) && !c.flags.isSet(connectReceived) { + // Under some conditions, a connection may hit a slow consumer write deadline + // before the authorization timeout. If that is the case, then we handle + // as slow consumer though we do not increase the counter as that can be + // misleading. + c.markConnAsClosed(SlowConsumerWriteDeadline) + return true + } + alreadySC := c.flags.isSet(isSlowConsumer) + scState := "Detected" + if alreadySC { + scState = "State" + } + + // Aggregate slow consumers. + atomic.AddInt64(&c.srv.slowConsumers, 1) + switch c.kind { + case CLIENT: + c.srv.scStats.clients.Add(1) + case ROUTER: + // Only count each Slow Consumer event once. + if !alreadySC { + c.srv.scStats.routes.Add(1) + } + case GATEWAY: + c.srv.scStats.gateways.Add(1) + case LEAF: + c.srv.scStats.leafs.Add(1) + } + if c.acc != nil { + c.acc.stats.Lock() + c.acc.stats.slowConsumers++ + c.acc.stats.Unlock() + } + c.Noticef("Slow Consumer %s: WriteDeadline of %v exceeded with %d chunks of %d total bytes.", + scState, c.out.wdl, numChunks, attempted) + + // We always close CLIENT connections, or when nothing was written at all... + if c.out.wtp == WriteTimeoutPolicyClose || written == 0 { + c.markConnAsClosed(SlowConsumerWriteDeadline) + return true + } else { + c.flags.setIfNotSet(isSlowConsumer) + } + return false +} + +// Marks this connection has closed with the given reason. +// Sets the connMarkedClosed flag and skipFlushOnClose depending on the reason. +// Depending on the kind of connection, the connection will be saved. +// If a writeLoop has been started, the final flush will be done there, otherwise +// flush and close of TCP connection is done here in place. +// Returns true if closed in place, flase otherwise. +// Lock is held on entry. +func (c *client) markConnAsClosed(reason ClosedState) { + // Possibly set skipFlushOnClose flag even if connection has already been + // mark as closed. The rationale is that a connection may be closed with + // a reason that justifies a flush (say after sending an -ERR), but then + // the flushOutbound() gets a write error. If that happens, connection + // being lost, there is no reason to attempt to flush again during the + // teardown when the writeLoop exits. + var skipFlush bool + switch reason { + case ReadError, WriteError, SlowConsumerPendingBytes, SlowConsumerWriteDeadline, TLSHandshakeError: + c.flags.set(skipFlushOnClose) + skipFlush = true + case StaleConnection: + // Track stale connections statistics. + atomic.AddInt64(&c.srv.staleConnections, 1) + switch c.kind { + case CLIENT: + c.srv.staleStats.clients.Add(1) + case ROUTER: + c.srv.staleStats.routes.Add(1) + case GATEWAY: + c.srv.staleStats.gateways.Add(1) + case LEAF: + c.srv.staleStats.leafs.Add(1) + } + } + if c.flags.isSet(connMarkedClosed) { + return + } + c.flags.set(connMarkedClosed) + // For a websocket client, unless we are told not to flush, enqueue + // a websocket CloseMessage based on the reason. + if !skipFlush && c.isWebsocket() && !c.ws.closeSent { + c.wsEnqueueCloseMessage(reason) + } + // Be consistent with the creation: for routes, gateways and leaf, + // we use Noticef on create, so use that too for delete. + if c.srv != nil { + if c.kind == LEAF || c.kind == ROUTER || c.kind == GATEWAY { + var tags []string + var remoteName string + switch { + case c.kind == LEAF && c.leaf != nil: + remoteName = c.leaf.remoteServer + case c.kind == ROUTER && c.route != nil: + remoteName = c.route.remoteName + case c.kind == GATEWAY && c.gw != nil: + remoteName = c.gw.remoteName + } + if remoteName != _EMPTY_ { + tags = append(tags, fmt.Sprintf("Remote: %s", remoteName)) + } + if len(tags) > 0 { + c.Noticef("%s connection closed: %s - %s", c.kindString(), reason, strings.Join(tags, ", ")) + } else { + c.Noticef("%s connection closed: %s", c.kindString(), reason) + } + } else { // Client, System, Jetstream, and Account connections. + c.Debugf("%s connection closed: %s", c.kindString(), reason) + } + } + + // Save off the connection if its a client or leafnode. + if c.kind == CLIENT || c.kind == LEAF { + if nc := c.nc; nc != nil && c.srv != nil { + // TODO: May want to send events to single go routine instead + // of creating a new go routine for each save. + // Pass the c.subs as a reference. It may be set to nil in + // closeConnection. + go c.srv.saveClosedClient(c, nc, c.subs, reason) + } + } + // If writeLoop exists, let it do the final flush, close and teardown. + if c.flags.isSet(writeLoopStarted) { + // Since we want the writeLoop to do the final flush and tcp close, + // we want the reconnect to be done there too. However, it should'nt + // happen before the connection has been removed from the server + // state (end of closeConnection()). This ref count allows us to + // guarantee that. + c.rref++ + c.flushSignal() + return + } + // Flush (if skipFlushOnClose is not set) and close in place. If flushing, + // use a small WriteDeadline. + c.flushAndClose(true) +} + +// flushSignal will use server to queue the flush IO operation to a pool of flushers. +// Lock must be held. +func (c *client) flushSignal() { + // Check that sg is not nil, which will happen if the connection is closed. + if c.out.sg != nil { + c.out.sg.Signal() + } +} + +// Traces a message. +// Will NOT check if tracing is enabled, does NOT need the client lock. +func (c *client) traceMsgInternal(msg []byte, delivered bool, hdrSize int) { + opts := c.srv.getOpts() + maxTrace := opts.MaxTracedMsgLen + headersOnly := opts.TraceHeaders + suffix := LEN_CR_LF + + // If TraceHeaders is enabled, extract only the header portion of the msg. + // If a header is present, it ends with an additional trailing CRLF. + if headersOnly { + if hdrSize > 0 && len(msg) >= hdrSize { + msg = msg[:hdrSize] + suffix += LEN_CR_LF + } else { + // No headers present, so nothing to trace. + return + } + } + + // Do not emit a log line for zero-length payloads. + l := len(msg) - suffix + if l <= 0 { + return + } + + const ( + traceInPrefix string = "<<-" + traceOutPrefix string = "->>" + ) + var prefix string + if delivered { + prefix = traceOutPrefix + } else { + prefix = traceInPrefix + } + if maxTrace > 0 && l > maxTrace { + tm := fmt.Sprintf("%q", msg[:maxTrace]) + c.Tracef("%s MSG_PAYLOAD: [\"%s...\"]", prefix, tm[1:len(tm)-1]) + } else { + c.Tracef("%s MSG_PAYLOAD: [%q]", prefix, msg[:l]) + } +} + +func (c *client) traceMsg(msg []byte) { + c.traceMsgInternal(msg, false, c.pa.hdr) +} + +func (c *client) traceMsgDelivery(msg []byte, hdrSize int) { + c.traceMsgInternal(msg, true, hdrSize) +} + +// Traces an incoming operation. +// Will NOT check if tracing is enabled, does NOT need the client lock. +func (c *client) traceInOp(op string, arg []byte) { + c.traceOp("<<- %s", op, arg) +} + +// Traces an outgoing operation. +// Will NOT check if tracing is enabled, does NOT need the client lock. +func (c *client) traceOutOp(op string, arg []byte) { + c.traceOp("->> %s", op, arg) +} + +func (c *client) traceOp(format, op string, arg []byte) { + opa := []any{} + if op != _EMPTY_ { + opa = append(opa, op) + } + if arg != nil { + opa = append(opa, bytesToString(arg)) + } + c.Tracef(format, opa) +} + +// Process the information messages from Clients and other Routes. +func (c *client) processInfo(arg []byte) error { + info := Info{} + if err := json.Unmarshal(arg, &info); err != nil { + return err + } + switch c.kind { + case ROUTER: + c.processRouteInfo(&info) + case GATEWAY: + c.processGatewayInfo(&info) + case LEAF: + c.processLeafnodeInfo(&info) + } + return nil +} + +func (c *client) processErr(errStr string) { + close := true + switch c.kind { + case CLIENT: + c.Errorf("Client Error %s", errStr) + case ROUTER: + c.Errorf("Route Error %s", errStr) + case GATEWAY: + c.Errorf("Gateway Error %s", errStr) + case LEAF: + c.Errorf("Leafnode Error %s", errStr) + c.leafProcessErr(errStr) + close = false + case JETSTREAM: + c.Errorf("JetStream Error %s", errStr) + } + if close { + c.closeConnection(ParseError) + } +} + +// Matcher for pass/password and auth_token fields. +var prefixAuthPat = regexp.MustCompile(`"?\s*(?:auth_token\S*?|pass\S*?)"?\s*[:=]\s*"?([^",\r\n}]*)`) + +// Exact matcher for fields sig, proxy_sig and nkey. +// Overlapping field "sig" does not match inside "proxy_sig". +var exactAuthPat = regexp.MustCompile(`(?:^|[^A-Za-z0-9_])"?\s*(?:proxy_sig|nkey|sig)"?\s*[:=]\s*"?([^",\r\n}]*)`) + +// removeSecretsFromTrace removes any notion of passwords/tokens from trace +// messages for logging. +func removeSecretsFromTrace(arg []byte) []byte { + buf := redact(prefixAuthPat, arg) + return redact(exactAuthPat, buf) +} + +func redact(pat *regexp.Regexp, proto []byte) []byte { + m := pat.FindAllSubmatchIndex(proto, -1) + if len(m) == 0 { + return proto + } + // Take a copy of the connect proto just for the trace message. + var _arg [4096]byte + buf := append(_arg[:0], proto...) + redactedPass := []byte("[REDACTED]") + for i := len(m) - 1; i >= 0; i-- { + match := m[i] + if len(match) < 4 { + continue + } + start, end := match[2], match[3] + // Replace value substring. + buf = append(buf[:start], append(redactedPass, buf[end:]...)...) + } + return buf +} + +// Returns the RTT by computing the elapsed time since now and `start`. +// On Windows VM where I (IK) run tests, time.Since() will return 0 +// (I suspect some time granularity issues). So return at minimum 1ns. +func computeRTT(start time.Time) time.Duration { + rtt := time.Since(start) + if rtt <= 0 { + rtt = time.Nanosecond + } + return rtt +} + +// processConnect will process a client connect op. +func (c *client) processConnect(arg []byte) error { + supportsHeaders := c.srv.supportsHeaders() + c.mu.Lock() + // If we can't stop the timer because the callback is in progress... + if !c.clearAuthTimer() { + // wait for it to finish and handle sending the failure back to + // the client. + for !c.isClosed() { + c.mu.Unlock() + time.Sleep(25 * time.Millisecond) + c.mu.Lock() + } + c.mu.Unlock() + return nil + } + c.last = time.Now().UTC() + // Estimate RTT to start. + if c.kind == CLIENT { + c.rtt = computeRTT(c.start) + if c.srv != nil { + c.clearPingTimer() + c.setFirstPingTimer() + } + } + kind := c.kind + srv := c.srv + + // Moved unmarshalling of clients' Options under the lock. + // The client has already been added to the server map, so it is possible + // that other routines lookup the client, and access its options under + // the client's lock, so unmarshalling the options outside of the lock + // would cause data RACEs. + if err := json.Unmarshal(arg, &c.opts); err != nil { + c.mu.Unlock() + return err + } + // Indicate that the CONNECT protocol has been received, and that the + // server now knows which protocol this client supports. + firstConnect := !c.flags.isSet(connectReceived) + c.flags.set(connectReceived) + // Capture these under lock + c.echo = c.opts.Echo + proto := c.opts.Protocol + verbose := c.opts.Verbose + lang := c.opts.Lang + account := c.opts.Account + accountNew := c.opts.AccountNew + + // if websocket client, maybe some options through cookies + if ws := c.ws; ws != nil { + // if JWT not in the CONNECT, use the cookie JWT (possibly empty). + if c.opts.JWT == _EMPTY_ { + c.opts.JWT = ws.cookieJwt + } + // if user not in the CONNECT, use the cookie user (possibly empty) + if c.opts.Username == _EMPTY_ { + c.opts.Username = ws.cookieUsername + } + // if pass not in the CONNECT, use the cookie password (possibly empty). + if c.opts.Password == _EMPTY_ { + c.opts.Password = ws.cookiePassword + } + // if token not in the CONNECT, use the cookie token (possibly empty). + if c.opts.Token == _EMPTY_ { + c.opts.Token = ws.cookieToken + } + } + + // when not in operator mode, discard the jwt + if srv != nil && srv.trustedKeys == nil { + c.opts.JWT = _EMPTY_ + } + ujwt := c.opts.JWT + + // For headers both client and server need to support. + c.headers = supportsHeaders && c.opts.Headers + c.mu.Unlock() + + if srv != nil { + // Applicable to clients only: + // As soon as c.opts is unmarshalled and if the proto is at + // least ClientProtoInfo, we need to increment the following counter. + // This is decremented when client is removed from the server's + // clients map. + if kind == CLIENT && proto >= ClientProtoInfo && firstConnect { + srv.mu.Lock() + srv.cproto++ + srv.mu.Unlock() + } + + // A second CONNECT may move the client into a different account via + // checkAuthentication. Drop any previously-registered subscriptions + // from the current account first so they don't leak in that account's + // sublist after the client switches. + if !firstConnect { + c.clearAccountSubs(false) + } + + // Check for Auth + if ok := srv.checkAuthentication(c); !ok { + // We may fail here because we reached max limits on an account. + if ujwt != _EMPTY_ { + c.mu.Lock() + acc := c.acc + c.mu.Unlock() + srv.mu.Lock() + tooManyAccCons := acc != nil && acc != srv.gacc + srv.mu.Unlock() + if tooManyAccCons { + return ErrTooManyAccountConnections + } + } + c.authViolation() + return ErrAuthentication + } + + // Check for Account designation, we used to have this as an optional feature for dynamic + // sandbox environments. Now its considered an error. + if accountNew || account != _EMPTY_ { + c.authViolation() + return ErrAuthentication + } + + // If no account designation. + // Do this only for CLIENT and LEAF connections. + if c.acc == nil && (c.kind == CLIENT || c.kind == LEAF) { + // By default register with the global account. + c.registerWithAccount(srv.globalAccount()) + } + + // Initialize user info used in logs. + c.mu.Lock() + acc := c.acc + if c.getRawAuthUser() != _EMPTY_ { + c.ncsUser.Store(c.getAuthUserLabel()) + } + c.mu.Unlock() + if acc != nil { + acc.mu.RLock() + c.ncsAcc.Store(acc.traceLabel()) + acc.mu.RUnlock() + } + + // Enable logging connection details and auth info for this client. + if c.kind == CLIENT && firstConnect && c.srv != nil { + var ncs string + if c.opts.Version != _EMPTY_ { + ncs = fmt.Sprintf("v%s", c.opts.Version) + } + if c.opts.Lang != _EMPTY_ { + if c.opts.Version == _EMPTY_ { + ncs = c.opts.Lang + } else { + ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Lang) + } + } + if c.opts.Name != _EMPTY_ { + if c.opts.Version == _EMPTY_ && c.opts.Lang == _EMPTY_ { + ncs = c.opts.Name + } else { + ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Name) + } + } + var acs string + accl := c.ncsAcc.Load() + authUser := c.ncsUser.Load() + if accl != nil && authUser != nil { + acs = fmt.Sprintf("%s/%s", accl, authUser) + } + switch { + case ncs != _EMPTY_ && acs != _EMPTY_: + c.ncs.Store(fmt.Sprintf("%s - %q - %q", c, ncs, acs)) + case ncs != _EMPTY_: + c.ncs.Store(fmt.Sprintf("%s - %q", c, ncs)) + case acs != _EMPTY_: + c.ncs.Store(fmt.Sprintf("%s - %q", c, acs)) + } + } + } + + switch kind { + case CLIENT: + // Check client protocol request if it exists. + if proto < ClientProtoZero || proto > ClientProtoInfo { + c.sendErr(ErrBadClientProtocol.Error()) + c.closeConnection(BadClientProtocolVersion) + return ErrBadClientProtocol + } + // Check to see that if no_responders is requested + // they have header support on as well. + c.mu.Lock() + misMatch := c.opts.NoResponders && !c.headers + c.mu.Unlock() + if misMatch { + c.sendErr(ErrNoRespondersRequiresHeaders.Error()) + c.closeConnection(NoRespondersRequiresHeaders) + return ErrNoRespondersRequiresHeaders + } + if verbose { + c.sendOK() + } + case ROUTER: + // Delegate the rest of processing to the route + return c.processRouteConnect(srv, arg, lang) + case GATEWAY: + // Delegate the rest of processing to the gateway + return c.processGatewayConnect(arg) + case LEAF: + // Delegate the rest of processing to the leaf node + return c.processLeafNodeConnect(srv, arg, lang) + } + return nil +} + +func (c *client) sendErrAndErr(err string) { + c.sendErr(err) + c.RateLimitErrorf(err) +} + +func (c *client) sendErrAndDebug(err string) { + c.sendErr(err) + c.RateLimitDebugf(err) +} + +func (c *client) authTimeout() { + c.sendErrAndDebug("Authentication Timeout") + c.closeConnection(AuthenticationTimeout) +} + +func (c *client) authExpired() { + c.sendErrAndDebug("User Authentication Expired") + c.closeConnection(AuthenticationExpired) +} + +func (c *client) accountAuthExpired() { + c.sendErrAndDebug("Account Authentication Expired") + c.closeConnection(AuthenticationExpired) +} + +func (c *client) authViolation() { + authErr := c.getAuthError() + if authErr == nil { + authErr = ErrAuthentication + } + reason := getAuthErrClosedState(authErr) + + var s *Server + if s = c.srv; s != nil { + defer s.sendAuthErrorEvent(c, reason.String()) + if c.getRawAuthUser() != _EMPTY_ { + c.Errorf("%v - %s", ErrAuthentication, c.getAuthUser()) + } else { + c.Errorf(ErrAuthentication.Error()) + } + } + if c.isMqtt() { + c.mqttEnqueueConnAck(mqttConnAckRCNotAuthorized, false) + } else { + // Send this to client, regardless of the authErr override. + c.sendErr("Authorization Violation") + } + c.closeConnection(reason) +} + +func (c *client) maxAccountConnExceeded() { + c.sendErrAndErr(ErrTooManyAccountConnections.Error()) + c.closeConnection(MaxAccountConnectionsExceeded) +} + +func (c *client) maxConnExceeded() { + c.sendErrAndErr(ErrTooManyConnections.Error()) + c.closeConnection(MaxConnectionsExceeded) +} + +func (c *client) maxSubsExceeded() { + if c.acc.shouldLogMaxSubErr() { + c.Errorf(ErrTooManySubs.Error()) + } + c.sendErr(ErrTooManySubs.Error()) +} + +func (c *client) maxPayloadViolation(sz int, max int32) { + c.Errorf("%s: %d vs %d", ErrMaxPayload.Error(), sz, max) + c.sendErr("Maximum Payload Violation") + c.closeConnection(MaxPayloadExceeded) +} + +// queueOutbound queues data for a clientconnection. +// Lock should be held. +func (c *client) queueOutbound(data []byte) { + // Do not keep going if closed + if c.isClosed() { + return + } + + // Add to pending bytes total. + c.out.pb += int64(len(data)) + + // Take a copy of the slice ref so that we can chop bits off the beginning + // without affecting the original "data" slice. + toBuffer := data + + // All of the queued []byte have a fixed capacity, so if there's a []byte + // at the tail of the buffer list that isn't full yet, we should top that + // up first. This helps to ensure we aren't pulling more []bytes from the + // pool than we need to. + if len(c.out.nb) > 0 { + last := &c.out.nb[len(c.out.nb)-1] + if free := cap(*last) - len(*last); free > 0 { + if l := len(toBuffer); l < free { + free = l + } + *last = append(*last, toBuffer[:free]...) + toBuffer = toBuffer[free:] + } + } + + // Now we can push the rest of the data into new []bytes from the pool + // in fixed size chunks. This ensures we don't go over the capacity of any + // of the buffers and end up reallocating. + for len(toBuffer) > 0 { + new := nbPoolGet(len(toBuffer)) + n := copy(new[:cap(new)], toBuffer) + c.out.nb = append(c.out.nb, new[:n]) + toBuffer = toBuffer[n:] + } + + // Check for slow consumer via pending bytes limit. + // ok to return here, client is going away. + if c.kind == CLIENT && c.out.pb > c.out.mp { + // Perf wise, it looks like it is faster to optimistically add than + // checking current pb+len(data) and then add to pb. + c.out.pb -= int64(len(data)) + + // Increment the total and client's slow consumer counters. + atomic.AddInt64(&c.srv.slowConsumers, 1) + c.srv.scStats.clients.Add(1) + if c.acc != nil { + c.acc.stats.Lock() + c.acc.stats.slowConsumers++ + c.acc.stats.Unlock() + } + c.Noticef("Slow Consumer Detected: MaxPending of %d Exceeded", c.out.mp) + c.markConnAsClosed(SlowConsumerPendingBytes) + return + } + + // Check here if we should create a stall channel if we are falling behind. + // We do this here since if we wait for consumer's writeLoop it could be + // too late with large number of fan in producers. + // If the outbound connection is > 75% of maximum pending allowed, create a stall gate. + if c.out.pb > c.out.mp/4*3 && c.out.stc == nil { + c.out.stc = make(chan struct{}) + } +} + +// Assume the lock is held upon entry. +func (c *client) enqueueProtoAndFlush(proto []byte, doFlush bool) { + if c.isClosed() { + return + } + c.queueOutbound(proto) + if !(doFlush && c.flushOutbound()) { + c.flushSignal() + } +} + +// Queues and then flushes the connection. This should only be called when +// the writeLoop cannot be started yet. Use enqueueProto() otherwise. +// Lock is held on entry. +func (c *client) sendProtoNow(proto []byte) { + c.enqueueProtoAndFlush(proto, true) +} + +// Enqueues the given protocol and signal the writeLoop if necessary. +// Lock is held on entry. +func (c *client) enqueueProto(proto []byte) { + c.enqueueProtoAndFlush(proto, false) +} + +// Assume the lock is held upon entry. +func (c *client) sendPong() { + if c.trace { + c.traceOutOp("PONG", nil) + } + c.enqueueProto([]byte(pongProto)) +} + +// Used to kick off a RTT measurement for latency tracking. +func (c *client) sendRTTPing() bool { + c.mu.Lock() + sent := c.sendRTTPingLocked() + c.mu.Unlock() + return sent +} + +// Used to kick off a RTT measurement for latency tracking. +// This is normally called only when the caller has checked that +// the c.rtt is 0 and wants to force an update by sending a PING. +// Client lock held on entry. +func (c *client) sendRTTPingLocked() bool { + if c.isMqtt() { + return false + } + // Most client libs send a CONNECT+PING and wait for a PONG from the + // server. So if firstPongSent flag is set, it is ok for server to + // send the PING. But in case we have client libs that don't do that, + // allow the send of the PING if more than 2 secs have elapsed since + // the client TCP connection was accepted. + if !c.isClosed() && + (c.flags.isSet(firstPongSent) || time.Since(c.start) > maxNoRTTPingBeforeFirstPong) { + c.sendPing() + return true + } + return false +} + +// Assume the lock is held upon entry. +func (c *client) sendPing() { + c.rttStart = time.Now().UTC() + c.ping.out++ + if c.trace { + c.traceOutOp("PING", nil) + } + c.enqueueProto([]byte(pingProto)) +} + +// Generates the INFO to be sent to the client with the client ID included. +// info arg will be copied since passed by value. +// Assume lock is held. +func (c *client) generateClientInfoJSON(info Info, includeClientIP bool) []byte { + info.CID = c.cid + if includeClientIP { + info.ClientIP = c.host + } + info.MaxPayload = atomic.LoadInt32(&c.mpay) + if c.isWebsocket() { + info.ClientConnectURLs = info.WSConnectURLs + // Otherwise lame duck info can panic + if c.srv != nil { + ws := &c.srv.websocket + info.TLSAvailable, info.TLSRequired = ws.tls, ws.tls + info.Host, info.Port = ws.host, ws.port + } + } + info.WSConnectURLs = nil + return generateInfoJSON(&info) +} + +func (c *client) sendErr(err string) { + c.mu.Lock() + if c.trace { + c.traceOutOp("-ERR", []byte(err)) + } + if !c.isMqtt() { + c.enqueueProto([]byte(fmt.Sprintf(errProto, err))) + } + c.mu.Unlock() +} + +func (c *client) sendOK() { + c.mu.Lock() + if c.trace { + c.traceOutOp("OK", nil) + } + c.enqueueProto([]byte(okProto)) + c.mu.Unlock() +} + +func (c *client) processPing() { + c.mu.Lock() + + if c.isClosed() { + c.mu.Unlock() + return + } + + c.sendPong() + + // Record this to suppress us sending one if this + // is within a given time interval for activity. + c.lastIn = time.Now() + + // If not a CLIENT, we are done. Also the CONNECT should + // have been received, but make sure it is so before proceeding + if c.kind != CLIENT || !c.flags.isSet(connectReceived) { + c.mu.Unlock() + return + } + + // If we are here, the CONNECT has been received so we know + // if this client supports async INFO or not. + var ( + sendConnectInfo bool + srv = c.srv + ) + // For the first PING (so firstPongSet is false) and for clients + // that support async INFO protocols, we will send one with ConnectInfo=true, + // the name of the account the client is bound to, and if the + // account is the system account. + if !c.flags.isSet(firstPongSent) { + // Flip the flag. + c.flags.set(firstPongSent) + // Evaluate if we should send the INFO protocol. + sendConnectInfo = srv != nil && c.opts.Protocol >= ClientProtoInfo + } + c.mu.Unlock() + + if sendConnectInfo { + srv.mu.Lock() + info := srv.copyInfo() + c.mu.Lock() + // Keep the in-process tls_required override from the initial INFO, + // otherwise this async INFO would flip it back to true. + if c.iproc && info.TLSRequired && !c.flags.isSet(didTLSFirst) { + info.TLSRequired = false + info.TLSAvailable = true + } + info.RemoteAccount = c.acc.Name + info.IsSystemAccount = c.acc == srv.SystemAccount() + info.ConnectInfo = true + c.enqueueProto(c.generateClientInfoJSON(info, true)) + c.mu.Unlock() + srv.mu.Unlock() + } +} + +func (c *client) processPong() { + c.mu.Lock() + c.ping.out = 0 + c.rtt = computeRTT(c.rttStart) + srv := c.srv + reorderGWs := c.kind == GATEWAY && c.gw.outbound + var ri *routeInfo + // For a route with pooling, we may be instructed to start a new route. + if c.kind == ROUTER && c.route != nil && c.route.startNewRoute != nil { + ri = c.route.startNewRoute + c.route.startNewRoute = nil + } + // If compression is currently active for a route/leaf connection, if the + // compression configuration is s2_auto, check if we should change + // the compression level. + if c.kind == ROUTER && needsCompression(c.route.compression) { + c.updateS2AutoCompressionLevel(&srv.getOpts().Cluster.Compression, &c.route.compression) + } else if c.kind == LEAF && needsCompression(c.leaf.compression) { + var co *CompressionOpts + if r := c.leaf.remote; r != nil { + co = &r.Compression + } else { + co = &srv.getOpts().LeafNode.Compression + } + c.updateS2AutoCompressionLevel(co, &c.leaf.compression) + } + c.mu.Unlock() + if reorderGWs { + srv.gateway.orderOutboundConnections() + } + if ri != nil { + srv.startGoRoutine(func() { + srv.connectToRoute(ri.url, ri.rtype, true, ri.gossipMode, _EMPTY_) + }) + } +} + +// Select the s2 compression level based on the client's current RTT and the configured +// RTT thresholds slice. If current level is different than selected one, save the +// new compression level string and create a new s2 writer. +// Lock held on entry. +func (c *client) updateS2AutoCompressionLevel(co *CompressionOpts, compression *string) { + if co.Mode != CompressionS2Auto { + return + } + if cm := selectS2AutoModeBasedOnRTT(c.rtt, co.RTTThresholds); cm != *compression { + *compression = cm + c.out.cw = s2.NewWriter(nil, s2WriterOptions(cm)...) + } +} + +// Will return the parts from the raw wire msg. +// We return the `hdr` as a slice that is capped to the length of the headers +// so that if the caller later tries to append to the returned header slice it +// does not affect the message content. +func (c *client) msgParts(data []byte) (hdr []byte, msg []byte) { + if c != nil && c.pa.hdr > 0 { + return data[:c.pa.hdr:c.pa.hdr], data[c.pa.hdr:] + } + return nil, data +} + +// Header pubs take form HPUB [reply] \r\n +func (c *client) processHeaderPub(arg, remaining []byte) error { + if !c.headers { + return ErrMsgHeadersNotSupported + } + + // Unroll splitArgs to avoid runtime/heap issues + a := [MAX_HPUB_ARGS][]byte{} + args := a[:0] + start := -1 + for i, b := range arg { + switch b { + case ' ', '\t': + if start >= 0 { + args = append(args, arg[start:i]) + start = -1 + } + default: + if start < 0 { + start = i + } + } + } + if start >= 0 { + args = append(args, arg[start:]) + } + + c.pa.arg = arg + switch len(args) { + case 3: + c.pa.subject = args[0] + c.pa.reply = nil + c.pa.hdr = parseSize(args[1]) + c.pa.size = parseSize(args[2]) + c.pa.hdb = args[1] + c.pa.szb = args[2] + case 4: + c.pa.subject = args[0] + c.pa.reply = args[1] + c.pa.hdr = parseSize(args[2]) + c.pa.size = parseSize(args[3]) + c.pa.hdb = args[2] + c.pa.szb = args[3] + default: + return fmt.Errorf("processHeaderPub Parse Error: %q", arg) + } + if c.pa.hdr < 0 { + return fmt.Errorf("processHeaderPub Bad or Missing Header Size: %q", arg) + } + // If number overruns an int64, parseSize() will have returned a negative value + if c.pa.size < 0 { + return fmt.Errorf("processHeaderPub Bad or Missing Total Size: %q", arg) + } + if c.pa.hdr > c.pa.size { + return fmt.Errorf("processHeaderPub Header Size larger then TotalSize: %q", arg) + } + maxPayload := atomic.LoadInt32(&c.mpay) + // Use int64() to avoid int32 overrun... + if maxPayload != jwt.NoLimit && int64(c.pa.size) > int64(maxPayload) { + // If we are given the remaining read buffer (since we do blind reads + // we may have the beginning of the message header/payload), we will + // look for the tracing header and if found, we will generate a + // trace event with the max payload ingress error. + // Do this only for CLIENT connections. + if c.kind == CLIENT && c.pa.hdr > 0 && len(remaining) > 0 { + hdr := remaining[:min(len(remaining), c.pa.hdr)] + c.sendMsgTraceIngressErrEvent(hdr, ErrMaxPayload) + } + c.maxPayloadViolation(c.pa.size, maxPayload) + return ErrMaxPayload + } + if c.opts.Pedantic && !IsValidLiteralSubject(bytesToString(c.pa.subject)) { + c.sendErr("Invalid Publish Subject") + } + return nil +} + +func (c *client) processPub(arg []byte) error { + // Unroll splitArgs to avoid runtime/heap issues + a := [MAX_PUB_ARGS][]byte{} + args := a[:0] + start := -1 + for i, b := range arg { + switch b { + case ' ', '\t': + if start >= 0 { + args = append(args, arg[start:i]) + start = -1 + } + default: + if start < 0 { + start = i + } + } + } + if start >= 0 { + args = append(args, arg[start:]) + } + + c.pa.arg = arg + switch len(args) { + case 2: + c.pa.subject = args[0] + c.pa.reply = nil + c.pa.size = parseSize(args[1]) + c.pa.szb = args[1] + case 3: + c.pa.subject = args[0] + c.pa.reply = args[1] + c.pa.size = parseSize(args[2]) + c.pa.szb = args[2] + default: + return fmt.Errorf("processPub Parse Error: %q", arg) + } + // If number overruns an int64, parseSize() will have returned a negative value + if c.pa.size < 0 { + return fmt.Errorf("processPub Bad or Missing Size: %q", arg) + } + maxPayload := atomic.LoadInt32(&c.mpay) + // Use int64() to avoid int32 overrun... + if maxPayload != jwt.NoLimit && int64(c.pa.size) > int64(maxPayload) { + c.maxPayloadViolation(c.pa.size, maxPayload) + return ErrMaxPayload + } + if c.opts.Pedantic && !IsValidLiteralSubject(bytesToString(c.pa.subject)) { + c.sendErr("Invalid Publish Subject") + } + return nil +} + +func splitArg(arg []byte) [][]byte { + a := [MAX_MSG_ARGS][]byte{} + args := a[:0] + start := -1 + for i, b := range arg { + switch b { + case ' ', '\t', '\r', '\n': + if start >= 0 { + args = append(args, arg[start:i]) + start = -1 + } + default: + if start < 0 { + start = i + } + } + } + if start >= 0 { + args = append(args, arg[start:]) + } + return args +} + +func (c *client) parseSub(argo []byte, noForward bool) error { + // Copy so we do not reference a potentially large buffer + // FIXME(dlc) - make more efficient. + arg := make([]byte, len(argo)) + copy(arg, argo) + args := splitArg(arg) + var ( + subject []byte + queue []byte + sid []byte + ) + switch len(args) { + case 2: + subject = args[0] + queue = nil + sid = args[1] + case 3: + subject = args[0] + queue = args[1] + sid = args[2] + default: + return fmt.Errorf("processSub Parse Error: %q", arg) + } + // If there was an error, it has been sent to the client. We don't return an + // error here to not close the connection as a parsing error. + c.processSub(subject, queue, sid, nil, noForward) + return nil +} + +func (c *client) processSub(subject, queue, bsid []byte, cb msgHandler, noForward bool) (*subscription, error) { + return c.processSubEx(subject, queue, bsid, cb, noForward, false, false) +} + +func (c *client) processSubEx(subject, queue, bsid []byte, cb msgHandler, noForward, si, rsi bool) (*subscription, error) { + // Create the subscription + sub := &subscription{client: c, subject: subject, queue: queue, sid: bsid, icb: cb, si: si, rsi: rsi} + + c.mu.Lock() + + // Indicate activity. + c.in.subs++ + + // Grab connection type, account and server info. + kind := c.kind + acc := c.acc + srv := c.srv + + sid := bytesToString(sub.sid) + + // This check does not apply to SYSTEM or JETSTREAM or ACCOUNT clients (because they don't have a `nc`...) + // When a connection is closed though, we set c.subs to nil. So check for the map to not be nil. + if (c.isClosed() && !isInternalClient(kind)) || (c.subs == nil) { + c.mu.Unlock() + return nil, ErrConnectionClosed + } + + // Check permissions if applicable. + if kind == CLIENT { + // First do a pass whether queue subscription is valid. This does not necessarily + // mean that it will not be able to plain subscribe. + // + // allow = ["foo"] -> can subscribe or queue subscribe to foo using any queue + // allow = ["foo v1"] -> can only queue subscribe to 'foo v1', no plain subs allowed. + // allow = ["foo", "foo v1"] -> can subscribe to 'foo' but can only queue subscribe to 'foo v1' + // + if sub.queue != nil { + if !c.canSubscribe(string(sub.subject), string(sub.queue)) || string(sub.queue) == sysGroup { + c.mu.Unlock() + c.subPermissionViolation(sub) + return nil, ErrSubscribePermissionViolation + } + } else if !c.canSubscribe(string(sub.subject)) { + c.mu.Unlock() + c.subPermissionViolation(sub) + return nil, ErrSubscribePermissionViolation + } + + if opts := srv.getOpts(); opts != nil && opts.MaxSubTokens > 0 { + if len(bytes.Split(sub.subject, []byte(tsep))) > int(opts.MaxSubTokens) { + c.mu.Unlock() + c.maxTokensViolation(sub) + return nil, ErrTooManySubTokens + } + } + } + + // Check if we have a maximum on the number of subscriptions. + if c.subsAtLimit() { + c.mu.Unlock() + c.maxSubsExceeded() + return nil, ErrTooManySubs + } + + var updateGWs bool + var err error + + // Subscribe here. + es := c.subs[sid] + if es == nil { + c.subs[sid] = sub + if acc != nil && acc.sl != nil { + err = acc.sl.Insert(sub) + if err != nil { + delete(c.subs, sid) + } else { + updateGWs = c.srv.gateway.enabled + } + } + } + // Unlocked from here onward + c.mu.Unlock() + + if err != nil { + c.sendErr("Invalid Subject") + return nil, ErrMalformedSubject + } else if c.opts.Verbose && kind != SYSTEM { + c.sendOK() + } + + // If it was already registered, return it. + if es != nil { + return es, nil + } + + // No account just return. + if acc == nil { + return sub, nil + } + + if err := c.addShadowSubscriptions(acc, sub); err != nil { + c.Errorf(err.Error()) + } + + if noForward { + return sub, nil + } + + // If we are routing and this is a local sub, add to the route map for the associated account. + if kind == CLIENT || kind == SYSTEM || kind == JETSTREAM || kind == ACCOUNT { + srv.updateRouteSubscriptionMap(acc, sub, 1) + if updateGWs { + srv.gatewayUpdateSubInterest(acc.Name, sub, 1) + } + } + // Now check on leafnode updates. + acc.updateLeafNodes(sub, 1) + return sub, nil +} + +// Used to pass stream import matches to addShadowSub +type ime struct { + im *streamImport + overlapSubj string + dyn bool +} + +// If the client's account has stream imports and there are matches for this +// subscription's subject, then add shadow subscriptions in the other accounts +// that export this subject. +func (c *client) addShadowSubscriptions(acc *Account, sub *subscription) error { + if acc == nil { + return ErrMissingAccount + } + + var ( + _ims [16]ime + ims = _ims[:0] + imTsa [32]string + tokens []string + tsa [32]string + hasWC bool + tokensModified bool + ) + + acc.mu.RLock() + // If this is from a service import, ignore. + if sub.si { + acc.mu.RUnlock() + return nil + } + subj := bytesToString(sub.subject) + if len(acc.imports.streams) > 0 { + tokens = tokenizeSubjectIntoSlice(tsa[:0], subj) + for _, tk := range tokens { + if tk == pwcs { + hasWC = true + break + } + } + if !hasWC && tokens[len(tokens)-1] == fwcs { + hasWC = true + } + } + // Loop over the import subjects. We have 4 scenarios. If we have an + // exact match or a superset match we should use the from field from + // the import. If we are a subset or overlap, we have to dynamically calculate + // the subject. On overlap, ime requires the overlap subject. + for _, im := range acc.imports.streams { + if im.invalid { + continue + } + if subj == im.to { + ims = append(ims, ime{im, _EMPTY_, false}) + continue + } + if tokensModified { + // re-tokenize subj to overwrite modifications from a previous iteration + tokens = tokenizeSubjectIntoSlice(tsa[:0], subj) + tokensModified = false + } + imTokens := tokenizeSubjectIntoSlice(imTsa[:0], im.to) + + if isSubsetMatchTokenized(tokens, imTokens) { + ims = append(ims, ime{im, _EMPTY_, true}) + } else if hasWC { + if isSubsetMatchTokenized(imTokens, tokens) { + ims = append(ims, ime{im, _EMPTY_, false}) + } else { + imTokensLen := len(imTokens) + for i, t := range tokens { + if i >= imTokensLen { + break + } + if t == pwcs && imTokens[i] != fwcs { + tokens[i] = imTokens[i] + tokensModified = true + } + } + tokensLen := len(tokens) + lastIdx := tokensLen - 1 + if tokens[lastIdx] == fwcs { + if imTokensLen >= tokensLen { + // rewrite ">" in tokens to be more specific + tokens[lastIdx] = imTokens[lastIdx] + tokensModified = true + if imTokensLen > tokensLen { + // copy even more specific parts from import + tokens = append(tokens, imTokens[tokensLen:]...) + } + } + } + if isSubsetMatchTokenized(tokens, imTokens) { + // As isSubsetMatchTokenized was already called with tokens and imTokens, + // we wouldn't be here if it where not for tokens being modified. + // Hence, Join to re compute the subject string + ims = append(ims, ime{im, strings.Join(tokens, tsep), true}) + } + } + } + } + acc.mu.RUnlock() + + var shadow []*subscription + + if len(ims) > 0 { + shadow = make([]*subscription, 0, len(ims)) + } + + // Now walk through collected stream imports that matched. + for i := 0; i < len(ims); i++ { + ime := &ims[i] + // We will create a shadow subscription. + nsub, err := c.addShadowSub(sub, ime) + if err != nil { + return err + } + shadow = append(shadow, nsub) + } + + if shadow != nil { + c.mu.Lock() + sub.shadow = shadow + c.mu.Unlock() + } + + return nil +} + +// Add in the shadow subscription. +func (c *client) addShadowSub(sub *subscription, ime *ime) (*subscription, error) { + c.mu.Lock() + nsub := *sub // copy + c.mu.Unlock() + + im := ime.im + nsub.im = im + + if !im.usePub && ime.dyn && im.tr != nil { + if im.rtr == nil { + im.rtr = im.tr.reverse() + } + s := bytesToString(nsub.subject) + if ime.overlapSubj != _EMPTY_ { + s = ime.overlapSubj + } + subj := im.rtr.TransformSubject(s) + + nsub.subject = []byte(subj) + } else if !im.usePub || (im.usePub && ime.overlapSubj != _EMPTY_) || !ime.dyn { + if ime.overlapSubj != _EMPTY_ { + nsub.subject = []byte(ime.overlapSubj) + } else { + nsub.subject = []byte(im.from) + } + } + // Else use original subject + + c.Debugf("Creating import subscription on %q from account %q", nsub.subject, im.acc.Name) + + if err := im.acc.sl.Insert(&nsub); err != nil { + errs := fmt.Sprintf("Could not add shadow import subscription for account %q", im.acc.Name) + c.Debugf(errs) + return nil, errors.New(errs) + } + + // Update our route map here. But only if we are not a leaf node or a hub leafnode. + if c.kind != LEAF || c.isHubLeafNode() { + c.srv.updateRemoteSubscription(im.acc, &nsub, 1) + } else if c.kind == LEAF { + // Update all leafnodes that connect to this server. Note that we could have + // used the updateLeafNodes() function since when it does invoke updateSmap() + // this function already takes care of not sending to a spoke leafnode since + // the `nsub` here is already from a spoke leafnode, but to be explicit, we + // use this version that updates only leafnodes that connect to this server. + im.acc.updateLeafNodesEx(&nsub, 1, true) + } + + return &nsub, nil +} + +// canSubscribeInternal determines if the client is authorized to subscribe to +// the given subject. Assumes caller is holding at least a read lock. +func (c *client) canSubscribeInternal(subject string, optQueue ...string) bool { + if c.perms == nil { + return true + } + + allowed, checkAllow := true, true + + // Optional queue group. + var queue string + if len(optQueue) > 0 { + queue = optQueue[0] + } + + // For CLIENT connections that are MQTT we will implicitly allow anything that starts with + // the "$MQTT.sub." or "$MQTT.deliver.pubrel." prefix. For other types of connections, we + // will implicitly allow anything that starts with the full "$MQTT." prefix. However, + // we don't just return here, we skip the check for "allow" but will check "deny". + if (c.isMqtt() && (strings.HasPrefix(subject, mqttSubPrefix) || strings.HasPrefix(subject, mqttPubRelDeliverySubjectPrefix))) || + (c.kind != CLIENT && strings.HasPrefix(subject, mqttPrefix)) { + checkAllow = false + } + // Check allow list. If no allow list that means all are allowed. Deny can overrule. + if checkAllow && c.perms.sub.allow != nil { + r := c.perms.sub.allow.Match(subject) + allowed = len(r.psubs) > 0 + if queue != _EMPTY_ && len(r.qsubs) > 0 { + // If the queue appears in the allow list, then DO allow. + allowed = queueMatches(queue, r.qsubs) + } + // Leafnodes operate slightly differently in that they allow broader scoped subjects. + // They will prune based on publish perms before sending to a leafnode client. + if !allowed && c.kind == LEAF && subjectHasWildcard(subject) { + r := c.perms.sub.allow.ReverseMatch(subject) + allowed = len(r.psubs) != 0 + } + } + // If we have a deny list and we think we are allowed, check that as well. + if allowed && c.perms.sub.deny != nil { + r := c.perms.sub.deny.Match(subject) + allowed = len(r.psubs) == 0 + + if allowed && queue != _EMPTY_ && len(r.qsubs) > 0 { + // If the queue appears in the deny list, then DO NOT allow. + allowed = !queueMatches(queue, r.qsubs) + } + } + return allowed +} + +// canSubscribe determines if the client is authorized to subscribe to the +// given subject and initializes the delivery-time deny filter when needed. +// Assumes caller is holding the write lock. +func (c *client) canSubscribe(subject string, optQueue ...string) bool { + if !c.canSubscribeInternal(subject, optQueue...) { + return false + } + c.loadMsgDenyFilterIfNeeded(subject, len(optQueue) > 0 && optQueue[0] != _EMPTY_) + return true +} + +// Initializes the delivery-time deny filter when a wildcard subscription, or +// an exact queue subscription, can overlap a deny entry. Assumes caller is +// holding the write lock. +func (c *client) loadMsgDenyFilterIfNeeded(subject string, hasQueue bool) { + // We use the actual subscription to signal us to spin up the deny mperms + // and cache. We check if the subject is a wildcard that intersects any of + // the deny clauses. + // FIXME(dlc) - We could be smarter and track when these go away and remove. + if c.mperms == nil && (hasQueue || subjectHasWildcard(subject)) { + // Whip through the deny array and check if this wildcard subject can + // overlap with any denied deliveries. + for _, sub := range c.darray { + if SubjectsCollide(bytesToString(sub.subject), subject) { + c.loadMsgDenyFilter() + break + } + } + } +} + +func queueMatches(queue string, qsubs [][]*subscription) bool { + if len(qsubs) == 0 { + return true + } + for _, qsub := range qsubs { + qs := qsub[0] + qname := bytesToString(qs.queue) + + // NOTE: '*' and '>' tokens can also be valid + // queue names so we first check against the + // literal name. e.g. v1.* == v1.* + if queue == qname || (subjectHasWildcard(qname) && subjectIsSubsetMatch(queue, qname)) { + return true + } + } + return false +} + +// Low level unsubscribe for a given client. +func (c *client) unsubscribe(acc *Account, sub *subscription, force, remove bool) { + if s := c.srv; s != nil && s.isShuttingDown() { + return + } + + c.mu.Lock() + if !force && sub.max > 0 && sub.nm < sub.max { + c.Debugf("Deferring actual UNSUB(%s): %d max, %d received", sub.subject, sub.max, sub.nm) + c.mu.Unlock() + return + } + + if c.trace { + c.traceOp("<-> %s", "DELSUB", sub.sid) + } + + // Remove accounting if requested. This will be false when we close a connection + // with open subscriptions. + if remove { + delete(c.subs, bytesToString(sub.sid)) + if acc != nil { + acc.sl.Remove(sub) + } + } + + // Check to see if we have shadow subscriptions. + var updateRoute bool + var isSpokeLeaf bool + shadowSubs := sub.shadow + sub.shadow = nil + if len(shadowSubs) > 0 { + isSpokeLeaf = c.isSpokeLeafNode() + updateRoute = !isSpokeLeaf && (c.kind == CLIENT || c.kind == SYSTEM || c.kind == LEAF || c.kind == JETSTREAM) && c.srv != nil + } + sub.close() + c.mu.Unlock() + + // Process shadow subs if we have them. + for _, nsub := range shadowSubs { + if err := nsub.im.acc.sl.Remove(nsub); err != nil { + c.Debugf("Could not remove shadow import subscription for account %q", nsub.im.acc.Name) + } + if updateRoute { + c.srv.updateRemoteSubscription(nsub.im.acc, nsub, -1) + } else if isSpokeLeaf { + nsub.im.acc.updateLeafNodesEx(nsub, -1, true) + } + } + + // Now check to see if this was part of a respMap entry for service imports. + // We can skip subscriptions on reserved replies. + if acc != nil && !isReservedReply(sub.subject) { + acc.checkForReverseEntry(string(sub.subject), nil, true) + } +} + +func (c *client) processUnsub(arg []byte) error { + args := splitArg(arg) + var sid []byte + max := int64(-1) + + switch len(args) { + case 1: + sid = args[0] + case 2: + sid = args[0] + max = int64(parseSize(args[1])) + default: + return fmt.Errorf("processUnsub Parse Error: %q", arg) + } + + var sub *subscription + var ok, unsub bool + + c.mu.Lock() + + // Indicate activity. + c.in.subs++ + + // Grab connection type. + kind := c.kind + srv := c.srv + var acc *Account + + updateGWs := false + if sub, ok = c.subs[string(sid)]; ok { + acc = c.acc + if max > 0 && max > sub.nm { + sub.max = max + } else { + // Clear it here to override + sub.max = 0 + unsub = true + } + updateGWs = srv.gateway.enabled + } + c.mu.Unlock() + + if c.opts.Verbose { + c.sendOK() + } + + if unsub { + c.unsubscribe(acc, sub, false, true) + if acc != nil && (kind == CLIENT || kind == SYSTEM || kind == ACCOUNT || kind == JETSTREAM) { + srv.updateRouteSubscriptionMap(acc, sub, -1) + if updateGWs { + srv.gatewayUpdateSubInterest(acc.Name, sub, -1) + } + } + // Now check on leafnode updates. + acc.updateLeafNodes(sub, -1) + } + + return nil +} + +// checkDenySub will check if we are allowed to deliver this message in the +// presence of deny clauses for subscriptions. Deny clauses will not prevent +// larger scoped wildcard subscriptions, so we need to check at delivery time. +// Lock should be held. +func (c *client) checkDenySub(subject, queue string) bool { + key := msgDenyKey{subject, queue} + if denied, ok := c.mperms.dcache[key]; ok { + return denied + } + r := c.mperms.deny.Match(subject) + denied := len(r.psubs) != 0 + if !denied && queue != _EMPTY_ && len(r.qsubs) != 0 { + denied = queueMatches(queue, r.qsubs) + } + c.mperms.dcache[key] = denied + if len(c.mperms.dcache) > maxDenyPermCacheSize { + c.pruneDenyCache() + } + return denied +} + +// Create a message header for routes or leafnodes. Header and origin cluster aware. +func (c *client) msgHeaderForRouteOrLeaf(subj, reply []byte, rt *routeTarget, acc *Account) []byte { + hasHeader := c.pa.hdr > 0 + subclient := rt.sub.client + canReceiveHeader := subclient.headers + + mh := c.msgb[:msgHeadProtoLen] + kind := subclient.kind + var lnoc bool + + if kind == ROUTER { + // If we are coming from a leaf with an origin cluster we need to handle differently + // if we can. We will send a route based LMSG which has origin cluster and headers + // by default. + if c.kind == LEAF && c.remoteCluster() != _EMPTY_ { + subclient.mu.Lock() + lnoc = subclient.route.lnoc + subclient.mu.Unlock() + } + if lnoc { + mh[0] = 'L' + mh = append(mh, c.remoteCluster()...) + mh = append(mh, ' ') + } else { + // Router (and Gateway) nodes are RMSG. Set here since leafnodes may rewrite. + mh[0] = 'R' + } + if len(subclient.route.accName) == 0 { + mh = append(mh, acc.Name...) + mh = append(mh, ' ') + } + } else { + // Leaf nodes are LMSG + mh[0] = 'L' + // Remap subject if its a shadow subscription, treat like a normal client. + if rt.sub.im != nil { + if rt.sub.im.tr != nil { + to := rt.sub.im.tr.TransformSubject(bytesToString(subj)) + subj = []byte(to) + } else if !rt.sub.im.usePub { + subj = []byte(rt.sub.im.to) + } + } + } + mh = append(mh, subj...) + mh = append(mh, ' ') + + if len(rt.qs) > 0 { + if len(reply) > 0 { + mh = append(mh, "+ "...) // Signal that there is a reply. + mh = append(mh, reply...) + mh = append(mh, ' ') + } else { + mh = append(mh, "| "...) // Only queues + } + mh = append(mh, rt.qs...) + } else if len(reply) > 0 { + mh = append(mh, reply...) + mh = append(mh, ' ') + } + + if lnoc { + // leafnode origin LMSG always have a header entry even if zero. + if c.pa.hdr <= 0 { + mh = append(mh, '0') + } else { + mh = append(mh, c.pa.hdb...) + } + mh = append(mh, ' ') + mh = append(mh, c.pa.szb...) + } else if hasHeader { + if canReceiveHeader { + mh[0] = 'H' + mh = append(mh, c.pa.hdb...) + mh = append(mh, ' ') + mh = append(mh, c.pa.szb...) + } else { + // If we are here we need to truncate the payload size + nsz := strconv.Itoa(c.pa.size - c.pa.hdr) + mh = append(mh, nsz...) + } + } else { + mh = append(mh, c.pa.szb...) + } + return append(mh, _CRLF_...) +} + +// Create a message header for clients. Header aware. +func (c *client) msgHeader(subj, reply []byte, sub *subscription) []byte { + // See if we should do headers. We have to have a headers msg and + // the client we are going to deliver to needs to support headers as well. + hasHeader := c.pa.hdr > 0 + canReceiveHeader := sub.client != nil && sub.client.headers + + var mh []byte + if hasHeader && canReceiveHeader { + mh = c.msgb[:msgHeadProtoLen] + mh[0] = 'H' + } else { + mh = c.msgb[1:msgHeadProtoLen] + } + mh = append(mh, subj...) + mh = append(mh, ' ') + + if len(sub.sid) > 0 { + mh = append(mh, sub.sid...) + mh = append(mh, ' ') + } + if reply != nil { + mh = append(mh, reply...) + mh = append(mh, ' ') + } + if hasHeader { + if canReceiveHeader { + mh = append(mh, c.pa.hdb...) + mh = append(mh, ' ') + mh = append(mh, c.pa.szb...) + } else { + // If we are here we need to truncate the payload size + nsz := strconv.Itoa(c.pa.size - c.pa.hdr) + mh = append(mh, nsz...) + } + } else { + mh = append(mh, c.pa.szb...) + } + mh = append(mh, _CRLF_...) + return mh +} + +func (c *client) stalledWait(producer *client) { + // Check to see if we have exceeded our total wait time per readLoop invocation. + if producer.in.tst > stallTotalAllowed { + return + } + + // Grab stall channel which the slow consumer will close when caught up. + stall := c.out.stc + + // Calculate stall time. + ttl := stallClientMinDuration + if c.out.pb >= c.out.mp { + ttl = stallClientMaxDuration + } + + c.mu.Unlock() + defer c.mu.Lock() + + // Track per client and total client stalls. + atomic.AddInt64(&c.stalls, 1) + if c.srv != nil { + atomic.AddInt64(&c.srv.stalls, 1) + } + + // Now check if we are close to total allowed. + if producer.in.tst+ttl > stallTotalAllowed { + ttl = stallTotalAllowed - producer.in.tst + } + delay := time.NewTimer(ttl) + defer delay.Stop() + + start := time.Now() + select { + case <-stall: + case <-delay.C: + producer.Debugf("Timed out of fast producer stall (%v)", ttl) + } + producer.in.tst += time.Since(start) +} + +// Used to treat maps as efficient set +var needFlush = struct{}{} + +// deliverMsg will deliver a message to a matching subscription and its underlying client. +// We process all connection/client types. mh is the part that will be protocol/client specific. +func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, subject, reply, mh, msg []byte, gwrply bool) bool { + // Check if message tracing is enabled. + mt, traceOnly := c.isMsgTraceEnabled() + + client := sub.client + // Check sub client and check echo. Only do this if not a service import. + if client == nil || (c == client && !client.echo && !sub.si && !sub.rsi) { + if client != nil && mt != nil { + client.mu.Lock() + mt.addEgressEvent(client, sub, errMsgTraceNoEcho) + client.mu.Unlock() + } + return false + } + + client.mu.Lock() + + // Check if we have a subscribe deny clause. This will trigger us to check the subject + // for a match against the denied subjects. + if client.mperms != nil && client.checkDenySub(string(subject), bytesToString(sub.queue)) { + mt.addEgressEvent(client, sub, errMsgTraceSubDeny) + client.mu.Unlock() + return false + } + + // New race detector forces this now. + if sub.isClosed() { + mt.addEgressEvent(client, sub, errMsgTraceSubClosed) + client.mu.Unlock() + return false + } + + // Check if we are a leafnode and have perms to check. + if client.kind == LEAF && client.perms != nil { + subjectToCheck, _ := getGWRoutedSubjectOrSelf(subject) + if !client.pubAllowedFullCheck(string(subjectToCheck), true, true) { + mt.addEgressEvent(client, sub, errMsgTracePubViolation) + client.mu.Unlock() + client.Debugf("Not permitted to deliver to %q", subjectToCheck) + return false + } + } + + var mtErr string + if mt != nil { + // For non internal subscription, and if the remote does not support + // the tracing feature... + if sub.icb == nil && !client.msgTraceSupport() { + if traceOnly { + // We are not sending the message at all because the user + // expects a trace-only and the remote does not support + // tracing, which means that it would process/deliver this + // message, which may break applications. + // Add the Egress with the no-support error message. + mt.addEgressEvent(client, sub, errMsgTraceOnlyNoSupport) + client.mu.Unlock() + return false + } + // If we are doing delivery, we will still forward the message, + // but we add an error to the Egress event to hint that one should + // not expect a tracing event from that remote. + mtErr = errMsgTraceNoSupport + } + // For ROUTER, GATEWAY and LEAF, even if we intend to do tracing only, + // we will still deliver the message. The remote side will + // generate an event based on what happened on that server. + if traceOnly && (client.kind == ROUTER || client.kind == GATEWAY || client.kind == LEAF) { + traceOnly = false + } + // If we skip delivery and this is not for a service import, we are done. + if traceOnly && (sub.icb == nil || c.noIcb) { + mt.addEgressEvent(client, sub, _EMPTY_) + client.mu.Unlock() + // Although the message is not actually delivered, for the + // purpose of "didDeliver", we need to return "true" here. + return true + } + } + + srv := client.srv + + // We don't want to bump the number of delivered messages to the subscription + // if we are doing trace-only (since really we are not sending it to the sub). + if !traceOnly { + sub.nm++ + } + + // Check if we should auto-unsubscribe. + if sub.max > 0 { + if client.kind == ROUTER && sub.nm >= sub.max { + // The only router based messages that we will see here are remoteReplies. + // We handle these slightly differently. + defer client.removeReplySub(sub) + } else { + // For routing.. + shouldForward := client.kind == CLIENT || client.kind == SYSTEM && client.srv != nil + // If we are at the exact number, unsubscribe but + // still process the message in hand, otherwise + // unsubscribe and drop message on the floor. + if sub.nm == sub.max { + client.Debugf("Auto-unsubscribe limit of %d reached for sid '%s'", sub.max, sub.sid) + // Due to defer, reverse the code order so that execution + // is consistent with other cases where we unsubscribe. + if shouldForward { + defer srv.updateRemoteSubscription(client.acc, sub, -1) + } + defer client.unsubscribe(client.acc, sub, true, true) + } else if sub.nm > sub.max { + client.Debugf("Auto-unsubscribe limit [%d] exceeded", sub.max) + mt.addEgressEvent(client, sub, errMsgTraceAutoSubExceeded) + client.mu.Unlock() + client.unsubscribe(client.acc, sub, true, true) + if shouldForward { + srv.updateRemoteSubscription(client.acc, sub, -1) + } + return false + } + } + } + + // Check here if we have a header with our message. If this client can not + // support we need to strip the headers from the payload. + // The actual header would have been processed correctly for us, so just + // need to update payload. + hdrSize := c.pa.hdr + if c.pa.hdr > 0 && !sub.client.headers { + msg = msg[c.pa.hdr:] + } + + // Update statistics + + // The msg includes the CR_LF, so pull back out for accounting. + msgSize := int64(len(msg)) + // MQTT producers send messages without CR_LF, so don't remove it for them. + if !prodIsMQTT { + msgSize -= int64(LEN_CR_LF) + } + + // We do not update the outbound stats if we are doing trace only since + // this message will not be sent out. + // Also do not update on internal callbacks. + if !traceOnly && sub.icb == nil { + // No atomic needed since accessed under client lock. + // Monitor is reading those also under client's lock. + client.outMsgs++ + client.outBytes += msgSize + } + + // Check for internal subscriptions. + if sub.icb != nil && !c.noIcb { + if gwrply { + // We will store in the account, not the client since it will likely + // be a different client that will send the reply. + srv.trackGWReply(nil, client.acc, reply, c.pa.reply) + } + client.mu.Unlock() + + // For service imports, track if we delivered. + didDeliver := true + + // Internal account clients are for service imports and need the '\r\n'. + start := time.Now() + if client.kind == ACCOUNT { + sub.icb(sub, c, acc, string(subject), string(reply), msg) + // If we are a service import check to make sure we delivered the message somewhere. + if sub.si { + didDeliver = c.pa.delivered + } + } else { + sub.icb(sub, c, acc, string(subject), string(reply), msg[:msgSize]) + } + if dur := time.Since(start); dur >= readLoopReportThreshold { + srv.Warnf("Internal subscription on %q took too long: %v", subject, dur) + } + + return didDeliver + } + + // If we are a client and we detect that the consumer we are + // sending to is in a stalled state, go ahead and wait here + // with a limit. + if c.kind == CLIENT && client.out.stc != nil { + if srv.getOpts().NoFastProducerStall { + mt.addEgressEvent(client, sub, errMsgTraceFastProdNoStall) + client.mu.Unlock() + return false + } + client.stalledWait(c) + } + + // Check for closed connection + if client.isClosed() { + mt.addEgressEvent(client, sub, errMsgTraceClientClosed) + client.mu.Unlock() + return false + } + + // We have passed cases where we could possibly fail to deliver. + // Do not call for service-import. + if mt != nil && sub.icb == nil { + mt.addEgressEvent(client, sub, mtErr) + } + + // Do a fast check here to see if we should be tracking this from a latency + // perspective. This will be for a request being received for an exported service. + // This needs to be from a non-client (otherwise tracking happens at requestor). + // + // Also this check captures if the original reply (c.pa.reply) is a GW routed + // reply (since it is known to be > minReplyLen). If that is the case, we need to + // track the binding between the routed reply and the reply set in the message + // header (which is c.pa.reply without the GNR routing prefix). + if client.kind == CLIENT && len(c.pa.reply) > minReplyLen { + if gwrply { + // Note that we keep track of the GW routed reply in the destination + // connection (`client`). The routed reply subject is in `c.pa.reply`, + // should that change, we would have to pass the GW routed reply as + // a parameter of deliverMsg(). + srv.trackGWReply(client, nil, reply, c.pa.reply) + } + + // If we do not have a registered RTT queue that up now. + if client.rtt == 0 { + client.sendRTTPingLocked() + } + // FIXME(dlc) - We may need to optimize this. + // We will have tagged this with a suffix ('.T') if we are tracking. This is + // needed from sampling. Not all will be tracked. + if c.kind != CLIENT && isTrackedReply(c.pa.reply) { + client.trackRemoteReply(string(subject), string(c.pa.reply)) + } + } + + // Queue to outbound buffer + client.queueOutbound(mh) + client.queueOutbound(msg) + if prodIsMQTT { + // Need to add CR_LF since MQTT producers don't send CR_LF + client.queueOutbound([]byte(CR_LF)) + } + + // If we are tracking dynamic publish permissions that track reply subjects, + // do that accounting here. We only look at client.replies which will be non-nil. + // Only reply subject permissions if the client is not already allowed to publish to the reply subject. + if client.replies != nil && len(reply) > 0 && !client.pubAllowedFullCheck(string(reply), true, true) { + client.replies[string(reply)] = &resp{time.Now(), 0} + client.repliesSincePrune++ + if client.repliesSincePrune > replyPermLimit || time.Since(client.lastReplyPrune) > replyPruneTime { + client.pruneReplyPerms() + } + } + + // Check outbound threshold and queue IO flush if needed. + // This is specifically looking at situations where we are getting behind and may want + // to intervene before this producer goes back to top of readloop. We are in the producer's + // readloop go routine at this point. + // FIXME(dlc) - We may call this alot, maybe suppress after first call? + if len(client.out.nb) != 0 { + client.flushSignal() + } + + // Add the data size we are responsible for here. This will be processed when we + // return to the top of the readLoop. + c.addToPCD(client) + + if client.trace { + client.traceOutOp(bytesToString(mh[:len(mh)-LEN_CR_LF]), nil) + client.traceMsgDelivery(msg, hdrSize) + } + + client.mu.Unlock() + + return true +} + +// Add the given sub's client to the list of clients that need flushing. +// This must be invoked from `c`'s readLoop. No lock for c is required, +// however, `client` lock must be held on entry. This holds true even +// if `client` is same than `c`. +func (c *client) addToPCD(client *client) { + if _, ok := c.pcd[client]; !ok { + client.out.fsp++ + c.pcd[client] = needFlush + } +} + +// This will track a remote reply for an exported service that has requested +// latency tracking. +// Lock assumed to be held. +func (c *client) trackRemoteReply(subject, reply string) { + a := c.acc + if a == nil { + return + } + + var lrt time.Duration + var respThresh time.Duration + + a.mu.RLock() + se := a.getServiceExport(subject) + if se != nil { + lrt = a.lowestServiceExportResponseTime() + respThresh = se.respThresh + } + a.mu.RUnlock() + + if se == nil { + return + } + + if c.rrTracking == nil { + c.rrTracking = &rrTracking{ + rmap: make(map[string]*remoteLatency), + ptmr: time.AfterFunc(lrt, c.pruneRemoteTracking), + lrt: lrt, + } + } + rl := remoteLatency{ + Account: a.Name, + ReqId: reply, + respThresh: respThresh, + } + rl.M2.RequestStart = time.Now().UTC() + c.rrTracking.rmap[reply] = &rl +} + +// pruneRemoteTracking will prune any remote tracking objects +// that are too old. These are orphaned when a service is not +// sending reponses etc. +// Lock should be held upon entry. +func (c *client) pruneRemoteTracking() { + c.mu.Lock() + if c.rrTracking == nil { + c.mu.Unlock() + return + } + now := time.Now() + for subject, rl := range c.rrTracking.rmap { + if now.After(rl.M2.RequestStart.Add(rl.respThresh)) { + delete(c.rrTracking.rmap, subject) + } + } + if len(c.rrTracking.rmap) > 0 { + t := c.rrTracking.ptmr + t.Stop() + t.Reset(c.rrTracking.lrt) + } else { + c.rrTracking.ptmr.Stop() + c.rrTracking = nil + } + c.mu.Unlock() +} + +// pruneReplyPerms will remove any stale or expired entries +// in our reply cache. We make sure to not check too often. +func (c *client) pruneReplyPerms() { + // Make sure we do not check too often. + if c.perms.resp == nil { + return + } + + mm := c.perms.resp.MaxMsgs + ttl := c.perms.resp.Expires + now := time.Now() + + for k, resp := range c.replies { + if mm > 0 && resp.n >= mm { + delete(c.replies, k) + } else if ttl > 0 && now.Sub(resp.t) > ttl { + delete(c.replies, k) + } + } + + c.repliesSincePrune = 0 + c.lastReplyPrune = now +} + +// pruneDenyCache will prune the deny cache via randomly +// deleting items. Doing so pruneSize items at a time. +// Lock must be held for this one since it is shared under +// deliverMsg. +func (c *client) pruneDenyCache() { + r := 0 + for key := range c.mperms.dcache { + delete(c.mperms.dcache, key) + if r++; r > pruneSize { + break + } + } +} + +// prunePubPermsCache will prune the cache via randomly +// deleting items. Doing so pruneSize items at a time. +func (c *client) prunePubPermsCache() { + // With parallel additions to the cache, it is possible that this function + // would not be able to reduce the cache to its max size in one go. We + // will try a few times but will release/reacquire the "lock" at each + // attempt to give a chance to another go routine to take over and not + // have this go routine do too many attempts. + for i := 0; i < 5; i++ { + // There is a case where we can invoke this from multiple go routines, + // (in deliverMsg() if sub.client is a LEAF), so we make sure to prune + // from only one go routine at a time. + if !atomic.CompareAndSwapInt32(&c.perms.prun, 0, 1) { + return + } + const maxPruneAtOnce = 1000 + r := 0 + c.perms.pcache.Range(func(k, _ any) bool { + c.perms.pcache.Delete(k) + if r++; (r > pruneSize && atomic.LoadInt32(&c.perms.pcsz) < int32(maxPermCacheSize)) || + (r > maxPruneAtOnce) { + return false + } + return true + }) + n := atomic.AddInt32(&c.perms.pcsz, -int32(r)) + atomic.StoreInt32(&c.perms.prun, 0) + if n <= int32(maxPermCacheSize) { + return + } + } +} + +// pubAllowed checks on publish permissioning. +// Lock should not be held. +func (c *client) pubAllowed(subject string) bool { + return c.pubAllowedFullCheck(subject, true, false) +} + +// pubAllowedFullCheck checks on all publish permissioning depending +// on the flag for dynamic reply permissions. +func (c *client) pubAllowedFullCheck(subject string, fullCheck, hasLock bool) bool { + if c.perms == nil || (c.perms.pub.allow == nil && c.perms.pub.deny == nil) { + return true + } + // Check if published subject is allowed if we have permissions in place. + v, ok := c.perms.pcache.Load(subject) + if ok { + return v.(bool) + } + allowed, checkAllow := true, true + // For any connections, other than CLIENT, we will implicitly allow anything that + // starts with the "$MQTT." prefix. However, we don't just return here, + // we skip the check for "allow" but will check "deny". + if c.kind != CLIENT && strings.HasPrefix(subject, mqttPrefix) { + checkAllow = false + } + // Cache miss, check allow then deny as needed. + if checkAllow && c.perms.pub.allow != nil { + np, _ := c.perms.pub.allow.NumInterest(subject) + allowed = np != 0 + } + // If we have a deny list and are currently allowed, check that as well. + if allowed && c.perms.pub.deny != nil { + np, _ := c.perms.pub.deny.NumInterest(subject) + allowed = np == 0 + } + + // If we are tracking reply subjects + // dynamically, check to see if we are allowed here but avoid pcache. + // We need to acquire the lock though. + if !allowed && fullCheck && c.perms.resp != nil { + if !hasLock { + c.mu.Lock() + } + allowed = c.responseAllowed(subject) + if !hasLock { + c.mu.Unlock() + } + } else { + // Update our cache here. + c.perms.pcache.Store(subject, allowed) + if n := atomic.AddInt32(&c.perms.pcsz, 1); n > maxPermCacheSize { + c.prunePubPermsCache() + } + } + return allowed +} + +// Returns true if this subject matches a tracked dynamic reply permission. +// Lock must be held. +func (c *client) responseAllowed(subject string) bool { + if c.perms == nil || c.perms.resp == nil { + return false + } + if resp := c.replies[subject]; resp != nil { + resp.n++ + if c.perms.resp.MaxMsgs > 0 && resp.n > c.perms.resp.MaxMsgs { + delete(c.replies, subject) + } else if c.perms.resp.Expires > 0 && time.Since(resp.t) > c.perms.resp.Expires { + delete(c.replies, subject) + } else { + return true + } + } + return false +} + +// Test whether a reply subject is a service import reply. +func isServiceReply(reply []byte) bool { + // This function is inlined and checking this way is actually faster + // than byte-by-byte comparison. + return len(reply) > 3 && bytesToString(reply[:4]) == replyPrefix +} + +// Test whether a subject is a JetStream ACK. +func isJSAckSubject(subject []byte) bool { + return len(subject) > jsAckPreLen && bytesToString(subject[:jsAckPreLen]) == jsAckPre +} + +// jsAckDeliverIdx returns the byte offset of the `@` separator in an encoded +// `$JS.ACK....@` reply, or -1 if reply is not in that form. Stream, +// consumer, and subject tokens may legally contain `@`, so we accept only the +// first `@` that follows the eight dots of the JS ACK token: +// +// $JS.ACK.......@ +func jsAckDeliverIdx(reply []byte) int { + if !isJSAckSubject(reply) { + return -1 + } + dots := 0 + for i, b := range reply { + switch b { + case '.': + dots++ + case '@': + if dots >= 8 { + return i + } + } + } + return -1 +} + +// replyHasJSAckSuffix reports whether reply is already in `$JS.ACK....@` +// form, so callers don't double-append the suffix on a re-entrant pass +// (service-import or chained JS push). +func replyHasJSAckSuffix(reply []byte) bool { + return jsAckDeliverIdx(reply) != -1 +} + +// Test whether a reply subject is a service import or a gateway routed reply. +func isReservedReply(reply []byte) bool { + if isServiceReply(reply) { + return true + } + // Faster to check with string([:]) than byte-by-byte + if isJSAckSubject(reply) { + return true + } else if len(reply) > gwReplyPrefixLen && bytesToString(reply[:gwReplyPrefixLen]) == gwReplyPrefix { + return true + } + return false +} + +// This will decide to call the client code or router code. +func (c *client) processInboundMsg(msg []byte) { + switch c.kind { + case CLIENT: + c.processInboundClientMsg(msg) + case ROUTER: + c.processInboundRoutedMsg(msg) + case GATEWAY: + c.processInboundGatewayMsg(msg) + case LEAF: + c.processInboundLeafMsg(msg) + } +} + +// selectMappedSubject will choose the mapped subject based on the client's inbound subject. +func (c *client) selectMappedSubject() bool { + nsubj, changed := c.acc.selectMappedSubject(bytesToString(c.pa.subject)) + if changed { + c.pa.mapped = c.pa.subject + c.pa.subject = []byte(nsubj) + } + return changed +} + +// clientNRGPrefix is used in processInboundClientMsg to detect if publishes +// are being made from normal clients to NRG subjects. +var clientNRGPrefix = []byte("$NRG.") + +// processInboundClientMsg is called to process an inbound msg from a client. +// Return if the message was delivered, and if the message was not delivered +// due to a permission issue. +func (c *client) processInboundClientMsg(msg []byte) (bool, bool) { + // Update statistics + // The msg includes the CR_LF, so pull back out for accounting. + c.in.msgs++ + c.in.bytes += int32(len(msg) - LEN_CR_LF) + + // Check that client (could be here with SYSTEM) is not publishing on reserved "$GNR" prefix. + if c.kind == CLIENT && hasGWRoutedReplyPrefix(c.pa.subject) { + c.pubPermissionViolation(c.pa.subject) + return false, true + } + + // Mostly under testing scenarios. + c.mu.Lock() + if c.srv == nil || c.acc == nil { + c.mu.Unlock() + return false, false + } + acc := c.acc + genidAddr := &acc.sl.genid + + // Check pub permissions + if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) { + if !c.pubAllowedFullCheck(string(c.pa.subject), true, true) { + c.mu.Unlock() + c.pubPermissionViolation(c.pa.subject) + return false, true + } + } + c.mu.Unlock() + + // Check if the client is trying to publish to reserved NRG subjects. + // Doesn't apply to NRGs themselves as they use SYSTEM-kind clients instead. + if c.kind == CLIENT && bytes.HasPrefix(c.pa.subject, clientNRGPrefix) && acc != c.srv.SystemAccount() { + c.pubPermissionViolation(c.pa.subject) + return false, true + } + + // Now check for reserved replies. These are used for service imports. + if c.kind == CLIENT && len(c.pa.reply) > 0 && isReservedReply(c.pa.reply) { + c.replySubjectViolation(c.pa.reply) + return false, true + } + + if c.opts.Verbose { + c.sendOK() + } + + // If MQTT client, check for retain flag now that we have passed permissions check + if c.isMqtt() { + c.mqttHandlePubRetain() + } + + // Doing this inline as opposed to create a function (which otherwise has a measured + // performance impact reported in our bench) + var isGWRouted bool + if c.kind != CLIENT { + if atomic.LoadInt32(&acc.gwReplyMapping.check) > 0 { + acc.mu.RLock() + c.pa.subject, isGWRouted = acc.gwReplyMapping.get(c.pa.subject) + acc.mu.RUnlock() + } + } else if atomic.LoadInt32(&c.gwReplyMapping.check) > 0 { + c.mu.Lock() + c.pa.subject, isGWRouted = c.gwReplyMapping.get(c.pa.subject) + c.mu.Unlock() + } + + // If we have an exported service and we are doing remote tracking, check this subject + // to see if we need to report the latency. + if c.rrTracking != nil { + c.mu.Lock() + rl := c.rrTracking.rmap[string(c.pa.subject)] + if rl != nil { + delete(c.rrTracking.rmap, bytesToString(c.pa.subject)) + } + c.mu.Unlock() + + if rl != nil { + sl := &rl.M2 + // Fill this in and send it off to the other side. + sl.Status = 200 + sl.Responder = c.getClientInfo(true) + sl.ServiceLatency = time.Since(sl.RequestStart) - sl.Responder.RTT + sl.TotalLatency = sl.ServiceLatency + sl.Responder.RTT + sanitizeLatencyMetric(sl) + lsub := remoteLatencySubjectForResponse(c.pa.subject) + c.srv.sendInternalAccountMsg(nil, lsub, rl) // Send to SYS account + } + } + + // If the subject was converted to the gateway routed subject, then handle it now + // and be done with the rest of this function. + if isGWRouted { + c.handleGWReplyMap(msg) + return true, false + } + + // Match the subscriptions. We will use our own L1 map if + // it's still valid, avoiding contention on the shared sublist. + var r *SublistResult + var ok bool + + genid := atomic.LoadUint64(genidAddr) + if genid == c.in.genid && c.in.results != nil { + r, ok = c.in.results[string(c.pa.subject)] + } else { + // Reset our L1 completely. + c.in.results = make(map[string]*SublistResult) + c.in.genid = genid + } + + // Go back to the sublist data structure. + if !ok { + // Match may use the subject here to populate a cache, so can not use bytesToString here. + r = acc.sl.Match(string(c.pa.subject)) + if len(r.psubs)+len(r.qsubs) > 0 { + // Prune the results cache. Keeps us from unbounded growth. Random delete. + if len(c.in.results) >= maxResultCacheSize { + n := 0 + for subject := range c.in.results { + delete(c.in.results, subject) + if n++; n > pruneSize { + break + } + } + } + // Then add the new cache entry. + c.in.results[string(c.pa.subject)] = r + } + } + + // Indication if we attempted to deliver the message to anyone. + var didDeliver bool + var qnames [][]byte + + // Check for no interest, short circuit if so. + // This is the fanout scale. + if len(r.psubs)+len(r.qsubs) > 0 { + flag := pmrNoFlag + // If there are matching queue subs and we are in gateway mode, + // we need to keep track of the queue names the messages are + // delivered to. When sending to the GWs, the RMSG will include + // those names so that the remote clusters do not deliver messages + // to their queue subs of the same names. + if len(r.qsubs) > 0 && c.srv.gateway.enabled && + atomic.LoadInt64(&c.srv.gateway.totalQSubs) > 0 { + flag |= pmrCollectQueueNames + } + didDeliver, qnames = c.processMsgResults(acc, r, msg, c.pa.deliver, c.pa.subject, c.pa.reply, flag) + } + + // Now deal with gateways + if c.srv.gateway.enabled { + reply := c.pa.reply + if len(c.pa.deliver) > 0 && c.kind == JETSTREAM && len(reply) > 0 && !replyHasJSAckSuffix(reply) { + reply = append(slices.Clip(reply), '@') + reply = append(reply, c.pa.deliver...) + } + didDeliver = c.sendMsgToGateways(acc, msg, c.pa.subject, reply, qnames, false) || didDeliver + } + + // Check to see if we did not deliver to anyone and the client has a reply subject set + // and wants notification of no_responders. + if !didDeliver && len(c.pa.reply) > 0 { + c.mu.Lock() + if c.opts.NoResponders { + if sub := c.subForReply(c.pa.reply); sub != nil { + hdrLen := 32 /* header without the subject */ + len(c.pa.subject) + proto := fmt.Sprintf("HMSG %s %s %d %d\r\nNATS/1.0 503\r\nNats-Subject: %s\r\n\r\n\r\n", c.pa.reply, sub.sid, hdrLen, hdrLen, c.pa.subject) + c.queueOutbound([]byte(proto)) + c.addToPCD(c) + } + } + c.mu.Unlock() + } + + return didDeliver, false +} + +// Return the subscription for this reply subject. Only look at normal subs for this client. +func (c *client) subForReply(reply []byte) *subscription { + r := c.acc.sl.Match(string(reply)) + for _, sub := range r.psubs { + if sub.client == c { + return sub + } + } + return nil +} + +// This is invoked knowing that c.pa.subject has been set to the gateway routed subject. +// This function will send the message to possibly LEAFs and directly back to the origin +// gateway. +func (c *client) handleGWReplyMap(msg []byte) bool { + // Check for leaf nodes + if c.srv.gwLeafSubs.Count() > 0 { + if r := c.srv.gwLeafSubs.MatchBytes(c.pa.subject); len(r.psubs) > 0 { + c.processMsgResults(c.acc, r, msg, c.pa.deliver, c.pa.subject, c.pa.reply, pmrNoFlag) + } + } + if c.srv.gateway.enabled { + reply := c.pa.reply + if len(c.pa.deliver) > 0 && c.kind == JETSTREAM && len(reply) > 0 && !replyHasJSAckSuffix(reply) { + reply = append(slices.Clip(reply), '@') + reply = append(reply, c.pa.deliver...) + } + c.sendMsgToGateways(c.acc, msg, c.pa.subject, reply, nil, false) + } + return true +} + +// Used to setup the response map for a service import request that has a reply subject. +func (c *client) setupResponseServiceImport(acc *Account, si *serviceImport, tracking bool, header http.Header) *serviceImport { + rsi := si.acc.addRespServiceImport(acc, string(c.pa.reply), si, tracking, header, nil) + if si.latency != nil { + if c.rtt == 0 { + // We have a service import that we are tracking but have not established RTT. + c.sendRTTPing() + } + si.acc.mu.Lock() + rsi.rc = c + si.acc.mu.Unlock() + } + return rsi +} + +// Will remove a status and description from the header if present. +func removeHeaderStatusIfPresent(hdr []byte) []byte { + k := []byte("NATS/1.0") + kl, i := len(k), bytes.IndexByte(hdr, '\r') + if !bytes.HasPrefix(hdr, k) || i <= kl { + return hdr + } + hdr = append(hdr[:kl], hdr[i:]...) + if len(hdr) == len(emptyHdrLine) { + return nil + } + return hdr +} + +// Will remove a header if present. +func removeHeaderIfPresent(hdr []byte, key string) []byte { + for { + start := getHeaderKeyIndex(key, hdr) + // key can't be first and we want to check that it is preceded by a '\n' + if start < 1 || hdr[start-1] != '\n' { + return hdr + } + index := start + len(key) + if index >= len(hdr) || hdr[index] != ':' { + return hdr + } + end := bytes.Index(hdr[start:], []byte(_CRLF_)) + if end < 0 { + return hdr + } + hdr = append(hdr[:start], hdr[start+end+len(_CRLF_):]...) + if len(hdr) <= len(emptyHdrLine) { + return nil + } + } +} + +func removeHeaderIfPrefixPresent(hdr []byte, prefix string) []byte { + var index int + for { + if index >= len(hdr) { + return hdr + } + + start := bytes.Index(hdr[index:], []byte(prefix)) + if start < 0 { + return hdr + } + index += start + if index < 1 || hdr[index-1] != '\n' { + index += len(prefix) + continue + } + + end := bytes.Index(hdr[index+len(prefix):], []byte(_CRLF_)) + if end < 0 { + return hdr + } + + hdr = append(hdr[:index], hdr[index+end+len(prefix)+len(_CRLF_):]...) + if len(hdr) <= len(emptyHdrLine) { + return nil + } + } +} + +// Generate a new header based on optional original header and key value. +// More used in JetStream layers. +func genHeader(hdr []byte, key, value string) []byte { + var bb bytes.Buffer + if len(hdr) > LEN_CR_LF { + bb.Write(hdr[:len(hdr)-LEN_CR_LF]) + } else { + bb.WriteString(hdrLine) + } + http.Header{key: []string{value}}.Write(&bb) + bb.WriteString(CR_LF) + return bb.Bytes() +} + +// This will set a header for the message. +// Lock does not need to be held but this should only be called +// from the inbound go routine. We will update the pubArgs. +// This will replace any previously set header and not add to it per normal spec. +func (c *client) setHeader(key, value string, msg []byte) []byte { + var bb bytes.Buffer + var omi int + // Write original header if present. + if c.pa.hdr > LEN_CR_LF { + omi = c.pa.hdr + // Need to copy since we're removing the header in place. + hdr := removeHeaderIfPresent(copyBytes(msg[:c.pa.hdr-LEN_CR_LF]), key) + if len(hdr) == 0 { + bb.WriteString(hdrLine) + } else { + bb.Write(hdr) + } + } else { + bb.WriteString(hdrLine) + } + http.Header{key: []string{value}}.Write(&bb) + bb.WriteString(CR_LF) + nhdr := bb.Len() + // Put the original message back. + // FIXME(dlc) - This is inefficient. + bb.Write(msg[omi:]) + nsize := bb.Len() - LEN_CR_LF + // MQTT producers don't have CRLF, so add it back. + if c.isMqtt() { + nsize += LEN_CR_LF + } + // Update pubArgs + // If others will use this later we need to save and restore original. + c.pa.hdr = nhdr + c.pa.size = nsize + c.pa.hdb = []byte(strconv.Itoa(nhdr)) + c.pa.szb = []byte(strconv.Itoa(nsize)) + return bb.Bytes() +} + +// Will return a copy of the value for the header denoted by key or nil if it does not exist. +// If you know that it is safe to refer to the underlying hdr slice for the period that the +// return value is used, then sliceHeader() will be faster. +func getHeader(key string, hdr []byte) []byte { + v := sliceHeader(key, hdr) + if v == nil { + return nil + } + return append(make([]byte, 0, len(v)), v...) +} + +// Will return the sliced value for the header denoted by key or nil if it does not exists. +// This function ignores errors and tries to achieve speed and no additional allocations. +func sliceHeader(key string, hdr []byte) []byte { + if len(hdr) == 0 { + return nil + } + index := getHeaderKeyIndex(key, hdr) + if index == -1 { + return nil + } + // Skip over the key and the : separator. + index += len(key) + 1 + hdrLen := len(hdr) + // Skip over whitespace before the value. + for index < hdrLen && hdr[index] == ' ' { + index++ + } + // Collect together the rest of the value until we hit a CRLF. + start := index + for index < hdrLen { + if hdr[index] == '\r' && index < hdrLen-1 && hdr[index+1] == '\n' { + break + } + index++ + } + return hdr[start:index:index] +} + +// getHeaderKeyIndex returns an index into the header slice for the given key. +// Returns -1 if not found. +func getHeaderKeyIndex(key string, hdr []byte) int { + if len(hdr) == 0 { + return -1 + } + bkey := stringToBytes(key) + keyLen, hdrLen := len(key), len(hdr) + var offset int + for { + index := bytes.Index(hdr[offset:], bkey) + // Check that we have enough characters, this will handle the -1 case of the key not + // being found and will also handle not having enough characters for trailing CRLF. + if index < 2 { + return -1 + } + index += offset + // There should be a terminating CRLF. + if index >= hdrLen-1 || hdr[index-1] != '\n' || hdr[index-2] != '\r' { + offset = index + keyLen + continue + } + // The key should be immediately followed by a : separator. + if index+keyLen >= hdrLen { + return -1 + } + if hdr[index+keyLen] != ':' { + offset = index + keyLen + continue + } + return index + } +} + +// setHeader will replace the value of the first existing key `key` +// with the given value `val`, or add this new key at the end of +// the headers. +// +// Note: If the key does not exist, or if it exists but the new value +// would make the resulting byte slice larger than the original one, +// a new byte slice is returned and the original is left untouched. +// This is to prevent situations where caller may have a `hdr` and +// `msg` that are the parts of an underlying buffer. Extending the +// `hdr` would otherwise overwrite the `msg` part. +// +// If the new value is smaller, then the original `hdr` byte slice +// is modified. +func setHeader(key, val string, hdr []byte) []byte { + start := getHeaderKeyIndex(key, hdr) + if start >= 0 { + valStart := start + len(key) + 1 + // Preserve single whitespace if used. + hdrLen := len(hdr) + if valStart < hdrLen && hdr[valStart] == ' ' { + valStart++ + } + valEnd := bytes.Index(hdr[valStart:], []byte("\r")) + if valEnd < 0 { + return hdr // malformed headers + } + valEnd += valStart + // Length of the existing value (before the `\r`) + oldValLen := valEnd - valStart + // This is how many extra bytes we need for the new value. + // If <= 0, it means that we need less and so will reuse the `hdr` buffer. + if extra := len(val) - oldValLen; extra > 0 { + // Check that we don't overflow an "int". + if rem := math.MaxInt - hdrLen; rem < extra { + // We don't grow, and return the existing header. + return hdr + } + // The new size is the old size plus the extra bytes. + newHdrSize := hdrLen + extra + newHdr := make([]byte, newHdrSize) + // Copy the parts from `hdr` and `val` into the new buffer. + n := copy(newHdr, hdr[:valStart]) + n += copy(newHdr[n:], val) + copy(newHdr[n:], hdr[valEnd:]) + return newHdr + } + // We can write in place since it fits in the existing `hdr` buffer. + n := copy(hdr[valStart:], val) + n += copy(hdr[valStart+n:], hdr[valEnd:]) + hdr = hdr[:valStart+n] + return hdr + } + if len(hdr) > 0 && bytes.HasSuffix(hdr, []byte("\r\n")) { + hdr = hdr[:len(hdr)-2] + val += "\r\n" + } + // Create the new buffer based on length of existing one and + // length of the new ": \r\n". Protect against "int" overflow. + newSize := uint64(len(hdr)) + uint64(len(key)) + 1 + 1 + uint64(len(val)) + 2 + if newSize > uint64(math.MaxInt) { + // We don't grow, and return the existing header. + return hdr + } + newHdr := make([]byte, 0, int(newSize)) + newHdr = append(newHdr, hdr...) + return fmt.Appendf(newHdr, "%s: %s\r\n", key, val) +} + +// For bytes.HasPrefix below. +var ( + jsRequestNextPreB = []byte(jsRequestNextPre) + jsDirectGetPreB = []byte(jsDirectGetPre) +) + +// processServiceImport is an internal callback when a subscription matches an imported service +// from another account. This includes response mappings as well. +func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byte) bool { + // If we are a GW and this is not a direct serviceImport ignore. + isResponse := si.isRespServiceImport() + if (c.kind == GATEWAY || c.kind == ROUTER) && !isResponse { + return false + } + // Detect cycles and ignore (return) when we detect one. + if len(c.pa.psi) > 0 { + for i := len(c.pa.psi) - 1; i >= 0; i-- { + if psi := c.pa.psi[i]; psi.se == si.se { + return false + } + } + } + + acc.mu.RLock() + var checkJS bool + shouldReturn := si.invalid || acc.sl == nil + if !shouldReturn && !isResponse && si.to == jsAllAPI { + if bytes.HasPrefix(c.pa.subject, jsDirectGetPreB) || bytes.HasPrefix(c.pa.subject, jsRequestNextPreB) { + checkJS = true + } + } + siAcc := si.acc + allowTrace := si.atrc + isMsgTraceResp := isResponse && si.mt != nil + acc.mu.RUnlock() + + // We have a special case where JetStream pulls in all service imports through one export. + // However the GetNext for consumers and DirectGet for streams are a no-op and causes buildups of service imports, + // response service imports and rrMap entries which all will need to simply expire. + // TODO(dlc) - Come up with something better. + if shouldReturn || (checkJS && si.se != nil && si.se.acc == c.srv.SystemAccount()) { + return false + } + + mt, traceOnly := c.isMsgTraceEnabled() + + var nrr []byte + var rsi *serviceImport + + // Check if there is a reply present and set up a response. + tracking, headers := shouldSample(si.latency, c) + if len(c.pa.reply) > 0 { + // Special case for now, need to formalize. + // TODO(dlc) - Formalize as a service import option for reply rewrite. + // For now we can't do $JS.ACK since that breaks pull consumers across accounts. + if !bytes.HasPrefix(c.pa.reply, []byte(jsAckPre)) { + if rsi = c.setupResponseServiceImport(acc, si, tracking, headers); rsi != nil { + nrr = []byte(rsi.from) + } + } else { + // This only happens when we do a pull subscriber that trampolines through another account. + // Normally this code is not called. + nrr = c.pa.reply + } + } else if !isResponse && si.latency != nil && tracking { + // Check to see if this was a bad request with no reply and we were supposed to be tracking. + siAcc.sendBadRequestTrackingLatency(si, c, headers) + } + + // Send tracking info here if we are tracking this response. + // This is always a response. + var didSendTL bool + if si.tracking && !si.didDeliver { + // Stamp that we attempted delivery. + si.didDeliver = true + didSendTL = acc.sendTrackingLatency(si, c) + } + + // Pick correct "to" subject. If we matched on a wildcard use the literal publish subject. + to, subject := si.to, string(c.pa.subject) + + if si.tr != nil { + // FIXME(dlc) - This could be slow, may want to look at adding cache to bare transforms? + to = si.tr.TransformSubject(subject) + } else if si.usePub { + to = subject + } + + // Copy our pubArg since this gets modified as we process the service import itself. + pacopy := c.pa + + // Now check to see if this account has mappings that could affect the service import. + // Can't use non-locked trick like in processInboundClientMsg, so just call into selectMappedSubject + // so we only lock once. + nsubj, changed := siAcc.selectMappedSubject(to) + if changed { + c.pa.mapped = []byte(to) + to = nsubj + } + + // Set previous service import to detect chaining. + lpsi := len(c.pa.psi) + hadPrevSi, share := lpsi > 0, si.share + if hadPrevSi { + share = c.pa.psi[lpsi-1].share + } + c.pa.psi = append(c.pa.psi, si) + + // Place our client info for the request in the original message. + // This will survive going across routes, etc. + if !isResponse { + isSysImport := siAcc == c.srv.SystemAccount() + var ci *ClientInfo + var cis *ClientInfo + if c.pa.hdr >= 0 { + var hci ClientInfo + if err := json.Unmarshal(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr]), &hci); err == nil { + cis = &hci + } + } + if c.kind == LEAF && c.pa.hdr >= 0 && len(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr])) > 0 { + // Leaf nodes may forward a Nats-Request-Info from a remote domain, + // but the local server must replace it with the identity of the + // authenticated leaf connection instead of trusting forwarded values. + ci = c.getClientInfo(share) + if hadPrevSi && cis != nil && cis.Reply != _EMPTY_ { + ci.Reply = cis.Reply + } else if bytes.HasSuffix(c.pa.reply, []byte(FastBatchSuffix)) { + // Fast batch requires knowledge of the original reply subject. + ci.Reply = bytesToString(c.pa.reply) + } + if hadPrevSi { + ci.Service = acc.Name + if !share && (si.share || isSysImport) { + c.addServerAndClusterInfo(ci) + } + } else if !share && isSysImport { + c.addServerAndClusterInfo(ci) + } + } else if hadPrevSi && cis != nil { + ci = cis + ci.Service = acc.Name + // Check if we are moving into a share details account from a non-shared + // and add in server and cluster details. + if !share && (si.share || isSysImport) { + c.addServerAndClusterInfo(ci) + } + } else if c.kind != LEAF || c.pa.hdr < 0 || len(sliceHeader(ClientInfoHdr, msg[:c.pa.hdr])) == 0 { + ci = c.getClientInfo(share) + // Fast batch requires knowledge of the original reply subject. + if bytes.HasSuffix(c.pa.reply, []byte(FastBatchSuffix)) { + ci.Reply = bytesToString(c.pa.reply) + } + // If we did not share but the imports destination is the system account add in the server and cluster info. + if !share && isSysImport { + c.addServerAndClusterInfo(ci) + } + } + // Set clientInfo if present. + if ci != nil { + if b, _ := json.Marshal(ci); b != nil { + msg = c.setHeader(ClientInfoHdr, bytesToString(b), msg) + } + } + } + + // Set our optional subject(to) and reply. + if !isResponse && to != subject { + c.pa.subject = []byte(to) + } + c.pa.reply = nrr + + if changed && c.isMqtt() && c.pa.hdr > 0 { + c.srv.mqttStoreQoSMsgForAccountOnNewSubject(c.pa.hdr, msg, siAcc.GetName(), to) + } + + // FIXME(dlc) - Do L1 cache trick like normal client? + rr := siAcc.sl.Match(to) + + // If we are a route or gateway or leafnode and this message is flipped to a queue subscriber we + // need to handle that since the processMsgResults will want a queue filter. + flags := pmrMsgImportedFromService + if c.kind == GATEWAY || c.kind == ROUTER || c.kind == LEAF { + flags |= pmrIgnoreEmptyQueueFilter + } + + // If this is a response service import that arrived via a route, allow delivery + // to route subscriptions. Service import replies use one-time _R_ subjects. + if isResponse && c.kind == ROUTER { + flags |= pmrAllowSendFromRouteToRoute + } + + // We will be calling back into processMsgResults since we are now being called as a normal sub. + // We need to take care of the c.in.rts, so save off what is there and use a local version. We + // will put back what was there after. + + orts := c.in.rts + + var lrts [routeTargetInit]routeTarget + c.in.rts = lrts[:0] + + var skipProcessing bool + var mtrsi *serviceImport + // If message tracing enabled, add the service import trace. + if mt != nil { + mt.addServiceImportEvent(siAcc.GetName(), string(pacopy.subject), to) + // If we are not allowing tracing and doing trace only, we stop at this level. + if !allowTrace { + if traceOnly { + skipProcessing = true + } else { + // We are going to do normal processing, and possibly chainning + // with other server imports, but the rest won't be traced. + // We do so by setting the c.pa.trace to nil (it will be restored + // with c.pa = pacopy). + c.pa.trace = nil + // We also need to disable the message trace headers so that + // if the message is routed, it does not initialize tracing in the + // remote. + msg = c.setHeader(MsgTraceDest, MsgTraceDestDisabled, msg) + } + } else { + // This code is invoked from a single thread so mutation of mt.dest + // here is safe. + dest := mt.dest + defer func() { mt.dest = dest }() + mtrsi, msg = mt.setupResponseServiceImport(c, acc, si, msg) + } + } + + var didDeliver bool + + if !skipProcessing { + // If this is not a gateway connection but gateway is enabled, + // try to send this converted message to all gateways. + if c.srv.gateway.enabled { + flags |= pmrCollectQueueNames + var queues [][]byte + didDeliver, queues = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags) + didDeliver = c.sendMsgToGateways(siAcc, msg, []byte(to), nrr, queues, false) || didDeliver + } else { + didDeliver, _ = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags) + } + } + + // Restore to original values. + c.in.rts = orts + c.pa = pacopy + + // Mark in the c.pa if the message was delivered or not (no responders status). + c.pa.delivered = didDeliver + + // Determine if we should remove this service import. This is for response service imports. + // We will remove if we did not deliver, or if we are a response service import and we are + // a singleton, or we have an EOF message. + shouldRemove := !didDeliver || (isResponse && (si.rt == Singleton || len(msg) == LEN_CR_LF)) + // If we are tracking and we did not actually send the latency info we need to suppress the removal. + if si.tracking && !didSendTL { + shouldRemove = false + } + // If we are streamed or chunked we need to update our timestamp to avoid cleanup. + if si.rt != Singleton && didDeliver { + acc.mu.Lock() + si.ts = time.Now().UnixNano() + acc.mu.Unlock() + } + // This is a message trace... + if mt != nil { + // If it was not routed anywhere, we can remove mtrsi that we created earlier. + mt.siMu.RLock() + remove := mt.rsi == nil + mt.siMu.RUnlock() + if remove { + reason := rsiOk + if !didDeliver { + reason = rsiNoDelivery + } + siAcc.removeRespServiceImport(mtrsi, reason) + } + // If we skip last-mile delivery, we need to remove the rsi that we + // may have created if there was a reply subject. + if rsi != nil && traceOnly { + shouldRemove = true + } + + } else if isMsgTraceResp { + shouldRemove = false + if !strings.HasPrefix(si.to, replyPrefix) { + var evt MsgTraceEvent + if err := json.Unmarshal(msg, &evt); err == nil { + si.mt.handleRespServiceImport(&evt) + } + } + } + + // Cleanup of a response service import + if shouldRemove { + reason := rsiOk + if !didDeliver { + reason = rsiNoDelivery + } + if isResponse { + acc.removeRespServiceImport(si, reason) + } else { + // This is a main import and since we could not even deliver to the exporting account + // go ahead and remove the respServiceImport we created above. + siAcc.removeRespServiceImport(rsi, reason) + } + } + + return didDeliver +} + +func (c *client) addSubToRouteTargets(sub *subscription) { + if c.in.rts == nil { + c.in.rts = make([]routeTarget, 0, routeTargetInit) + } + + for i := range c.in.rts { + rt := &c.in.rts[i] + if rt.sub.client == sub.client { + if sub.queue != nil { + rt.qs = append(rt.qs, sub.queue...) + rt.qs = append(rt.qs, ' ') + rt.qsubs = append(rt.qsubs, sub) + } + return + } + } + + var rt *routeTarget + lrts := len(c.in.rts) + + // If we are here we do not have the sub yet in our list + // If we have to grow do so here. + if lrts == cap(c.in.rts) { + c.in.rts = append(c.in.rts, routeTarget{}) + } + + c.in.rts = c.in.rts[:lrts+1] + rt = &c.in.rts[lrts] + rt.sub = sub + rt.qs = rt._qs[:0] + rt.qsubs = rt._qsubs[:0] + if sub.queue != nil { + rt.qs = append(rt.qs, sub.queue...) + rt.qs = append(rt.qs, ' ') + rt.qsubs = append(rt.qsubs, sub) + } +} + +// Filters queue groups in a coalesced route or leaf target against the +// destination connection's delivery-time subscription denies. +func filterRouteTargetDeny(subject []byte, rt *routeTarget) bool { + dc := rt.sub.client + dc.mu.Lock() + defer dc.mu.Unlock() + if dc.mperms == nil { + return true + } + + dsubject := string(subject) + if len(rt.sub.queue) == 0 { + if dc.checkDenySub(dsubject, _EMPTY_) { + return false + } + } else { + rt.sub = nil + } + + qs := rt.qs[:0] + qsubs := rt.qsubs[:0] + for _, qsub := range rt.qsubs { + if dc.checkDenySub(dsubject, bytesToString(qsub.queue)) { + continue + } + if rt.sub == nil { + rt.sub = qsub + } + qs = append(qs, qsub.queue...) + qs = append(qs, ' ') + qsubs = append(qsubs, qsub) + } + rt.qs = qs + rt.qsubs = qsubs + return rt.sub != nil +} + +// This processes the sublist results for a given message. +// Returns if the message was delivered to at least target and queue filters. +func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver, subject, reply []byte, flags int) (bool, [][]byte) { + // For sending messages across routes and leafnodes. + // Reset if we have one since we reuse this data structure. + if c.in.rts != nil { + c.in.rts = c.in.rts[:0] + } + + var rplyHasGWPrefix bool + creply := reply + + // If the reply subject is a GW routed reply, we will perform some + // tracking in deliverMsg(). We also want to send to the user the + // reply without the prefix. `creply` will be set to that and be + // used to create the message header for client connections. + if rplyHasGWPrefix = isGWRoutedReply(reply); rplyHasGWPrefix { + creply = reply[gwSubjectOffset:] + } + + // With JetStream we now have times where we want to match a subscription + // on one subject, but deliver it with another. e.g. JetStream deliverables. + // This only works for last mile, meaning to a client. For other types we need + // to use the original subject. + subj := subject + if len(deliver) > 0 { + subj = deliver + } + + // Check for JetStream encoded reply subjects. + // For now these will only be on $JS.ACK prefixed reply subjects. + var remapped bool + if len(creply) > 0 && c.kind != CLIENT && !isInternalClient(c.kind) { + // We need to rewrite the subject and the reply. + if li := jsAckDeliverIdx(creply); li != -1 && li < len(creply)-1 { + remapped = true + subj, creply = creply[li+1:], creply[:li] + } + } + + var didDeliver bool + + // delivery subject for clients + var dsubj []byte + // Used as scratch if mapping + var _dsubj [128]byte + + // For stats, we will keep track of the number of messages that have been + // delivered and then multiply by the size of that message and update + // server and account stats in a "single" operation (instead of per-sub). + // However, we account for situations where the message is possibly changed + // by having an extra size + var dlvMsgs int64 + var dlvExtraSize int64 + var dlvRouteMsgs int64 + var dlvLeafMsgs int64 + var dlvClientMsgs int64 + + // We need to know if this is a MQTT producer because they send messages + // without CR_LF (we otherwise remove the size of CR_LF from message size). + prodIsMQTT := c.isMqtt() + + updateStats := func() { + if dlvMsgs == 0 { + return + } + + totalBytes := dlvMsgs*int64(len(msg)) + dlvExtraSize + routeBytes := dlvRouteMsgs*int64(len(msg)) + dlvExtraSize + leafBytes := dlvLeafMsgs*int64(len(msg)) + dlvExtraSize + // dlvExtraSize applies to route/leaf header overhead, not client deliveries + clientBytes := dlvClientMsgs * int64(len(msg)) + + // For non MQTT producers, remove the CR_LF * number of messages + if !prodIsMQTT { + totalBytes -= dlvMsgs * int64(LEN_CR_LF) + routeBytes -= dlvRouteMsgs * int64(LEN_CR_LF) + leafBytes -= dlvLeafMsgs * int64(LEN_CR_LF) + clientBytes -= dlvClientMsgs * int64(LEN_CR_LF) + } + + if acc != nil { + acc.stats.Lock() + acc.stats.outMsgs += dlvMsgs + acc.stats.outBytes += totalBytes + if dlvRouteMsgs > 0 { + acc.stats.rt.outMsgs += dlvRouteMsgs + acc.stats.rt.outBytes += routeBytes + } + if dlvLeafMsgs > 0 { + acc.stats.ln.outMsgs += dlvLeafMsgs + acc.stats.ln.outBytes += leafBytes + } + acc.stats.Unlock() + } + + if srv := c.srv; srv != nil { + atomic.AddInt64(&srv.outMsgs, dlvMsgs) + atomic.AddInt64(&srv.outBytes, totalBytes) + + atomic.AddInt64(&srv.outClientMsgs, dlvClientMsgs) + atomic.AddInt64(&srv.outClientBytes, clientBytes) + } + } + + mt, traceOnly := c.isMsgTraceEnabled() + + // Loop over all normal subscriptions that match. + for _, sub := range r.psubs { + // Check if this is a send to a ROUTER. We now process + // these after everything else. + switch sub.client.kind { + case ROUTER: + if (c.kind != ROUTER && !c.isSpokeLeafNode()) || (flags&pmrAllowSendFromRouteToRoute != 0) { + c.addSubToRouteTargets(sub) + } + continue + case GATEWAY: + // Never send to gateway from here. + continue + case LEAF: + // We handle similarly to routes and use the same data structures. + // Leaf node delivery audience is different however. + // Also leaf nodes are always no echo, so we make sure we are not + // going to send back to ourselves here. For messages from routes we want + // to suppress in general unless we know from the hub or its a service reply. + if c != sub.client && (c.kind != ROUTER || sub.client.isHubLeafNode() || isServiceReply(c.pa.subject)) { + c.addSubToRouteTargets(sub) + } + continue + } + + // Assume delivery subject is the normal subject to this point. + dsubj = subj + + // We may need to disable tracing, by setting c.pa.trace to `nil` + // before the call to deliverMsg, if so, this will indicate that + // we need to put it back. + var restorePaTrace bool + + // Check for stream import mapped subs (shadow subs). These apply to local subs only. + if sub.im != nil { + // If this message was a service import do not re-export to an exported stream. + if flags&pmrMsgImportedFromService != 0 { + continue + } + if sub.im.tr != nil { + to := sub.im.tr.TransformSubject(bytesToString(subject)) + dsubj = append(_dsubj[:0], to...) + } else if sub.im.usePub { + dsubj = append(_dsubj[:0], subj...) + } else { + dsubj = append(_dsubj[:0], sub.im.to...) + } + + if mt != nil { + mt.addStreamExportEvent(sub.client, dsubj) + // If allow_trace is false... + if !sub.im.atrc { + // If we are doing only message tracing, we can move to the + // next sub. + if traceOnly { + // Although the message was not delivered, for the purpose + // of didDeliver, we need to set to true (to avoid possible + // no responders). + didDeliver = true + continue + } + // If we are delivering the message, we need to disable tracing + // before calling deliverMsg(). + c.pa.trace, restorePaTrace = nil, true + } + } + + // Make sure deliver is set if inbound from a route. + if remapped && (c.kind == GATEWAY || c.kind == ROUTER || c.kind == LEAF) { + deliver = subj + } + // If we are mapping for a deliver subject we will reverse roles. + // The original subj we set from above is correct for the msg header, + // but we need to transform the deliver subject to properly route. + if len(deliver) > 0 { + dsubj, subj = subj, dsubj + } + } + + // Remap to the original subject if internal. + if sub.icb != nil && sub.rsi { + dsubj = subject + } + + // Normal delivery + mh := c.msgHeader(dsubj, creply, sub) + if c.deliverMsg(prodIsMQTT, sub, acc, dsubj, creply, mh, msg, rplyHasGWPrefix) { + // We don't count internal deliveries, so do only when sub.icb is nil. + if sub.icb == nil { + dlvMsgs++ + if sub.client.kind == CLIENT { + dlvClientMsgs++ + } + } + didDeliver = true + } + if restorePaTrace { + c.pa.trace = mt + } + } + + // Set these up to optionally filter based on the queue lists. + // This is for messages received from routes which will have directed + // guidance on which queue groups we should deliver to. + qf := c.pa.queues + + // Declared here because of goto. + var queues [][]byte + + var leafOrigin string + switch c.kind { + case ROUTER: + if len(c.pa.origin) > 0 { + // Picture a message sent from a leafnode to a server that then routes + // this message: CluserA -leaf-> HUB1 -route-> HUB2 + // Here we are in HUB2, so c.kind is a ROUTER, but the message will + // contain a c.pa.origin set to "ClusterA" to indicate that this message + // originated from that leafnode cluster. + leafOrigin = bytesToString(c.pa.origin) + } + case LEAF: + leafOrigin = c.remoteCluster() + } + + // For all routes/leaf/gateway connections, we may still want to send messages to + // leaf nodes or routes even if there are no queue filters since we collect + // them above and do not process inline like normal clients. + // However, do select queue subs if asked to ignore empty queue filter. + if (c.kind == LEAF || c.kind == ROUTER || c.kind == GATEWAY) && len(qf) == 0 && flags&pmrIgnoreEmptyQueueFilter == 0 { + goto sendToRoutesOrLeafs + } + + // Process queue subs + for i := 0; i < len(r.qsubs); i++ { + qsubs := r.qsubs[i] + // If we have a filter check that here. We could make this a map or someting more + // complex but linear search since we expect queues to be small. Should be faster + // and more cache friendly. + if qf != nil && len(qsubs) > 0 { + tqn := qsubs[0].queue + for _, qn := range qf { + if bytes.Equal(qn, tqn) { + goto selectQSub + } + } + continue + } + + selectQSub: + // We will hold onto remote or lead qsubs when we are coming from + // a route or a leaf node just in case we can no longer do local delivery. + var rsub, sub *subscription + var _ql [32]*subscription + + src := c.kind + // If we just came from a route we want to prefer local subs. + // So only select from local subs but remember the first rsub + // in case all else fails. + if src == ROUTER { + ql := _ql[:0] + for i := 0; i < len(qsubs); i++ { + sub = qsubs[i] + if dst := sub.client.kind; dst == LEAF || dst == ROUTER { + // If the destination is a LEAF, we first need to make sure + // that we would not pick one that was the origin of this + // message. + if dst == LEAF && leafOrigin != _EMPTY_ && leafOrigin == sub.client.remoteCluster() { + continue + } + // If we have assigned a ROUTER rsub already, replace if + // the destination is a LEAF since we want to favor that. + if rsub == nil || (rsub.client.kind == ROUTER && dst == LEAF) { + rsub = sub + } else if dst == LEAF { + // We already have a LEAF and this is another one. + // Flip a coin to see if we swap it or not. + // See https://github.com/nats-io/nats-server/issues/6040 + if rand.Uint32()%2 == 1 { + rsub = sub + } + } + } else { + ql = append(ql, sub) + } + } + qsubs = ql + } + + sindex := 0 + lqs := len(qsubs) + if lqs > 1 { + sindex = int(rand.Uint32() % uint32(lqs)) + } + + // Find a subscription that is able to deliver this message starting at a random index. + // Note that if the message came from a ROUTER, we will only have CLIENT or LEAF + // queue subs here, otherwise we can have all types. + for i := 0; i < lqs; i++ { + if sindex+i < lqs { + sub = qsubs[sindex+i] + } else { + sub = qsubs[(sindex+i)%lqs] + } + if sub == nil { + continue + } + + // If we are a spoke leaf node make sure to not forward across routes. + // This mimics same behavior for normal subs above. + if c.kind == LEAF && c.isSpokeLeafNode() && sub.client.kind == ROUTER { + continue + } + + // We have taken care of preferring local subs for a message from a route above. + // Here we just care about a client or leaf and skipping a leaf and preferring locals. + if dst := sub.client.kind; dst == ROUTER || dst == LEAF { + if (src == LEAF || src == CLIENT) && dst == LEAF { + // If we come from a LEAF and are about to pick a LEAF connection, + // make sure this is not the same leaf cluster. + if src == LEAF && leafOrigin != _EMPTY_ && leafOrigin == sub.client.remoteCluster() { + continue + } + // Remember that leaf in case we don't find any other candidate. + // We already start randomly in lqs slice, so we don't need + // to do a random swap if we already have an rsub like we do + // when src == ROUTER above. + if rsub == nil { + rsub = sub + } + continue + } else { + // We want to favor qsubs in our own cluster. If the routed + // qsub is on behalf of a leaf, we need to treat it differently. + if sub.leaf { + // If we already have an rsub, nothing to do. Also, do + // not pick a routed qsub for a LEAF origin cluster + // that is the same than where the message comes from. + if rsub == nil && (leafOrigin == _EMPTY_ || leafOrigin != bytesToString(sub.origin)) { + rsub = sub + } + continue + } + // This is a qsub that is local on the remote server (or + // we are connected to an older server and we don't know). + // Pick this one and be done. + rsub = sub + break + } + } + + // Assume delivery subject is normal subject to this point. + dsubj = subj + + // We may need to disable tracing, by setting c.pa.trace to `nil` + // before the call to deliverMsg, if so, this will indicate that + // we need to put it back. + var restorePaTrace bool + var skipDelivery bool + + // Check for stream import mapped subs. These apply to local subs only. + if sub.im != nil { + // If this message was a service import do not re-export to an exported stream. + if flags&pmrMsgImportedFromService != 0 { + continue + } + if sub.im.tr != nil { + to := sub.im.tr.TransformSubject(bytesToString(subject)) + dsubj = append(_dsubj[:0], to...) + } else if sub.im.usePub { + dsubj = append(_dsubj[:0], subj...) + } else { + dsubj = append(_dsubj[:0], sub.im.to...) + } + + if mt != nil { + mt.addStreamExportEvent(sub.client, dsubj) + // If allow_trace is false... + if !sub.im.atrc { + // If we are doing only message tracing, we are done + // with this queue group. + if traceOnly { + skipDelivery = true + } else { + // If we are delivering, we need to disable tracing + // before the call to deliverMsg() + c.pa.trace, restorePaTrace = nil, true + } + } + } + + // Make sure deliver is set if inbound from a route. + if remapped && (c.kind == GATEWAY || c.kind == ROUTER || c.kind == LEAF) { + deliver = subj + } + // If we are mapping for a deliver subject we will reverse roles. + // The original subj we set from above is correct for the msg header, + // but we need to transform the deliver subject to properly route. + if len(deliver) > 0 { + dsubj, subj = subj, dsubj + } + } + + var delivered bool + if !skipDelivery { + mh := c.msgHeader(dsubj, creply, sub) + delivered = c.deliverMsg(prodIsMQTT, sub, acc, subject, creply, mh, msg, rplyHasGWPrefix) + if restorePaTrace { + c.pa.trace = mt + } + } + if skipDelivery || delivered { + // Update only if not skipped. + if !skipDelivery && sub.icb == nil { + dlvMsgs++ + switch sub.client.kind { + case ROUTER: + dlvRouteMsgs++ + case LEAF: + dlvLeafMsgs++ + case CLIENT: + dlvClientMsgs++ + } + } + // Do the rest even when message delivery was skipped. + didDeliver = true + // Clear rsub + rsub = nil + if flags&pmrCollectQueueNames != 0 { + queues = append(queues, sub.queue) + } + break + } + } + + if rsub != nil { + // We are here if we have selected a leaf or route as the destination, + // or if we tried to deliver to a local qsub but failed. + c.addSubToRouteTargets(rsub) + } + } + +sendToRoutesOrLeafs: + + // If no messages for routes or leafnodes return here. + if len(c.in.rts) == 0 { + updateStats() + return didDeliver, queues + } + + // If we do have a deliver subject we need to do something with it. + // Again this is when JetStream (but possibly others) wants the system + // to rewrite the delivered subject. The way we will do that is place it + // at the end of the reply subject if it exists. But only if this wasn't + // already performed, otherwise we'd end up with a duplicate '@' suffix + // resulting in a protocol error. + if len(deliver) > 0 && len(reply) > 0 && !remapped && !replyHasJSAckSuffix(reply) { + reply = append(slices.Clip(reply), '@') + reply = append(reply, deliver...) + } + + // Copy off original pa in case it changes. + pa := c.pa + + // We address by index to avoid struct copy. + // We have inline structs for memory layout and cache coherency. + for i := range c.in.rts { + rt := &c.in.rts[i] + if (len(rt.qsubs) > 1 || (len(rt.sub.queue) == 0 && len(rt.qsubs) > 0)) && + !filterRouteTargetDeny(subject, rt) { + continue + } + dc := rt.sub.client + dmsg, hset := msg, false + + // Check if we have an origin cluster set from a leafnode message. + // If so make sure we do not send it back to the same cluster for a different + // leafnode. Cluster wide no echo. + if dc.kind == LEAF { + // Check two scenarios. One is inbound from a route (c.pa.origin), + // and the other is leaf to leaf. In both case, leafOrigin is the one + // to use for the comparison. + if leafOrigin != _EMPTY_ && leafOrigin == dc.remoteCluster() { + continue + } + + // We need to check if this is a request that has a stamped client information header. + // This will contain an account but will represent the account from the leafnode. If + // they are not named the same this would cause an account lookup failure trying to + // process the request for something like JetStream or other system services that rely + // on the client info header. We can just check for reply and the presence of a header + // to avoid slow downs for all traffic. + if len(c.pa.reply) > 0 && c.pa.hdr >= 0 { + dmsg, hset = c.checkLeafClientInfoHeader(msg) + } + } + + if mt != nil { + dmsg = mt.setHopHeader(c, dmsg) + hset = true + } + + mh := c.msgHeaderForRouteOrLeaf(subject, reply, rt, acc) + if c.deliverMsg(prodIsMQTT, rt.sub, acc, subject, reply, mh, dmsg, false) { + if flags&pmrCollectQueueNames != 0 { + for _, qsub := range rt.qsubs { + queues = append(queues, qsub.queue) + } + } + if rt.sub.icb == nil { + dlvMsgs++ + switch dc.kind { + case ROUTER: + dlvRouteMsgs++ + case LEAF: + dlvLeafMsgs++ + } + dlvExtraSize += int64(len(dmsg) - len(msg)) + } + didDeliver = true + } + + // If we set the header reset the origin pub args. + if hset { + c.pa = pa + } + } + updateStats() + return didDeliver, queues +} + +// Check and swap accounts on a client info header destined across a leafnode. +func (c *client) checkLeafClientInfoHeader(msg []byte) (dmsg []byte, setHdr bool) { + if c.pa.hdr < 0 || len(msg) < c.pa.hdr { + return msg, false + } + cir := sliceHeader(ClientInfoHdr, msg[:c.pa.hdr]) + if len(cir) == 0 { + return msg, false + } + + dmsg = msg + + var ci ClientInfo + if err := json.Unmarshal(cir, &ci); err == nil { + if v, _ := c.srv.leafRemoteAccounts.Load(ci.Account); v != nil { + remoteAcc := v.(string) + if ci.Account != remoteAcc { + ci.Account = remoteAcc + if b, _ := json.Marshal(ci); b != nil { + dmsg, setHdr = c.setHeader(ClientInfoHdr, bytesToString(b), msg), true + } + } + } + } + return dmsg, setHdr +} + +func (c *client) pubPermissionViolation(subject []byte) { + errTxt := fmt.Sprintf("Permissions Violation for Publish to %q", subject) + if mt, _ := c.isMsgTraceEnabled(); mt != nil { + mt.setIngressError(errTxt) + } + c.sendErr(errTxt) + c.Errorf("Publish Violation - Subject %q", subject) +} + +func (c *client) subPermissionViolation(sub *subscription) { + errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q", sub.subject) + logTxt := fmt.Sprintf("Subscription Violation - Subject %q, SID %s", sub.subject, sub.sid) + + if sub.queue != nil { + errTxt = fmt.Sprintf("Permissions Violation for Subscription to %q using queue %q", sub.subject, sub.queue) + logTxt = fmt.Sprintf("Subscription Violation - Subject %q, Queue: %q, SID %s", sub.subject, sub.queue, sub.sid) + } + + c.sendErr(errTxt) + c.Errorf(logTxt) +} + +func (c *client) replySubjectViolation(reply []byte) { + errTxt := fmt.Sprintf("Permissions Violation for Publish with Reply of %q", reply) + if mt, _ := c.isMsgTraceEnabled(); mt != nil { + mt.setIngressError(errTxt) + } + c.sendErr(errTxt) + c.Errorf("Publish Violation - Reply %q", reply) +} + +func (c *client) maxTokensViolation(sub *subscription) { + errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q, too many tokens", sub.subject) + logTxt := fmt.Sprintf("Subscription Violation Too Many Tokens - Subject %q, SID %s", sub.subject, sub.sid) + c.sendErr(errTxt) + c.Errorf(logTxt) +} + +func (c *client) processPingTimer() { + c.mu.Lock() + c.ping.tmr = nil + // Check if connection is still opened + if c.isClosed() { + c.mu.Unlock() + return + } + + c.Debugf("%s Ping Timer", c.kindString()) + + var sendPing bool + + opts := c.srv.getOpts() + pingInterval := opts.PingInterval + if c.kind == ROUTER && opts.Cluster.PingInterval > 0 { + pingInterval = opts.Cluster.PingInterval + } + if c.isWebsocket() && opts.Websocket.PingInterval > 0 { + pingInterval = opts.Websocket.PingInterval + } + pingInterval = adjustPingInterval(c.kind, pingInterval) + now := time.Now() + needRTT := c.rtt == 0 || now.Sub(c.rttStart) > DEFAULT_RTT_MEASUREMENT_INTERVAL + + // Do not delay PINGs for ROUTER, GATEWAY or spoke LEAF connections. + if c.kind == ROUTER || c.kind == GATEWAY || c.isSpokeLeafNode() { + sendPing = true + } else { + // If we received client data or a ping from the other side within the PingInterval, + // then there is no need to send a ping. + if delta := now.Sub(c.lastIn); delta < pingInterval && !needRTT { + c.Debugf("Delaying PING due to remote client data or ping %v ago", delta.Round(time.Second)) + } else { + sendPing = true + } + } + + if sendPing { + // Check for violation + maxPingsOut := opts.MaxPingsOut + if c.kind == ROUTER && opts.Cluster.MaxPingsOut > 0 { + maxPingsOut = opts.Cluster.MaxPingsOut + } + if c.ping.out+1 > maxPingsOut { + c.Debugf("Stale Client Connection - Closing") + c.enqueueProto([]byte(fmt.Sprintf(errProto, "Stale Connection"))) + c.mu.Unlock() + c.closeConnection(StaleConnection) + return + } + // Send PING + c.sendPing() + } + + // Reset to fire again. + c.setPingTimer() + c.mu.Unlock() +} + +// Returns the smallest value between the given `d` and some max value +// based on the connection kind. +func adjustPingInterval(kind int, d time.Duration) time.Duration { + switch kind { + case ROUTER: + if d > routeMaxPingInterval { + return routeMaxPingInterval + } + case GATEWAY: + if d > gatewayMaxPingInterval { + return gatewayMaxPingInterval + } + } + return d +} + +// This is used when a connection cannot yet start to send PINGs because +// the remote would not be able to handle them (case of compression, +// or outbound gateway, etc...), but we still want to close the connection +// if the timer has not been reset by the time we reach the time equivalent +// to have sent the max number of pings. +// +// Lock should be held +func (c *client) watchForStaleConnection(pingInterval time.Duration, pingMax int) { + c.ping.tmr = time.AfterFunc(pingInterval*time.Duration(pingMax+1), func() { + c.mu.Lock() + c.Debugf("Stale Client Connection - Closing") + c.enqueueProto([]byte(fmt.Sprintf(errProto, "Stale Connection"))) + c.mu.Unlock() + c.closeConnection(StaleConnection) + }) +} + +// Lock should be held +func (c *client) setPingTimer() { + if c.srv == nil { + return + } + opts := c.srv.getOpts() + d := opts.PingInterval + if c.kind == ROUTER && opts.Cluster.PingInterval > 0 { + d = opts.Cluster.PingInterval + } + if c.isWebsocket() && opts.Websocket.PingInterval > 0 { + d = opts.Websocket.PingInterval + } + d = adjustPingInterval(c.kind, d) + c.ping.tmr = time.AfterFunc(d, c.processPingTimer) +} + +// Lock should be held +func (c *client) clearPingTimer() { + if c.ping.tmr == nil { + return + } + c.ping.tmr.Stop() + c.ping.tmr = nil +} + +func (c *client) clearTlsToTimer() { + if c.tlsTo == nil { + return + } + c.tlsTo.Stop() + c.tlsTo = nil +} + +// Lock should be held +func (c *client) setAuthTimer(d time.Duration) { + c.atmr = time.AfterFunc(d, c.authTimeout) +} + +// Lock should be held +func (c *client) clearAuthTimer() bool { + if c.atmr == nil { + return true + } + stopped := c.atmr.Stop() + c.atmr = nil + return stopped +} + +// Track whether the parser should still enforce pre-CONNECT rules. +// This is handshake state, not timer state, since some handshakes +// use a different timer while still expecting CONNECT. +// Lock assume held on entry. +func (c *client) awaitingAuth() bool { + return c.flags.isSet(expectConnect) && !c.flags.isSet(connectReceived) +} + +// This will set the atmr for the JWT expiration time. +// We will lock on entry. +func (c *client) setExpirationTimer(d time.Duration) { + c.mu.Lock() + c.setExpirationTimerUnlocked(d) + c.mu.Unlock() +} + +// This will set the atmr for the JWT expiration time. client lock should be held before call +func (c *client) setExpirationTimerUnlocked(d time.Duration) { + c.atmr = time.AfterFunc(d, c.authExpired) + // This is an JWT expiration. + if c.flags.isSet(connectReceived) { + c.expires = time.Now().Add(d).Truncate(time.Second) + } +} + +// Return when this client expires via a claim, or 0 if not set. +func (c *client) claimExpiration() time.Duration { + c.mu.Lock() + defer c.mu.Unlock() + if c.expires.IsZero() { + return 0 + } + return time.Until(c.expires).Truncate(time.Second) +} + +// Possibly flush the connection and then close the low level connection. +// The boolean `minimalFlush` indicates if the flush operation should have a +// minimal write deadline. +// Lock is held on entry. +func (c *client) flushAndClose(minimalFlush bool) { + if !c.flags.isSet(skipFlushOnClose) && c.out.pb > 0 { + if minimalFlush { + const lowWriteDeadline = 100 * time.Millisecond + + // Reduce the write deadline if needed. + if c.out.wdl > lowWriteDeadline { + c.out.wdl = lowWriteDeadline + } + } + c.flushOutbound() + } + for i := range c.out.nb { + nbPoolPut(c.out.nb[i]) + } + c.out.nb = nil + // We can't touch c.out.wnb when a flushOutbound is in progress since it + // is accessed outside the lock there. If in progress, the cleanup will be + // done in flushOutbound when detecting that connection is closed. + if !c.flags.isSet(flushOutbound) { + for i := range c.out.wnb { + nbPoolPut(c.out.wnb[i]) + } + c.out.wnb = nil + } + // This seem to be important (from experimentation) for the GC to release + // the connection. + c.out.sg = nil + + // Close the low level connection. + if c.nc != nil { + // Starting with Go 1.16, the low level close will set its own deadline + // of 5 seconds, so setting our own deadline does not work. Instead, + // we will close the TLS connection in separate go routine. + nc := c.nc + c.nc = nil + if _, ok := nc.(*tls.Conn); ok { + go func() { nc.Close() }() + } else { + nc.Close() + } + } +} + +var kindStringMap = map[int]string{ + CLIENT: "Client", + ROUTER: "Router", + GATEWAY: "Gateway", + LEAF: "Leafnode", + JETSTREAM: "JetStream", + ACCOUNT: "Account", + SYSTEM: "System", +} + +func (c *client) kindString() string { + if kindStringVal, ok := kindStringMap[c.kind]; ok { + return kindStringVal + } + return "Unknown Type" +} + +// swapAccountAfterReload will check to make sure the bound account for this client +// is current. Under certain circumstances after a reload we could be pointing to +// an older one. +func (c *client) swapAccountAfterReload() { + c.mu.Lock() + srv := c.srv + an := c.acc.GetName() + c.mu.Unlock() + if srv == nil { + return + } + if acc, _ := srv.LookupAccount(an); acc != nil { + c.mu.Lock() + if c.acc != acc { + c.acc = acc + } + c.mu.Unlock() + } +} + +// processSubsOnConfigReload removes any subscriptions the client has that are no +// longer authorized, and checks for imports (accounts) due to a config reload. +func (c *client) processSubsOnConfigReload(awcsti map[string]struct{}) { + c.mu.Lock() + var ( + checkPerms = c.perms != nil + checkAcc = c.acc != nil + acc = c.acc + ) + if !checkPerms && !checkAcc { + c.mu.Unlock() + return + } + var ( + _subs [32]*subscription + subs = _subs[:0] + _removed [32]*subscription + removed = _removed[:0] + srv = c.srv + ) + if checkAcc { + // We actually only want to check if stream imports have changed. + if _, ok := awcsti[acc.Name]; !ok { + checkAcc = false + } + } + // We will clear any mperms we have here. It will rebuild on the fly with canSubscribe, + // so we do that here as we collect them. We will check result down below. + c.mperms = nil + // Collect client's subs under the lock + for _, sub := range c.subs { + // Just checking to rebuild mperms under the lock, will collect removed though here. + // Only collect under subs array of canSubscribe and checkAcc true. + var allowed bool + if len(sub.queue) > 0 { + allowed = c.canSubscribe(string(sub.subject), string(sub.queue)) + } else { + allowed = c.canSubscribe(string(sub.subject)) + } + if !allowed { + removed = append(removed, sub) + } else if checkAcc { + subs = append(subs, sub) + } + } + c.mu.Unlock() + + // This list is all subs who are allowed and we need to check accounts. + for _, sub := range subs { + c.mu.Lock() + oldShadows := sub.shadow + sub.shadow = nil + c.mu.Unlock() + c.addShadowSubscriptions(acc, sub) + for _, nsub := range oldShadows { + nsub.im.acc.sl.Remove(nsub) + } + } + + // Unsubscribe all that need to be removed and report back to client and logs. + for _, sub := range removed { + c.unsubscribe(acc, sub, true, true) + if len(sub.queue) > 0 { + c.sendErr(fmt.Sprintf("Permissions Violation for Subscription to %q using queue %q (sid %q)", sub.subject, sub.queue, sub.sid)) + srv.Noticef("Removed sub %q using queue %q (sid %q) for %s - not authorized", sub.subject, sub.queue, sub.sid, c.getAuthUser()) + } else { + c.sendErr(fmt.Sprintf("Permissions Violation for Subscription to %q (sid %q)", sub.subject, sub.sid)) + srv.Noticef("Removed sub %q (sid %q) for %s - not authorized", sub.subject, sub.sid, c.getAuthUser()) + } + } +} + +// Allows us to count up all the queue subscribers during close. +type qsub struct { + sub *subscription + n int32 +} + +func (c *client) closeConnection(reason ClosedState) { + c.mu.Lock() + if c.flags.isSet(closeConnection) { + c.mu.Unlock() + return + } + // Note that we may have markConnAsClosed() invoked before closeConnection(), + // so don't set this to 1, instead bump the count. + c.rref++ + c.flags.set(closeConnection) + c.clearAuthTimer() + c.clearPingTimer() + c.clearTlsToTimer() + c.markConnAsClosed(reason) + + // Unblock anyone who is potentially stalled waiting on us. + if c.out.stc != nil { + close(c.out.stc) + c.out.stc = nil + } + + // If we have remote latency tracking running shut that down. + if c.rrTracking != nil { + c.rrTracking.ptmr.Stop() + c.rrTracking = nil + } + + // If we are shutting down, no need to do all the accounting on subs, etc. + // During LDM we'll still do the accounting, otherwise account limits could close others after this reconnects. + if reason == ServerShutdown && c.srv.isShuttingDown() { + s := c.srv + c.mu.Unlock() + if s != nil { + // Unregister + s.removeClient(c) + } + return + } + + var ( + kind = c.kind + srv = c.srv + noReconnect = c.flags.isSet(noReconnect) + acc = c.acc + ) + c.mu.Unlock() + + if kind == ROUTER { + c.removeRemoteSubs() + } + if srv != nil { + // Unregister + srv.removeClient(c) + + if acc != nil { + // Update remote subscriptions. + if kind == CLIENT || kind == LEAF || kind == JETSTREAM { + // Remove client's subscriptions from the account and unregister + // client from that account. Keep c.subs populated because + // saveClosedClient (started as a goroutine in markConnAsClosed) + // still needs to read it. + c.clearAccountSubs(true) + } + // Always remove from the account, otherwise we can leak clients. + // Note that SYSTEM and ACCOUNT types from above cleanup their own subs. + if prev := acc.removeClient(c); prev == 1 { + srv.decActiveAccounts() + } + } + } + + // Now that we are done with subscriptions, clear the field so that the + // connection can be released and gc'ed. + if kind == CLIENT || kind == LEAF { + c.mu.Lock() + c.subs = nil + c.mu.Unlock() + } + + // Don't reconnect connections that have been marked with + // the no reconnect flag. + if noReconnect { + return + } + + c.reconnect() +} + +// clearAccountSubs removes the client's subscriptions from its current account +// and unregisters it from that account. If close is true, c.subs is left +// populated for saveClosedClient; otherwise c.subs is cleared and c.acc +// registered back to the global account. +// Client lock MUST NOT be held on entry. +func (c *client) clearAccountSubs(close bool) { + c.mu.Lock() + kind := c.kind + srv := c.srv + acc := c.acc + if acc == nil || (kind != CLIENT && kind != LEAF && kind != JETSTREAM) { + c.mu.Unlock() + return + } + var _subs [32]*subscription + subs := _subs[:0] + // Do not set c.subs to nil or delete the sub from c.subs here because + // it will be needed in saveClosedClient (which has been started as a + // go routine in markConnAsClosed). Cleanup will be done there. + for _, sub := range c.subs { + // Auto-unsubscribe subscriptions must be unsubscribed forcibly. + sub.max = 0 + sub.close() + subs = append(subs, sub) + if !close { + delete(c.subs, string(sub.sid)) + } + } + spoke := c.isSpokeLeafNode() + c.mu.Unlock() + + acc.sl.RemoveBatch(subs) + + if srv != nil { + qsubs := map[string]*qsub{} + for _, sub := range subs { + // Call unsubscribe here to cleanup shadow subscriptions and such. + c.unsubscribe(acc, sub, true, false) + // Update route as normal for a normal subscriber. + if sub.queue == nil { + if !spoke { + srv.updateRouteSubscriptionMap(acc, sub, -1) + if srv.gateway.enabled { + srv.gatewayUpdateSubInterest(acc.Name, sub, -1) + } + } + acc.updateLeafNodes(sub, -1) + } else { + // We handle queue subscribers special in case we + // have a bunch we can just send one update to the + // connected routes. + num := int32(1) + if kind == LEAF { + num = sub.qw + } + key := keyFromSub(sub) + if esub, ok := qsubs[key]; ok { + esub.n += num + } else { + qsubs[key] = &qsub{sub, num} + } + } + } + // Process any qsubs here. + for _, esub := range qsubs { + if !spoke { + srv.updateRouteSubscriptionMap(acc, esub.sub, -esub.n) + if srv.gateway.enabled { + srv.gatewayUpdateSubInterest(acc.Name, esub.sub, -esub.n) + } + } + acc.updateLeafNodes(esub.sub, -esub.n) + } + } + + if !close { + // Register back to global account, mimicking the state after client initialization. + c.registerWithAccount(srv.globalAccount()) + } +} + +// Depending on the kind of connections, this may attempt to recreate a connection. +// The actual reconnect attempt will be started in a go routine. +func (c *client) reconnect() { + var ( + retryImplicit bool + gwName string + gwIsOutbound bool + gwCfg *gatewayCfg + leafCfg *leafNodeCfg + ) + + c.mu.Lock() + // Decrease the ref count and perform the reconnect only if == 0. + c.rref-- + if c.flags.isSet(noReconnect) || c.rref > 0 { + c.mu.Unlock() + return + } + if c.route != nil { + // A route is marked as solicited if it was given an URL to connect to, + // which would be the case even with implicit (due to gossip), so mark this + // as a retry for a route that is solicited and not explicit. + retryImplicit = c.route.retry || (c.route.didSolicit && c.route.routeType == Implicit) + } + kind := c.kind + switch kind { + case GATEWAY: + gwName = c.gw.name + gwIsOutbound = c.gw.outbound + gwCfg = c.gw.cfg + case LEAF: + if c.isSolicitedLeafNode() { + leafCfg = c.leaf.remote + } + } + srv := c.srv + c.mu.Unlock() + + // Check for a solicited route. If it was, start up a reconnect unless + // we are already connected to the other end. + if didSolicit := c.isSolicitedRoute(); didSolicit || retryImplicit { + srv.mu.Lock() + defer srv.mu.Unlock() + + // Capture these under lock + c.mu.Lock() + rid := c.route.remoteID + rtype := c.route.routeType + rurl := c.route.url + accName := string(c.route.accName) + checkRID := accName == _EMPTY_ && srv.getOpts().Cluster.PoolSize < 1 && rid != _EMPTY_ + c.mu.Unlock() + + // It is possible that the server is being shutdown. + // If so, don't try to reconnect + if !srv.isRunning() { + return + } + + if checkRID && srv.routes[rid] != nil { + // This is the case of "no pool". Make sure that the registered one + // is upgraded to solicited if the connection trying to reconnect + // was a solicited one. + if didSolicit { + if remote := srv.routes[rid][0]; remote != nil { + upgradeRouteToSolicited(remote, rurl, rtype) + } + } + srv.Debugf("Not attempting reconnect for solicited route, already connected to %q", rid) + return + } else if rid == srv.info.ID { + srv.Debugf("Detected route to self, ignoring %q", rurl.Redacted()) + return + } else if rtype != Implicit || retryImplicit { + srv.Debugf("Attempting reconnect for solicited route %q", rurl.Redacted()) + // Keep track of this go-routine so we can wait for it on + // server shutdown. + srv.startGoRoutine(func() { srv.reConnectToRoute(rurl, rtype, accName) }) + } + } else if srv != nil && kind == GATEWAY && gwIsOutbound { + if gwCfg != nil { + srv.Debugf("Attempting reconnect for gateway %q", gwName) + // Run this as a go routine since we may be called within + // the solicitGateway itself if there was an error during + // the creation of the gateway connection. + srv.startGoRoutine(func() { srv.reconnectGateway(gwCfg) }) + } else { + srv.Debugf("Gateway %q not in configuration, not attempting reconnect", gwName) + } + } else if leafCfg != nil { + // This is a solicited leaf node. Start up a reconnect. + srv.startGoRoutine(func() { srv.reConnectToRemoteLeafNode(leafCfg) }) + } +} + +// Set the noReconnect flag. This is used before a call to closeConnection() +// to prevent the connection to reconnect (routes, gateways). +func (c *client) setNoReconnect() { + c.mu.Lock() + c.flags.set(noReconnect) + c.mu.Unlock() +} + +// Returns the client's RTT value with the protection of the client's lock. +func (c *client) getRTTValue() time.Duration { + c.mu.Lock() + rtt := c.rtt + c.mu.Unlock() + return rtt +} + +// This function is used by ROUTER and GATEWAY connections to +// look for a subject on a given account (since these type of +// connections are not bound to a specific account). +// If the c.pa.subject is found in the cache, the cached result +// is returned, otherwse, we match the account's sublist and update +// the cache. The cache is pruned if reaching a certain size. +func (c *client) getAccAndResultFromCache() (*Account, *SublistResult) { + var ( + acc *Account + pac *perAccountCache + r *SublistResult + ok bool + ) + // Check our cache. + if pac, ok = c.in.pacache[string(c.pa.pacache)]; ok { + // Check the genid to see if it's still valid. + // Since v2.10.0, the config reload of accounts has been fixed + // and an account's sublist pointer should not change, so no need to + // lock to access it. + sl := pac.acc.sl + + if genid := atomic.LoadUint64(&sl.genid); genid != pac.genid { + ok = false + clear(c.in.pacache) + } else { + acc = pac.acc + r = pac.results + } + } + + if !ok { + if c.kind == ROUTER && len(c.route.accName) > 0 { + if acc = c.acc; acc == nil { + return nil, nil + } + } else { + // Match correct account and sublist. + if acc, _ = c.srv.LookupAccount(bytesToString(c.pa.account)); acc == nil { + return nil, nil + } + } + sl := acc.sl + + // Match against the account sublist. + r = sl.MatchBytes(c.pa.subject) + + // Check if we need to prune. This should give us a perAccountCache struct + // to reuse instead of having to allocate a new one. + // Previously we would have removed multiple entries but now we will only + // prune the minimum number required to maintain the cache size, so that + // we reduce the amount of GC pressure and maintain cache stability as best + // as possible. + if len(c.in.pacache) >= maxPerAccountCacheSize { + for cacheKey, p := range c.in.pacache { + delete(c.in.pacache, cacheKey) + pac = p + if len(c.in.pacache) < maxPerAccountCacheSize { + break + } + } + } + + // If we can reuse the pac from earlier (i.e. we loaded one but it was an + // old generation or we pruned the cache) then do so. + if pac == nil { + pac = &perAccountCache{} + } + pac.acc = acc + pac.results = r + pac.genid = atomic.LoadUint64(&sl.genid) + + // Store in our cache,make sure to do so after we prune. + c.in.pacache[string(c.pa.pacache)] = pac + } + return acc, r +} + +// Account will return the associated account for this client. +func (c *client) Account() *Account { + if c == nil { + return nil + } + c.mu.Lock() + acc := c.acc + c.mu.Unlock() + return acc +} + +// pruneClosedSubFromPerAccountCache remove entries that contain subscriptions +// that have been closed. +func (c *client) pruneClosedSubFromPerAccountCache() { + for cacheKey, pac := range c.in.pacache { + for _, sub := range pac.results.psubs { + if sub.isClosed() { + goto REMOVE + } + } + for _, qsub := range pac.results.qsubs { + for _, sub := range qsub { + if sub.isClosed() { + goto REMOVE + } + } + } + continue + REMOVE: + delete(c.in.pacache, cacheKey) + } +} + +// Returns our service account for this request. +func (ci *ClientInfo) serviceAccount() string { + if ci == nil { + return _EMPTY_ + } + if ci.Service != _EMPTY_ { + return ci.Service + } + return ci.Account +} + +// Add in our server and cluster information to this client info. +func (c *client) addServerAndClusterInfo(ci *ClientInfo) { + if ci == nil { + return + } + // Server + if c.kind != LEAF { + ci.Server = c.srv.Name() + } else if c.kind == LEAF { + ci.Server = c.leaf.remoteServer + } + // Cluster + ci.Cluster = c.srv.cachedClusterName() + // If we have gateways fill in cluster alternates. + // These will be in RTT asc order. + if c.srv.gateway.enabled { + var gws []*client + c.srv.getOutboundGatewayConnections(&gws) + for _, c := range gws { + c.mu.Lock() + cn := c.gw.name + c.mu.Unlock() + ci.Alternates = append(ci.Alternates, cn) + } + } +} + +// Grabs the information for this client. +func (c *client) getClientInfo(detailed bool) *ClientInfo { + if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) { + return nil + } + + // Result + var ci ClientInfo + + if detailed { + c.addServerAndClusterInfo(&ci) + } + + c.mu.Lock() + // RTT and Account are always added. + ci.Account = accForClient(c) + ci.RTT = c.rtt + // Detailed signals additional opt in. + if detailed { + ci.Start = &c.start + ci.Host = c.host + ci.ID = c.cid + ci.Name = c.opts.Name + ci.User = c.getRawAuthUser() + ci.Lang = c.opts.Lang + ci.Version = c.opts.Version + ci.Jwt = c.opts.JWT + ci.IssuerKey = issuerForClient(c) + ci.NameTag = c.nameTag + ci.Tags = c.tags + ci.Kind = c.kindString() + ci.ClientType = c.clientTypeString() + } + c.mu.Unlock() + return &ci +} + +func (c *client) doTLSServerHandshake(typ string, tlsConfig *tls.Config, timeout float64, pCerts PinnedCertSet) error { + _, err := c.doTLSHandshake(typ, false, nil, tlsConfig, _EMPTY_, timeout, pCerts) + return err +} + +func (c *client) doTLSClientHandshake(typ string, url *url.URL, tlsConfig *tls.Config, tlsName string, timeout float64, pCerts PinnedCertSet) (bool, error) { + return c.doTLSHandshake(typ, true, url, tlsConfig, tlsName, timeout, pCerts) +} + +// Performs either server or client side (if solicit is true) TLS Handshake. +// On error, the TLS handshake error has been logged and the connection +// has been closed. +// +// Lock is held on entry. +func (c *client) doTLSHandshake(typ string, solicit bool, url *url.URL, tlsConfig *tls.Config, tlsName string, timeout float64, pCerts PinnedCertSet) (bool, error) { + var host string + var resetTLSName bool + var err error + + // Capture kind for some debug/error statements. + kind := c.kind + + // If we solicited, we will act like the client, otherwise the server. + if solicit { + c.Debugf("Starting TLS %s client handshake", typ) + if tlsConfig.ServerName == _EMPTY_ { + // If the given url is a hostname, use this hostname for the + // ServerName. If it is an IP, use the cfg's tlsName. If none + // is available, resort to current IP. + host = url.Hostname() + if tlsName != _EMPTY_ && net.ParseIP(host) != nil { + host = tlsName + } + tlsConfig.ServerName = host + } + c.nc = tls.Client(c.nc, tlsConfig) + } else { + if kind == CLIENT { + c.Debugf("Starting TLS client connection handshake") + } else { + c.Debugf("Starting TLS %s server handshake", typ) + } + c.nc = tls.Server(c.nc, tlsConfig) + } + + conn := c.nc.(*tls.Conn) + + // Setup the timeout + ttl := secondsToDuration(timeout) + c.tlsTo = time.AfterFunc(ttl, func() { tlsTimeout(c, conn) }) + conn.SetReadDeadline(time.Now().Add(ttl)) + + c.mu.Unlock() + if err = conn.Handshake(); err != nil { + if solicit { + // Based on type of error, possibly clear the saved tlsName + // See: https://github.com/nats-io/nats-server/issues/1256 + // NOTE: As of Go 1.20, the HostnameError is wrapped so cannot + // type assert to check directly. + var hostnameErr x509.HostnameError + if errors.As(err, &hostnameErr) { + if host == tlsName { + resetTLSName = true + } + } + } + } else if !c.matchesPinnedCert(pCerts) { + err = ErrCertNotPinned + } + + if err != nil { + var detail string + var subjs []string + if ve, ok := err.(*tls.CertificateVerificationError); ok { + for _, cert := range ve.UnverifiedCertificates { + fp := sha256.Sum256(cert.Raw) + fph := hex.EncodeToString(fp[:]) + subjs = append(subjs, fmt.Sprintf("%s SHA-256: %s", cert.Subject.String(), fph)) + } + } + if len(subjs) > 0 { + detail = fmt.Sprintf(" (%s)", strings.Join(subjs, "; ")) + } + if kind == ROUTER || kind == GATEWAY { + // Always surface these as errors, as these ports shouldn't be behind a load + // balancer or regularly probed. + c.Errorf("TLS %s handshake error: %v%s", typ, err, detail) + } else { + logf := c.Errorf + if isClientProbeTLSHandshakeError(err) { + logf = c.Debugf + } + if kind == CLIENT { + logf("TLS handshake error: %v%s", err, detail) + } else { + logf("TLS %s handshake error: %v%s", typ, err, detail) + } + } + c.closeConnection(TLSHandshakeError) + + // Grab the lock before returning since the caller was holding the lock on entry + c.mu.Lock() + // Returning any error is fine. Since the connection is closed ErrConnectionClosed + // is appropriate. + return resetTLSName, ErrConnectionClosed + } + + // Reset the read deadline + conn.SetReadDeadline(time.Time{}) + + // Re-Grab lock + c.mu.Lock() + + // To be consistent with client, set this flag to indicate that handshake is done + c.flags.set(handshakeComplete) + + // The connection still may have been closed on success handshake due + // to a race with tls timeout. If that the case, return error indicating + // that the connection is closed. + if c.isClosed() { + err = ErrConnectionClosed + } + + return false, err +} + +func isClientProbeTLSHandshakeError(err error) bool { + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return true + } + var recordHeaderErr tls.RecordHeaderError + // Conn is only set by crypto/tls when the invalid record was the peer's + // initial handshake bytes, which is the non-TLS probe/load-balancer case. + return errors.As(err, &recordHeaderErr) && recordHeaderErr.Conn != nil +} + +// getRawAuthUser returns the raw auth user for the client. +// Lock should be held. +func (c *client) getRawAuthUser() string { + switch { + case c.opts.Nkey != _EMPTY_: + return c.opts.Nkey + case c.opts.Username != _EMPTY_: + return c.opts.Username + case c.opts.JWT != _EMPTY_: + return c.pubKey + case c.opts.Token != _EMPTY_: + return "[REDACTED]" + default: + return _EMPTY_ + } +} + +// getAuthUser returns the auth user for the client. +// Lock should be held. +func (c *client) getAuthUser() string { + switch { + case c.opts.Nkey != _EMPTY_: + return fmt.Sprintf("Nkey %q", c.opts.Nkey) + case c.opts.Username != _EMPTY_: + return fmt.Sprintf("User %q", c.opts.Username) + case c.opts.JWT != _EMPTY_: + return fmt.Sprintf("JWT User %q", c.pubKey) + case c.opts.Token != _EMPTY_: + return fmt.Sprintf("Token %q", "[REDACTED]") + default: + return `User "N/A"` + } +} + +// getAuthUserLabel returns a label for the auth user for the client. +func (c *client) getAuthUserLabel() string { + switch { + case c.opts.Nkey != _EMPTY_: + return fmt.Sprintf("nkey:%s", c.opts.Nkey) + case c.opts.Username != _EMPTY_: + return fmt.Sprintf("user:%s", c.opts.Username) + case c.opts.JWT != _EMPTY_: + return fmt.Sprintf("jwt:%s", c.pubKey) + case c.opts.Token != _EMPTY_: + return "token" + default: + return "" + } +} + +// Given an array of strings, this function converts it to a map as long +// as all the content (converted to upper-case) matches some constants. + +// Converts the given array of strings to a map of string. +// The strings are converted to upper-case and added to the map only +// if the server recognize them as valid connection types. +// If there are unknown connection types, the map of valid ones is returned +// along with an error that contains the name of the unknown. +func convertAllowedConnectionTypes(cts []string) (map[string]struct{}, error) { + var unknown []string + m := make(map[string]struct{}, len(cts)) + for _, i := range cts { + i = strings.ToUpper(i) + switch i { + case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket, + jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeLeafnodeWS, + jwt.ConnectionTypeMqtt, jwt.ConnectionTypeMqttWS, + jwt.ConnectionTypeInProcess: + m[i] = struct{}{} + default: + unknown = append(unknown, i) + } + } + var err error + // We will still return the map of valid ones. + if len(unknown) != 0 { + err = fmt.Errorf("invalid connection types %q", unknown) + } + return m, err +} + +// This will return true if the connection is of a type present in the given `acts` map. +// Note that so far this is used only for CLIENT or LEAF connections. +// But a CLIENT can be standard or websocket (and other types in the future). +func (c *client) connectionTypeAllowed(acts map[string]struct{}) bool { + // Empty means all type of clients are allowed + if len(acts) == 0 { + return true + } + var want string + switch c.kind { + case CLIENT: + switch c.clientType() { + case NATS: + if c.iproc { + want = jwt.ConnectionTypeInProcess + } else { + want = jwt.ConnectionTypeStandard + } + case WS: + want = jwt.ConnectionTypeWebsocket + case MQTT: + if c.isWebsocket() { + want = jwt.ConnectionTypeMqttWS + } else { + want = jwt.ConnectionTypeMqtt + } + } + case LEAF: + if c.isWebsocket() { + want = jwt.ConnectionTypeLeafnodeWS + } else { + want = jwt.ConnectionTypeLeafnode + } + } + _, ok := acts[want] + return ok +} + +// isClosed returns true if either closeConnection or connMarkedClosed +// flag have been set, or if `nc` is nil, which may happen in tests. +func (c *client) isClosed() bool { + return c.flags.isSet(closeConnection) || c.flags.isSet(connMarkedClosed) || c.nc == nil +} + +func (c *client) format(format string) string { + if s := c.String(); s != _EMPTY_ { + return fmt.Sprintf("%s - %s", s, format) + } else { + return format + } +} + +func (c *client) formatNoClientInfo(format string) string { + acc := c.ncsAcc.Load() + if acc != nil { + return fmt.Sprintf("%s - Account:%s", format, acc) + } else { + return format + } +} + +func (c *client) formatClientSuffix() string { + user := c.ncsUser.Load() + if user == nil || user.(string) == _EMPTY_ { + return _EMPTY_ + } + return fmt.Sprintf(" - %s", user) +} + +// Logging functionality scoped to a client or route. +func (c *client) Error(err error) { + c.srv.Errorf(c.format(err.Error())) +} + +func (c *client) Errorf(format string, v ...any) { + c.srv.Errorf(c.format(format), v...) +} + +func (c *client) Debugf(format string, v ...any) { + c.srv.Debugf(c.format(format), v...) +} + +func (c *client) Noticef(format string, v ...any) { + c.srv.Noticef(c.format(format), v...) +} + +func (c *client) Tracef(format string, v ...any) { + c.srv.Tracef(c.format(format), v...) +} + +func (c *client) Warnf(format string, v ...any) { + c.srv.Warnf(c.format(format), v...) +} + +func (c *client) RateLimitErrorf(format string, v ...any) { + // Do the check before adding the client info to the format... + statement := fmt.Sprintf(c.formatNoClientInfo(format), v...) + if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded { + return + } + if s := c.String(); s != _EMPTY_ { + c.srv.Errorf("%s - %s%s", c, statement, c.formatClientSuffix()) + } else { + c.srv.Errorf("%s%s", statement, c.formatClientSuffix()) + } +} + +func (c *client) rateLimitFormatWarnf(format string, v ...any) { + // Do the check before adding the client info to the format... + format = c.formatNoClientInfo(format) + if _, loaded := c.srv.rateLimitLogging.LoadOrStore(format, time.Now()); loaded { + return + } + statement := fmt.Sprintf(format, v...) + if s := c.String(); s != _EMPTY_ { + c.srv.Warnf("%s - %s%s", c, statement, c.formatClientSuffix()) + } else { + c.srv.Warnf("%s%s", statement, c.formatClientSuffix()) + } +} + +func (c *client) RateLimitWarnf(format string, v ...any) { + // Do the check before adding the client info to the format... + statement := fmt.Sprintf(c.formatNoClientInfo(format), v...) + if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded { + return + } + if s := c.String(); s != _EMPTY_ { + c.srv.Warnf("%s - %s%s", c, statement, c.formatClientSuffix()) + } else { + c.srv.Warnf("%s%s", statement, c.formatClientSuffix()) + } +} + +func (c *client) RateLimitDebugf(format string, v ...any) { + // Do the check before adding the client info to the format... + statement := fmt.Sprintf(c.formatNoClientInfo(format), v...) + if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded { + return + } + if s := c.String(); s != _EMPTY_ { + c.srv.Debugf("%s - %s%s", c, statement, c.formatClientSuffix()) + } else { + c.srv.Debugf("%s%s", statement, c.formatClientSuffix()) + } +} + +// Set the very first PING to a lower interval to capture the initial RTT. +// After that the PING interval will be set to the user defined value. +// Client lock should be held. +func (c *client) setFirstPingTimer() { + s := c.srv + if s == nil { + return + } + opts := s.getOpts() + d := opts.PingInterval + + if c.kind == ROUTER && opts.Cluster.PingInterval > 0 { + d = opts.Cluster.PingInterval + } + if c.isWebsocket() && opts.Websocket.PingInterval > 0 { + d = opts.Websocket.PingInterval + } + if !opts.DisableShortFirstPing { + if c.kind != CLIENT { + if d > firstPingInterval { + d = firstPingInterval + } + d = adjustPingInterval(c.kind, d) + } else if d > firstClientPingInterval { + d = firstClientPingInterval + } + } + // We randomize the first one by an offset up to 20%, e.g. 2m ~= max 24s. + addDelay := rand.Int64N(int64(d / 5)) + d += time.Duration(addDelay) + // In the case of ROUTER/LEAF and when compression is configured, it is possible + // that this timer was already set, but just to detect a stale connection + // since we have to delay the first PING after compression negotiation + // occurred. + if c.ping.tmr != nil { + c.ping.tmr.Stop() + } + c.ping.tmr = time.AfterFunc(d, c.processPingTimer) +} + +// Sets this error as the authentication error. To be used in authViolation() +// to report an error different of `ErrAuthentication`. +func (c *client) setAuthError(err error) { + c.mu.Lock() + c.authErr = err + c.mu.Unlock() +} + +// Returns the authentication error set in the connection, possibly nil. +func (c *client) getAuthError() error { + c.mu.Lock() + defer c.mu.Unlock() + return c.authErr +} diff --git a/server/consumer.go b/server/consumer.go new file mode 100644 index 0000000..8187be4 --- /dev/null +++ b/server/consumer.go @@ -0,0 +1,7329 @@ +// 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 ( + "bytes" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "maps" + "math" + "math/rand/v2" + "os" + "path/filepath" + "reflect" + "regexp" + "slices" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/nats-io/nats-server/v2/server/avl" + "github.com/nats-io/nats-server/v2/server/gsl" + "github.com/nats-io/nuid" + "golang.org/x/time/rate" +) + +// Headers sent with Request Timeout +const ( + JSPullRequestPendingMsgs = "Nats-Pending-Messages" + JSPullRequestPendingBytes = "Nats-Pending-Bytes" + JSPullRequestNatsPinId = "Nats-Pin-Id" +) + +var ( + validGroupName = regexp.MustCompile(`^[a-zA-Z0-9/_=-]{1,16}$`) +) + +// Headers sent when batch size was completed, but there were remaining bytes. +const JsPullRequestRemainingBytesT = "NATS/1.0 409 Batch Completed\r\n%s: %d\r\n%s: %d\r\n\r\n" + +type ConsumerInfo struct { + Stream string `json:"stream_name"` + Name string `json:"name"` + Created time.Time `json:"created"` + Config *ConsumerConfig `json:"config,omitempty"` + Delivered SequenceInfo `json:"delivered"` + AckFloor SequenceInfo `json:"ack_floor"` + NumAckPending int `json:"num_ack_pending"` + NumRedelivered int `json:"num_redelivered"` + NumWaiting int `json:"num_waiting"` + NumPending uint64 `json:"num_pending"` + Cluster *ClusterInfo `json:"cluster,omitempty"` + PushBound bool `json:"push_bound,omitempty"` + Paused bool `json:"paused,omitempty"` + PauseRemaining time.Duration `json:"pause_remaining,omitempty"` + // TimeStamp indicates when the info was gathered + TimeStamp time.Time `json:"ts"` + PriorityGroups []PriorityGroupState `json:"priority_groups,omitempty"` +} + +// consumerInfoClusterResponse is a response used in a cluster to communicate the consumer info +// back to the meta leader as part of a consumer list request. +type consumerInfoClusterResponse struct { + ConsumerInfo + OfflineReason string `json:"offline_reason,omitempty"` // Reporting when a consumer is offline. +} + +type PriorityGroupState struct { + Group string `json:"group"` + PinnedClientID string `json:"pinned_client_id,omitempty"` + PinnedTS time.Time `json:"pinned_ts,omitempty"` +} + +type ConsumerConfig struct { + Durable string `json:"durable_name,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + DeliverPolicy DeliverPolicy `json:"deliver_policy"` + OptStartSeq uint64 `json:"opt_start_seq,omitempty"` + OptStartTime *time.Time `json:"opt_start_time,omitempty"` + AckPolicy AckPolicy `json:"ack_policy"` + AckWait time.Duration `json:"ack_wait,omitempty"` + MaxDeliver int `json:"max_deliver,omitempty"` + BackOff []time.Duration `json:"backoff,omitempty"` + FilterSubject string `json:"filter_subject,omitempty"` + FilterSubjects []string `json:"filter_subjects,omitempty"` + ReplayPolicy ReplayPolicy `json:"replay_policy"` + RateLimit uint64 `json:"rate_limit_bps,omitempty"` // Bits per sec + SampleFrequency string `json:"sample_freq,omitempty"` + MaxWaiting int `json:"max_waiting,omitempty"` + MaxAckPending int `json:"max_ack_pending,omitempty"` + FlowControl bool `json:"flow_control,omitempty"` + HeadersOnly bool `json:"headers_only,omitempty"` + + // Pull based options. + MaxRequestBatch int `json:"max_batch,omitempty"` + MaxRequestExpires time.Duration `json:"max_expires,omitempty"` + MaxRequestMaxBytes int `json:"max_bytes,omitempty"` + + // Push based consumers. + DeliverSubject string `json:"deliver_subject,omitempty"` + DeliverGroup string `json:"deliver_group,omitempty"` + Heartbeat time.Duration `json:"idle_heartbeat,omitempty"` + + // Ephemeral inactivity threshold. + InactiveThreshold time.Duration `json:"inactive_threshold,omitempty"` + + // Generally inherited by parent stream and other markers, now can be configured directly. + Replicas int `json:"num_replicas"` + // Force memory storage. + MemoryStorage bool `json:"mem_storage,omitempty"` + + // Don't add to general clients. + Direct bool `json:"direct,omitempty"` + Sourcing bool `json:"sourcing,omitempty"` + + // Metadata is additional metadata for the Consumer. + Metadata map[string]string `json:"metadata,omitempty"` + + // PauseUntil is for suspending the consumer until the deadline. + PauseUntil *time.Time `json:"pause_until,omitempty"` + + // Priority groups + PriorityGroups []string `json:"priority_groups,omitempty"` + PriorityPolicy PriorityPolicy `json:"priority_policy,omitempty"` + PinnedTTL time.Duration `json:"priority_timeout,omitempty"` +} + +// clone performs a deep copy of the ConsumerConfig struct, returning a new clone with +// all values copied. +func (cfg *ConsumerConfig) clone() *ConsumerConfig { + clone := *cfg + if cfg.BackOff != nil { + clone.BackOff = slices.Clone(cfg.BackOff) + } + if cfg.FilterSubjects != nil { + clone.FilterSubjects = slices.Clone(cfg.FilterSubjects) + } + if cfg.Metadata != nil { + clone.Metadata = maps.Clone(cfg.Metadata) + } + if cfg.PriorityGroups != nil { + clone.PriorityGroups = slices.Clone(cfg.PriorityGroups) + } + return &clone +} + +// SequenceInfo has both the consumer and the stream sequence and last activity. +type SequenceInfo struct { + Consumer uint64 `json:"consumer_seq"` + Stream uint64 `json:"stream_seq"` + Last *time.Time `json:"last_active,omitempty"` +} + +type CreateConsumerRequest struct { + Stream string `json:"stream_name"` + Config ConsumerConfig `json:"config"` + Action ConsumerAction `json:"action"` + Pedantic bool `json:"pedantic,omitempty"` +} + +type ConsumerAction int + +const ( + ActionCreateOrUpdate ConsumerAction = iota + ActionUpdate + ActionCreate +) + +const ( + actionUpdateJSONString = `"update"` + actionCreateJSONString = `"create"` + actionCreateOrUpdateJSONString = `""` +) + +var ( + actionUpdateJSONBytes = []byte(actionUpdateJSONString) + actionCreateJSONBytes = []byte(actionCreateJSONString) + actionCreateOrUpdateJSONBytes = []byte(actionCreateOrUpdateJSONString) +) + +func (a ConsumerAction) String() string { + switch a { + case ActionCreateOrUpdate: + return actionCreateOrUpdateJSONString + case ActionCreate: + return actionCreateJSONString + case ActionUpdate: + return actionUpdateJSONString + } + return actionCreateOrUpdateJSONString +} + +func (a ConsumerAction) MarshalJSON() ([]byte, error) { + switch a { + case ActionCreate: + return actionCreateJSONBytes, nil + case ActionUpdate: + return actionUpdateJSONBytes, nil + case ActionCreateOrUpdate: + return actionCreateOrUpdateJSONBytes, nil + default: + return nil, fmt.Errorf("can not marshal %v", a) + } +} + +func (a *ConsumerAction) UnmarshalJSON(data []byte) error { + switch string(data) { + case actionCreateJSONString: + *a = ActionCreate + case actionUpdateJSONString: + *a = ActionUpdate + case actionCreateOrUpdateJSONString: + *a = ActionCreateOrUpdate + default: + return fmt.Errorf("unknown consumer action: %v", string(data)) + } + return nil +} + +// ConsumerNakOptions is for optional NAK values, e.g. delay. +type ConsumerNakOptions struct { + Delay time.Duration `json:"delay"` +} + +// PriorityPolicy determines policy for selecting messages based on priority. +type PriorityPolicy int + +const ( + // No priority policy. + PriorityNone PriorityPolicy = iota + // Clients will get the messages only if certain criteria are specified. + PriorityOverflow + // Single client takes over handling of the messages, while others are on standby. + PriorityPinnedClient + // Clients with lowest priority will be selected first. + PriorityPrioritized +) + +const ( + PriorityNoneJSONString = `"none"` + PriorityOverflowJSONString = `"overflow"` + PriorityPinnedClientJSONString = `"pinned_client"` + PriorityPrioritizedJSONString = `"prioritized"` +) + +var ( + PriorityNoneJSONBytes = []byte(PriorityNoneJSONString) + PriorityOverflowJSONBytes = []byte(PriorityOverflowJSONString) + PriorityPinnedClientJSONBytes = []byte(PriorityPinnedClientJSONString) + PriorityPrioritizedJSONBytes = []byte(PriorityPrioritizedJSONString) +) + +func (pp PriorityPolicy) String() string { + switch pp { + case PriorityOverflow: + return PriorityOverflowJSONString + case PriorityPinnedClient: + return PriorityPinnedClientJSONString + case PriorityPrioritized: + return PriorityPrioritizedJSONString + default: + return PriorityNoneJSONString + } +} + +func (pp PriorityPolicy) MarshalJSON() ([]byte, error) { + switch pp { + case PriorityOverflow: + return PriorityOverflowJSONBytes, nil + case PriorityPinnedClient: + return PriorityPinnedClientJSONBytes, nil + case PriorityPrioritized: + return PriorityPrioritizedJSONBytes, nil + case PriorityNone: + return PriorityNoneJSONBytes, nil + default: + return nil, fmt.Errorf("unknown priority policy: %v", pp) + } +} + +func (pp *PriorityPolicy) UnmarshalJSON(data []byte) error { + switch string(data) { + case PriorityOverflowJSONString: + *pp = PriorityOverflow + case PriorityPinnedClientJSONString: + *pp = PriorityPinnedClient + case PriorityPrioritizedJSONString: + *pp = PriorityPrioritized + case PriorityNoneJSONString: + *pp = PriorityNone + default: + return fmt.Errorf("unknown priority policy: %v", string(data)) + } + return nil +} + +// DeliverPolicy determines how the consumer should select the first message to deliver. +type DeliverPolicy int + +const ( + // DeliverAll will be the default so can be omitted from the request. + DeliverAll DeliverPolicy = iota + // DeliverLast will start the consumer with the last sequence received. + DeliverLast + // DeliverNew will only deliver new messages that are sent after the consumer is created. + DeliverNew + // DeliverByStartSequence will look for a defined starting sequence to start. + DeliverByStartSequence + // DeliverByStartTime will select the first messsage with a timestamp >= to StartTime. + DeliverByStartTime + // DeliverLastPerSubject will start the consumer with the last message for all subjects received. + DeliverLastPerSubject +) + +func (dp DeliverPolicy) String() string { + switch dp { + case DeliverAll: + return "all" + case DeliverLast: + return "last" + case DeliverNew: + return "new" + case DeliverByStartSequence: + return "by_start_sequence" + case DeliverByStartTime: + return "by_start_time" + case DeliverLastPerSubject: + return "last_per_subject" + default: + return "undefined" + } +} + +// AckPolicy determines how the consumer should acknowledge delivered messages. +type AckPolicy int + +const ( + // AckNone requires no acks for delivered messages. + AckNone AckPolicy = iota + // AckAll when acking a sequence number, this implicitly acks all sequences below this one as well. + AckAll + // AckExplicit requires ack or nack for all messages. + AckExplicit + // AckFlowControl functions like AckAll, but acks based on responses to flow control. + AckFlowControl +) + +func (a AckPolicy) String() string { + switch a { + case AckNone: + return "none" + case AckAll: + return "all" + case AckFlowControl: + return "flow_control" + default: + return "explicit" + } +} + +// ReplayPolicy determines how the consumer should replay messages it already has queued in the stream. +type ReplayPolicy int + +const ( + // ReplayInstant will replay messages as fast as possible. + ReplayInstant ReplayPolicy = iota + // ReplayOriginal will maintain the same timing as the messages were received. + ReplayOriginal +) + +func (r ReplayPolicy) String() string { + switch r { + case ReplayInstant: + return replayInstantPolicyJSONString + default: + return replayOriginalPolicyJSONString + } +} + +// OK +const OK = "+OK" + +// Ack responses. Note that a nil or no payload is same as AckAck +var ( + // Ack + AckAck = []byte("+ACK") // nil or no payload to ack subject also means ACK + AckOK = []byte(OK) // deprecated but +OK meant ack as well. + + // Nack + AckNak = []byte("-NAK") + // Progress indicator + AckProgress = []byte("+WPI") + // Ack + Deliver the next message(s). + AckNext = []byte("+NXT") + // Terminate delivery of the message. + AckTerm = []byte("+TERM") +) + +const ( + // reasons to supply when terminating messages using limits + ackTermLimitsReason = "Message deleted by stream limits" + ackTermUnackedLimitsReason = "Unacknowledged message was deleted" +) + +// Calculate accurate replicas for the consumer config with the parent stream config. +func (consCfg *ConsumerConfig) replicas(strCfg *StreamConfig) int { + if consCfg.Replicas == 0 || consCfg.Replicas > strCfg.Replicas { + if !isDurableConsumer(consCfg) && strCfg.Retention == LimitsPolicy && consCfg.Replicas == 0 { + // Matches old-school ephemerals only, where the replica count is 0. + return 1 + } + return strCfg.Replicas + } + return consCfg.Replicas +} + +// Consumer is a jetstream consumer. +type consumer struct { + // Atomic used to notify that we want to process an ack. + // This will be checked in checkPending to abort processing + // and let ack be processed in priority. + awl int64 + leader atomic.Bool + mu sync.RWMutex + js *jetStream + mset *stream + acc *Account + srv *Server + client *client + sysc *client + sid int + name string + stream string + sseq uint64 // next stream sequence + subjf subjectFilters // subject filters and their sequences + filters *gsl.SimpleSublist // When we have multiple filters we will use LoadNextMsgMulti and pass this in. + dseq uint64 // delivered consumer sequence + adflr uint64 // ack delivery floor + asflr uint64 // ack store floor + chkflr uint64 // our check floor, interest streams only. + npc int64 // Num Pending Count + npf uint64 // Num Pending Floor Sequence + dsubj string + qgroup string + lss *lastSeqSkipList + rlimit *rate.Limiter + reqSub *subscription + resetSub *subscription + ackSubOld *subscription + ackReplyOldT string + ackSubjOld string + ackSub *subscription + ackReplyT string + ackSubj string + fcPreOld string + fcSubjOld string + fcPre string + fcSubj string + nextMsgSubj string + nextMsgReqs *ipQueue[*nextMsgReq] + resetSubj string + maxp int + pblimit int + maxpb int + pbytes int + fcsz int + fcid string + fcSubOld *subscription + fcSub *subscription + outq *jsOutQ + pending map[uint64]*Pending + ptmr *time.Timer + ptmrEnd time.Time + rdq []uint64 + rdqi avl.SequenceSet + rdc map[uint64]uint64 + replies map[uint64]string + pendingDeliveries map[uint64]*jsPubMsg // Messages that can be delivered after achieving quorum. + waitingDeliveries map[string]*waitingDelivery // (Optional) request timeout messages that need to wait for replicated deliveries first. + maxdc uint64 + waiting *waitQueue + cfg ConsumerConfig + direct bool // Immutable, enforced by checkNewConsumerConfig. Read without o.mu. + sourcing bool // Immutable, enforced by checkNewConsumerConfig. Read without o.mu. + ici *ConsumerInfo + store ConsumerStore + active bool + replay bool + useV2Ack bool + dtmr *time.Timer + uptmr *time.Timer // Unpause timer + gwdtmr *time.Timer + dthresh time.Duration + mch chan struct{} // Message channel + qch chan struct{} // Quit channel + mqch chan struct{} // The monitor's quit channel. + inch chan bool // Interest change channel + sfreq int32 + ackEventT string + nakEventT string + deliveryExcEventT string + created time.Time + ldt time.Time + lat time.Time + lwqic time.Time + closed bool + restoring bool + restoreLeader bool + restoreTerm uint64 + + // Clustered. + ca *consumerAssignment + node RaftNode + term uint64 // Raft term, used to determine if we are still the leader for the current term (if applicable, 0 otherwise). + werr error // If a write error was encountered while applying entries, and if so what error. + infoSub *subscription + lqsent time.Time + prm map[string]struct{} + rsm map[string]resetRequest // Reset requests that need to be responded to. + prOk bool + uch chan struct{} + retention RetentionPolicy + + monitorWg sync.WaitGroup + monitorMu sync.Mutex // Serializes monitorWg's Add against Wait to prevent a WaitGroup reuse panic. + inMonitor bool + + // R>1 proposals + pch chan struct{} + phead *proposal + ptail *proposal + + // Ack queue + ackMsgs *ipQueue[*jsAckMsg] + + // for stream signaling when multiple filters are set. + sigSubs []string + + // Priority groups + // Details described in ADR-42. + + // currentPinId is the current nuid for the pinned consumer. + // If the Consumer is running in `PriorityPinnedClient` mode, server will + // pick up a new nuid and assign it to first pending pull request. + currentPinId string + /// pinnedTtl is the remaining time before the current PinId expires. + pinnedTtl *time.Timer + pinnedTS time.Time + + // If standalone/single-server, the offline reason needs to be stored directly in the consumer. + // Otherwise, if clustered it will be part of the consumer assignment. + offlineReason string +} + +// A single subject filter. +type subjectFilter struct { + subject string + tokenizedSubject []string + hasWildcard bool +} + +type subjectFilters []*subjectFilter + +// subjects is a helper function used for updating consumers. +// It is not used and should not be used in hotpath. +func (s subjectFilters) subjects() []string { + subjects := make([]string, 0, len(s)) + for _, filter := range s { + subjects = append(subjects, filter.subject) + } + return subjects +} + +type proposal struct { + data []byte + next *proposal +} + +const ( + // JsAckWaitDefault is the default AckWait, only applicable on explicit ack policy consumers. + JsAckWaitDefault = 30 * time.Second + // JsDeleteWaitTimeDefault is the default amount of time we will wait for non-durable + // consumers to be in an inactive state before deleting them. + JsDeleteWaitTimeDefault = 5 * time.Second + // JsFlowControlMaxPending specifies default pending bytes during flow control that can be outstanding. + JsFlowControlMaxPending = 32 * 1024 * 1024 + // JsDefaultMaxAckPending is set for consumers with explicit ack that do not set the max ack pending. + JsDefaultMaxAckPending = 1000 + // JsDefaultPinnedTTL is the default grace period for the pinned consumer to send a new request before a new pin + // is picked by a server. + JsDefaultPinnedTTL = 2 * time.Minute +) + +// Helper function to set consumer config defaults from above. +func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig, lim *JSLimitOpts, accLim *JetStreamAccountLimits, pedantic bool) *ApiError { + // Setup default of -1, meaning no limit for MaxDeliver. + if config.MaxDeliver == 0 || config.MaxDeliver < -1 { + if pedantic && config.MaxDeliver < -1 { + return NewJSPedanticError(errors.New("max_deliver must be set to -1")) + } + config.MaxDeliver = -1 + } + // Setup zero defaults. + if config.MaxWaiting < 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_waiting must not be negative")) + } + config.MaxWaiting = 0 + } + if config.MaxAckPending < -1 { + if pedantic { + return NewJSPedanticError(errors.New("max_ack_pending must be set to -1")) + } + config.MaxAckPending = -1 + } + if config.MaxRequestBatch < 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_batch must not be negative")) + } + config.MaxRequestBatch = 0 + } + if config.MaxRequestExpires < 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_expires must not be negative")) + } + config.MaxRequestExpires = 0 + } + if config.MaxRequestMaxBytes < 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_bytes must not be negative")) + } + config.MaxRequestMaxBytes = 0 + } + if config.Heartbeat < 0 { + if pedantic { + return NewJSPedanticError(errors.New("idle_heartbeat must not be negative")) + } + config.Heartbeat = 0 + } + if config.InactiveThreshold < 0 { + if pedantic { + return NewJSPedanticError(errors.New("inactive_threshold must not be negative")) + } + config.InactiveThreshold = 0 + } + if config.PinnedTTL < 0 { + if pedantic { + return NewJSPedanticError(errors.New("priority_timeout must not be negative")) + } + config.PinnedTTL = 0 + } + + // Set to default if not specified. + if config.DeliverSubject == _EMPTY_ && config.MaxWaiting == 0 { + config.MaxWaiting = JSWaitQueueDefaultMax + } + // Setup proper default for ack wait if we are in explicit ack mode. + if config.AckWait == 0 && (config.AckPolicy == AckExplicit || config.AckPolicy == AckAll) { + config.AckWait = JsAckWaitDefault + } + // If BackOff was specified that will override the AckWait and the MaxDeliver. + if len(config.BackOff) > 0 { + if pedantic && config.AckWait != config.BackOff[0] { + return NewJSPedanticError(errors.New("first backoff value has to equal batch AckWait")) + } + config.AckWait = config.BackOff[0] + } + if config.MaxAckPending == 0 { + if pedantic && streamCfg.ConsumerLimits.MaxAckPending > 0 { + return NewJSPedanticError(errors.New("max_ack_pending must be set if it's configured in stream limits")) + } + config.MaxAckPending = streamCfg.ConsumerLimits.MaxAckPending + } + if config.InactiveThreshold == 0 { + if pedantic && streamCfg.ConsumerLimits.InactiveThreshold > 0 { + return NewJSPedanticError(errors.New("inactive_threshold must be set if it's configured in stream limits")) + } + config.InactiveThreshold = streamCfg.ConsumerLimits.InactiveThreshold + } + // Set proper default for max ack pending if we are ack explicit and none has been set. + if config.MaxAckPending == 0 && config.AckPolicy != AckNone { + ackPending := JsDefaultMaxAckPending + if lim.MaxAckPending > 0 && lim.MaxAckPending < ackPending { + ackPending = lim.MaxAckPending + } + if accLim.MaxAckPending > 0 && accLim.MaxAckPending < ackPending { + ackPending = accLim.MaxAckPending + } + config.MaxAckPending = ackPending + } + // if applicable set max request batch size + if config.DeliverSubject == _EMPTY_ && config.MaxRequestBatch == 0 && lim.MaxRequestBatch > 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_request_batch must be set if it's JetStream limits are set")) + } + config.MaxRequestBatch = lim.MaxRequestBatch + } + + // set the default value only if pinned policy is used. + if config.PriorityPolicy == PriorityPinnedClient && config.PinnedTTL == 0 { + config.PinnedTTL = JsDefaultPinnedTTL + } + + // Set default values for flow control policy. + if config.AckPolicy == AckFlowControl && !pedantic { + config.FlowControl = true + config.Heartbeat = sourceHealthHB + } + return nil +} + +// Check the consumer config. If we are recovering don't check filter subjects. +func checkConsumerCfg( + config *ConsumerConfig, + srvLim *JSLimitOpts, + cfg *StreamConfig, + _ *Account, + accLim *JetStreamAccountLimits, + isRecovering bool, +) *ApiError { + + if config.Name != _EMPTY_ && !isValidAssetName(config.Name) { + return NewJSStreamInvalidConfigError(errors.New("consumer name can not contain '.', '*', '>', '\\', '/'")) + } + if config.Durable != _EMPTY_ && !isValidAssetName(config.Durable) { + return NewJSStreamInvalidConfigError(errors.New("consumer durable name can not contain '.', '*', '>', '\\', '/'")) + } + + // Check if replicas is defined but exceeds parent stream. + if config.Replicas > 0 && config.Replicas > cfg.Replicas { + return NewJSConsumerReplicasExceedsStreamError() + } + // Check that it is not negative + if config.Replicas < 0 { + return NewJSReplicasCountCannotBeNegativeError() + } + // If the stream is interest or workqueue retention make sure the replicas + // match that of the stream. This is REQUIRED for now. + if cfg.Retention == InterestPolicy || cfg.Retention == WorkQueuePolicy { + // Only error here if not recovering. + // We handle recovering in a different spot to allow consumer to come up + // if previous version allowed it to be created. We do not want it to not come up. + if !isRecovering && config.Replicas != 0 && config.Replicas != cfg.Replicas { + return NewJSConsumerReplicasShouldMatchStreamError() + } + } + + if _, err := config.AckPolicy.MarshalJSON(); err != nil { + return NewJSConsumerAckPolicyInvalidError() + } + if _, err := config.ReplayPolicy.MarshalJSON(); err != nil { + return NewJSConsumerReplayPolicyInvalidError() + } + + // Check not negative AckWait/BackOff + for _, backoff := range config.BackOff { + if backoff < 0 { + return NewJSConsumerBackOffNegativeError() + } + } + if config.AckWait < 0 { + return NewJSConsumerAckWaitNegativeError() + } + + // Ack Flow Control policy requires push-based flow-controlled consumer. + if config.AckPolicy == AckFlowControl { + if config.DeliverSubject == _EMPTY_ { + return NewJSConsumerAckFCRequiresPushError() + } + if !config.FlowControl { + return NewJSConsumerAckFCRequiresFCError() + } + // We currently limit using heartbeat of 1s, since those are used for ephemeral sourcing consumers as well. + // We could decide to relax this in the future, but need to be careful to not allow a heartbeat larger + // than the stalled source timeout. + if config.Heartbeat != sourceHealthHB { + return NewJSStreamInvalidConfigError(fmt.Errorf("flow control ack policy heartbeat needs to be 1s")) + } + if config.MaxAckPending <= 0 { + return NewJSConsumerAckFCRequiresMaxAckPendingError() + } + if config.AckWait != 0 || len(config.BackOff) > 0 { + return NewJSConsumerAckFCRequiresNoAckWaitError() + } + if config.MaxDeliver > 0 { + return NewJSConsumerAckFCRequiresNoMaxDeliverError() + } + } + + // Check if we have a BackOff defined that MaxDeliver is within range etc. + if lbo := len(config.BackOff); lbo > 0 && config.MaxDeliver != -1 && lbo > config.MaxDeliver { + return NewJSConsumerMaxDeliverBackoffError() + } + + if len(config.Description) > JSMaxDescriptionLen { + return NewJSConsumerDescriptionTooLongError(JSMaxDescriptionLen) + } + + // For now expect a literal subject if its not empty. Empty means work queue mode (pull mode). + if config.DeliverSubject != _EMPTY_ { + if !subjectIsLiteral(config.DeliverSubject) { + return NewJSConsumerDeliverToWildcardsError() + } + if !IsValidSubject(config.DeliverSubject) { + return NewJSConsumerInvalidDeliverSubjectError() + } + if deliveryFormsCycle(cfg, config.DeliverSubject) { + return NewJSConsumerDeliverCycleError() + } + if config.MaxWaiting != 0 { + return NewJSConsumerPushMaxWaitingError() + } + if config.MaxAckPending > 0 && config.AckPolicy == AckNone { + return NewJSConsumerMaxPendingAckPolicyRequiredError() + } + if config.Heartbeat > 0 && config.Heartbeat < 100*time.Millisecond { + return NewJSConsumerSmallHeartbeatError() + } + } else { + // Pull mode with work queue retention from the stream requires an explicit ack. + if config.AckPolicy == AckNone && cfg.Retention == WorkQueuePolicy { + return NewJSConsumerPullRequiresAckError() + } + if config.RateLimit > 0 { + return NewJSConsumerPullWithRateLimitError() + } + if config.MaxWaiting < 0 { + return NewJSConsumerMaxWaitingNegativeError() + } + if config.Heartbeat > 0 { + return NewJSConsumerHBRequiresPushError() + } + if config.FlowControl { + return NewJSConsumerFCRequiresPushError() + } + if config.MaxRequestBatch < 0 { + return NewJSConsumerMaxRequestBatchNegativeError() + } + if config.MaxRequestExpires != 0 && config.MaxRequestExpires < time.Millisecond { + return NewJSConsumerMaxRequestExpiresTooSmallError() + } + if srvLim.MaxRequestBatch > 0 && config.MaxRequestBatch > srvLim.MaxRequestBatch { + return NewJSConsumerMaxRequestBatchExceededError(srvLim.MaxRequestBatch) + } + } + if srvLim.MaxAckPending > 0 && config.MaxAckPending > srvLim.MaxAckPending { + return NewJSConsumerMaxPendingAckExcessError(srvLim.MaxAckPending) + } + if accLim.MaxAckPending > 0 && config.MaxAckPending > accLim.MaxAckPending { + return NewJSConsumerMaxPendingAckExcessError(accLim.MaxAckPending) + } + if cfg.ConsumerLimits.MaxAckPending > 0 && config.MaxAckPending > cfg.ConsumerLimits.MaxAckPending { + return NewJSConsumerMaxPendingAckExcessError(cfg.ConsumerLimits.MaxAckPending) + } + if cfg.ConsumerLimits.InactiveThreshold > 0 && config.InactiveThreshold > cfg.ConsumerLimits.InactiveThreshold { + return NewJSConsumerInactiveThresholdExcessError(cfg.ConsumerLimits.InactiveThreshold) + } + + // Direct need to be non-mapped ephemerals. + if config.Direct { + if config.DeliverSubject == _EMPTY_ { + return NewJSConsumerDirectRequiresPushError() + } + if isDurableConsumer(config) { + return NewJSConsumerDirectRequiresEphemeralError() + } + } + + // Do not allow specifying both FilterSubject and FilterSubjects, + // as that's probably unintentional without any difference from passing + // all filters in FilterSubjects. + if config.FilterSubject != _EMPTY_ && len(config.FilterSubjects) > 0 { + return NewJSConsumerDuplicateFilterSubjectsError() + } + + if config.FilterSubject != _EMPTY_ && !IsValidSubject(config.FilterSubject) { + return NewJSStreamInvalidConfigError(ErrBadSubject) + } + + // We treat FilterSubjects: []string{""} as a misconfig, so we validate against it. + for _, filter := range config.FilterSubjects { + if filter == _EMPTY_ { + return NewJSConsumerEmptyFilterError() + } + } + subjectFilters := gatherSubjectFilters(config.FilterSubject, config.FilterSubjects) + + // Check subject filters do not overlap. + for outer, subject := range subjectFilters { + if !IsValidSubject(subject) { + return NewJSStreamInvalidConfigError(ErrBadSubject) + } + for inner, ssubject := range subjectFilters { + if inner != outer && subjectIsSubsetMatch(subject, ssubject) { + return NewJSConsumerOverlappingSubjectFiltersError() + } + } + } + + // Helper function to formulate similar errors. + badStart := func(dp, start string) error { + return fmt.Errorf("consumer delivery policy is deliver %s, but optional start %s is also set", dp, start) + } + notSet := func(dp, notSet string) error { + return fmt.Errorf("consumer delivery policy is deliver %s, but optional %s is not set", dp, notSet) + } + + // Check on start position conflicts. + switch config.DeliverPolicy { + case DeliverAll: + if config.OptStartSeq > 0 { + return NewJSConsumerInvalidPolicyError(badStart("all", "sequence")) + } + if config.OptStartTime != nil { + return NewJSConsumerInvalidPolicyError(badStart("all", "time")) + } + case DeliverLast: + if config.OptStartSeq > 0 { + return NewJSConsumerInvalidPolicyError(badStart("last", "sequence")) + } + if config.OptStartTime != nil { + return NewJSConsumerInvalidPolicyError(badStart("last", "time")) + } + case DeliverLastPerSubject: + if config.OptStartSeq > 0 { + return NewJSConsumerInvalidPolicyError(badStart("last per subject", "sequence")) + } + if config.OptStartTime != nil { + return NewJSConsumerInvalidPolicyError(badStart("last per subject", "time")) + } + if config.FilterSubject == _EMPTY_ && len(config.FilterSubjects) == 0 { + return NewJSConsumerInvalidPolicyError(notSet("last per subject", "filter subject")) + } + case DeliverNew: + if config.OptStartSeq > 0 { + return NewJSConsumerInvalidPolicyError(badStart("new", "sequence")) + } + if config.OptStartTime != nil { + return NewJSConsumerInvalidPolicyError(badStart("new", "time")) + } + case DeliverByStartSequence: + if config.OptStartSeq == 0 { + return NewJSConsumerInvalidPolicyError(notSet("by start sequence", "start sequence")) + } + if config.OptStartTime != nil { + return NewJSConsumerInvalidPolicyError(badStart("by start sequence", "time")) + } + case DeliverByStartTime: + if config.OptStartTime == nil { + return NewJSConsumerInvalidPolicyError(notSet("by start time", "start time")) + } + if config.OptStartSeq != 0 { + return NewJSConsumerInvalidPolicyError(badStart("by start time", "start sequence")) + } + } + + if config.SampleFrequency != _EMPTY_ { + s := strings.TrimSuffix(config.SampleFrequency, "%") + if sampleFreq, err := strconv.Atoi(s); err != nil || sampleFreq < 0 { + return NewJSConsumerInvalidSamplingError(err) + } + } + + // We reject if flow control is set without heartbeats. + if config.FlowControl && config.Heartbeat == 0 { + return NewJSConsumerWithFlowControlNeedsHeartbeatsError() + } + + if config.Durable != _EMPTY_ && config.Name != _EMPTY_ { + if config.Name != config.Durable { + return NewJSConsumerCreateDurableAndNameMismatchError() + } + } + + var metadataLen int + for k, v := range config.Metadata { + metadataLen += len(k) + len(v) + } + if metadataLen > JSMaxMetadataLen { + return NewJSConsumerMetadataLengthError(fmt.Sprintf("%dKB", JSMaxMetadataLen/1024)) + } + + if config.PriorityPolicy != PriorityNone { + if config.DeliverSubject != "" { + return NewJSConsumerPushWithPriorityGroupError() + } + if len(config.PriorityGroups) == 0 { + return NewJSConsumerPriorityPolicyWithoutGroupError() + } + + for _, group := range config.PriorityGroups { + if group == _EMPTY_ { + return NewJSConsumerEmptyGroupNameError() + } + if !validGroupName.MatchString(group) { + return NewJSConsumerInvalidGroupNameError() + } + } + } else { + // If PriorityPolicy is None or not set, reject if PriorityGroups or PinnedTTL are set + if len(config.PriorityGroups) > 0 { + return NewJSConsumerPriorityGroupWithPolicyNoneError() + } + if config.PinnedTTL > 0 { + return NewJSConsumerPinnedTTLWithoutPriorityPolicyNoneError() + } + } + + // For now don't allow preferred server in placement. + if cfg.Placement != nil && cfg.Placement.Preferred != _EMPTY_ { + return NewJSStreamInvalidConfigError(fmt.Errorf("preferred server not permitted in placement")) + } + + return nil +} + +func (mset *stream) addConsumerWithAction(config *ConsumerConfig, action ConsumerAction, pedantic bool) (*consumer, error) { + return mset.addConsumerWithAssignment(config, _EMPTY_, nil, false, action, pedantic) +} + +func (mset *stream) addConsumer(config *ConsumerConfig) (*consumer, error) { + return mset.addConsumerWithAction(config, ActionCreateOrUpdate, false) +} + +func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname string, ca *consumerAssignment, isRecovering bool, action ConsumerAction, pedantic bool) (*consumer, error) { + return mset.addConsumerWithAssignmentAndMode(config, oname, ca, isRecovering, action, pedantic, false) +} + +func (mset *stream) addConsumerForRestore(config *ConsumerConfig) (*consumer, error) { + return mset.addConsumerWithAssignmentAndMode(config, _EMPTY_, nil, false, ActionCreateOrUpdate, false, true) +} + +func (mset *stream) addConsumerWithAssignmentAndMode(config *ConsumerConfig, oname string, ca *consumerAssignment, isRecovering bool, action ConsumerAction, pedantic, restoring bool) (*consumer, error) { + // Check if this stream has closed. + if mset.closed.Load() { + return nil, NewJSStreamInvalidError() + } + + mset.mu.RLock() + s, js, jsa, cfg, acc, lseq := mset.srv, mset.js, mset.jsa, mset.cfg, mset.acc, mset.lseq + mset.mu.RUnlock() + + // If we do not have the consumer 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 oname != _EMPTY_ && !jsa.consumerAssigned(mset.name(), oname) { + s.Debugf("Consumer %q > %q does not seem to be assigned to this server", mset.name(), oname) + } + + if config == nil { + return nil, NewJSConsumerConfigRequiredError() + } + + selectedLimits, _, _, _ := acc.selectLimits(config.replicas(&cfg)) + if selectedLimits == nil { + return nil, NewJSNoLimitsError() + } + + srvLim := &s.getOpts().JetStreamLimits + // Make sure we have sane defaults. Do so with the JS lock, otherwise a + // badly timed meta snapshot can result in a race condition. + mset.js.mu.Lock() + err := setConsumerConfigDefaults(config, &cfg, srvLim, selectedLimits, pedantic) + mset.js.mu.Unlock() + if err != nil { + return nil, err + } + + if err := checkConsumerCfg(config, srvLim, &cfg, acc, selectedLimits, isRecovering); err != nil { + return nil, err + } + sampleFreq := 0 + if config.SampleFrequency != _EMPTY_ { + // Can't fail as checkConsumerCfg checks correct format + sampleFreq, _ = strconv.Atoi(strings.TrimSuffix(config.SampleFrequency, "%")) + } + + // Grab the client, account and server reference. + c := mset.client + if c == nil { + return nil, NewJSStreamInvalidError() + } + var accName string + c.mu.Lock() + s, a := c.srv, c.acc + if a != nil { + accName = a.Name + } + c.mu.Unlock() + + // Hold mset lock here. + mset.mu.Lock() + if mset.client == nil || mset.store == nil || mset.consumers == nil { + mset.mu.Unlock() + return nil, NewJSStreamInvalidError() + } + + // If this one is durable and already exists, we let that be ok as long as only updating what should be allowed. + var cName string + if isDurableConsumer(config) { + cName = config.Durable + } else if config.Name != _EMPTY_ { + cName = config.Name + } + if cName != _EMPTY_ { + if eo, ok := mset.consumers[cName]; ok { + if action == ActionCreate { + ocfg := eo.config() + copyConsumerMetadata(config, &ocfg) + if !reflect.DeepEqual(config, &ocfg) { + mset.mu.Unlock() + return nil, NewJSConsumerAlreadyExistsError() + } + } + // Check for overlapping subjects if we are a workqueue + if cfg.Retention == WorkQueuePolicy { + subjects := gatherSubjectFilters(config.FilterSubject, config.FilterSubjects) + if !mset.partitionUnique(cName, subjects) { + mset.mu.Unlock() + return nil, NewJSConsumerWQConsumerNotUniqueError() + } + } + mset.mu.Unlock() + err := eo.updateConfig(config) + if err == nil { + return eo, nil + } + return nil, NewJSConsumerCreateError(err, Unless(err)) + } + } + if action == ActionUpdate { + mset.mu.Unlock() + return nil, NewJSConsumerDoesNotExistError() + } + + standalone := !s.JetStreamIsClustered() && s.standAloneMode() + + // If we're clustered we've already done this check, only do this if we're a standalone server. + // But if we're standalone, only enforce if we're not recovering, since the MaxConsumers could've + // been updated while we already had more consumers on disk. + if standalone && !isRecovering && !config.Direct && !config.Sourcing { + // Check for any limits, if the config for the consumer sets a limit we check against that + // but if not we use the value from account limits, if account limits is more restrictive + // than stream config we prefer the account limits to handle cases where account limits are + // updated during the lifecycle of the stream + maxc := cfg.MaxConsumers + if maxc <= 0 || (selectedLimits.MaxConsumers > 0 && selectedLimits.MaxConsumers < maxc) { + maxc = selectedLimits.MaxConsumers + } + // Only apply default limits if the user specified neither stream nor account limit. + // An unlimited (-1) stream or account limit counts as not set, since that is also + // the default for accounts, so the default can't be opted out of per stream or account. + if maxConsumers := srvLim.DefaultMaxConsumers; maxConsumers > 0 && maxc <= 0 { + maxc = maxConsumers + } + if maxc > 0 && mset.numLimitableConsumers() >= maxc { + mset.mu.Unlock() + return nil, NewJSMaximumConsumersLimitError() + } + } + + // Check on stream type conflicts with WorkQueues. + if cfg.Retention == WorkQueuePolicy && !config.Direct && !config.Sourcing { + // Force explicit acks here. + if config.AckPolicy != AckExplicit && config.AckPolicy != AckFlowControl { + mset.mu.Unlock() + return nil, NewJSConsumerWQRequiresExplicitAckError() + } + if config.DeliverPolicy != DeliverAll { + mset.mu.Unlock() + return nil, NewJSConsumerWQConsumerNotDeliverAllError() + } + if mset.numLimitableConsumers() > 0 { + subjects := gatherSubjectFilters(config.FilterSubject, config.FilterSubjects) + if len(subjects) == 0 { + mset.mu.Unlock() + return nil, NewJSConsumerWQMultipleUnfilteredError() + } else if !mset.partitionUnique(cName, subjects) { + // Prior to v2.9.7, on a stream with WorkQueue policy, the servers + // were not catching the error of having multiple consumers with + // overlapping filter subjects depending on the scope, for instance + // creating "foo.*.bar" and then "foo.>" was not detected, while + // "foo.>" and then "foo.*.bar" would have been. Failing here + // in recovery mode would leave the rejected consumer in a bad state, + // so we will simply warn here, asking the user to remove this + // consumer administratively. Otherwise, if this is the creation + // of a new consumer, we will return the error. + if isRecovering { + s.Warnf("Consumer %q > %q has a filter subject that overlaps "+ + "with other consumers, which is not allowed for a stream "+ + "with WorkQueue policy, it should be administratively deleted", + cfg.Name, cName) + } else { + // We have a partition but it is not unique amongst the others. + mset.mu.Unlock() + return nil, NewJSConsumerWQConsumerNotUniqueError() + } + } + } + } + + // Set name, which will be durable name if set, otherwise we create one at random. + o := &consumer{ + mset: mset, + js: s.getJetStream(), + acc: a, + srv: s, + client: s.createInternalJetStreamClient(), + sysc: s.createInternalJetStreamClient(), + cfg: *config, + direct: config.Direct, + sourcing: config.Sourcing, + dsubj: config.DeliverSubject, + outq: mset.outq, + active: true, + qch: make(chan struct{}), + mqch: make(chan struct{}), + uch: make(chan struct{}, 1), + mch: make(chan struct{}, 1), + sfreq: int32(sampleFreq), + maxdc: uint64(max(config.MaxDeliver, 0)), // MaxDeliver is negative (-1) when infinite. + maxp: config.MaxAckPending, + retention: cfg.Retention, + created: time.Now().UTC(), + restoring: restoring, + } + + // Add created timestamp used for the store, must match that of the consumer assignment if it exists. + if ca != nil { + js.mu.RLock() + o.created = ca.Created + js.mu.RUnlock() + } + + // Bind internal client to the user account. + o.client.registerWithAccount(a) + // Bind to the system account. + o.sysc.registerWithAccount(s.SystemAccount()) + + if isDurableConsumer(config) { + if len(config.Durable) > JSMaxNameLen { + mset.mu.Unlock() + // Release the temporary consumer we built; it was never registered. + _ = o.stop() + return nil, NewJSConsumerNameTooLongError(JSMaxNameLen) + } + o.name = config.Durable + } else if oname != _EMPTY_ { + o.name = oname + } else { + if config.Name != _EMPTY_ { + o.name = config.Name + } else { + // Legacy ephemeral auto-generated. + for { + o.name = createConsumerName() + if _, ok := mset.consumers[o.name]; !ok { + break + } + } + config.Name = o.name + } + } + // already under lock, mset.Name() would deadlock + o.stream = cfg.Name + o.ackEventT = JSMetricConsumerAckPre + "." + o.stream + "." + o.name + o.nakEventT = JSAdvisoryConsumerMsgNakPre + "." + o.stream + "." + o.name + o.deliveryExcEventT = JSAdvisoryConsumerMaxDeliveryExceedPre + "." + o.stream + "." + o.name + + if !isValidAssetName(o.name) { + mset.mu.Unlock() + // Release the temporary consumer we built; it was never registered. + _ = o.stop() + return nil, NewJSConsumerBadDurableNameError() + } + + // Check if we already have this one registered. + if eo, ok := mset.consumers[o.name]; ok { + mset.mu.Unlock() + if !o.isDurable() || !o.isPushMode() { + _ = o.stop() + return nil, NewJSConsumerNameExistError() + } + // If we are here we have already registered this durable. If it is still active that is an error. + if eo.isActive() { + _ = o.stop() + return nil, NewJSConsumerExistingActiveError() + } + // Since we are here this means we have a potentially new durable so we should update here. + // Check that configs are the same. + if !configsEqualSansDelivery(o.cfg, eo.cfg) { + _ = o.stop() + return nil, NewJSConsumerReplacementWithDifferentNameError() + } + // Once we are here we have a replacement push-based durable. + eo.updateDeliverSubject(o.cfg.DeliverSubject) + // Release the temporary consumer we built; it was never registered. + _ = o.stop() + return eo, nil + } + + // Setup our storage if not a direct consumer. + if !config.Direct { + store, err := mset.store.ConsumerStore(o.name, o.created, config) + if err != nil { + mset.mu.Unlock() + // Store creation failed, so just cleanup. + _ = o.stop() + return nil, NewJSConsumerStoreFailedError(err) + } + o.store = store + } + + for _, filter := range gatherSubjectFilters(o.cfg.FilterSubject, o.cfg.FilterSubjects) { + sub := &subjectFilter{ + subject: filter, + hasWildcard: subjectHasWildcard(filter), + tokenizedSubject: tokenizeSubjectIntoSlice(nil, filter), + } + o.subjf = append(o.subjf, sub) + } + + // If we have multiple filter subjects, create a sublist which we will use + // in calling store.LoadNextMsgMulti. + if len(o.subjf) <= 1 { + o.filters = nil + } else { + o.filters = gsl.NewSublist[struct{}]() + for _, filter := range o.subjf { + o.filters.Insert(filter.subject, struct{}{}) + } + } + + if o.store != nil && o.store.HasState() { + // Restore our saved state. + o.mu.Lock() + o.readStoredState() + + replicas := o.cfg.replicas(&mset.cfg) + + // Starting sequence represents the next sequence to be delivered, so decrement it + // since that's the minimum amount the stream should have as its last sequence. + sseq := o.sseq + if sseq > 0 { + sseq-- + } + + o.mu.Unlock() + + // A stream observing data loss rolls back in its sequence. Check if we need to reconcile the consumer state + // to ensure new messages aren't skipped. + // Only performed for non-replicated consumers for now. + if replicas == 1 && lseq < sseq && isRecovering { + s.Warnf("JetStream consumer '%s > %s > %s' delivered sequence %d past last stream sequence of %d", + o.acc.Name, o.stream, o.name, sseq, lseq) + + o.mu.Lock() + o.reconcileStateWithStream(lseq) + + // Save the reconciled state + state := &ConsumerState{ + Delivered: SequencePair{ + Stream: o.sseq - 1, + Consumer: o.dseq - 1, + }, + AckFloor: SequencePair{ + Stream: o.asflr, + Consumer: o.adflr, + }, + Pending: o.pending, + Redelivered: o.rdc, + } + err := o.store.ForceUpdate(state) + o.mu.Unlock() + if err != nil { + s.Errorf("JetStream consumer '%s > %s > %s' errored while updating state: %v", o.acc.Name, o.stream, o.name, err) + mset.mu.Unlock() + // Release the temporary consumer we built; it was never registered. + _ = o.stop() + return nil, NewJSConsumerStoreFailedError(err) + } + } + } else if config.Direct || standalone { + // Clustered non-direct consumers defer this to setLeader so the + // expensive store scans don't block the meta apply goroutine. + if err := o.selectStartingSeqNo(); err != nil { + // Delete our store while holding the stream lock, so a concurrent create + // for the same name cannot have registered and be sharing this on-disk + // directory. Then release the rest of the consumer non-destructively. + if o.store != nil { + _ = o.store.Delete() + o.store = nil + } + mset.mu.Unlock() + _ = o.stop() + return nil, err + } + } + + // Create ackMsgs queue now that we have a consumer name + o.ackMsgs = newIPQueue[*jsAckMsg](s, fmt.Sprintf("[ACC:%s] consumer '%s' on stream '%s' ackMsgs", accName, o.name, cfg.Name)) + + // Create our request waiting queue. + if o.isPullMode() { + o.waiting = newWaitQueue(config.MaxWaiting) + o.nextMsgReqs = newIPQueue[*nextMsgReq](s, fmt.Sprintf("[ACC:%s] consumer '%s' on stream '%s' pull requests", accName, o.name, cfg.Name)) + } + + // Set up the ack subscription for this consumer. Will use wildcard for all acks. + // We will remember the template to generate replies with sequence numbers and use + // that to scanf them back in. + // Escape '%' in consumer and stream names, as `pre` is used as a template later + // in consumer.ackReply(), resulting in erroneous formatting of the ack subject. + mn := strings.ReplaceAll(cfg.Name, "%", "%%") + on := strings.ReplaceAll(o.name, "%", "%%") + domain := o.srv.getOpts().JetStreamDomain + if domain == _EMPTY_ { + domain = "_" + } + dn := strings.ReplaceAll(domain, "%", "%%") + accHash := getHash(accName) + + o.useV2Ack = s.getOpts().getFeatureFlag(FeatureFlagJsAckFormatV2) + + // v1 format: $JS.(ACK|FC)...etc. + o.fcPreOld = jsFlowControlPre + o.fcSubjOld = fmt.Sprintf(jsFlowControl, cfg.Name, o.name) + preOld := fmt.Sprintf(jsAckT, mn, on) + o.ackReplyOldT = fmt.Sprintf("%s.%%d.%%d.%%d.%%d.%%d", preOld) + o.ackSubjOld = fmt.Sprintf(jsAckT+".*.*.*.*.*", cfg.Name, o.name) + + // v2 format: $JS.(ACK|FC).....etc. + o.fcPre = fmt.Sprintf("%s%s.%s.", jsFlowControlPre, domain, accHash) + o.fcSubj = fmt.Sprintf(jsFlowControlV2, domain, accHash, cfg.Name, o.name) + pre := fmt.Sprintf(jsAckTv2, dn, accHash, mn, on) + o.ackReplyT = fmt.Sprintf("%s.%%d.%%d.%%d.%%d.%%d", pre) + // Subscribe on this ack subject for v2, we require 11 tokens, but allow for more tokens/extension. + o.ackSubj = fmt.Sprintf(jsAckTv2+".*.*.*.*.>", domain, accHash, cfg.Name, o.name) + + o.nextMsgSubj = fmt.Sprintf(JSApiRequestNextT, cfg.Name, o.name) + o.resetSubj = fmt.Sprintf(JSApiConsumerResetT, cfg.Name, o.name) + + // Check/update the inactive threshold + o.updateInactiveThreshold(&o.cfg) + + if o.isPushMode() { + // Check if we are running only 1 replica and that the delivery subject has interest. + // Check in place here for interest. Will setup properly in setLeader. + if config.replicas(&cfg) == 1 { + interest := o.acc.sl.HasInterest(o.cfg.DeliverSubject) + if !o.hasDeliveryInterest(interest) { + // Let the interest come to us eventually, but setup delete timer. + o.updateDeliveryInterest(false) + } + } + } + + // Set our ca. + if ca != nil { + o.setConsumerAssignment(ca) + } + + // Check if we have a rate limit set. + if config.RateLimit != 0 { + o.setRateLimit(config.RateLimit) + } + + mset.setConsumer(o) + mset.mu.Unlock() + + if config.Sourcing && standalone { + o.resetStartingSeq(0, _EMPTY_, false, false) + } + if config.Direct || standalone { + o.setLeader(true, 0) + } + + // This is always true in single server mode. + if o.IsLeader() { + // Send advisory. + var suppress bool + if !s.standAloneMode() && ca == nil { + suppress = true + } else if ca != nil { + suppress = ca.hasResponded() + } + if !suppress { + o.sendCreateAdvisory() + } + } + + return o, nil +} + +// Updates the consumer `dthresh` delete timer duration and set +// cfg.InactiveThreshold to JsDeleteWaitTimeDefault for ephemerals +// if not explicitly already specified by the user. +// Lock should be held. +func (o *consumer) updateInactiveThreshold(cfg *ConsumerConfig) { + // Ephemerals will always have inactive thresholds. + if !o.isDurable() && cfg.InactiveThreshold <= 0 { + // Add in 1 sec of jitter above and beyond the default of 5s. + o.dthresh = JsDeleteWaitTimeDefault + 100*time.Millisecond + time.Duration(rand.Int64N(900))*time.Millisecond + // Only stamp config with default sans jitter. + cfg.InactiveThreshold = JsDeleteWaitTimeDefault + } else if cfg.InactiveThreshold > 0 { + // Add in up to 1 sec of jitter if pull mode. + if o.isPullMode() { + o.dthresh = cfg.InactiveThreshold + 100*time.Millisecond + time.Duration(rand.Int64N(900))*time.Millisecond + } else { + o.dthresh = cfg.InactiveThreshold + } + } else if cfg.InactiveThreshold <= 0 { + // We accept InactiveThreshold be set to 0 (for durables) + o.dthresh = 0 + } +} + +// Updates the paused state. If we are the leader and the pause deadline +// hasn't passed yet then we will start a timer to kick the consumer once +// that deadline is reached. Lock should be held. +func (o *consumer) updatePauseState(cfg *ConsumerConfig) { + if o.uptmr != nil { + stopAndClearTimer(&o.uptmr) + } + if !o.isLeader() { + // Only the leader will run the timer as only the leader will run + // loopAndGatherMsgs. + return + } + if cfg.PauseUntil == nil || cfg.PauseUntil.IsZero() || cfg.PauseUntil.Before(time.Now()) { + // Either the PauseUntil is unset (is effectively zero) or the + // deadline has already passed, in which case there is nothing + // to do. + return + } + o.uptmr = time.AfterFunc(time.Until(*cfg.PauseUntil), func() { + o.mu.Lock() + defer o.mu.Unlock() + + stopAndClearTimer(&o.uptmr) + o.sendPauseAdvisoryLocked(&o.cfg) + o.signalNewMessages() + }) +} + +func (o *consumer) consumerAssignment() *consumerAssignment { + o.mu.RLock() + defer o.mu.RUnlock() + return o.ca +} + +func (o *consumer) setConsumerAssignment(ca *consumerAssignment) { + o.mu.Lock() + defer o.mu.Unlock() + + o.ca = ca + if ca == nil { + return + } + // Set our node. + o.node = ca.Group.node + + // Trigger update chan. + select { + case o.uch <- struct{}{}: + default: + } +} + +func (o *consumer) monitorQuitC() <-chan struct{} { + if o == nil { + return nil + } + o.mu.Lock() + defer o.mu.Unlock() + // Recreate if a prior monitor routine was stopped. + if o.mqch == nil { + o.mqch = make(chan struct{}) + } + return o.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 (o *consumer) signalMonitorQuit() { + o.mu.Lock() + defer o.mu.Unlock() + if o.mqch != nil { + close(o.mqch) + o.mqch = nil + } +} + +func (o *consumer) updateC() <-chan struct{} { + o.mu.RLock() + defer o.mu.RUnlock() + return o.uch +} + +// checkQueueInterest will check on our interest's queue group status. +// Lock should be held. +func (o *consumer) checkQueueInterest() { + if !o.active || o.cfg.DeliverSubject == _EMPTY_ { + return + } + subj := o.dsubj + if subj == _EMPTY_ { + subj = o.cfg.DeliverSubject + } + + if rr := o.acc.sl.Match(subj); len(rr.qsubs) > 0 { + // Just grab first + if qsubs := rr.qsubs[0]; len(qsubs) > 0 { + if sub := rr.qsubs[0][0]; len(sub.queue) > 0 { + o.qgroup = string(sub.queue) + } + } + } +} + +// clears our node if we have one. When we scale down to 1. +func (o *consumer) clearNode() { + o.mu.Lock() + defer o.mu.Unlock() + if o.node != nil { + o.node.Delete() + o.node = nil + } +} + +// IsLeader will return if we are the current leader. +func (o *consumer) IsLeader() bool { + return o.isLeader() +} + +// Lock should be held. +func (o *consumer) isLeader() bool { + return o.leader.Load() +} + +func (o *consumer) setLeader(isLeader bool, term uint64) error { + o.mu.Lock() + if o.restoring { + o.restoreLeader, o.restoreTerm = isLeader, term + o.mu.Unlock() + return nil + } + mset, closed := o.mset, o.closed + wasLeader := o.leader.Swap(isLeader) + + // For clustered new consumers, starting seq selection was deferred from + // addConsumerWithAssignment so the scan wouldn't block the meta apply + // goroutine, run it here on leader-elect instead. + needsSelect := isLeader && !wasLeader && o.dseq == 0 && (o.store == nil || !o.store.HasState()) + + // We can skip the teardown if we were leader before and are still the leader now. + // But only at term 1, since that means scale up from or down to an unreplicated config. + // R1 assets have no raft node and use the coerced term 1. + if term < 1 { + term = 1 + } + skipTeardown := wasLeader && isLeader && term == 1 + o.term = term + + if skipTeardown { + movingToClustered := o.node != nil && o.pch == nil + movingToNonClustered := o.node == nil && o.pch != nil + + // If we detect we are scaling up, make sure to create clustered routines and channels. + if movingToClustered { + // We are moving from R1 to clustered. + o.pch = make(chan struct{}, 1) + go o.loopAndForwardProposals(o.node, o.qch, o.pch, term) + if o.phead != nil { + select { + case o.pch <- struct{}{}: + default: + } + } + } else if movingToNonClustered { + // We are moving from clustered to non-clustered now. + // Set pch to nil so if we scale back up we will recreate the loopAndForward from above. + pch := o.pch + o.pch = nil + select { + case pch <- struct{}{}: + default: + } + } + o.mu.Unlock() + return nil + } + + // Shutdown the go routines and the subscriptions. + if o.qch != nil { + close(o.qch) + o.qch = nil + } + // Stop any inactivity timers. Should only be running on leaders. + stopAndClearTimer(&o.dtmr) + // Stop any unpause timers. Should only be running on leaders. + stopAndClearTimer(&o.uptmr) + // Make sure to clear out any re-deliver queues + o.stopAndClearPtmr() + o.rdc = nil + o.rdq = nil + o.rdqi.Empty() + o.pending = nil + o.rsm = nil + o.resetPendingDeliveries() + // Reset num pending, these are only authoritative on the leader. + o.npc, o.npf = 0, 0 + // Reset flow control accounting. + o.pbytes, o.fcsz, o.fcid = 0, 0, _EMPTY_ + // ok if they are nil, we protect inside unsubscribe() + o.unsubscribe(o.ackSubOld) + o.unsubscribe(o.ackSub) + o.unsubscribe(o.reqSub) + o.unsubscribe(o.resetSub) + o.unsubscribe(o.fcSubOld) + o.unsubscribe(o.fcSub) + o.ackSubOld, o.ackSub, o.reqSub, o.resetSub, o.fcSubOld, o.fcSub = nil, nil, nil, nil, nil, nil + if o.infoSub != nil { + o.srv.sysUnsubscribe(o.infoSub) + o.infoSub = nil + } + // Reset waiting if we are in pull mode. + if o.isPullMode() { + o.waiting = newWaitQueue(o.cfg.MaxWaiting) + o.nextMsgReqs.drain() + } else if o.srv.gateway.enabled { + stopAndClearTimer(&o.gwdtmr) + } + o.unassignPinId() + + // Make sure to drain queued up acks. + o.ackMsgs.drain() + // Reset amount of acks that need to be processed. + atomic.StoreInt64(&o.awl, 0) + // Also remove any pending replies since we should not be the one to respond at this point. + o.replies = nil + + // Set pch to nil, we will recreate the loopAndForwardProposals for the new term. + if pch := o.pch; pch != nil { + o.pch = nil + select { + case pch <- struct{}{}: + default: + } + } + // Clear proposals, none are valid anymore. + o.phead, o.ptail = nil, nil + o.mu.Unlock() + + // If we are here we have a change in leader status. + if isLeader { + if closed || mset == nil { + return nil + } + + if needsSelect { + o.mu.Lock() + if err := o.selectStartingSeqNo(); err != nil { + o.srv.Errorf("JetStream consumer '%s > %s > %s' select starting seq failed: %v", + o.acc.Name, o.stream, o.name, err) + o.leader.Store(false) + node := o.node + o.mu.Unlock() + if node != nil { + _ = node.StepDown() + } + return err + } + o.mu.Unlock() + } + + mset.mu.RLock() + s, jsa, stream := mset.srv, mset.jsa, mset.getCfgName() + mset.mu.RUnlock() + + o.mu.Lock() + o.rdq = nil + o.rdqi.Empty() + + // Restore our saved state. + // During non-leader status we just update our underlying store when not clustered. + // If clustered we need to propose our initial (possibly skipped ahead) o.sseq to the group. + if o.node == nil || o.dseq > 1 || (o.store != nil && o.store.HasState()) { + o.readStoredState() + } else if o.node != nil && o.sseq >= 1 { + o.updateSkipped(o.sseq) + } + + // Setup initial num pending. + o.streamNumPending() + + // Cleanup lss when we take over in clustered mode. + if o.hasSkipListPending() && o.sseq >= o.lss.resume { + o.lss = nil + } + + // Do info sub. + if o.infoSub == nil && jsa != nil { + isubj := fmt.Sprintf(clusterConsumerInfoT, jsa.acc(), stream, o.name) + // Note below the way we subscribe here is so that we can send requests to ourselves. + o.infoSub, _ = s.systemSubscribe(isubj, _EMPTY_, false, o.sysc, o.handleClusterConsumerInfoRequest) + } + + var err error + if o.cfg.AckPolicy != AckNone && o.cfg.AckPolicy != AckFlowControl { + if o.ackSubOld, err = o.subscribeInternal(o.ackSubjOld, o.pushAck); err != nil { + o.mu.Unlock() + return nil + } + if o.ackSub, err = o.subscribeInternal(o.ackSubj, o.pushAck); err != nil { + o.mu.Unlock() + return nil + } + } + + // Setup the internal sub for next message requests regardless. + // Will error if wrong mode to provide feedback to users. + if o.reqSub, err = o.subscribeInternal(o.nextMsgSubj, o.processNextMsgReq); err != nil { + o.mu.Unlock() + return nil + } + if o.resetSub, err = o.subscribeInternal(o.resetSubj, o.processResetReq); err != nil { + o.mu.Unlock() + return nil + } + + // Check on flow control settings. + if o.cfg.FlowControl { + o.setMaxPendingBytes(JsFlowControlMaxPending) + if o.fcSubOld, err = o.subscribeInternal(o.fcSubjOld, o.processFlowControl); err != nil { + o.mu.Unlock() + return nil + } + if o.fcSub, err = o.subscribeInternal(o.fcSubj, o.processFlowControl); err != nil { + o.mu.Unlock() + return nil + } + } + + // If push mode, register for notifications on interest. + if o.isPushMode() { + o.inch = make(chan bool, 8) + o.acc.sl.registerNotification(o.cfg.DeliverSubject, o.cfg.DeliverGroup, o.inch) + if o.active = <-o.inch; o.active { + o.checkQueueInterest() + } + + // Check gateways in case they are enabled. + if s.gateway.enabled { + if !o.active { + o.active = s.hasGatewayInterest(o.acc.Name, o.cfg.DeliverSubject) + } + stopAndClearTimer(&o.gwdtmr) + o.gwdtmr = time.AfterFunc(time.Second, func() { o.watchGWinterest() }) + } + } + + if o.dthresh > 0 && (o.isPullMode() || !o.active) { + // Pull consumer. We run the dtmr all the time for this one. + stopAndClearTimer(&o.dtmr) + o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive) + } + + // Update the consumer pause tracking. + o.updatePauseState(&o.cfg) + + // If we are not in ReplayInstant mode mark us as in replay state until resolved. + if o.cfg.ReplayPolicy != ReplayInstant { + o.replay = true + } + + // Recreate quit channel. + o.qch = make(chan struct{}) + qch := o.qch + node := o.node + var pch chan struct{} + if node != nil && o.pch == nil { + o.pch = make(chan struct{}, 1) + pch = o.pch + } + pullMode := o.isPullMode() + o.mu.Unlock() + + // Check if there are any pending we might need to clean up etc. + o.checkPending() + + // Snapshot initial info. + o.infoWithSnap(true) + + // These are the labels we will use to annotate our goroutines. + labels := pprofLabels{ + "type": "consumer", + "account": mset.accName(), + "stream": mset.name(), + "consumer": o.name, + } + + // Now start up Go routine to deliver msgs. + go func() { + setGoRoutineLabels(labels) + o.loopAndGatherMsgs(qch) + }() + + // Now start up Go routine to process acks. + go func() { + setGoRoutineLabels(labels) + o.processInboundAcks(qch) + }() + + if pullMode { + // Now start up Go routine to process inbound next message requests. + go func() { + setGoRoutineLabels(labels) + o.processInboundNextMsgReqs(qch) + }() + } + + // If we are R>1 spin up our proposal loop. + if node != nil && pch != nil { + // Determine if we can send pending requests info to the group. + // They must be on server versions >= 2.7.1 + o.checkAndSetPendingRequestsOk() + o.checkPendingRequests() + go func() { + setGoRoutineLabels(labels) + o.loopAndForwardProposals(node, qch, pch, term) + }() + } + } + return nil +} + +func (o *consumer) completeRestore() error { + o.mu.Lock() + if !o.restoring { + o.mu.Unlock() + return nil + } + o.restoring = false + isLeader, term := o.restoreLeader, o.restoreTerm + o.restoreLeader, o.restoreTerm = false, 0 + o.mu.Unlock() + if isLeader { + return o.setLeader(true, term) + } + return nil +} + +// This is coming on the wire so do not block here. +func (o *consumer) handleClusterConsumerInfoRequest(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + go o.infoWithSnapAndReply(false, reply) +} + +// Lock should be held. +func (o *consumer) subscribeInternal(subject string, cb msgHandler) (*subscription, error) { + c := o.client + if c == nil { + return nil, fmt.Errorf("invalid consumer") + } + if !c.srv.EventsEnabled() { + return nil, ErrNoSysAccount + } + if cb == nil { + return nil, fmt.Errorf("undefined message handler") + } + + o.sid++ + + // Now create the subscription + return c.processSub([]byte(subject), nil, []byte(strconv.Itoa(o.sid)), cb, false) +} + +// Unsubscribe from our subscription. +// Lock should be held. +func (o *consumer) unsubscribe(sub *subscription) { + if sub == nil || o.client == nil { + return + } + o.client.processUnsub(sub.sid) +} + +// We need to make sure we protect access to the outq. +// Do all advisory sends here. +func (o *consumer) sendAdvisory(subject string, e any) { + if o.acc == nil { + return + } + + // If there is no one listening for this advisory then save ourselves the effort + // and don't bother encoding the JSON or sending it. + if sl := o.acc.sl; (sl != nil && !sl.HasInterest(subject)) && !o.srv.hasGatewayInterest(o.acc.Name, subject) { + return + } + + j, err := json.Marshal(e) + if err != nil { + return + } + + o.outq.sendMsg(subject, j) +} + +func (o *consumer) sendDeleteAdvisoryLocked() { + e := JSConsumerActionAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerActionAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: o.stream, + Consumer: o.name, + Action: DeleteEvent, + Domain: o.srv.getOpts().JetStreamDomain, + } + + subj := JSAdvisoryConsumerDeletedPre + "." + o.stream + "." + o.name + o.sendAdvisory(subj, e) +} + +func (o *consumer) sendPinnedAdvisoryLocked(group string) { + e := JSConsumerGroupPinnedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerGroupPinnedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Account: o.acc.Name, + Stream: o.stream, + Consumer: o.name, + Domain: o.srv.getOpts().JetStreamDomain, + PinnedClientId: o.currentPinId, + Group: group, + } + + subj := JSAdvisoryConsumerPinnedPre + "." + o.stream + "." + o.name + o.sendAdvisory(subj, e) + +} +func (o *consumer) sendUnpinnedAdvisoryLocked(group string, reason string) { + e := JSConsumerGroupUnpinnedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerGroupUnpinnedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Account: o.acc.Name, + Stream: o.stream, + Consumer: o.name, + Domain: o.srv.getOpts().JetStreamDomain, + Group: group, + Reason: reason, + } + + subj := JSAdvisoryConsumerUnpinnedPre + "." + o.stream + "." + o.name + o.sendAdvisory(subj, e) + +} + +func (o *consumer) sendCreateAdvisory() { + o.mu.Lock() + defer o.mu.Unlock() + + e := JSConsumerActionAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerActionAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: o.stream, + Consumer: o.name, + Action: CreateEvent, + Domain: o.srv.getOpts().JetStreamDomain, + } + + subj := JSAdvisoryConsumerCreatedPre + "." + o.stream + "." + o.name + o.sendAdvisory(subj, e) +} + +func (o *consumer) sendPauseAdvisoryLocked(cfg *ConsumerConfig) { + e := JSConsumerPauseAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerPauseAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: o.stream, + Consumer: o.name, + Domain: o.srv.getOpts().JetStreamDomain, + } + + if cfg.PauseUntil != nil { + e.PauseUntil = *cfg.PauseUntil + e.Paused = time.Now().Before(e.PauseUntil) + } + + subj := JSAdvisoryConsumerPausePre + "." + o.stream + "." + o.name + o.sendAdvisory(subj, e) +} + +// Created returns created time. +func (o *consumer) createdTime() time.Time { + o.mu.Lock() + created := o.created + o.mu.Unlock() + return created +} + +// Internal to allow creation time to be restored. +func (o *consumer) setCreatedTime(created time.Time) { + o.mu.Lock() + o.created = created + o.mu.Unlock() +} + +// This will check for extended interest in a subject. If we have local interest we just return +// that, but in the absence of local interest and presence of gateways or service imports we need +// to check those as well. +func (o *consumer) hasDeliveryInterest(localInterest bool) bool { + o.mu.RLock() + mset := o.mset + if mset == nil { + o.mu.RUnlock() + return false + } + acc := o.acc + deliver := o.cfg.DeliverSubject + o.mu.RUnlock() + + if localInterest { + return true + } + + // If we are here check gateways. + if s := acc.srv; s != nil && s.hasGatewayInterest(acc.Name, deliver) { + return true + } + return false +} + +func (s *Server) hasGatewayInterest(account, subject string) bool { + gw := s.gateway + if !gw.enabled { + return false + } + gw.RLock() + defer gw.RUnlock() + for _, gwc := range gw.outo { + psi, qr := gwc.gatewayInterest(account, stringToBytes(subject)) + if psi || qr != nil { + return true + } + } + return false +} + +// This processes an update to the local interest for a deliver subject. +func (o *consumer) updateDeliveryInterest(localInterest bool) bool { + interest := o.hasDeliveryInterest(localInterest) + + o.mu.Lock() + defer o.mu.Unlock() + + mset := o.mset + if mset == nil || o.isPullMode() { + return false + } + + if interest && !o.active { + o.signalNewMessages() + } + // Update active status, if not active clear any queue group we captured. + if o.active = interest; !o.active { + o.qgroup = _EMPTY_ + } else { + o.checkQueueInterest() + } + + // If the delete timer has already been set do not clear here and return. + // Note that durable can now have an inactive threshold, so don't check + // for durable status, instead check for dthresh > 0. + if o.dtmr != nil && o.dthresh > 0 && !interest { + return true + } + + // Stop and clear the delete timer always. + stopAndClearTimer(&o.dtmr) + + // If we do not have interest anymore and have a delete threshold set, then set + // a timer to delete us. We wait for a bit in case of server reconnect. + if !interest && o.dthresh > 0 { + o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive) + return true + } + return false +} + +const ( + defaultConsumerNotActiveStartInterval = 30 * time.Second + defaultConsumerNotActiveMaxInterval = 5 * time.Minute +) + +var ( + consumerNotActiveStartInterval = defaultConsumerNotActiveStartInterval + consumerNotActiveMaxInterval = defaultConsumerNotActiveMaxInterval +) + +// deleteNotActive must only be called from time.AfterFunc or in its own +// goroutine, as it can block on clean-up. +func (o *consumer) deleteNotActive() { + // Take a copy of these when the goroutine starts, mostly it avoids a + // race condition with tests that modify these consts, such as + // TestJetStreamClusterGhostEphemeralsAfterRestart. + cnaMax := consumerNotActiveMaxInterval + cnaStart := consumerNotActiveStartInterval + + o.mu.Lock() + if o.mset == nil || !o.isLeader() { + o.mu.Unlock() + return + } + // Push mode just look at active. + if o.isPushMode() { + // If we are active simply return. + if o.active { + o.mu.Unlock() + return + } + } else { + // Pull mode. + // Check if we still have valid requests waiting. This also expires + // requests, which updates the last activity, so must be checked first. + if o.checkWaitingForInterest() { + if o.dtmr != nil { + o.dtmr.Reset(o.dthresh) + } else { + o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive) + } + o.mu.Unlock() + return + } + elapsed := time.Since(o.waiting.last) + if elapsed < o.dthresh { + // These need to keep firing so reset but use delta. + if o.dtmr != nil { + o.dtmr.Reset(o.dthresh - elapsed) + } else { + o.dtmr = time.AfterFunc(o.dthresh-elapsed, o.deleteNotActive) + } + o.mu.Unlock() + return + } + + // We now know we have no waiting requests, and our last request was long ago. + // However, based on AckWait the consumer could still be actively processing, + // even if we haven't been informed if there were no acks in the meantime. + // We must wait for the message that expires last and start counting down the + // inactive threshold from there. + now := time.Now().UnixNano() + l := len(o.cfg.BackOff) + var delay time.Duration + var ackWait time.Duration + for seq, p := range o.pending { + if l == 0 { + ackWait = o.ackWait(0) + } else { + bi := int(o.rdc[seq]) + if bi < 0 { + bi = 0 + } else if bi >= l { + bi = l - 1 + } + ackWait = o.ackWait(o.cfg.BackOff[bi]) + } + if ts := p.Timestamp + ackWait.Nanoseconds() + o.dthresh.Nanoseconds(); ts > now { + delay = max(delay, time.Duration(ts-now)) + } + } + // We'll wait for the latest time we expect an ack, plus the inactive threshold. + // Acknowledging a message will reset this back down to just the inactive threshold. + if delay > 0 { + if o.dtmr != nil { + o.dtmr.Reset(delay) + } else { + o.dtmr = time.AfterFunc(delay, o.deleteNotActive) + } + o.mu.Unlock() + return + } + } + + s, js := o.mset.srv, o.srv.js.Load() + acc, stream, name, isDirect := o.acc.Name, o.stream, o.name, o.direct + // Capture our own view of the assignment while we still hold the lock. + ca := o.ca + var qch, cqch chan struct{} + if o.srv != nil { + qch = o.srv.quitCh + } + oqch := o.qch + o.mu.Unlock() + if js != nil { + cqch = js.clusterQuitC() + } + + // Useful for pprof. + setGoRoutineLabels(pprofLabels{ + "account": acc, + "stream": stream, + "consumer": name, + }) + + // If we are clustered, check if we still have this consumer assigned. + // If we do, ask the meta leader to delete us. + if !isDirect && s.JetStreamIsClustered() { + js.mu.RLock() + nca := js.consumerAssignment(acc, stream, name) + // Only request the delete if the meta-layer assignment still refers to + // the consumer we captured, otherwise we'd be racing a recreated + // consumer with the same name. + requested := js.cluster != nil && ca != nil && ca.sameIdentity(nca) + js.mu.RUnlock() + + if requested { + // The creation time is part of the consumer's identity, so the meta leader + // can equally reject the request if the consumer got recreated meanwhile. + s.sendConsumerAssignmentDelete(acc, stream, name, ca.Created) + + // Check to make sure we went away. + // Don't think this needs to be a monitored go routine. + jitter := time.Duration(rand.Int64N(int64(cnaStart))) + interval := cnaStart + jitter + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + case <-qch: + return + case <-cqch: + return + case <-oqch: + // The consumer has stopped already, likely by an earlier delete proposal being applied. + return + } + js.mu.RLock() + if js.shuttingDown { + js.mu.RUnlock() + return + } + nca = js.consumerAssignment(acc, stream, name) + // Make sure this is the same consumer assignment, and not a new consumer with the same name. + match := ca.sameIdentity(nca) + js.mu.RUnlock() + if match { + s.Warnf("Consumer assignment for '%s > %s > %s' not cleaned up, retrying", acc, stream, name) + s.sendConsumerAssignmentDelete(acc, stream, name, ca.Created) + if interval < cnaMax { + interval *= 2 + ticker.Reset(interval) + } + continue + } + // We saw that consumer has been removed, all done. + return + } + } + } else { + // Otherwise, we can delete locally. Either a consumer that's not tracked + // by the meta layer (direct), or a standalone non-clustered server. + o.delete() + } +} + +func (o *consumer) watchGWinterest() { + pa := o.isActive() + // If there is no local interest... + if o.hasNoLocalInterest() { + o.updateDeliveryInterest(false) + if !pa && o.isActive() { + o.signalNewMessages() + } + } + + // We want this to always be running so we can also pick up on interest returning. + o.mu.Lock() + if o.gwdtmr != nil { + o.gwdtmr.Reset(time.Second) + } else { + stopAndClearTimer(&o.gwdtmr) + o.gwdtmr = time.AfterFunc(time.Second, func() { o.watchGWinterest() }) + } + o.mu.Unlock() +} + +// Config returns the consumer's configuration. +func (o *consumer) config() ConsumerConfig { + o.mu.Lock() + defer o.mu.Unlock() + return o.cfg +} + +// Check if we have hit max deliveries. If so do notification and cleanup. +// Return whether or not the max was hit. +// Lock should be held. +func (o *consumer) hasMaxDeliveries(seq uint64) bool { + if o.maxdc == 0 { + return false + } + if dc := o.deliveryCount(seq); dc >= o.maxdc { + // We have hit our max deliveries for this sequence. + // Only send the advisory once. + if dc == o.maxdc { + o.notifyDeliveryExceeded(seq, dc) + } + // Determine if we signal to start flow of messages again. + if o.maxp > 0 && len(o.pending) >= o.maxp { + o.signalNewMessages() + } + // Make sure to remove from pending. + if p, ok := o.pending[seq]; ok && p != nil { + delete(o.pending, seq) + // Increment by one, since the delivery count hasn't been increased above. + o.updateDelivered(p.Sequence, seq, dc+1, p.Timestamp) + o.moveAckFloor(p.Sequence, seq) + } + // Ensure redelivered state is set, if not already. + if o.rdc == nil { + o.rdc = make(map[uint64]uint64) + } + o.rdc[seq] = dc + return true + } + return false +} + +// Force expiration of all pending. +// Lock should be held. +func (o *consumer) forceExpirePending() { + var expired []uint64 + for seq := range o.pending { + if !o.onRedeliverQueue(seq) && !o.hasMaxDeliveries(seq) { + expired = append(expired, seq) + } + } + if len(expired) > 0 { + slices.Sort(expired) + o.addToRedeliverQueue(expired...) + // Now we should update the timestamp here since we are redelivering. + // We will use an incrementing time to preserve order for any other redelivery. + off := time.Now().UnixNano() - o.pending[expired[0]].Timestamp + for _, seq := range expired { + if p, ok := o.pending[seq]; ok && p != nil { + p.Timestamp += off + } + } + o.resetPtmr(o.ackWait(0)) + } + o.signalNewMessages() +} + +// Acquire proper locks and update rate limit. +// Will use what is in config. +func (o *consumer) setRateLimitNeedsLocks() { + o.mu.RLock() + mset := o.mset + o.mu.RUnlock() + + if mset == nil { + return + } + + mset.mu.RLock() + o.mu.Lock() + o.setRateLimit(o.cfg.RateLimit) + o.mu.Unlock() + mset.mu.RUnlock() +} + +// Set the rate limiter +// Both mset and consumer lock should be held. +func (o *consumer) setRateLimit(bps uint64) { + if bps == 0 { + o.rlimit = nil + return + } + + // TODO(dlc) - Make sane values or error if not sane? + // We are configured in bits per sec so adjust to bytes. + rl := rate.Limit(bps / 8) + mset := o.mset + + // Burst should be set to maximum msg size for this account, etc. + var burst int + // We don't need to get cfgMu's rlock here since this function + // is already invoked under mset.mu.RLock(), which superseeds cfgMu. + if mset.cfg.MaxMsgSize > 0 { + burst = int(mset.cfg.MaxMsgSize) + } else { + acc := mset.jsa.account + acc.mu.RLock() + mpay := acc.mpay + acc.mu.RUnlock() + if mpay > 0 { + burst = int(mpay) + } else { + burst = int(acc.srv.getOpts().MaxPayload) + } + } + + o.rlimit = rate.NewLimiter(rl, burst) +} + +// Check if new consumer config allowed vs old. +func (acc *Account) checkNewConsumerConfig(cfg, ncfg *ConsumerConfig) error { + if reflect.DeepEqual(cfg, ncfg) { + return nil + } + // Something different, so check since we only allow certain things to be updated. + if cfg.DeliverPolicy != ncfg.DeliverPolicy { + return errors.New("deliver policy can not be updated") + } + if cfg.MemoryStorage != ncfg.MemoryStorage { + return errors.New("storage type can not be updated") + } + // Direct and Sourcing classify the consumer for its whole lifetime, which the + // stream relies on when walking its consumer list, so they can not change. + if cfg.Direct != ncfg.Direct { + return errors.New("direct can not be updated") + } + if cfg.Sourcing != ncfg.Sourcing { + return errors.New("sourcing can not be updated") + } + if cfg.OptStartSeq != ncfg.OptStartSeq { + return errors.New("start sequence can not be updated") + } + if cfg.OptStartTime != nil && ncfg.OptStartTime != nil { + // Both have start times set, compare them directly: + if !cfg.OptStartTime.Equal(*ncfg.OptStartTime) { + return errors.New("start time can not be updated") + } + } else if cfg.OptStartTime != nil || ncfg.OptStartTime != nil { + // At least one start time is set and the other is not + return errors.New("start time can not be updated") + } + if cfg.AckPolicy != ncfg.AckPolicy { + return errors.New("ack policy can not be updated") + } + if cfg.ReplayPolicy != ncfg.ReplayPolicy { + return errors.New("replay policy can not be updated") + } + if cfg.Heartbeat != ncfg.Heartbeat { + return errors.New("heart beats can not be updated") + } + if cfg.FlowControl != ncfg.FlowControl { + return errors.New("flow control can not be updated") + } + + // Deliver Subject is conditional on if its bound. + if cfg.DeliverSubject != ncfg.DeliverSubject { + if cfg.DeliverSubject == _EMPTY_ { + return errors.New("can not update pull consumer to push based") + } + if ncfg.DeliverSubject == _EMPTY_ { + return errors.New("can not update push consumer to pull based") + } + if acc.sl.HasInterest(cfg.DeliverSubject) { + return NewJSConsumerNameExistError() + } + } + + if cfg.MaxWaiting != ncfg.MaxWaiting { + return errors.New("max waiting can not be updated") + } + + // Check if BackOff is defined, MaxDeliver is within range. + if lbo := len(ncfg.BackOff); lbo > 0 && ncfg.MaxDeliver != -1 && lbo > ncfg.MaxDeliver { + return NewJSConsumerMaxDeliverBackoffError() + } + + return nil +} + +// Update the config based on the new config, or error if update not allowed. +func (o *consumer) updateConfig(cfg *ConsumerConfig) error { + o.mu.Lock() + defer o.mu.Unlock() + + if o.closed || o.mset == nil { + return NewJSConsumerDoesNotExistError() + } + + if err := o.acc.checkNewConsumerConfig(&o.cfg, cfg); err != nil { + return err + } + + // Make sure we always store PauseUntil in UTC. + if cfg.PauseUntil != nil { + utc := (*cfg.PauseUntil).UTC() + cfg.PauseUntil = &utc + } + + if o.store != nil { + // Update local state always. + if err := o.store.UpdateConfig(cfg); err != nil { + return err + } + } + + // DeliverSubject + if cfg.DeliverSubject != o.cfg.DeliverSubject { + o.updateDeliverSubjectLocked(cfg.DeliverSubject) + } + + // MaxAckPending + if cfg.MaxAckPending != o.cfg.MaxAckPending { + o.maxp = cfg.MaxAckPending + o.signalNewMessages() + // If MaxAckPending is lowered, we could have allocated a pending deliveries map of larger size. + // Reset it here, so we can shrink the map. + if cfg.MaxAckPending < o.cfg.MaxAckPending { + o.resetPendingDeliveries() + } + } + // AckWait + if cfg.AckWait != o.cfg.AckWait { + if o.ptmr != nil { + o.resetPtmr(100 * time.Millisecond) + } + } + // Rate Limit + if cfg.RateLimit != o.cfg.RateLimit { + // We need both locks here so do in Go routine. + go o.setRateLimitNeedsLocks() + } + if cfg.SampleFrequency != o.cfg.SampleFrequency { + s := strings.TrimSuffix(cfg.SampleFrequency, "%") + if sampleFreq, err := strconv.ParseInt(s, 10, 32); err == nil { + o.sfreq = int32(sampleFreq) + } + } + // Set MaxDeliver if changed + if cfg.MaxDeliver != o.cfg.MaxDeliver { + // MaxDeliver is negative (-1) when infinite. + o.maxdc = uint64(max(cfg.MaxDeliver, 0)) + } + // Set InactiveThreshold if changed. + if val := cfg.InactiveThreshold; val != o.cfg.InactiveThreshold { + o.updateInactiveThreshold(cfg) + stopAndClearTimer(&o.dtmr) + // Restart timer only if we are the leader. + if o.isLeader() && o.dthresh > 0 { + o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive) + } + } + // Check whether the pause has changed + { + var old, new time.Time + if o.cfg.PauseUntil != nil { + old = *o.cfg.PauseUntil + } + if cfg.PauseUntil != nil { + new = *cfg.PauseUntil + } + if !old.Equal(new) { + o.updatePauseState(cfg) + if o.isLeader() { + o.sendPauseAdvisoryLocked(cfg) + } + } + } + + // Check for Subject Filters update. + newSubjects := gatherSubjectFilters(cfg.FilterSubject, cfg.FilterSubjects) + updatedFilters := !subjectSliceEqual(newSubjects, o.subjf.subjects()) + if updatedFilters { + newSubjf := make(subjectFilters, 0, len(newSubjects)) + for _, newFilter := range newSubjects { + fs := &subjectFilter{ + subject: newFilter, + hasWildcard: subjectHasWildcard(newFilter), + tokenizedSubject: tokenizeSubjectIntoSlice(nil, newFilter), + } + newSubjf = append(newSubjf, fs) + } + // Make sure we have correct signaling setup. + // Consumer lock can not be held. + mset := o.mset + o.mu.Unlock() + mset.swapSigSubs(o, newSubjf.subjects()) + o.mu.Lock() + + // When we're done with signaling, we can replace the subjects. + // If filters were removed, set `o.subjf` to nil. + if len(newSubjf) == 0 { + o.subjf = nil + o.filters = nil + } else { + o.subjf = newSubjf + if len(o.subjf) == 1 { + o.filters = nil + } else { + o.filters = gsl.NewSublist[struct{}]() + for _, filter := range o.subjf { + o.filters.Insert(filter.subject, struct{}{}) + } + } + } + } + + // Record new config for others that do not need special handling. + // Allowed but considered no-op, [Description, SampleFrequency, MaxWaiting, HeadersOnly] + o.cfg = *cfg + + if cfg.Sourcing && (!o.srv.JetStreamIsClustered() && o.srv.standAloneMode()) { + o.resetStartingSeqLocked(0, _EMPTY_, false, false) + } + if updatedFilters { + // Cleanup messages that lost interest. + if o.retention == InterestPolicy { + // Capture mset under the lock. + mset := o.mset + o.mu.Unlock() + if mset != nil { + o.cleanupNoInterestMessages(mset, false) + } + o.mu.Lock() + } + + // Re-calculate num pending on update. + o.streamNumPending() + } + + return nil +} + +// This is a config change for the delivery subject for a +// push based consumer. +func (o *consumer) updateDeliverSubject(newDeliver string) { + // Update the config and the dsubj + o.mu.Lock() + defer o.mu.Unlock() + o.updateDeliverSubjectLocked(newDeliver) +} + +// This is a config change for the delivery subject for a +// push based consumer. +func (o *consumer) updateDeliverSubjectLocked(newDeliver string) { + if o.closed || o.isPullMode() || o.cfg.DeliverSubject == newDeliver { + return + } + + // Force redeliver of all pending on change of delivery subject. + if len(o.pending) > 0 { + o.forceExpirePending() + } + + o.acc.sl.clearNotification(o.dsubj, o.cfg.DeliverGroup, o.inch) + o.dsubj, o.cfg.DeliverSubject = newDeliver, newDeliver + // When we register new one it will deliver to update state loop. + o.acc.sl.registerNotification(newDeliver, o.cfg.DeliverGroup, o.inch) +} + +// Check that configs are equal but allow delivery subjects to be different. +func configsEqualSansDelivery(a, b ConsumerConfig) bool { + // These were copied in so can set Delivery here. + a.DeliverSubject, b.DeliverSubject = _EMPTY_, _EMPTY_ + return reflect.DeepEqual(a, b) +} + +// Helper to send a reply to an ack. +func (o *consumer) sendAckReply(subj string) { + o.mu.RLock() + defer o.mu.RUnlock() + o.outq.sendMsg(subj, nil) +} + +type jsAckMsg struct { + subject string + reply string + hdr int + msg []byte +} + +var jsAckMsgPool sync.Pool + +func newJSAckMsg(subj, reply string, hdr int, msg []byte) *jsAckMsg { + var m *jsAckMsg + am := jsAckMsgPool.Get() + if am != nil { + m = am.(*jsAckMsg) + } else { + m = &jsAckMsg{} + } + // 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) = jsAckMsg{subj, reply, hdr, msg} + return m +} + +func (am *jsAckMsg) returnToPool() { + if am == nil { + return + } + am.subject, am.reply, am.hdr, am.msg = _EMPTY_, _EMPTY_, -1, nil + jsAckMsgPool.Put(am) +} + +// Push the ack message to the consumer's ackMsgs queue +func (o *consumer) pushAck(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + atomic.AddInt64(&o.awl, 1) + o.ackMsgs.push(newJSAckMsg(subject, reply, c.pa.hdr, copyBytes(rmsg))) +} + +// Processes a message for the ack reply subject delivered with a message. +func (o *consumer) processAck(subject, reply string, hdr int, rmsg []byte) { + defer atomic.AddInt64(&o.awl, -1) + + var msg []byte + if hdr > 0 { + msg = rmsg[hdr:] + } else { + msg = rmsg + } + + sseq, dseq, dc, _, _ := ackReplyInfo(subject) + + skipAckReply := sseq == 0 + + switch { + case len(msg) == 0, bytes.Equal(msg, AckAck), bytes.Equal(msg, AckOK): + if !o.processAckMsg(sseq, dseq, dc, reply, true) { + // We handle replies for acks in updateAcks + skipAckReply = true + } + case bytes.HasPrefix(msg, AckNext): + o.processAckMsg(sseq, dseq, dc, _EMPTY_, true) + o.processNextMsgRequest(reply, msg[len(AckNext):]) + skipAckReply = true + case bytes.HasPrefix(msg, AckNak): + o.processNak(sseq, dseq, dc, msg) + case bytes.Equal(msg, AckProgress): + o.progressUpdate(sseq) + case bytes.HasPrefix(msg, AckTerm): + var reason string + if buf := msg[len(AckTerm):]; len(buf) > 0 { + reason = string(bytes.TrimSpace(buf)) + } + if !o.processTerm(sseq, dseq, dc, reason, reply) { + // We handle replies for acks in updateAcks + skipAckReply = true + } + } + + // Ack the ack if requested. + if len(reply) > 0 && !skipAckReply { + o.sendAckReply(reply) + } +} + +// Used to process a working update to delay redelivery. +func (o *consumer) progressUpdate(seq uint64) { + o.mu.Lock() + defer o.mu.Unlock() + + if p, ok := o.pending[seq]; ok { + p.Timestamp = time.Now().UnixNano() + // Update store system. + o.updateDelivered(p.Sequence, seq, 1, p.Timestamp) + } +} + +// Lock should be held. +func (o *consumer) updateSkipped(seq uint64) { + // Clustered mode and R>1 only. + if o.node == nil || !o.isLeader() { + return + } + var b [1 + 8]byte + b[0] = byte(updateSkipOp) + var le = binary.LittleEndian + le.PutUint64(b[1:], seq) + o.propose(b[:]) +} + +// resetRequest tracks a reset request that still needs to be responded to +// once the reset has been applied. +type resetRequest struct { + internal bool // Respond on the internal sys account. + identity bool // Include the stream identity header in the response. +} + +func (o *consumer) resetStartingSeq(seq uint64, reply string, internal, identity bool) (uint64, bool, error) { + o.mu.Lock() + defer o.mu.Unlock() + return o.resetStartingSeqLocked(seq, reply, internal, identity) +} + +// Lock should be held. +func (o *consumer) resetStartingSeqLocked(seq uint64, reply string, internal, identity bool) (uint64, bool, error) { + // Reset to a specific sequence, or back to the ack floor. + if seq == 0 { + seq = o.asflr + 1 + } else if o.cfg.DeliverPolicy == DeliverAll { + // Always allowed. + goto VALID + } else if o.cfg.DeliverPolicy == DeliverByStartSequence { + // Only allowed if not going below what's configured. + if seq < o.cfg.OptStartSeq { + return 0, false, errors.New("below start seq") + } + goto VALID + } else if o.cfg.DeliverPolicy == DeliverByStartTime && o.mset != nil { + // Only allowed if not going below what's configured. + nseq := o.mset.store.GetSeqFromTime(*o.cfg.OptStartTime) + if seq < nseq { + return 0, false, errors.New("below start time") + } + goto VALID + } else { + return 0, false, errors.New("not allowed") + } + +VALID: + // Must be a minimum of 1. + if seq <= 0 { + seq = 1 + } + // The replicated path requires quorum first before the reset actually takes effect. + if o.node != nil { + if !o.isLeader() { + return 0, false, nil + } + b := make([]byte, 1+8+len(reply)) + b[0] = byte(resetSeqOp) + var le = binary.LittleEndian + le.PutUint64(b[1:], seq) + copy(b[1+8:], reply) + o.propose(b[:]) + if reply != _EMPTY_ { + if o.rsm == nil { + o.rsm = make(map[string]resetRequest, 1) + } + o.rsm[reply] = resetRequest{internal: internal, identity: identity} + } + return seq, false, nil + } + recalcPending := o.resetLocalStartingSeq(seq) + if o.store != nil { + o.store.Reset(seq - 1) + // Cleanup messages that lost interest. + if o.retention == InterestPolicy { + if mset := o.mset; mset != nil { + o.mu.Unlock() + ss := mset.state() + o.checkStateForInterestStream(&ss) + o.mu.Lock() + } + } + } + // Recalculate pending, and re-trigger message delivery. + if recalcPending { + o.streamNumPending() + } + o.signalNewMessages() + return seq, true, nil +} + +// Lock should be held. +func (o *consumer) resetLocalStartingSeq(seq uint64) bool { + recalcPending := o.sseq != seq || len(o.pending) > 0 + // A reset back to the ack floor can be optimized since we know how many were pending. + // But only for AckAll/AckFlowControl, since those guarantee no out-of-order acks. + if recalcPending && seq == o.asflr+1 && o.isLeader() && + (o.cfg.AckPolicy == AckAll || o.cfg.AckPolicy == AckFlowControl) { + o.npc += int64(len(o.pending)) + recalcPending = false + } + o.pending, o.rdc = nil, nil + o.rdq = nil + o.rdqi.Empty() + o.sseq, o.dseq = seq, 1 + o.adflr, o.asflr = o.dseq-1, o.sseq-1 + o.ldt, o.lat = time.Time{}, time.Time{} + return recalcPending +} + +func (o *consumer) loopAndForwardProposals(node RaftNode, qch, pch chan struct{}, term uint64) { + if node == nil || qch == nil || pch == nil { + return + } + + forwardProposals := func() error { + o.mu.Lock() + if node == nil || !node.Leader() || o.term != term { + o.mu.Unlock() + return errors.New("no longer leader") + } + proposal := o.phead + o.phead, o.ptail = nil, nil + o.mu.Unlock() + // 256k max for now per batch. + const maxBatch = 256 * 1024 + var entries []*Entry + for sz := 0; proposal != nil; proposal = proposal.next { + entries = append(entries, newEntry(EntryNormal, proposal.data)) + sz += len(proposal.data) + if sz > maxBatch { + node.ProposeMulti(term, entries) + // We need to re-create `entries` because there is a reference + // to it in the node's pae map. + sz, entries = 0, nil + } + } + if len(entries) > 0 { + node.ProposeMulti(term, entries) + } + return nil + } + + // In case we have anything pending on entry. + forwardProposals() + + for { + select { + case <-qch: + forwardProposals() + return + case <-pch: + if err := forwardProposals(); err != nil { + return + } + } + } +} + +// Lock should be held. +func (o *consumer) propose(entry []byte) { + p := &proposal{data: entry} + if o.phead == nil { + o.phead = p + } else { + o.ptail.next = p + } + o.ptail = p + + // Kick our looper routine. + select { + case o.pch <- struct{}{}: + default: + } +} + +// Lock should be held. +func (o *consumer) updateDelivered(dseq, sseq, dc uint64, ts int64) { + // Clustered mode and R>1. + if o.node != nil { + // Inline for now, use variable compression. + var b [4*binary.MaxVarintLen64 + 1]byte + b[0] = byte(updateDeliveredOp) + n := 1 + n += binary.PutUvarint(b[n:], dseq) + n += binary.PutUvarint(b[n:], sseq) + n += binary.PutUvarint(b[n:], dc) + n += binary.PutVarint(b[n:], ts) + o.propose(b[:n]) + } else if o.store != nil { + o.store.UpdateDelivered(dseq, sseq, dc, ts) + } + // Update activity. + o.ldt = time.Now() +} + +// Used to remember a pending ack reply in a replicated consumer. +// Lock should be held. +func (o *consumer) addAckReply(sseq uint64, reply string) { + if o.replies == nil { + o.replies = make(map[uint64]string) + } + o.replies[sseq] = reply +} + +// Used to remember messages that need to be sent for a replicated consumer, after delivered quorum. +// Lock should be held. +func (o *consumer) addReplicatedQueuedMsg(pmsg *jsPubMsg) { + // Is not explicitly limited in size, but will at most hold maximum ack pending. + if o.pendingDeliveries == nil { + o.pendingDeliveries = make(map[uint64]*jsPubMsg) + } + o.pendingDeliveries[pmsg.seq] = pmsg + + // Is not explicitly limited in size, but will at most hold maximum waiting requests. + if o.waitingDeliveries == nil { + o.waitingDeliveries = make(map[string]*waitingDelivery) + } + if wd, ok := o.waitingDeliveries[pmsg.dsubj]; ok { + wd.seq = pmsg.seq + } else { + wd := wdPool.Get().(*waitingDelivery) + wd.seq = pmsg.seq + o.waitingDeliveries[pmsg.dsubj] = wd + } +} + +// Lock should be held. +func (o *consumer) updateAcks(dseq, sseq uint64, reply string) { + if o.node != nil { + // Inline for now, use variable compression. + var b [2*binary.MaxVarintLen64 + 1]byte + b[0] = byte(updateAcksOp) + n := 1 + n += binary.PutUvarint(b[n:], dseq) + n += binary.PutUvarint(b[n:], sseq) + o.propose(b[:n]) + if reply != _EMPTY_ { + o.addAckReply(sseq, reply) + } + } else if o.store != nil { + o.store.UpdateAcks(dseq, sseq) + if reply != _EMPTY_ { + // Already locked so send direct. + o.outq.sendMsg(reply, nil) + } + } + // Update activity. + o.lat = time.Now() +} + +// Communicate to the cluster an addition of a pending request. +// Lock should be held. +func (o *consumer) addClusterPendingRequest(reply string) { + if o.node == nil || !o.pendingRequestsOk() { + return + } + b := make([]byte, len(reply)+1) + b[0] = byte(addPendingRequest) + copy(b[1:], reply) + o.propose(b) +} + +// Communicate to the cluster a removal of a pending request. +// Lock should be held. +func (o *consumer) removeClusterPendingRequest(reply string) { + if o.node == nil || !o.pendingRequestsOk() { + return + } + b := make([]byte, len(reply)+1) + b[0] = byte(removePendingRequest) + copy(b[1:], reply) + o.propose(b) +} + +// Set whether or not we can send pending requests to followers. +func (o *consumer) setPendingRequestsOk(ok bool) { + o.mu.Lock() + o.prOk = ok + o.mu.Unlock() +} + +// Lock should be held. +func (o *consumer) pendingRequestsOk() bool { + return o.prOk +} + +// Set whether or not we can send info about pending pull requests to our group. +// Will require all peers have a minimum version. +func (o *consumer) checkAndSetPendingRequestsOk() { + o.mu.RLock() + s, isValid := o.srv, o.mset != nil + o.mu.RUnlock() + if !isValid { + return + } + + if ca := o.consumerAssignment(); ca != nil && len(ca.Group.Peers) > 1 { + for _, pn := range ca.Group.Peers { + if si, ok := s.nodeToInfo.Load(pn); ok { + if !versionAtLeast(si.(nodeInfo).version, 2, 7, 1) { + // We expect all of our peers to eventually be up to date. + // So check again in awhile. + time.AfterFunc(eventsHBInterval, func() { o.checkAndSetPendingRequestsOk() }) + o.setPendingRequestsOk(false) + return + } + } + } + } + o.setPendingRequestsOk(true) +} + +// On leadership change make sure we alert the pending requests that they are no longer valid. +func (o *consumer) checkPendingRequests() { + o.mu.Lock() + defer o.mu.Unlock() + if o.mset == nil || o.outq == nil { + return + } + hdr := []byte("NATS/1.0 409 Leadership Change\r\n\r\n") + for reply := range o.prm { + o.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + } + o.prm = nil +} + +// This will release any pending pull requests if applicable. +// Should be called only by the leader being deleted or stopped. +// Lock should be held. +func (o *consumer) releaseAnyPendingRequests(isAssigned bool) { + if o.mset == nil || o.outq == nil || o.waiting.len() == 0 { + return + } + wq := o.waiting + for wr := wq.head; wr != nil; { + if !isAssigned { + hdr := []byte("NATS/1.0 409 Consumer Deleted\r\n\r\n") + o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + } + next := wr.next + wr.recycle() + wr = next + } + // Nil out old queue. + o.waiting = nil +} + +// Process a NAK. +func (o *consumer) processNak(sseq, dseq, dc uint64, nak []byte) { + o.mu.Lock() + defer o.mu.Unlock() + + // Check for out of range. + if dseq <= o.adflr || dseq > o.dseq { + return + } + // If we are explicit ack make sure this is still on our pending list. + if _, ok := o.pending[sseq]; !ok { + return + } + + // Deliver an advisory + e := JSConsumerDeliveryNakAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerDeliveryNakAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: o.stream, + Consumer: o.name, + ConsumerSeq: dseq, + StreamSeq: sseq, + Deliveries: dc, + Domain: o.srv.getOpts().JetStreamDomain, + } + + o.sendAdvisory(o.nakEventT, e) + + // Check to see if we have delays attached. + if len(nak) > len(AckNak) { + arg := bytes.TrimSpace(nak[len(AckNak):]) + if len(arg) > 0 { + var d time.Duration + var err error + if arg[0] == '{' { + var nd ConsumerNakOptions + if err = json.Unmarshal(arg, &nd); err == nil { + d = nd.Delay + } + } else { + d, err = time.ParseDuration(string(arg)) + } + if err != nil { + // Treat this as normal NAK. + o.srv.Warnf("JetStream consumer '%s > %s > %s' bad NAK delay value: %q", o.acc.Name, o.stream, o.name, arg) + } else { + // We have a parsed duration that the user wants us to wait before retrying. + // Make sure we are not on the rdq. + o.removeFromRedeliverQueue(sseq) + if p, ok := o.pending[sseq]; ok { + // now - ackWait is expired now, so offset from there. + p.Timestamp = time.Now().Add(-o.cfg.AckWait).Add(d).UnixNano() + // Update store system which will update followers as well. + o.updateDelivered(p.Sequence, sseq, dc, p.Timestamp) + if o.ptmr != nil { + // Want checkPending to run and figure out the next timer ttl. + // TODO(dlc) - We could optimize this maybe a bit more and track when we expect the timer to fire. + o.resetPtmr(10 * time.Millisecond) + } + } + // Nothing else for use to do now so return. + return + } + } + } + + // If already queued up also ignore. + if !o.onRedeliverQueue(sseq) { + o.addToRedeliverQueue(sseq) + } + + o.signalNewMessages() +} + +// Process a TERM +// Returns `true` if the ack was processed in place and the sender can now respond +// to the client, or `false` if there was an error or the ack is replicated (in which +// case the reply will be sent later). +func (o *consumer) processTerm(sseq, dseq, dc uint64, reason, reply string) bool { + return o.processTermLocked(sseq, dseq, dc, reason, reply, true) +} + +func (o *consumer) processTermLocked(sseq, dseq, dc uint64, reason, reply string, needLock bool) bool { + // Treat like an ack to suppress redelivery. + ackedInPlace := o.processAckMsgLocked(sseq, dseq, dc, reply, false, needLock) + + if needLock { + o.mu.Lock() + defer o.mu.Unlock() + } + + // Deliver an advisory + e := JSConsumerDeliveryTerminatedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerDeliveryTerminatedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: o.stream, + Consumer: o.name, + ConsumerSeq: dseq, + StreamSeq: sseq, + Deliveries: dc, + Reason: reason, + Domain: o.srv.getOpts().JetStreamDomain, + } + + subj := JSAdvisoryConsumerMsgTerminatedPre + "." + o.stream + "." + o.name + o.sendAdvisory(subj, e) + return ackedInPlace +} + +// Introduce a small delay in when timer fires to check pending. +// Allows bursts to be treated in same time frame. +const ackWaitDelay = time.Millisecond + +// ackWait returns how long to wait to fire the pending timer. +func (o *consumer) ackWait(next time.Duration) time.Duration { + if next > 0 { + return next + ackWaitDelay + } + return o.cfg.AckWait + ackWaitDelay +} + +func (o *consumer) removeRedeliveredBelow(seq uint64) { + if seq == 0 { + return + } + o.mu.Lock() + for sseq := range o.rdc { + if sseq < seq { + delete(o.rdc, sseq) + o.removeFromRedeliverQueue(sseq) + } + } + o.mu.Unlock() + + if o.store != nil { + o.store.RemoveRedeliveredBelow(seq) + } +} + +// checkRedelivered drops rdq entries at/below asflr or below stream's first sequence. +// But rdc is kept until the message leaves the stream: needAck relies on rdc to mark +// messages past MaxDeliver. +// Lock should be held. +func (o *consumer) checkRedelivered() { + if o.mset == nil { + return + } + var ss StreamState + o.mset.store.FastState(&ss) + + var shouldUpdateState bool + for sseq := range o.rdc { + if sseq <= o.asflr || sseq < ss.FirstSeq { + o.removeFromRedeliverQueue(sseq) + } + if sseq < ss.FirstSeq { + delete(o.rdc, sseq) + shouldUpdateState = true + } + } + if shouldUpdateState { + if err := o.writeStoreStateUnlocked(); err != nil && o.srv != nil && o.mset != nil && !o.closed { + s, acc, mset, name := o.srv, o.acc, o.mset, o.name + s.Warnf("Consumer '%s > %s > %s' error on write store state from check redelivered: %v", acc, mset.getCfgName(), name, err) + } + } +} + +// This will restore the state from disk. +// Lock should be held. +func (o *consumer) readStoredState() error { + if o.store == nil { + return nil + } + state, err := o.store.State() + if err == nil { + o.applyState(state) + if len(o.rdc) > 0 { + o.checkRedelivered() + } + } + return err +} + +// Apply the consumer stored state. +// Lock should be held. +func (o *consumer) applyState(state *ConsumerState) { + if state == nil { + return + } + + o.sseq = state.Delivered.Stream + 1 + o.dseq = state.Delivered.Consumer + 1 + o.adflr = state.AckFloor.Consumer + o.asflr = state.AckFloor.Stream + o.pending = state.Pending + o.rdc = state.Redelivered + + // Setup tracking timer if we have restored pending. + if o.isLeader() && len(o.pending) > 0 { + // This is on startup or leader change. We want to check pending + // sooner in case there are inconsistencies etc. Pick between 500ms - 1.5s + delay := 500*time.Millisecond + time.Duration(rand.Int64N(1000))*time.Millisecond + + // If normal is lower than this just use that. + if o.cfg.AckWait < delay { + delay = o.ackWait(0) + } + o.resetPtmr(delay) + } +} + +// Sets our store state from another source. Used in clustered mode on snapshot restore. +// Lock should be held. +func (o *consumer) setStoreState(state *ConsumerState) error { + if state == nil || o.store == nil { + return nil + } + err := o.store.Update(state) + if err == nil { + o.applyState(state) + } else if err == ErrStoreOldUpdate { + // Our store already has a newer state, which is normal during recovery + // when the consumer was loaded from disk before the meta snapshot state + // was applied. + return nil + } + return err +} + +// Update our state to the store. +func (o *consumer) writeStoreState() error { + o.mu.Lock() + defer o.mu.Unlock() + return o.writeStoreStateUnlocked() +} + +// Update our state to the store. +// Lock should be held. +func (o *consumer) writeStoreStateUnlocked() error { + if o.store == nil { + return nil + } + state := ConsumerState{ + Delivered: SequencePair{ + Consumer: o.dseq - 1, + Stream: o.sseq - 1, + }, + AckFloor: SequencePair{ + Consumer: o.adflr, + Stream: o.asflr, + }, + Pending: o.pending, + Redelivered: o.rdc, + } + return o.store.Update(&state) +} + +// Returns an initial info. Only applicable for non-clustered consumers. +// We will clear after we return it, so one shot. +func (o *consumer) initialInfo() *ConsumerInfo { + o.mu.Lock() + ici := o.ici + o.ici = nil // gc friendly + o.mu.Unlock() + if ici == nil { + ici = o.info() + } + return ici +} + +// Clears our initial info. +// Used when we have a leader change in cluster mode but do not send a response. +func (o *consumer) clearInitialInfo() { + o.mu.Lock() + o.ici = nil // gc friendly + o.mu.Unlock() +} + +// Info returns our current consumer state. +func (o *consumer) info() *ConsumerInfo { + return o.infoWithSnap(false) +} + +func (o *consumer) infoWithSnap(snap bool) *ConsumerInfo { + return o.infoWithSnapAndReply(snap, _EMPTY_) +} + +func (o *consumer) infoWithSnapAndReply(snap bool, reply string) *ConsumerInfo { + o.mu.Lock() + mset := o.mset + if o.closed || mset == nil || mset.srv == nil { + o.mu.Unlock() + return nil + } + js := o.js + if js == nil { + o.mu.Unlock() + return nil + } + + // Capture raftGroup. + var rg *raftGroup + if o.ca != nil { + rg = o.ca.Group + } + + priorityGroups := []PriorityGroupState{} + // TODO(jrm): when we introduce supporting many priority groups, we need to update assigning `o.currentNuid` for each group. + if len(o.cfg.PriorityGroups) > 0 { + priorityGroups = append(priorityGroups, PriorityGroupState{ + Group: o.cfg.PriorityGroups[0], + PinnedClientID: o.currentPinId, + PinnedTS: o.pinnedTS, + }) + } + + np, err := o.checkNumPending() + if err != nil { + o.mu.Unlock() + return nil + } + + dseq, sseq := o.dseq, o.sseq + if dseq <= 0 { + dseq = 1 + } + if sseq <= 0 { + sseq = 1 + } + + cfg := o.cfg + info := &ConsumerInfo{ + Stream: o.stream, + Name: o.name, + Created: o.created, + Config: &cfg, + Delivered: SequenceInfo{ + Consumer: dseq - 1, + Stream: sseq - 1, + }, + AckFloor: SequenceInfo{ + Consumer: o.adflr, + Stream: o.asflr, + }, + NumAckPending: len(o.pending), + NumRedelivered: len(o.rdc), + NumPending: np, + PushBound: o.isPushMode() && o.active, + TimeStamp: time.Now().UTC(), + PriorityGroups: priorityGroups, + } + // Reset redelivered for MaxDeliver 1. Redeliveries are disabled so must not report it (is confusing otherwise). + // The state does still keep track of these messages. + if o.cfg.MaxDeliver == 1 { + info.NumRedelivered = 0 + } + if o.cfg.PauseUntil != nil { + p := *o.cfg.PauseUntil + if info.Paused = time.Now().Before(p); info.Paused { + info.PauseRemaining = time.Until(p) + } + } + + // We always need to pull certain data from our store. + if o.store != nil { + state, err := o.store.BorrowState() + if err != nil { + o.mu.Unlock() + return nil + } + + // If we are the leader we could have o.sseq that is skipped ahead. + // To maintain consistency in reporting (e.g. jsz) we always take the state for our delivered/ackfloor stream sequence. + // Only use skipped ahead o.sseq if we're a new consumer and have not yet replicated this state yet. + leader := o.isLeader() + if !leader || o.store.HasState() { + info.Delivered.Consumer, info.Delivered.Stream = state.Delivered.Consumer, state.Delivered.Stream + } + info.AckFloor.Consumer, info.AckFloor.Stream = state.AckFloor.Consumer, state.AckFloor.Stream + if !leader { + info.NumAckPending = len(state.Pending) + info.NumRedelivered = len(state.Redelivered) + } + } + + // Adjust active based on non-zero etc. Also make UTC here. + if !o.ldt.IsZero() { + ldt := o.ldt.UTC() // This copies as well. + info.Delivered.Last = &ldt + } + if !o.lat.IsZero() { + lat := o.lat.UTC() // This copies as well. + info.AckFloor.Last = &lat + } + + // If we are a pull mode consumer, report on number of waiting requests. + if o.isPullMode() { + o.processWaiting(false) + info.NumWaiting = o.waiting.len() + } + // If we were asked to snapshot do so here. + if snap { + o.ici = info + } + sysc := o.sysc + o.mu.Unlock() + + // Do cluster. + if rg != nil { + info.Cluster = js.clusterInfo(rg) + } + + // If we have a reply subject send the response here. + if reply != _EMPTY_ && sysc != nil { + sysc.sendInternalMsg(reply, _EMPTY_, nil, info) + } + + return info +} + +// Will signal us that new messages are available. Will break out of waiting. +func (o *consumer) signalNewMessages() { + // Kick our new message channel + select { + case o.mch <- struct{}{}: + default: + } +} + +// shouldSample lets us know if we are sampling metrics on acks. +func (o *consumer) shouldSample() bool { + switch { + case o.sfreq <= 0: + return false + case o.sfreq >= 100: + return true + } + + // TODO(ripienaar) this is a tad slow so we need to rethink here, however this will only + // hit for those with sampling enabled and its not the default + return rand.Int32N(100) <= o.sfreq +} + +func (o *consumer) sampleAck(sseq, dseq, dc uint64) { + if !o.shouldSample() { + return + } + + now := time.Now().UTC() + unow := now.UnixNano() + + e := JSConsumerAckMetric{ + TypedEvent: TypedEvent{ + Type: JSConsumerAckMetricType, + ID: nuid.Next(), + Time: now, + }, + Stream: o.stream, + Consumer: o.name, + ConsumerSeq: dseq, + StreamSeq: sseq, + Delay: unow - o.pending[sseq].Timestamp, + Deliveries: dc, + Domain: o.srv.getOpts().JetStreamDomain, + } + + o.sendAdvisory(o.ackEventT, e) +} + +// Process an ACK. +// Returns `true` if the ack was processed in place and the sender can now respond +// to the client, or `false` if there was an error or the ack is replicated (in which +// case the reply will be sent later). +func (o *consumer) processAckMsg(sseq, dseq, dc uint64, reply string, doSample bool) bool { + return o.processAckMsgLocked(sseq, dseq, dc, reply, doSample, true) +} + +func (o *consumer) processAckMsgLocked(sseq, dseq, dc uint64, reply string, doSample bool, needLock bool) bool { + lock := func() { + if needLock { + o.mu.Lock() + } + } + unlock := func() { + if needLock { + o.mu.Unlock() + } + } + lock() + if o.closed { + unlock() + return false + } + + mset := o.mset + if mset == nil || mset.closed.Load() { + unlock() + return false + } + + // Check if this ack is above the current pointer to our next to deliver. + // Ignore if it's a flow-controlled consumer, its state could end up further ahead + // since its state is not replicated before delivery. + if sseq >= o.sseq && !o.cfg.FlowControl { + // Let's make sure this is valid. + // This is only received on the consumer leader, so should never be higher + // than the last stream sequence. But could happen if we've just become + // consumer leader, and we are not up-to-date on the stream yet. + var ss StreamState + mset.store.FastState(&ss) + if sseq > ss.LastSeq { + o.srv.Warnf("JetStream consumer '%s > %s > %s' ACK sequence %d past last stream sequence of %d", + o.acc.Name, o.stream, o.name, sseq, ss.LastSeq) + // FIXME(dlc) - For 2.11 onwards should we return an error here to the caller? + } + // Even though another leader must have delivered a message with this sequence, we must not adjust + // the current pointer. This could otherwise result in a stuck consumer, where messages below this + // sequence can't be redelivered, and we'll have incorrect pending state and ack floors. + unlock() + return false + } + + // Let the owning stream know if we are interest or workqueue retention based. + // If this consumer is clustered (o.node != nil) this will be handled by + // processReplicatedAck after the ack has propagated. + // If we're already holding the lock we can't ack in place, since that will + // violate lock ordering with respect to the stream. + ackInPlace := o.node == nil && o.retention != LimitsPolicy && needLock + + var ackAllSeqs []uint64 + var needSignal bool + + switch o.cfg.AckPolicy { + case AckExplicit: + if p, ok := o.pending[sseq]; ok { + if doSample { + o.sampleAck(sseq, dseq, dc) + } + if o.maxp > 0 && len(o.pending) >= o.maxp { + needSignal = true + } + delete(o.pending, sseq) + // Use the original deliver sequence from our pending record. + dseq = p.Sequence + o.moveAckFloor(dseq, sseq) + } + delete(o.rdc, sseq) + o.removeFromRedeliverQueue(sseq) + case AckAll, AckFlowControl: + // no-op + if dseq <= o.adflr || sseq <= o.asflr { + unlock() + // Return true to let caller respond back to the client. + return true + } + if o.maxp > 0 && len(o.pending) >= o.maxp { + needSignal = true + } + sgap := sseq - o.asflr + o.adflr, o.asflr = dseq, sseq + + // Only needed if we ack in place, otherwise we'd never collect anything. + // At most the pending entries below the ack, don't over-allocate. + if ackInPlace { + ackAllSeqs = make([]uint64, 0, min(uint64(len(o.pending)), sgap-1)) + } + + remove := func(seq uint64) { + // Only collect what was actually delivered to us. + if ackInPlace && seq != sseq { + if _, ok := o.pending[seq]; ok { + ackAllSeqs = append(ackAllSeqs, seq) + } + } + delete(o.pending, seq) + delete(o.rdc, seq) + o.removeFromRedeliverQueue(seq) + } + // Determine if smarter to walk all of pending vs the sequence range. + if sgap > uint64(len(o.pending)) { + for seq := range o.pending { + if seq <= sseq { + remove(seq) + } + } + } else { + for seq := sseq; seq > sseq-sgap && len(o.pending) > 0; seq-- { + remove(seq) + } + } + case AckNone: + // FIXME(dlc) - This is error but do we care? + unlock() + return ackInPlace + } + + // No ack replication, so we set reply to "" so that updateAcks does not + // send the reply. The caller will. + if ackInPlace { + reply = _EMPTY_ + } + // Update underlying store. + o.updateAcks(dseq, sseq, reply) + unlock() + + if ackInPlace { + mset.ackMsg(o, sseq) + for _, seq := range ackAllSeqs { + mset.ackMsg(o, seq) + } + } + + // If we had max ack pending set and were at limit we need to unblock ourselves. + if needSignal { + o.signalNewMessages() + } + return ackInPlace +} + +// Lock should be held. +func (o *consumer) moveAckFloor(dseq, sseq uint64) { + // Only move floors if we matched an existing pending. + if len(o.pending) == 0 { + o.adflr = o.dseq - 1 + o.asflr = o.sseq - 1 + } else if dseq == o.adflr+1 { + o.adflr, o.asflr = dseq, sseq + for ss := sseq + 1; ss < o.sseq; ss++ { + if p, ok := o.pending[ss]; ok { + if p.Sequence > 0 { + o.adflr, o.asflr = p.Sequence-1, ss-1 + } + break + } + } + } +} + +// Determine if this is a truly filtered consumer. Modern clients will place filtered subjects +// even if the stream only has a single non-wildcard subject designation. +// Read lock should be held. +func (o *consumer) isFiltered() bool { + if o.subjf == nil { + return false + } + // If we are here we want to check if the filtered subject is + // a direct match for our only listed subject. + mset := o.mset + if mset == nil { + return true + } + + // Protect access to mset.cfg with the cfgMu mutex. + mset.cfgMu.RLock() + msetSubjects := mset.cfg.Subjects + mset.cfgMu.RUnlock() + + // `isFiltered` need to be performant, so we do + // as any checks as possible to avoid unnecessary work. + // Here we avoid iteration over slices if there is only one subject in stream + // and one filter for the consumer. + if len(msetSubjects) == 1 && len(o.subjf) == 1 { + return msetSubjects[0] != o.subjf[0].subject + } + + // if the list is not equal length, we can return early, as this is filtered. + if len(msetSubjects) != len(o.subjf) { + return true + } + + // if in rare case scenario that user passed all stream subjects as consumer filters, + // we need to do a more expensive operation. + // reflect.DeepEqual would return false if the filters are the same, but in different order + // so it can't be used here. + cfilters := make(map[string]struct{}, len(o.subjf)) + for _, val := range o.subjf { + cfilters[val.subject] = struct{}{} + } + for _, val := range msetSubjects { + if _, ok := cfilters[val]; !ok { + return true + } + } + return false +} + +// Check if we need an ack for this store seq. +// This is called for interest based retention streams to remove messages. +func (o *consumer) needAck(sseq uint64, subj string) bool { + var needAck bool + var asflr, osseq uint64 + var pending map[uint64]*Pending + var rdc map[uint64]uint64 + + o.mu.RLock() + defer o.mu.RUnlock() + + isFiltered := o.isFiltered() + if isFiltered && o.mset == nil { + return false + } + + // Check if we are filtered, and if so check if this is even applicable to us. + if isFiltered { + if subj == _EMPTY_ { + var err error + if subj, err = o.mset.store.SubjectForSeq(sseq); err != nil { + return false + } + } + if !o.isFilteredMatch(subj) { + return false + } + } + if o.isLeader() { + asflr, osseq = o.asflr, o.sseq + pending, rdc = o.pending, o.rdc + } else { + if o.store == nil { + return false + } + state, err := o.store.BorrowState() + if err != nil || state == nil { + // Fall back to what we track internally for now. + return sseq > o.asflr && !o.isFiltered() + } + // If loading state as here, the osseq is +1. + asflr, osseq, pending, rdc = state.AckFloor.Stream, state.Delivered.Stream+1, state.Pending, state.Redelivered + } + + switch o.cfg.AckPolicy { + case AckNone, AckAll, AckFlowControl: + needAck = sseq > asflr + case AckExplicit: + if sseq > asflr { + if sseq >= osseq { + needAck = true + } else { + _, needAck = pending[sseq] + } + } + } + + // Finally check if redelivery of this message is tracked. + // If the message is not pending, it should be preserved if it reached max delivery. + if !needAck { + _, needAck = rdc[sseq] + } + + return needAck +} + +type PriorityGroup struct { + Group string `json:"group,omitempty"` + MinPending int64 `json:"min_pending,omitempty"` + MinAckPending int64 `json:"min_ack_pending,omitempty"` + Id string `json:"id,omitempty"` + Priority int `json:"priority,omitempty"` +} + +// Used in nextReqFromMsg, since the json.Unmarshal causes the request +// struct to escape to the heap always. This should reduce GC pressure. +var jsGetNextPool = sync.Pool{ + New: func() any { + return &JSApiConsumerGetNextRequest{} + }, +} + +// Helper for the next message requests. +func nextReqFromMsg(msg []byte) (time.Time, int, int, bool, time.Duration, time.Time, *PriorityGroup, error) { + req := bytes.TrimSpace(msg) + + switch { + case len(req) == 0: + return time.Time{}, 1, 0, false, 0, time.Time{}, nil, nil + + case req[0] == '{': + cr := jsGetNextPool.Get().(*JSApiConsumerGetNextRequest) + defer func() { + *cr = JSApiConsumerGetNextRequest{} + jsGetNextPool.Put(cr) + }() + if err := json.Unmarshal(req, &cr); err != nil { + return time.Time{}, -1, 0, false, 0, time.Time{}, nil, err + } + var hbt time.Time + if cr.Heartbeat > 0 { + if cr.Heartbeat*2 > cr.Expires { + return time.Time{}, 1, 0, false, 0, time.Time{}, nil, errors.New("heartbeat value too large") + } + hbt = time.Now().Add(cr.Heartbeat) + } + priorityGroup := cr.PriorityGroup + if cr.Expires == time.Duration(0) { + return time.Time{}, cr.Batch, cr.MaxBytes, cr.NoWait, cr.Heartbeat, hbt, &priorityGroup, nil + } + return time.Now().Add(cr.Expires), cr.Batch, cr.MaxBytes, cr.NoWait, cr.Heartbeat, hbt, &priorityGroup, nil + default: + if n, err := strconv.Atoi(string(req)); err == nil { + return time.Time{}, n, 0, false, 0, time.Time{}, nil, nil + } + } + + return time.Time{}, 1, 0, false, 0, time.Time{}, nil, nil +} + +// Represents a request that is on the internal waiting queue +type waitingRequest struct { + next *waitingRequest + acc *Account + interest string + reply string + n int // For batching + d int // num delivered + b int // For max bytes tracking + expires time.Time + received time.Time + hb time.Duration + hbt time.Time + noWait bool + priorityGroup *PriorityGroup +} + +// sync.Pool for waiting requests. +var wrPool = sync.Pool{ + New: func() any { + return new(waitingRequest) + }, +} + +// Recycle this request. This request can not be accessed after this call. +func (wr *waitingRequest) recycleIfDone() bool { + if wr != nil && wr.n <= 0 { + wr.recycle() + return true + } + return false +} + +// Force a recycle. +func (wr *waitingRequest) recycle() { + if wr != nil { + wr.next, wr.acc, wr.interest, wr.reply = nil, nil, _EMPTY_, _EMPTY_ + wrPool.Put(wr) + } +} + +// Represents an (optional) request timeout that's sent after waiting for replicated deliveries. +type waitingDelivery struct { + seq uint64 + pn int // Pending messages. + pb int // Pending bytes. +} + +// sync.Pool for waiting deliveries. +var wdPool = sync.Pool{ + New: func() any { + return new(waitingDelivery) + }, +} + +// Force a recycle. +func (wd *waitingDelivery) recycle() { + if wd != nil { + wd.seq, wd.pn, wd.pb = 0, 0, 0 + wdPool.Put(wd) + } +} + +// waiting queue for requests that are waiting for new messages to arrive. +type waitQueue struct { + n, max int + last time.Time + head *waitingRequest + tail *waitingRequest +} + +// Create a new ring buffer with at most max items. +func newWaitQueue(max int) *waitQueue { + return &waitQueue{max: max} +} + +var ( + errWaitQueueFull = errors.New("wait queue is full") + errWaitQueueNil = errors.New("wait queue is nil") +) + +// insertSorted inserts wr at the correct position based on priority +func (wq *waitQueue) insertSorted(wr *waitingRequest) { + + // Handle empty queue + if wq.head == nil { + wq.head = wr + wq.tail = wr + wr.next = nil + return + } + insertAtPosition(wr, wq) +} + +func (wq *waitQueue) addPrioritized(wr *waitingRequest) error { + if wq == nil { + return errWaitQueueNil + } + if wq.isFull() { + return errWaitQueueFull + } + + wq.insertSorted(wr) + wq.n++ + wq.last = wr.received + return nil +} + +// Adds in a new request. +func (wq *waitQueue) add(wr *waitingRequest) error { + if wq == nil { + return errWaitQueueNil + } + if wq.isFull() { + return errWaitQueueFull + } + if wq.head == nil { + wq.head = wr + } else { + wq.tail.next = wr + } + // Always set tail. + wq.tail = wr + // Make sure nil + wr.next = nil + + // Track last active via when we receive a request. + wq.last = wr.received + wq.n++ + return nil +} + +func (wq *waitQueue) isFull() bool { + if wq == nil { + return false + } + return wq.n == wq.max +} + +func (wq *waitQueue) isEmpty() bool { + if wq == nil { + return true + } + return wq.n == 0 +} + +func (wq *waitQueue) len() int { + if wq == nil { + return 0 + } + return wq.n +} + +// Peek will return the next request waiting or nil if empty. +func (wq *waitQueue) peek() *waitingRequest { + if wq == nil { + return nil + } + return wq.head +} + +func (wq *waitQueue) cycle() { + wr := wq.peek() + if wr != nil { + // Always remove current now on a pop, and move to end if still valid. + // If we were the only one don't need to remove since this can be a no-op. + wq.removeCurrent() + wq.add(wr) + } +} + +func (wq *waitQueue) popOrPopAndRequeue(priority PriorityPolicy) *waitingRequest { + if wq == nil || wq.head == nil { + return nil + } + + if priority == PriorityPrioritized { + return wq.popAndRequeue() + } + return wq.pop() +} + +// pop will return the next request and move the read cursor. +// This will now place a request that still has pending items at the ends of the list. +func (wq *waitQueue) pop() *waitingRequest { + wr := wq.peek() + if wr != nil { + wr.d++ + wr.n-- + // Always remove current now on a pop, and move to end if still valid. + // If we were the only one don't need to remove since this can be a no-op. + if wr.n > 0 && wq.n > 1 { + wq.removeCurrent() + wq.add(wr) + } else if wr.n <= 0 { + wq.removeCurrent() + } + } + return wr +} + +// popAndRequeue pops the head element and requeues it at the end of its priority group. +// This maintains FIFO order within the same priority level. +func (wq *waitQueue) popAndRequeue() *waitingRequest { + if wq == nil || wq.head == nil { + return nil + } + + // Save the head + wr := wq.head + + if wr == nil { + return wr + } + + wr.d++ + wr.n-- + + if wr.n > 0 && wq.n > 1 { + if wr.next == nil { + return wr + } + wq.head = wq.head.next + wr.next = nil + + insertAtPosition(wr, wq) + + } else if wr.n <= 0 { + wq.removeCurrent() + return wr + + } + + return wr +} + +func insertAtPosition(wr *waitingRequest, wq *waitQueue) { + priority := math.MaxInt32 + if wr.priorityGroup != nil { + priority = wr.priorityGroup.Priority + } + + var prev *waitingRequest + current := wq.head + for current != nil { + currentPriority := math.MaxInt32 + if current.priorityGroup != nil { + currentPriority = current.priorityGroup.Priority + } + if currentPriority > priority { + break + } + prev = current + current = current.next + } + + if prev == nil { + // All remaining elements have higher priority + wr.next = wq.head + wq.head = wr + } else { + wr.next = prev.next + prev.next = wr + if wr.next == nil { + wq.tail = wr + } + } +} + +// Removes the current read pointer (head FIFO) entry. +func (wq *waitQueue) removeCurrent() { + wq.remove(nil, wq.head) +} + +// Remove the wr element from the wait queue. +func (wq *waitQueue) remove(pre, wr *waitingRequest) { + if wr == nil { + return + } + if pre != nil { + pre.next = wr.next + } else if wr == wq.head { + // We are removing head here. + wq.head = wr.next + } + // Check if wr was our tail. + if wr == wq.tail { + // Check if we need to assign to pre. + if wr.next == nil { + wq.tail = pre + } else { + wq.tail = wr.next + } + } + wq.n-- +} + +// Return the map of pending requests keyed by the reply subject. +// No-op if push consumer or invalid etc. +func (o *consumer) pendingRequests() map[string]*waitingRequest { + if o.waiting == nil { + return nil + } + wq, m := o.waiting, make(map[string]*waitingRequest) + for wr := wq.head; wr != nil; wr = wr.next { + m[wr.reply] = wr + } + + return m +} + +func (o *consumer) setPinnedTimer(priorityGroup string) { + if o.pinnedTtl != nil { + o.pinnedTtl.Reset(o.cfg.PinnedTTL) + } else { + o.pinnedTtl = time.AfterFunc(o.cfg.PinnedTTL, func() { + o.mu.Lock() + // Skip if already unset. + if o.currentPinId == _EMPTY_ { + o.mu.Unlock() + return + } + o.unassignPinId() + o.sendUnpinnedAdvisoryLocked(priorityGroup, "timeout") + o.mu.Unlock() + o.signalNewMessages() + }) + } +} + +// Lock should be held. +func (o *consumer) assignNewPinId(wr *waitingRequest) { + if wr.priorityGroup == nil || wr.priorityGroup.Group == _EMPTY_ { + return + } + o.currentPinId = nuid.Next() + o.pinnedTS = time.Now().UTC() + wr.priorityGroup.Id = o.currentPinId + o.setPinnedTimer(wr.priorityGroup.Group) + o.sendPinnedAdvisoryLocked(wr.priorityGroup.Group) +} + +// Lock should be held. +func (o *consumer) unassignPinId() { + o.currentPinId = _EMPTY_ + o.pinnedTS = time.Time{} + if o.pinnedTtl != nil { + o.pinnedTtl.Stop() + o.pinnedTtl = nil + } +} + +// Return next waiting request. This will check for expirations but not noWait or interest. +// That will be handled by processWaiting. +// Lock should be held. +func (o *consumer) nextWaiting(sz int) *waitingRequest { + if o.waiting == nil || o.waiting.isEmpty() { + return nil + } + + // Check if server needs to assign a new pin id. + needNewPin := o.currentPinId == _EMPTY_ && o.cfg.PriorityPolicy == PriorityPinnedClient + + numCycled := 0 + for wr := o.waiting.peek(); !o.waiting.isEmpty(); wr = o.waiting.peek() { + if wr == nil { + break + } + + if wr.expires.IsZero() || time.Now().Before(wr.expires) { + // Track whether this iteration just claimed a new pin for this request. + assignedPin := false + if needNewPin { + if wr.priorityGroup == nil || wr.priorityGroup.Id == _EMPTY_ { + o.assignNewPinId(wr) + assignedPin = true + } else { + // There is pin id set, but not a matching one. Send a notification to the client and remove the request. + // Probably this is the old pin id. + hdr := fmt.Appendf(nil, "NATS/1.0 423 Nats-Wrong-Pin-Id\r\n%s: %d\r\n%s: %d\r\n\r\n", JSPullRequestPendingMsgs, wr.n, JSPullRequestPendingBytes, wr.b) + o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + o.waiting.removeCurrent() + if o.node != nil { + o.removeClusterPendingRequest(wr.reply) + } + wr.recycle() + continue + } + } else if o.currentPinId != _EMPTY_ { + // Check if we have a match on the currentNuid + if wr.priorityGroup != nil && wr.priorityGroup.Id == o.currentPinId { + // If we have a match, we do nothing here and will deliver the message later down the code path. + } else if wr.priorityGroup == nil || wr.priorityGroup.Id == _EMPTY_ { + o.waiting.cycle() + numCycled++ + if numCycled >= o.waiting.len() { + return nil + } + continue + } else { + // There is pin id set, but not a matching one. Send a notification to the client and remove the request. + hdr := fmt.Appendf(nil, "NATS/1.0 423 Nats-Wrong-Pin-Id\r\n%s: %d\r\n%s: %d\r\n\r\n", JSPullRequestPendingMsgs, wr.n, JSPullRequestPendingBytes, wr.b) + o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + o.waiting.removeCurrent() + if o.node != nil { + o.removeClusterPendingRequest(wr.reply) + } + wr.recycle() + continue + } + } + + if o.cfg.PriorityPolicy == PriorityOverflow { + if wr.priorityGroup != nil && + // If both limits are zero we don't cycle and the request will be fulfilled. + (wr.priorityGroup.MinPending > 0 || wr.priorityGroup.MinAckPending > 0) && + // We need to check o.npc+1, because before calling nextWaiting, we do o.npc-- + // If one OR the other limit is exceeded, we want to fulfill the request. + // This is an inverted check. For clarity, we check the positive condition and negate. + !((wr.priorityGroup.MinPending > 0 && wr.priorityGroup.MinPending <= o.npc+1) || + (wr.priorityGroup.MinAckPending > 0 && wr.priorityGroup.MinAckPending <= int64(len(o.pending)))) { + o.waiting.cycle() + numCycled++ + // We're done cycling through the requests. + if numCycled >= o.waiting.len() { + return nil + } + continue + } + } + // Check if we have max bytes set. + if wr.b > 0 { + if sz <= wr.b { + wr.b -= sz + // If we are right now at zero, set batch to 1 to deliver this one but stop after. + if wr.b == 0 { + wr.n = 1 + } + } else { + // Since we can't send that message to the requestor, we need to + // notify that we are closing the request. + const maxBytesT = "NATS/1.0 409 Message Size Exceeds MaxBytes\r\n%s: %d\r\n%s: %d\r\n\r\n" + hdr := fmt.Appendf(nil, maxBytesT, JSPullRequestPendingMsgs, wr.n, JSPullRequestPendingBytes, wr.b) + o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + // If we just claimed the pin for this request, release it. + if assignedPin { + o.unassignPinId() + } + // Remove the current one, no longer valid due to max bytes limit. + o.waiting.removeCurrent() + if o.node != nil { + o.removeClusterPendingRequest(wr.reply) + } + wr.recycle() + continue + } + } + + if wr.acc.sl.HasInterest(wr.interest) { + return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy) + } else if time.Since(wr.received) < defaultGatewayRecentSubExpiration && (o.srv.leafNodeEnabled || o.srv.gateway.enabled) { + return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy) + } else if o.srv.gateway.enabled && o.srv.hasGatewayInterest(wr.acc.Name, wr.interest) { + return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy) + } + } else { + // We do check for expiration in `processWaiting`, but it is possible to hit the expiry here, and not there. + rdWait := o.replicateDeliveries() + if rdWait { + // Check if we need to send the timeout after pending replicated deliveries, or can do so immediately. + if wd, ok := o.waitingDeliveries[wr.reply]; !ok { + rdWait = false + } else { + wd.pn, wd.pb = wr.n, wr.b + } + } + if !rdWait { + hdr := fmt.Appendf(nil, "NATS/1.0 408 Request Timeout\r\n%s: %d\r\n%s: %d\r\n\r\n", JSPullRequestPendingMsgs, wr.n, JSPullRequestPendingBytes, wr.b) + o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + } + // Expiring a request counts as activity for the inactive threshold. + if wr.expires.After(o.waiting.last) { + o.waiting.last = wr.expires + } + o.waiting.removeCurrent() + if o.node != nil { + o.removeClusterPendingRequest(wr.reply) + } + wr.recycle() + continue + + } + if wr.interest != wr.reply { + const intExpT = "NATS/1.0 408 Interest Expired\r\n%s: %d\r\n%s: %d\r\n\r\n" + hdr := fmt.Appendf(nil, intExpT, JSPullRequestPendingMsgs, wr.n, JSPullRequestPendingBytes, wr.b) + o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + } + // Remove the current one, no longer valid. + o.waiting.removeCurrent() + if o.node != nil { + o.removeClusterPendingRequest(wr.reply) + } + wr.recycle() + } + + return nil +} + +// Next message request. +type nextMsgReq struct { + reply string + msg []byte +} + +var nextMsgReqPool sync.Pool + +func newNextMsgReq(reply string, msg []byte) *nextMsgReq { + var nmr *nextMsgReq + m := nextMsgReqPool.Get() + if m != nil { + nmr = m.(*nextMsgReq) + } else { + nmr = &nextMsgReq{} + } + // 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. + (*nmr) = nextMsgReq{reply, msg} + return nmr +} + +func (nmr *nextMsgReq) returnToPool() { + if nmr == nil { + return + } + nmr.reply, nmr.msg = _EMPTY_, nil + nextMsgReqPool.Put(nmr) +} + +// processNextMsgReq will process a request for the next message available. A nil message payload means deliver +// a single message. If the payload is a formal request or a number parseable with Atoi(), then we will send a +// batch of messages without requiring another request to this endpoint, or an ACK. +func (o *consumer) processNextMsgReq(_ *subscription, c *client, _ *Account, _, reply string, rmsg []byte) { + if reply == _EMPTY_ { + return + } + + // Short circuit error here. + if o.nextMsgReqs == nil { + hdr := []byte("NATS/1.0 409 Consumer is push based\r\n\r\n") + o.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + return + } + + hdr, msg := c.msgParts(rmsg) + if errorOnRequiredApiLevel(hdr) { + hdr = []byte("NATS/1.0 412 Required Api Level\r\n\r\n") + o.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + return + } + o.nextMsgReqs.push(newNextMsgReq(reply, copyBytes(msg))) +} + +// processResetReq will reset a consumer to a new starting sequence. +func (o *consumer) processResetReq(_ *subscription, c *client, a *Account, _, reply string, rmsg []byte) { + if reply == _EMPTY_ { + return + } + + s := o.srv + var resp = JSApiConsumerResetResponse{ApiResponse: ApiResponse{Type: JSApiConsumerResetResponseType}} + + hdr, msg := c.msgParts(rmsg) + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendInternalAccountMsg(a, reply, s.jsonResponse(&resp)) + return + } + + // If the user provided an expected stream identity. + // We don't reject the request, we only populate the identity in the response. + var streamIdentity string + if sliceHeader(JSStreamIdentity, hdr) != nil { + streamIdentity = o.streamIdentity() + } + + // An empty message resets back to the ack floor, otherwise a custom sequence can be used. + var req JSApiConsumerResetRequest + if len(msg) > 0 { + if err := json.Unmarshal(msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendInternalAccountMsg(a, reply, s.jsonResponse(&resp)) + return + } + } + resetSeq, canRespond, err := o.resetStartingSeq(req.Seq, reply, false, streamIdentity != _EMPTY_) + if err != nil { + resp.Error = NewJSConsumerInvalidResetError(err) + s.sendInternalAccountMsg(a, reply, s.jsonResponse(&resp)) + } else if canRespond { + if resp.ConsumerInfo = setDynamicConsumerInfoMetadata(o.info()); resp.ConsumerInfo == nil { + // The consumer was closed before we could respond. + resp.Error = NewJSConsumerInvalidResetError(errConsumerClosed) + s.sendInternalAccountMsg(a, reply, s.jsonResponse(&resp)) + return + } + resp.ResetSeq = resetSeq + if streamIdentity != _EMPTY_ { + rhdr := genHeader(nil, JSStreamIdentity, streamIdentity) + s.sendInternalAccountMsgWithReply(a, reply, _EMPTY_, rhdr, s.jsonResponse(&resp), false) + } else { + s.sendInternalAccountMsg(a, reply, s.jsonResponse(&resp)) + } + } +} + +func (o *consumer) processNextMsgRequest(reply string, msg []byte) { + o.mu.Lock() + defer o.mu.Unlock() + + mset := o.mset + if mset == nil { + return + } + + sendErr := func(status int, description string) { + hdr := fmt.Appendf(nil, "NATS/1.0 %d %s\r\n\r\n", status, description) + o.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + } + + if o.isPushMode() || o.waiting == nil { + sendErr(409, "Consumer is push based") + return + } + + // Check payload here to see if they sent in batch size or a formal request. + expires, batchSize, maxBytes, noWait, hb, hbt, priorityGroup, err := nextReqFromMsg(msg) + if err != nil { + sendErr(400, fmt.Sprintf("Bad Request - %v", err)) + return + } + + // Check for request limits + if o.cfg.MaxRequestBatch > 0 && batchSize > o.cfg.MaxRequestBatch { + sendErr(409, fmt.Sprintf("Exceeded MaxRequestBatch of %d", o.cfg.MaxRequestBatch)) + return + } + + if !expires.IsZero() && o.cfg.MaxRequestExpires > 0 && expires.After(time.Now().Add(o.cfg.MaxRequestExpires)) { + sendErr(409, fmt.Sprintf("Exceeded MaxRequestExpires of %v", o.cfg.MaxRequestExpires)) + return + } + + if maxBytes > 0 && o.cfg.MaxRequestMaxBytes > 0 && maxBytes > o.cfg.MaxRequestMaxBytes { + sendErr(409, fmt.Sprintf("Exceeded MaxRequestMaxBytes of %v", o.cfg.MaxRequestMaxBytes)) + return + } + + if priorityGroup != nil { + if (priorityGroup.MinPending != 0 || priorityGroup.MinAckPending != 0) && o.cfg.PriorityPolicy != PriorityOverflow { + sendErr(400, "Bad Request - Not a Overflow Priority consumer") + return + } + if priorityGroup.Id != _EMPTY_ && o.cfg.PriorityPolicy != PriorityPinnedClient { + sendErr(400, "Bad Request - Not a Pinned Client Priority consumer") + return + } + if priorityGroup.Priority < 0 || priorityGroup.Priority > 9 { + sendErr(400, "Bad Request - Priority must be between 0 and 9") + return + } + } + + if priorityGroup != nil && o.cfg.PriorityPolicy != PriorityNone { + if priorityGroup.Group == _EMPTY_ { + sendErr(400, "Bad Request - Priority Group missing") + return + } + if !slices.Contains(o.cfg.PriorityGroups, priorityGroup.Group) { + sendErr(400, "Bad Request - Invalid Priority Group") + return + } + + if o.currentPinId != _EMPTY_ { + if priorityGroup.Id == o.currentPinId { + o.setPinnedTimer(priorityGroup.Group) + } else if priorityGroup.Id != _EMPTY_ { + sendErr(423, "Nats-Pin-Id mismatch") + return + } + } + } + + // If we have the max number of requests already pending try to expire. + if o.waiting.isFull() { + // Try to expire some of the requests. + // We do not want to push too hard here so at maximum process once per sec. + if time.Since(o.lwqic) > time.Second { + o.processWaiting(false) + } + } + + // If the request is for noWait and we have pending requests already, check if we have room. + if noWait { + msgsPending := o.numPending() + uint64(len(o.rdq)) + // If no pending at all, decide what to do with request. + // If no expires was set then fail. + if msgsPending == 0 && expires.IsZero() { + o.waiting.last = time.Now() + sendErr(404, "No Messages") + return + } + if msgsPending > 0 { + _, _, batchPending, _ := o.processWaiting(false) + if msgsPending < uint64(batchPending) { + o.waiting.last = time.Now() + sendErr(408, "Requests Pending") + return + } + } + // If we are here this should be considered a one-shot situation. + // We will wait for expires but will return as soon as we have any messages. + } + + // If we receive this request though an account export, we need to track that interest subject and account. + acc, interest := trackDownAccountAndInterest(o.acc, reply) + + // Create a waiting request. + wr := wrPool.Get().(*waitingRequest) + wr.acc, wr.interest, wr.reply, wr.n, wr.d, wr.noWait, wr.expires, wr.hb, wr.hbt, wr.priorityGroup = acc, interest, reply, batchSize, 0, noWait, expires, hb, hbt, priorityGroup + wr.b = maxBytes + wr.received = time.Now() + + if o.cfg.PriorityPolicy == PriorityPrioritized { + if err := o.waiting.addPrioritized(wr); err != nil { + if hb == 0 { + sendErr(409, "Exceeded MaxWaiting") + } + wr.recycle() + return + } + } else { + if err := o.waiting.add(wr); err != nil { + // If the client has a heartbeat interval set, don't bother responding with a 409, + // otherwise we can end up in a hot loop with the client re-requesting instead of + // waiting for the missing heartbeats instead and retrying. + if hb == 0 { + sendErr(409, "Exceeded MaxWaiting") + } + wr.recycle() + return + } + } + o.signalNewMessages() + // If we are clustered update our followers about this request. + if o.node != nil { + o.addClusterPendingRequest(wr.reply) + } +} + +func trackDownAccountAndInterest(acc *Account, interest string) (*Account, string) { + for strings.HasPrefix(interest, replyPrefix) { + oa := acc + oa.mu.RLock() + if oa.exports.responses == nil { + oa.mu.RUnlock() + break + } + si := oa.exports.responses[interest] + if si == nil { + oa.mu.RUnlock() + break + } + acc, interest = si.acc, si.to + oa.mu.RUnlock() + } + return acc, interest +} + +// Return current delivery count for a given sequence. +func (o *consumer) deliveryCount(seq uint64) uint64 { + if o.rdc == nil { + return 1 + } + if dc := o.rdc[seq]; dc >= 1 { + return dc + } + return 1 +} + +// Increase the delivery count for this message. +// ONLY used on redelivery semantics. +// Lock should be held. +func (o *consumer) incDeliveryCount(sseq uint64) uint64 { + if o.rdc == nil { + o.rdc = make(map[uint64]uint64) + } + o.rdc[sseq] += 1 + return o.rdc[sseq] + 1 +} + +// Used if we have to adjust on failed delivery or bad lookups. +// Those failed attempts should not increase deliver count. +// Lock should be held. +func (o *consumer) decDeliveryCount(sseq uint64) { + if o.rdc == nil { + return + } + dc, ok := o.rdc[sseq] + if !ok { + return + } + // Mirror incDeliveryCount, which goes from "no entry" to 1 on the first + // redelivery. Going back to the initial delivery therefore has to remove + // the entry rather than leave a zero behind, since a number of call sites + // (needAck, decStreamPending) test for key presence rather than value. + if dc > 1 { + o.rdc[sseq] = dc - 1 + } else { + delete(o.rdc, sseq) + } +} + +// send a delivery exceeded advisory. +func (o *consumer) notifyDeliveryExceeded(sseq, dc uint64) { + e := JSConsumerDeliveryExceededAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerDeliveryExceededAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: o.stream, + Consumer: o.name, + StreamSeq: sseq, + Deliveries: dc, + Domain: o.srv.getOpts().JetStreamDomain, + } + + o.sendAdvisory(o.deliveryExcEventT, e) +} + +// Check if the candidate subject matches a filter if its present. +// Lock should be held. +func (o *consumer) isFilteredMatch(subj string) bool { + // No filter is automatic match. + if o.subjf == nil { + return true + } + for _, filter := range o.subjf { + if !filter.hasWildcard && subj == filter.subject { + return true + } + } + // It's quicker to first check for non-wildcard filters, then + // iterate again to check for subset match. + tsa := [32]string{} + tts := tokenizeSubjectIntoSlice(tsa[:0], subj) + for _, filter := range o.subjf { + if isSubsetMatchTokenized(tts, filter.tokenizedSubject) { + return true + } + } + return false +} + +// Check if the candidate filter subject is equal to or a subset match +// of one of the filter subjects. +// Lock should be held. +func (o *consumer) isEqualOrSubsetMatch(subj string) bool { + for _, filter := range o.subjf { + if !filter.hasWildcard && subj == filter.subject { + return true + } + } + tsa := [32]string{} + tts := tokenizeSubjectIntoSlice(tsa[:0], subj) + for _, filter := range o.subjf { + if isSubsetMatchTokenized(filter.tokenizedSubject, tts) { + return true + } + } + return false +} + +// Check if all consumer filter subjects are subsets of the candidate subject. +// Lock should be held. +func (o *consumer) isFilterSubsetOf(subj string) bool { + if len(o.subjf) == 0 { + return false + } + tsa := [32]string{} + tts := tokenizeSubjectIntoSlice(tsa[:0], subj) + for _, filter := range o.subjf { + if !isSubsetMatchTokenized(filter.tokenizedSubject, tts) { + return false + } + } + return true +} + +var ( + errMaxAckPending = errors.New("max ack pending reached") + errBadConsumer = errors.New("consumer not valid") + errNoInterest = errors.New("consumer requires interest for delivery subject when ephemeral") +) + +// Get next available message from underlying store. +// Is partition aware and redeliver aware. +// Lock should be held. +func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) { + if o.mset == nil || o.mset.store == nil { + return nil, 0, errBadConsumer + } + // Process redelivered messages before looking at possibly "skip list" (deliver last per subject) + if o.hasRedeliveries() { + var seq, dc uint64 + for seq = o.getNextToRedeliver(); seq > 0; seq = o.getNextToRedeliver() { + dc = o.incDeliveryCount(seq) + if o.maxdc > 0 && dc > o.maxdc { + // Only send once + if dc == o.maxdc+1 { + o.notifyDeliveryExceeded(seq, dc-1) + } + // Make sure to remove from pending. + if p, ok := o.pending[seq]; ok && p != nil { + delete(o.pending, seq) + // The delivery count has already been incremented once. + o.updateDelivered(p.Sequence, seq, dc, p.Timestamp) + o.moveAckFloor(p.Sequence, seq) + } + continue + } + pmsg := getJSPubMsgFromPool() + sm, err := o.mset.store.LoadMsg(seq, &pmsg.StoreMsg) + if sm == nil || err != nil { + pmsg.returnToPool() + pmsg = nil + // Adjust back deliver count. + o.decDeliveryCount(seq) + } + // Message was scheduled for redelivery but was removed in the meantime. + if err == ErrStoreMsgNotFound || err == errDeletedMsg { + // This is a race condition where the message is still in o.pending and + // scheduled for redelivery, but it has been removed from the stream. + // o.processTerm is called in a goroutine so would normally run. However, + // if we get here this likely didn't fire, or we are replicated and changed leaders. + // That will correct the pending state and delivery/ack floors, so just skip here. + pmsg.returnToPool() + pmsg = nil + if p, ok := o.pending[seq]; ok { + o.processTermLocked(seq, p.Sequence, dc-1, ackTermUnackedLimitsReason, _EMPTY_, false) + } + continue + } + return pmsg, dc, err + } + } + + // Check if we have max pending. + if o.maxp > 0 && len(o.pending) >= o.maxp { + // maxp only set when ack policy != AckNone and user set MaxAckPending + // Stall if we have hit max pending. + return nil, 0, errMaxAckPending + } + + if o.hasSkipListPending() { + seq := o.lss.seqs[0] + if len(o.lss.seqs) == 1 { + o.sseq = o.lss.resume + o.lss = nil + o.updateSkipped(o.sseq) + } else { + o.lss.seqs = o.lss.seqs[1:] + o.sseq = seq + } + pmsg := getJSPubMsgFromPool() + sm, err := o.mset.store.LoadMsg(seq, &pmsg.StoreMsg) + if sm == nil || err != nil { + pmsg.returnToPool() + pmsg = nil + } + o.sseq++ + return pmsg, 1, err + } + + var sseq uint64 + var err error + var sm *StoreMsg + var pmsg = getJSPubMsgFromPool() + + // Grab next message applicable to us. + filters, subjf, fseq := o.filters, o.subjf, o.sseq + // Check if we are multi-filtered or not. + if filters != nil { + sm, sseq, err = o.mset.store.LoadNextMsgMulti(filters, fseq, &pmsg.StoreMsg) + } else if len(subjf) > 0 { // Means single filtered subject since o.filters means > 1. + filter, wc := subjf[0].subject, subjf[0].hasWildcard + sm, sseq, err = o.mset.store.LoadNextMsg(filter, wc, fseq, &pmsg.StoreMsg) + } else { + // No filter here. + sm, sseq, err = o.mset.store.LoadNextMsg(_EMPTY_, false, fseq, &pmsg.StoreMsg) + } + if sm == nil { + pmsg.returnToPool() + pmsg = nil + } + // Check if we should move our o.sseq. + if sseq >= o.sseq { + // If we are moving step by step then sseq == o.sseq. + // If we have jumped we should update skipped for other replicas. + if sseq != o.sseq && err == ErrStoreEOF { + o.updateSkipped(sseq + 1) + } + o.sseq = sseq + 1 + } + return pmsg, 1, err +} + +// Will check for expiration and lack of interest on waiting requests. +// Will also do any heartbeats and return the next expiration or HB interval. +func (o *consumer) processWaiting(eos bool) (int, int, int, time.Time) { + var fexp time.Time + if o.srv == nil || o.waiting.isEmpty() { + return 0, 0, 0, fexp + } + // Mark our last check time. + o.lwqic = time.Now() + + var expired, brp int + s, now := o.srv, time.Now() + + wq := o.waiting + remove := func(pre, wr *waitingRequest) *waitingRequest { + expired++ + if o.node != nil { + o.removeClusterPendingRequest(wr.reply) + } + next := wr.next + wq.remove(pre, wr) + wr.recycle() + return next + } + + var pre *waitingRequest + for wr := wq.head; wr != nil; { + // Check expiration. + expires := !wr.expires.IsZero() && now.After(wr.expires) + if (eos && wr.noWait) || expires { + rdWait := o.replicateDeliveries() + if rdWait { + // Check if we need to send the timeout after pending replicated deliveries, or can do so immediately. + if wd, ok := o.waitingDeliveries[wr.reply]; !ok { + rdWait = false + } else { + wd.pn, wd.pb = wr.n, wr.b + } + // If we still need to wait for replicated deliveries, remove from waiting list. + if rdWait { + wr = remove(pre, wr) + continue + } + } + // Normally it's a timeout. + if expires { + hdr := fmt.Appendf(nil, "NATS/1.0 408 Request Timeout\r\n%s: %d\r\n%s: %d\r\n\r\n", JSPullRequestPendingMsgs, wr.n, JSPullRequestPendingBytes, wr.b) + o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + if wr.expires.After(wq.last) { + wq.last = wr.expires + } + wr = remove(pre, wr) + continue + } else if wr.expires.IsZero() || wr.d > 0 { + // But if we're NoWait without expiry, we've reached the end of the stream, and we've not delivered any messages. + // Return no messages instead, which is the same as if we'd rejected the pull request initially. + hdr := fmt.Appendf(nil, "NATS/1.0 404 No Messages\r\n\r\n") + o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + if now.After(wq.last) { + wq.last = now + } + wr = remove(pre, wr) + continue + } + } + // Now check interest. + interest := wr.acc.sl.HasInterest(wr.interest) + if !interest && (s.leafNodeEnabled || s.gateway.enabled) { + // If we are here check on gateways and leaf nodes (as they can mask gateways on the other end). + // If we have interest or the request is too young break and do not expire. + if time.Since(wr.received) < defaultGatewayRecentSubExpiration { + interest = true + } else if s.gateway.enabled && s.hasGatewayInterest(wr.acc.Name, wr.interest) { + interest = true + } + } + // Check if we have interest. + if !interest { + // No more interest here so go ahead and remove this one from our list. + wr = remove(pre, wr) + continue + } + + // If interest, update batch pending requests counter and update fexp timer. + brp += wr.n + if !wr.hbt.IsZero() { + if now.After(wr.hbt) { + // Fire off a heartbeat here. + o.sendIdleHeartbeat(wr.reply) + // Update next HB. + wr.hbt = now.Add(wr.hb) + } + if fexp.IsZero() || wr.hbt.Before(fexp) { + fexp = wr.hbt + } + } + if !wr.expires.IsZero() && (fexp.IsZero() || wr.expires.Before(fexp)) { + fexp = wr.expires + } + // Update pre and wr here. + pre = wr + wr = wr.next + } + + return expired, wq.len(), brp, fexp +} + +// Will check to make sure those waiting still have registered interest. +func (o *consumer) checkWaitingForInterest() bool { + o.processWaiting(true) + return o.waiting.len() > 0 +} + +// Lock should be held. +func (o *consumer) hbTimer() (time.Duration, *time.Timer) { + if o.cfg.Heartbeat == 0 { + return 0, nil + } + return o.cfg.Heartbeat, time.NewTimer(o.cfg.Heartbeat) +} + +// Check here for conditions when our ack floor may have drifted below the streams first sequence. +// In general this is accounted for in normal operations, but if the consumer misses the signal from +// the stream it will not clear the message and move the ack state. +// Should only be called from consumer leader. +func (o *consumer) checkAckFloor() { + o.mu.RLock() + mset, closed, asflr, numPending := o.mset, o.closed, o.asflr, len(o.pending) + o.mu.RUnlock() + + if asflr == 0 || closed || mset == nil { + return + } + + var ss StreamState + mset.store.FastState(&ss) + + // If our floor is equal or greater that is normal and nothing for us to do. + if ss.FirstSeq == 0 || asflr >= ss.FirstSeq-1 { + return + } + + // Check which linear space is less to walk. + if ss.FirstSeq-asflr-1 < uint64(numPending) { + // Process all messages that no longer exist. + for seq := asflr + 1; seq < ss.FirstSeq; seq++ { + // Check if this message was pending. + o.mu.RLock() + p, isPending := o.pending[seq] + rdc := o.deliveryCount(seq) + o.mu.RUnlock() + // If it was pending for us, get rid of it. + if isPending { + o.processTerm(seq, p.Sequence, rdc, ackTermLimitsReason, _EMPTY_) + } + } + } else if numPending > 0 { + // here it is shorter to walk pending. + // toTerm is seq, dseq, rcd for each entry. + toTerm := make([]uint64, 0, numPending*3) + o.mu.RLock() + for seq, p := range o.pending { + if seq < ss.FirstSeq { + var dseq uint64 = 1 + if p != nil { + dseq = p.Sequence + } + rdc := o.deliveryCount(seq) + toTerm = append(toTerm, seq, dseq, rdc) + } + } + o.mu.RUnlock() + + for i := 0; i < len(toTerm); i += 3 { + seq, dseq, rdc := toTerm[i], toTerm[i+1], toTerm[i+2] + o.processTerm(seq, dseq, rdc, ackTermLimitsReason, _EMPTY_) + } + } + + // Do one final check here. + o.mu.Lock() + defer o.mu.Unlock() + + // If we are closed do not change anything and simply return. + if o.closed { + return + } + + // If we are here, and this should be rare, we still are off with our ack floor. + // We will make sure we are not doing un-necessary work here if only off by a bit + // since this could be normal for a high activity wq or stream. + // We will set it explicitly to 1 behind our current lowest in pending, or if + // pending is empty, to our current delivered -1. + const minOffThreshold = 50 + if ss.FirstSeq >= minOffThreshold && o.asflr < ss.FirstSeq-minOffThreshold { + var psseq, pdseq uint64 + for seq, p := range o.pending { + if psseq == 0 || seq < psseq { + psseq, pdseq = seq, p.Sequence + } + } + // If we still have none, set to current delivered -1. + if psseq == 0 { + psseq, pdseq = o.sseq-1, o.dseq-1 + // If still not adjusted. + if psseq < ss.FirstSeq-1 { + psseq = ss.FirstSeq - 1 + } + } else { + // Since this was set via the pending, we should not include + // it directly but set floors to -1. + psseq, pdseq = psseq-1, pdseq-1 + } + o.asflr, o.adflr = psseq, pdseq + } +} + +func (o *consumer) processInboundAcks(qch chan struct{}) { + // Grab the server lock to watch for server quit. + o.mu.RLock() + s, mset := o.srv, o.mset + hasInactiveThresh := o.cfg.InactiveThreshold > 0 + + o.mu.RUnlock() + + if s == nil || mset == nil { + return + } + + // We will check this on entry and periodically. + o.checkAckFloor() + + // How often we will check for ack floor drift. + // Spread these out for large numbers on a server restart. + delta := time.Duration(rand.Int64N(int64(time.Minute))) + ticker := time.NewTicker(time.Minute + delta) + defer ticker.Stop() + + for { + select { + case <-o.ackMsgs.ch: + // If we have an inactiveThreshold set, mark our activity. + // Do this before processing acks, otherwise we might race if there are no pending messages + // anymore and the inactivity threshold kicks in before we're able to mark activity. + if hasInactiveThresh { + o.suppressDeletion() + } + + acks := o.ackMsgs.pop() + for _, ack := range acks { + o.processAck(ack.subject, ack.reply, ack.hdr, ack.msg) + ack.returnToPool() + } + o.ackMsgs.recycle(&acks) + case <-ticker.C: + o.checkAckFloor() + case <-qch: + return + case <-s.quitCh: + return + } + } +} + +// Process inbound next message requests. +func (o *consumer) processInboundNextMsgReqs(qch chan struct{}) { + // Grab the server lock to watch for server quit. + o.mu.RLock() + s := o.srv + o.mu.RUnlock() + + for { + select { + case <-o.nextMsgReqs.ch: + reqs := o.nextMsgReqs.pop() + for _, req := range reqs { + o.processNextMsgRequest(req.reply, req.msg) + req.returnToPool() + } + o.nextMsgReqs.recycle(&reqs) + case <-qch: + return + case <-s.quitCh: + return + } + } +} + +// Suppress auto cleanup on ack activity of any kind. +func (o *consumer) suppressDeletion() { + o.mu.Lock() + defer o.mu.Unlock() + + if o.closed { + return + } + + if o.isPushMode() && o.dtmr != nil { + // if dtmr is not nil we have started the countdown, simply reset to threshold. + o.dtmr.Reset(o.dthresh) + } else if o.isPullMode() && o.waiting != nil { + // Pull mode always has timer running, update last on waiting queue. + o.waiting.last = time.Now() + if o.dtmr != nil { + o.dtmr.Reset(o.dthresh) + } + } +} + +// loopAndGatherMsgs waits for messages for the consumer. qch is the quit channel, +// upch is the unpause channel which fires when the PauseUntil deadline is reached. +func (o *consumer) loopAndGatherMsgs(qch chan struct{}) { + // On startup check to see if we are in a reply situation where replay policy is not instant. + var ( + lts int64 // last time stamp seen, used for replay. + lseq uint64 + ) + + o.mu.RLock() + mset := o.mset + getLSeq := o.replay + o.mu.RUnlock() + // consumer is closed when mset is set to nil. + if mset == nil { + return + } + if getLSeq { + lseq = mset.state().LastSeq + } + + o.mu.Lock() + s := o.srv + // need to check again if consumer is closed + if o.mset == nil { + o.mu.Unlock() + return + } + // For idle heartbeat support. + var hbc <-chan time.Time + hbd, hb := o.hbTimer() + if hb != nil { + hbc = hb.C + } + // Interest changes. + inch := o.inch + o.mu.Unlock() + + // Grab the stream's retention policy and name + mset.cfgMu.RLock() + stream, rp := mset.cfg.Name, mset.cfg.Retention + mset.cfgMu.RUnlock() + + var err error + + // Deliver all the msgs we have now, once done or on a condition, we wait for new ones. + for { + var ( + pmsg *jsPubMsg + dc uint64 + dsubj string + ackReply string + delay time.Duration + sz int + wrn, wrb int + ) + + o.mu.Lock() + + // consumer is closed when mset is set to nil. + if o.closed || o.mset == nil { + o.mu.Unlock() + return + } + + // Clear last error. + err = nil + + // If the consumer is paused then stop sending. + if o.cfg.PauseUntil != nil && !o.cfg.PauseUntil.IsZero() && time.Now().Before(*o.cfg.PauseUntil) { + // If the consumer is paused and we haven't reached the deadline yet then + // go back to waiting. + goto waitForMsgs + } + + // If we are in push mode and not active or under flowcontrol let's stop sending. + if o.isPushMode() { + if !o.active || (o.maxpb > 0 && o.pbytes > o.maxpb) { + goto waitForMsgs + } + } else if o.waiting.isEmpty() { + // If we are in pull mode and no one is waiting already break and wait. + goto waitForMsgs + } + + // Grab our next msg. + pmsg, dc, err = o.getNextMsg() + + // We can release the lock now under getNextMsg so need to check this condition again here. + if o.closed || o.mset == nil { + o.mu.Unlock() + return + } + + // On error either wait or return. + if err != nil || pmsg == nil { + // On EOF we can optionally fast sync num pending state. + if err == ErrStoreEOF { + o.checkNumPendingOnEOF() + } + if err == ErrStoreMsgNotFound || err == errDeletedMsg || err == ErrStoreEOF || err == errMaxAckPending { + goto waitForMsgs + } else { + if pmsg != nil { + s.Errorf("Received an error looking up message with sequence %d for consumer '%s > %s > %s': %v", + pmsg.seq, o.mset.acc, stream, o.cfg.Name, err) + } else { + s.Errorf("Received an error looking up message for consumer '%s > %s > %s': %v", + o.mset.acc, stream, o.cfg.Name, err) + } + goto waitForMsgs + } + } + + // Update our cached num pending here first. + if dc == 1 { + o.npc-- + } + // Pre-calculate ackReply + ackReply = o.ackReply(pmsg.seq, o.dseq, dc, pmsg.ts, o.numPending()) + + // If headers only do not send msg payload. + // Add in msg size itself as header. + if o.cfg.HeadersOnly { + convertToHeadersOnly(pmsg) + } + // Calculate payload size. This can be calculated on client side. + // We do not include transport subject here since not generally known on client. + sz = len(pmsg.subj) + len(ackReply) + len(pmsg.hdr) + len(pmsg.msg) + + if o.isPushMode() { + dsubj = o.dsubj + } else if wr := o.nextWaiting(sz); wr != nil { + wrn, wrb = wr.n, wr.b + dsubj = wr.reply + if o.cfg.PriorityPolicy == PriorityPinnedClient { + pmsg.hdr = genHeader(pmsg.hdr, JSPullRequestNatsPinId, o.currentPinId) + pmsg.buf = append(pmsg.hdr, pmsg.msg...) + sz = len(pmsg.subj) + len(ackReply) + len(pmsg.hdr) + len(pmsg.msg) + } + if done := wr.recycleIfDone(); done && o.node != nil { + o.removeClusterPendingRequest(dsubj) + } else if !done && wr.hb > 0 { + wr.hbt = time.Now().Add(wr.hb) + } + } else { + // We will redo this one as long as this is not a redelivery. + // Need to also test that this is not going backwards since if + // we fail to deliver we can end up here from rdq but we do not + // want to decrement o.sseq if that is the case. + if dc == 1 && pmsg.seq == o.sseq-1 { + o.sseq-- + o.npc++ + } else if !o.onRedeliverQueue(pmsg.seq) { + // We are not on the rdq so decrement the delivery count + // and add it back. + o.decDeliveryCount(pmsg.seq) + o.addToRedeliverQueue(pmsg.seq) + } + pmsg.returnToPool() + pmsg = nil + goto waitForMsgs + } + + // If we are in a replay scenario and have not caught up check if we need to delay here. + if o.replay && lts > 0 { + if delay = time.Duration(pmsg.ts - lts); delay > time.Millisecond { + o.mu.Unlock() + select { + case <-qch: + pmsg.returnToPool() + pmsg = nil + return + case <-time.After(delay): + } + o.mu.Lock() + } + } + + // Track this regardless. + lts = pmsg.ts + + // If we have a rate limit set make sure we check that here. + if o.rlimit != nil { + now := time.Now() + r := o.rlimit.ReserveN(now, sz) + delay := r.DelayFrom(now) + if delay > 0 { + o.mu.Unlock() + select { + case <-qch: + pmsg.returnToPool() + pmsg = nil + return + case <-time.After(delay): + } + o.mu.Lock() + } + } + + // Do actual delivery. + o.deliverMsg(dsubj, ackReply, pmsg, dc, rp) + + // If given request fulfilled batch size, but there are still pending bytes, send information about it. + if wrn <= 0 && wrb > 0 { + msg := fmt.Appendf(nil, JsPullRequestRemainingBytesT, JSPullRequestPendingMsgs, wrn, JSPullRequestPendingBytes, wrb) + o.outq.send(newJSPubMsg(dsubj, _EMPTY_, _EMPTY_, msg, nil, nil, 0)) + } + // Reset our idle heartbeat timer if set. + if hb != nil { + hb.Reset(hbd) + } + + o.mu.Unlock() + continue + + waitForMsgs: + // If we were in a replay state check to see if we are caught up. If so clear. + if o.replay && o.sseq > lseq { + o.replay = false + } + + // Make sure to process any expired requests that are pending. + var wrExp <-chan time.Time + if o.isPullMode() { + // Dont expire oneshots if we are here because of max ack pending limit. + _, _, _, fexp := o.processWaiting(err != errMaxAckPending) + if !fexp.IsZero() { + expires := time.Until(fexp) + if expires <= 0 { + expires = time.Millisecond + } + wrExp = time.NewTimer(expires).C + } + } + + // We will wait here for new messages to arrive. + mch, odsubj := o.mch, o.cfg.DeliverSubject + o.mu.Unlock() + + select { + case <-mch: + // Messages are waiting. + case interest := <-inch: + // inch can be nil on pull-based, but then this will + // just block and not fire. + o.updateDeliveryInterest(interest) + case <-qch: + return + case <-wrExp: + o.mu.Lock() + o.processWaiting(true) + o.mu.Unlock() + case <-hbc: + if o.isActive() { + o.mu.Lock() + o.sendIdleHeartbeat(odsubj) + // Send flow control on EOS if it's used for acknowledgements. + if o.cfg.AckPolicy == AckFlowControl && len(o.pending) > 0 && o.fcid == _EMPTY_ { + o.sendFlowControl() + } + o.mu.Unlock() + } + // Reset our idle heartbeat timer. + hb.Reset(hbd) + } + } +} + +// Lock should be held. +func (o *consumer) sendIdleHeartbeat(subj string) { + const t = "NATS/1.0 100 Idle Heartbeat\r\n%s: %d\r\n%s: %d\r\n\r\n" + sseq, dseq := o.sseq-1, o.dseq-1 + hdr := fmt.Appendf(nil, t, JSLastConsumerSeq, dseq, JSLastStreamSeq, sseq) + if fcp := o.fcid; fcp != _EMPTY_ { + // Add in that we are stalled on flow control here. + addOn := fmt.Appendf(nil, "%s: %s\r\n\r\n", JSConsumerStalled, fcp) + hdr = append(hdr[:len(hdr)-LEN_CR_LF], []byte(addOn)...) + } + o.outq.send(newJSPubMsg(subj, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) +} + +func (o *consumer) ackReply(sseq, dseq, dc uint64, ts int64, pending uint64) string { + if o.useV2Ack { + return fmt.Sprintf(o.ackReplyT, dc, sseq, dseq, ts, pending) + } + return fmt.Sprintf(o.ackReplyOldT, dc, sseq, dseq, ts, pending) +} + +// Used mostly for testing. Sets max pending bytes for flow control setups. +func (o *consumer) setMaxPendingBytes(limit int) { + o.pblimit = limit + o.maxpb = limit / 16 + if o.maxpb == 0 { + o.maxpb = 1 + } +} + +// Does some sanity checks to see if we should re-calculate. +// Since there is a race when decrementing when there is contention at the beginning of the stream. +// The race is a getNextMsg skips a deleted msg, and then the decStreamPending call fires. +// This does some quick sanity checks to see if we should re-calculate num pending. +// Lock should be held. +func (o *consumer) checkNumPending() (uint64, error) { + if o.mset != nil && o.mset.store != nil { + var state StreamState + o.mset.store.FastState(&state) + npc := o.numPending() + // Make sure we can't report more messages than there are. + // TODO(nat): It's not great that this means consumer info has side effects, + // since we can't know whether anyone will call it or not. The previous num + // pending calculation that this replaces had the same problem though. + if o.sseq > state.LastSeq { + o.npc = 0 + } else if npc > 0 { + o.npc = int64(min(npc, state.Msgs, state.LastSeq-o.sseq+1)) + } + } + return o.numPending(), nil +} + +// Lock should be held. +func (o *consumer) numPending() uint64 { + if o.npc < 0 { + return 0 + } + return uint64(o.npc) +} + +// This will do a quick sanity check on num pending when we encounter +// and EOF in the loop and gather. +// Lock should be held. +func (o *consumer) checkNumPendingOnEOF() { + if o.mset == nil { + return + } + var state StreamState + o.mset.store.FastState(&state) + if o.sseq > state.LastSeq && o.npc != 0 { + // We know here we can reset our running state for num pending. + o.npc, o.npf = 0, state.LastSeq + } +} + +// Call into streamNumPending after acquiring the consumer lock. +func (o *consumer) streamNumPendingLocked() (uint64, error) { + o.mu.Lock() + defer o.mu.Unlock() + return o.streamNumPending() +} + +// Will force a set from the stream store of num pending on the consumer leader. +// Depends on delivery policy, for last per subject we calculate differently. +// Lock should be held. +func (o *consumer) streamNumPending() (uint64, error) { + if o.mset == nil || o.mset.store == nil || !o.isLeader() { + o.npc, o.npf = 0, 0 + return 0, nil + } + npc, npf, err := o.calculateNumPending() + if err != nil { + return 0, err + } + o.npc, o.npf = int64(npc), npf + return o.numPending(), nil +} + +// Will calculate num pending but only requires a read lock. +// Depends on delivery policy, for last per subject we calculate differently. +// At least RLock should be held. +func (o *consumer) calculateNumPending() (npc, npf uint64, err error) { + if o.mset == nil || o.mset.store == nil { + return 0, 0, nil + } + + isLastPerSubject := o.cfg.DeliverPolicy == DeliverLastPerSubject + filters, subjf := o.filters, o.subjf + + if filters != nil { + return o.mset.store.NumPendingMulti(o.sseq, filters, isLastPerSubject) + } else if len(subjf) > 0 { + filter := subjf[0].subject + return o.mset.store.NumPending(o.sseq, filter, isLastPerSubject) + } + return o.mset.store.NumPending(o.sseq, _EMPTY_, isLastPerSubject) +} + +func convertToHeadersOnly(pmsg *jsPubMsg) { + // If headers only do not send msg payload. + // Add in msg size itself as header. + hdr, msg := pmsg.hdr, pmsg.msg + var bb bytes.Buffer + if len(hdr) == 0 { + bb.WriteString(hdrLine) + } else { + bb.Write(hdr) + bb.Truncate(len(hdr) - LEN_CR_LF) + } + bb.WriteString(JSMsgSize) + bb.WriteString(": ") + bb.WriteString(strconv.FormatInt(int64(len(msg)), 10)) + bb.WriteString(CR_LF) + bb.WriteString(CR_LF) + // Replace underlying buf which we can use directly when we send. + // TODO(dlc) - Probably just use directly when forming bytes.Buffer? + pmsg.buf = pmsg.buf[:0] + pmsg.buf = append(pmsg.buf, bb.Bytes()...) + // Replace with new header. + pmsg.hdr = pmsg.buf + // Cancel msg payload + pmsg.msg = nil +} + +// Deliver a msg to the consumer. +// Lock should be held and o.mset validated to be non-nil. +func (o *consumer) deliverMsg(dsubj, ackReply string, pmsg *jsPubMsg, dc uint64, rp RetentionPolicy) { + if o.mset == nil { + pmsg.returnToPool() + return + } + + dseq := o.dseq + o.dseq++ + + pmsg.dsubj, pmsg.reply, pmsg.o = dsubj, ackReply, o + psz := pmsg.size() + + if o.maxpb > 0 { + o.pbytes += psz + } + + mset := o.mset + ap := o.cfg.AckPolicy + + // Cant touch pmsg after this sending so capture what we need. + seq, ts := pmsg.seq, pmsg.ts + + // Update delivered first. + o.updateDelivered(dseq, seq, dc, ts) + + if ap == AckNone { + o.adflr = dseq + o.asflr = seq + } else { + o.trackPending(seq, dseq) + } + + // Send message. + if o.replicateDeliveries() { + o.addReplicatedQueuedMsg(pmsg) + } else { + o.outq.send(pmsg) + } + + // Flow control. + if o.maxpb > 0 && o.needFlowControl(psz) { + o.sendFlowControl() + } + + // If pull mode and we have inactivity threshold, signaled by dthresh, update last activity. + if o.isPullMode() && o.dthresh > 0 { + o.waiting.last = time.Now() + } + + // If we are ack none and mset is interest only we should make sure stream removes interest. + if ap == AckNone && rp != LimitsPolicy { + if mset != nil && mset.ackq != nil && (o.node == nil || o.direct) { + mset.ackq.push(seq) + } else { + o.updateAcks(dseq, seq, _EMPTY_) + } + } +} + +// replicateDeliveries returns whether deliveries should be replicated before sending them. +// If we're replicated we MUST only send the message AFTER we've got quorum for updating +// delivered state. Otherwise, we could be in an invalid state after a leader change. +// We can send immediately if not replicated, not using acks, or using flow control (incompatible). +// Lock should be held. +func (o *consumer) replicateDeliveries() bool { + return o.node != nil && o.cfg.AckPolicy != AckNone && !o.cfg.FlowControl +} + +func (o *consumer) needFlowControl(sz int) bool { + if o.maxpb == 0 { + return false + } + // Decide whether to send a flow control message which we will need the user to respond. + // We send when we are over 50% of our current window limit. + if o.fcid == _EMPTY_ && o.pbytes > o.maxpb/2 { + return true + } + // Or, when acking based on flow control, we need to send it if we've hit the max pending limit earlier. + if o.fcid == _EMPTY_ && o.cfg.AckPolicy == AckFlowControl && o.maxp > 0 && len(o.pending) >= o.maxp { + return true + } + // If we have an existing outstanding FC, check to see if we need to expand the o.fcsz + if o.fcid != _EMPTY_ && (o.pbytes-o.fcsz) >= o.maxpb { + o.fcsz += sz + } + return false +} + +func (o *consumer) processFlowControl(_ *subscription, c *client, _ *Account, subj, _ string, rmsg []byte) { + o.mu.Lock() + + // Ignore if not the latest we have sent out. + if subj != o.fcid { + o.mu.Unlock() + return + } + + // For slow starts and ramping up. + if o.maxpb < o.pblimit { + o.maxpb *= 2 + if o.maxpb > o.pblimit { + o.maxpb = o.pblimit + } + } + + // Update accounting. + o.pbytes -= o.fcsz + if o.pbytes < 0 { + o.pbytes = 0 + } + o.fcid, o.fcsz = _EMPTY_, 0 + + o.signalNewMessages() + ackFlowControl := o.cfg.AckPolicy == AckFlowControl + o.mu.Unlock() + + if !ackFlowControl { + return + } + hdr, _ := c.msgParts(rmsg) + if len(hdr) > 0 { + ldseq := parseInt64(sliceHeader(JSLastConsumerSeq, hdr)) + lsseq := parseInt64(sliceHeader(JSLastStreamSeq, hdr)) + if lsseq > 0 { + // Delivered sequence is allowed to be zero as a response + // to flow control without any deliveries. + if ldseq <= 0 { + ldseq = 0 + } + o.processAckMsg(uint64(lsseq), uint64(ldseq), 1, _EMPTY_, false) + } + } +} + +// Lock should be held. +func (o *consumer) fcReply() string { + var sb strings.Builder + if o.useV2Ack { + sb.WriteString(o.fcPre) + } else { + sb.WriteString(o.fcPreOld) + } + sb.WriteString(o.stream) + sb.WriteByte(btsep) + sb.WriteString(o.name) + sb.WriteByte(btsep) + var b [4]byte + rn := rand.Int64() + for i, l := 0, rn; i < len(b); i++ { + b[i] = digits[l%base] + l /= base + } + sb.Write(b[:]) + return sb.String() +} + +// sendFlowControl will send a flow control packet to the consumer. +// Lock should be held. +func (o *consumer) sendFlowControl() { + if !o.isPushMode() { + return + } + subj, rply := o.cfg.DeliverSubject, o.fcReply() + o.fcsz, o.fcid = o.pbytes, rply + hdr := []byte("NATS/1.0 100 FlowControl Request\r\n\r\n") + o.outq.send(newJSPubMsg(subj, _EMPTY_, rply, hdr, nil, nil, 0)) +} + +// Tracks our outstanding pending acks. Only applicable to AckExplicit mode. +// Lock should be held. +func (o *consumer) trackPending(sseq, dseq uint64) { + if o.pending == nil { + o.pending = make(map[uint64]*Pending) + } + + now := time.Now() + if p, ok := o.pending[sseq]; ok { + // Update timestamp but keep original consumer delivery sequence. + // So do not update p.Sequence. + p.Timestamp = now.UnixNano() + } else { + o.pending[sseq] = &Pending{dseq, now.UnixNano()} + } + + // We could have a backoff that set a timer higher than what we need for this message. + // In that case, reset to lowest backoff required for a message redelivery. + minDelay := o.ackWait(0) + if l := len(o.cfg.BackOff); l > 0 { + bi := int(o.rdc[sseq]) + if bi < 0 { + bi = 0 + } else if bi >= l { + bi = l - 1 + } + minDelay = o.ackWait(o.cfg.BackOff[bi]) + } + minDeadline := now.Add(minDelay) + if o.ptmr == nil || o.ptmrEnd.After(minDeadline) { + o.resetPtmr(minDelay) + } +} + +// Credit back a failed delivery. +// lock should be held. +func (o *consumer) creditWaitingRequest(reply string) { + wq := o.waiting + for wr := wq.head; wr != nil; wr = wr.next { + if wr.reply == reply { + wr.n++ + wr.d-- + return + } + } +} + +// didNotDeliver is called when a delivery for a consumer message failed. +// Depending on our state, we will process the failure. +func (o *consumer) didNotDeliver(seq uint64, subj string) { + o.mu.Lock() + mset := o.mset + if mset == nil { + o.mu.Unlock() + return + } + // Adjust back deliver count. + o.decDeliveryCount(seq) + + var checkDeliveryInterest bool + if o.isPushMode() { + o.active = false + checkDeliveryInterest = true + } else if o.pending != nil { + // Good chance we did not deliver because no interest so force a check. + o.processWaiting(false) + // If it is still there credit it back. + o.creditWaitingRequest(subj) + // pull mode and we have pending. + if _, ok := o.pending[seq]; ok { + // We found this messsage on pending, we need + // to queue it up for immediate redelivery since + // we know it was not delivered + if !o.onRedeliverQueue(seq) { + o.addToRedeliverQueue(seq) + if !o.waiting.isEmpty() { + o.signalNewMessages() + } + } + } + } + o.mu.Unlock() + + if checkDeliveryInterest { + localInterest := !o.hasNoLocalInterest() + o.updateDeliveryInterest(localInterest) + } +} + +// Lock should be held. +func (o *consumer) addToRedeliverQueue(seqs ...uint64) { + o.rdq = append(o.rdq, seqs...) + for _, seq := range seqs { + o.rdqi.Insert(seq) + } +} + +// Lock should be held. +func (o *consumer) hasRedeliveries() bool { + return len(o.rdq) > 0 +} + +func (o *consumer) getNextToRedeliver() uint64 { + if len(o.rdq) == 0 { + return 0 + } + seq := o.rdq[0] + if len(o.rdq) == 1 { + o.rdq = nil + o.rdqi.Empty() + } else { + o.rdq = append(o.rdq[:0], o.rdq[1:]...) + o.rdqi.Delete(seq) + } + return seq +} + +// This checks if we already have this sequence queued for redelivery. +// FIXME(dlc) - This is O(n) but should be fast with small redeliver size. +// Lock should be held. +func (o *consumer) onRedeliverQueue(seq uint64) bool { + return o.rdqi.Exists(seq) +} + +// Remove a sequence from the redelivery queue. +// Lock should be held. +func (o *consumer) removeFromRedeliverQueue(seq uint64) bool { + if !o.onRedeliverQueue(seq) { + return false + } + for i, rseq := range o.rdq { + if rseq == seq { + if len(o.rdq) == 1 { + o.rdq = nil + o.rdqi.Empty() + } else { + o.rdq = append(o.rdq[:i], o.rdq[i+1:]...) + o.rdqi.Delete(seq) + } + return true + } + } + return false +} + +// Checks the pending messages. +func (o *consumer) checkPending() { + o.mu.Lock() + defer o.mu.Unlock() + + mset := o.mset + ttl := int64(o.cfg.AckWait) + // On stop, mset and timer will be nil. + if o.closed || mset == nil || o.ptmr == nil || ttl == 0 { + o.stopAndClearPtmr() + return + } + + var shouldUpdateState bool + var state StreamState + mset.store.FastState(&state) + fseq := state.FirstSeq + + now := time.Now().UnixNano() + next := int64(o.ackWait(0)) + // However, if there is backoff, initializes with the largest backoff. + // It will be adjusted as needed. + if l := len(o.cfg.BackOff); l > 0 { + next = int64(o.cfg.BackOff[l-1]) + } + + // Since we can update timestamps, we have to review all pending. + // We will now bail if we see an ack pending inbound to us via o.awl. + var expired []uint64 + check := len(o.pending) > 1024 + for seq, p := range o.pending { + if check && atomic.LoadInt64(&o.awl) > 0 { + o.resetPtmr(100 * time.Millisecond) + return + } + // Check if these are no longer valid. + if seq < fseq || seq <= o.asflr { + delete(o.pending, seq) + delete(o.rdc, seq) + o.removeFromRedeliverQueue(seq) + shouldUpdateState = true + // Check if we need to move ack floors. + if seq > o.asflr { + o.asflr = seq + } + if p.Sequence > o.adflr { + o.adflr = p.Sequence + } + continue + } + elapsed, deadline := now-p.Timestamp, ttl + if len(o.cfg.BackOff) > 0 { + // This is ok even if o.rdc is nil, we would get dc == 0, which is what we want. + dc := int(o.rdc[seq]) + if dc < 0 { + // Prevent consumer backoff from going backwards. + dc = 0 + } + // This will be the index for the next backoff, will set to last element if needed. + nbi := dc + 1 + if dc+1 >= len(o.cfg.BackOff) { + dc = len(o.cfg.BackOff) - 1 + nbi = dc + } + deadline = int64(o.cfg.BackOff[dc]) + // Set `next` to the next backoff (if smaller than current `next` value). + if nextBackoff := int64(o.cfg.BackOff[nbi]); nextBackoff < next { + next = nextBackoff + } + } + if elapsed >= deadline { + // We will check if we have hit our max deliveries. Previously we would do this on getNextMsg() which + // worked well for push consumers, but with pull based consumers would require a new pull request to be + // present to process and redelivered could be reported incorrectly. + if !o.onRedeliverQueue(seq) && !o.hasMaxDeliveries(seq) { + expired = append(expired, seq) + } + } else if deadline-elapsed < next { + // Update when we should fire next. + next = deadline - elapsed + } + } + + if len(expired) > 0 { + // We need to sort. + slices.Sort(expired) + o.addToRedeliverQueue(expired...) + // Now we should update the timestamp here since we are redelivering. + // We will use an incrementing time to preserve order for any other redelivery. + off := now - o.pending[expired[0]].Timestamp + for _, seq := range expired { + if p, ok := o.pending[seq]; ok { + p.Timestamp += off + } + } + o.signalNewMessages() + } + + if len(o.pending) > 0 { + o.resetPtmr(time.Duration(next)) + } else { + // Make sure to stop timer and clear out any re delivery queues + o.stopAndClearPtmr() + o.rdq = nil + o.rdqi.Empty() + o.pending = nil + // Mimic behavior in processAckMsg when pending is empty. + o.adflr, o.asflr = o.dseq-1, o.sseq-1 + } + + // Update our state if needed. + if shouldUpdateState { + if err := o.writeStoreStateUnlocked(); err != nil && o.srv != nil && o.mset != nil && !o.closed { + s, acc, mset, name := o.srv, o.acc, o.mset, o.name + s.Warnf("Consumer '%s > %s > %s' error on write store state from check pending: %v", acc, mset.getCfgName(), name, err) + } + } +} + +// SeqFromReply will extract a sequence number from a reply subject. +func (o *consumer) seqFromReply(reply string) uint64 { + _, dseq, _, _, _ := ackReplyInfo(reply) + return dseq +} + +// StreamSeqFromReply will extract the stream sequence from the reply subject. +func (o *consumer) streamSeqFromReply(reply string) uint64 { + sseq, _, _, _, _ := ackReplyInfo(reply) + return sseq +} + +// Quick parser for positive numbers in ack reply encoding. +func parseAckReplyNum(d string) (n int64) { + if len(d) == 0 { + return -1 + } + for _, dec := range d { + if dec < asciiZero || dec > asciiNine { + return -1 + } + n = n*10 + (int64(dec) - asciiZero) + } + return n +} + +const ( + expectedNumReplyTokensV1 = 9 + expectedNumReplyTokensV2 = 11 +) + +// Grab encoded information in the reply subject for a delivered message. +func ackReplyInfo(subject string) (sseq, dseq, dc uint64, ts int64, pending uint64) { + tsa := [expectedNumReplyTokensV2]string{} + start, tokens := 0, tsa[:0] + for i := 0; i < len(subject); i++ { + if subject[i] == btsep { + tokens = append(tokens, subject[start:i]) + start = i + 1 + } + } + tokens = append(tokens, subject[start:]) + if (len(tokens) != expectedNumReplyTokensV1 && len(tokens) < expectedNumReplyTokensV2) || tokens[0] != "$JS" || tokens[1] != "ACK" { + return 0, 0, 0, 0, 0 + } + offset := 2 + if len(tokens) >= expectedNumReplyTokensV2 { + offset = 4 + } + // TODO(dlc) - Should we error if we do not match consumer name? + // stream is tokens[offset], consumer is offset+1. + dc = uint64(parseAckReplyNum(tokens[offset+2])) + sseq, dseq = uint64(parseAckReplyNum(tokens[offset+3])), uint64(parseAckReplyNum(tokens[offset+4])) + ts = parseAckReplyNum(tokens[offset+5]) + pending = uint64(parseAckReplyNum(tokens[offset+6])) + + return sseq, dseq, dc, ts, pending +} + +// NextSeq returns the next delivered sequence number for this consumer. +func (o *consumer) nextSeq() uint64 { + o.mu.RLock() + dseq := o.dseq + o.mu.RUnlock() + return dseq +} + +// Used to hold skip list when deliver policy is last per subject. +type lastSeqSkipList struct { + resume uint64 + seqs []uint64 +} + +// Let's us know we have a skip list, which is for deliver last per subject and we are just starting. +// Lock should be held. +func (o *consumer) hasSkipListPending() bool { + return o.lss != nil && len(o.lss.seqs) > 0 +} + +// reconcileStateWithStream reconciles consumer state when the stream has reverted +// due to data loss (e.g., VM crash). This handles the case where consumer state +// is ahead of the stream's last sequence. +// Lock should be held. +func (o *consumer) reconcileStateWithStream(streamLastSeq uint64) { + // If an ack floor is higher than stream last sequence, + // reset back down but keep the highest known sequences. + if o.asflr > streamLastSeq { + o.asflr = streamLastSeq + // Delivery floor is one below the delivered sequence, + // but if it is zero somehow, ensure we don't underflow. + o.adflr = o.dseq + if o.adflr > 0 { + o.adflr-- + } + o.pending = nil + o.rdc = nil + } + + // Remove pending entries that are beyond the stream's last sequence + if len(o.pending) > 0 { + for seq := range o.pending { + if seq > streamLastSeq { + delete(o.pending, seq) + } + } + } + + // Remove redelivered entries that are beyond the stream's last sequence + if len(o.rdc) > 0 { + for seq := range o.rdc { + if seq > streamLastSeq { + delete(o.rdc, seq) + } + } + } + + // Update starting sequence and delivery sequence based on pending state + if len(o.pending) == 0 { + o.sseq = o.asflr + 1 + o.dseq = o.adflr + 1 + } else { + // Find highest stream sequence in pending + var maxStreamSeq uint64 + var maxConsumerSeq uint64 + + for streamSeq, p := range o.pending { + if streamSeq > maxStreamSeq { + maxStreamSeq = streamSeq + } + if p.Sequence > maxConsumerSeq { + maxConsumerSeq = p.Sequence + } + } + + // Set next sequences based on highest pending + o.sseq = maxStreamSeq + 1 + o.dseq = maxConsumerSeq + 1 + } +} + +// Will select the starting sequence. +func (o *consumer) selectStartingSeqNo() error { + if o.mset == nil || o.mset.store == nil { + o.sseq = 1 + } else { + var state StreamState + o.mset.store.FastState(&state) + if o.cfg.OptStartSeq == 0 { + if o.cfg.DeliverPolicy == DeliverAll { + o.sseq = state.FirstSeq + } else if o.cfg.DeliverPolicy == DeliverLast { + if o.subjf == nil { + o.sseq = state.LastSeq + } else { + // If we are partitioned here this will be properly set when we become leader. + for _, filter := range o.subjf { + ss, err := o.mset.store.FilteredState(1, filter.subject) + if err != nil { + return err + } + if ss.Last > o.sseq { + o.sseq = ss.Last + } + } + } + } else if o.cfg.DeliverPolicy == DeliverLastPerSubject { + // If our parent stream is set to max msgs per subject of 1 this is just + // a normal consumer at this point. We can avoid any heavy lifting. + o.mset.cfgMu.RLock() + mmp := o.mset.cfg.MaxMsgsPer + o.mset.cfgMu.RUnlock() + if mmp == 1 { + o.sseq = state.FirstSeq + } else { + filters := make([]string, 0, len(o.subjf)) + if o.subjf == nil { + filters = append(filters, o.cfg.FilterSubject) + } else { + for _, filter := range o.subjf { + filters = append(filters, filter.subject) + } + } + + lss := &lastSeqSkipList{resume: state.LastSeq} + lss.seqs, _ = o.mset.store.MultiLastSeqs(filters, 0, 0) + + if len(lss.seqs) == 0 { + o.sseq = state.LastSeq + } else { + o.sseq = lss.seqs[0] + } + // Assign skip list. + o.lss = lss + } + } else if o.cfg.OptStartTime != nil { + // If we are here we are time based. + // TODO(dlc) - Once clustered can't rely on this. + o.sseq = o.mset.store.GetSeqFromTime(*o.cfg.OptStartTime) + // Here we want to see if we are filtered, and if so possibly close the gap + // to the nearest first given our starting sequence from time. This is so we do + // not force the system to do a linear walk between o.sseq and the real first. + if len(o.subjf) > 0 { + nseq := state.LastSeq + for _, filter := range o.subjf { + // Use first sequence since this is more optimized atm. + ss, err := o.mset.store.FilteredState(state.FirstSeq, filter.subject) + if err != nil { + return err + } + if ss.First >= o.sseq && ss.First < nseq { + nseq = ss.First + } + } + // Skip ahead if possible. + if nseq > o.sseq && nseq < state.LastSeq { + o.sseq = nseq + } + } + } else { + // DeliverNew + o.sseq = state.LastSeq + 1 + } + } else { + o.sseq = o.cfg.OptStartSeq + } + + if state.FirstSeq == 0 && o.cfg.OptStartSeq == 0 { + // If the stream is empty, deliver only new. + o.sseq = 1 + } else if o.sseq > state.LastSeq && o.cfg.OptStartSeq == 0 { + // If selected sequence is in the future, clamp back down. + o.sseq = state.LastSeq + 1 + } else if o.sseq < state.FirstSeq { + // If the first sequence is further ahead than the starting sequence, + // there are no messages there anymore, so move the sequence up. + o.sseq = state.FirstSeq + } + } + + // Always set delivery sequence to 1. + o.dseq = 1 + // Set ack delivery floor to delivery-1 + o.adflr = o.dseq - 1 + // Set ack store floor to store-1 + o.asflr = o.sseq - 1 + // Set our starting sequence state. + // But only if we're not clustered, if clustered we propose upon becoming leader. + o.mset.cfgMu.RLock() + isR1 := o.cfg.replicas(&o.mset.cfg) == 1 + o.mset.cfgMu.RUnlock() + if o.store != nil && o.sseq > 0 && isR1 { + if err := o.store.SetStarting(o.sseq - 1); err != nil { + return err + } + } + return nil +} + +// Test whether a config represents a durable subscriber. +func isDurableConsumer(config *ConsumerConfig) bool { + return config != nil && config.Durable != _EMPTY_ +} + +func (o *consumer) isDurable() bool { + return o.cfg.Durable != _EMPTY_ +} + +// Are we in push mode, delivery subject, etc. +func (o *consumer) isPushMode() bool { + return o.cfg.DeliverSubject != _EMPTY_ +} + +func (o *consumer) isPullMode() bool { + return o.cfg.DeliverSubject == _EMPTY_ +} + +// Name returns the name of this consumer. +func (o *consumer) String() string { + o.mu.RLock() + n := o.name + o.mu.RUnlock() + return n +} + +func createConsumerName() string { + return getHash(nuid.Next()) +} + +// Lock should be held. +func (mset *stream) createStableConsumerHash() string { + id := fmt.Sprintf("%s %s", mset.cfg.Name, mset.acc.Name) + if domain := mset.srv.getOpts().JetStreamDomain; domain != _EMPTY_ { + id = fmt.Sprintf("%s %s", id, domain) + } + return getHash(id) +} + +// Lock should be held. +func (mset *stream) createSourcingConsumerHash(ssi *StreamSource, sources []*StreamSource) string { + id := mset.createStableConsumerHash() + + // If the stream sources contain the same stream at least twice, we use a more strict hash of + // an ID that also contains filter subjects etc. If the stream name is only used once, we can + // support the stable identifier above. + var once bool + for _, src := range sources { + if src.Name == ssi.Name { + if once { + if ssi.iname == _EMPTY_ { + ssi.setIndexName() + } + // Append identifying information of the filter subjects, etc. to make it unique + id = fmt.Sprintf("%s %s", id, ssi.iname) + break + } else { + once = true + } + } + } + + return getHash(id) +} + +// deleteConsumer will delete the consumer from this stream. +func (mset *stream) deleteConsumer(o *consumer) error { + return o.delete() +} + +func (o *consumer) getStream() *stream { + o.mu.RLock() + mset := o.mset + o.mu.RUnlock() + return mset +} + +func (o *consumer) streamName() string { + o.mu.RLock() + mset := o.mset + o.mu.RUnlock() + if mset != nil { + return mset.name() + } + return _EMPTY_ +} + +// streamIdentity returns the identity of the consumer's stream, which changes +// if the stream is recreated. Returns empty if the stream is not available. +func (o *consumer) streamIdentity() string { + o.mu.RLock() + mset := o.mset + o.mu.RUnlock() + if mset != nil { + return mset.identity() + } + return _EMPTY_ +} + +// Active indicates if this consumer is still active. +func (o *consumer) isActive() bool { + o.mu.RLock() + active := o.active && o.mset != nil + o.mu.RUnlock() + return active +} + +// hasNoLocalInterest return true if we have no local interest. +func (o *consumer) hasNoLocalInterest() bool { + o.mu.RLock() + interest := o.acc.sl.HasInterest(o.cfg.DeliverSubject) + o.mu.RUnlock() + return !interest +} + +// This is when the underlying stream has been purged. +// sseq is the new first seq for the stream after purge. +// Consumer lock should NOT be held but the parent stream +// lock MUST be held. +func (o *consumer) purge(sseq uint64, slseq uint64, isWider bool) { + // Do not update our state unless we know we are the leader. + if !o.isLeader() { + return + } + // Signals all have been purged for this consumer. + if sseq == 0 && !isWider { + sseq = slseq + 1 + } + + var store StreamStore + if isWider { + o.mu.RLock() + if o.mset != nil { + store = o.mset.store + } + o.mu.RUnlock() + } + + o.mu.Lock() + // Do not go backwards + if o.sseq < sseq { + o.sseq = sseq + } + + if o.asflr < sseq { + o.asflr = sseq - 1 + // We need to remove those no longer relevant from pending. + for seq, p := range o.pending { + if seq <= o.asflr { + if p.Sequence > o.adflr { + o.adflr = p.Sequence + if o.adflr > o.dseq { + o.dseq = o.adflr + } + } + delete(o.pending, seq) + delete(o.rdc, seq) + o.updateAcks(p.Sequence, seq, _EMPTY_) + // rdq handled below. + } else if isWider && store != nil { + // Our filtered subject, which could be all, is wider than the underlying purge. + // We need to check if the pending items left are still valid. + var smv StoreMsg + if _, err := store.LoadMsg(seq, &smv); err == errDeletedMsg || err == ErrStoreMsgNotFound { + if p.Sequence > o.adflr { + o.adflr = p.Sequence + if o.adflr > o.dseq { + o.dseq = o.adflr + } + } + delete(o.pending, seq) + delete(o.rdc, seq) + o.updateAcks(p.Sequence, seq, _EMPTY_) + } + } + } + } + + // This means we can reset everything at this point. + if len(o.pending) == 0 { + o.pending, o.rdc = nil, nil + o.adflr, o.asflr = o.dseq-1, o.sseq-1 + } + + // We need to remove all those being queued for redelivery under o.rdq + if len(o.rdq) > 0 { + rdq := o.rdq + o.rdq = nil + o.rdqi.Empty() + for _, sseq := range rdq { + if sseq >= o.sseq { + o.addToRedeliverQueue(sseq) + } + } + } + // Grab some info in case of error below. + s, acc, mset, name := o.srv, o.acc, o.mset, o.name + o.mu.Unlock() + + if err := o.writeStoreState(); err != nil && s != nil && mset != nil { + s.Warnf("Consumer '%s > %s > %s' error on write store state from purge: %v", acc, mset.nameLocked(false), name, err) + } +} + +func stopAndClearTimer(tp **time.Timer) { + if *tp == nil { + return + } + // Will get drained in normal course, do not try to + // drain here. + (*tp).Stop() + *tp = nil +} + +// Stop will shutdown the consumer for the associated stream. +func (o *consumer) stop() error { + return o.stopWithFlags(false, false, true, false) +} + +func (o *consumer) deleteWithoutAdvisory() error { + return o.stopWithFlags(true, false, true, false) +} + +// Delete will delete the consumer for the associated stream and send advisories. +func (o *consumer) delete() error { + return o.stopWithFlags(true, false, true, true) +} + +// To test for closed state. +func (o *consumer) isClosed() bool { + o.mu.RLock() + defer o.mu.RUnlock() + return o.closed +} + +func (o *consumer) stopWithFlags(dflag, sdflag, doSignal, advisory bool) error { + // If dflag is true determine if we are still assigned. + var isAssigned bool + if dflag { + o.mu.RLock() + acc, stream, consumer := o.acc, o.stream, o.name + isClustered := o.js != nil && o.js.isClustered() + o.mu.RUnlock() + if isClustered { + // Grab jsa to check assignment. + var jsa *jsAccount + if acc != nil { + // Need lock here to avoid data race. + acc.mu.RLock() + jsa = acc.js + acc.mu.RUnlock() + } + if jsa != nil { + isAssigned = jsa.consumerAssigned(stream, consumer) + } + } + } + + o.mu.Lock() + if o.closed { + o.mu.Unlock() + return nil + } + o.closed = true + + // Signal to the monitor loop. + // Can't use only qch here, since that's used when stepping down as a leader. + if o.mqch != nil { + close(o.mqch) + o.mqch = nil + } + + // Check if we are the leader and are being deleted (as a node). + if dflag && o.isLeader() { + // If we are clustered and node leader (probable from above), stepdown. + if node := o.node; node != nil { + node.StepDown() + } + + // dflag does not necessarily mean that the consumer is being deleted, + // just that the consumer node is being removed from this peer, so we + // send delete advisories only if we are no longer assigned at the meta layer, + // or we are not clustered. + if !isAssigned && advisory { + o.sendDeleteAdvisoryLocked() + } + if o.isPullMode() { + // Release any pending. + o.releaseAnyPendingRequests(isAssigned) + } + } + + if o.qch != nil { + close(o.qch) + o.qch = nil + } + + a := o.acc + store := o.store + mset := o.mset + o.mset = nil + o.active = false + o.unsubscribe(o.ackSubOld) + o.unsubscribe(o.ackSub) + o.unsubscribe(o.reqSub) + o.unsubscribe(o.resetSub) + o.unsubscribe(o.fcSubOld) + o.unsubscribe(o.fcSub) + o.ackSubOld = nil + o.ackSub = nil + o.reqSub = nil + o.resetSub = nil + o.fcSubOld = nil + o.fcSub = nil + if o.infoSub != nil { + o.srv.sysUnsubscribe(o.infoSub) + o.infoSub = nil + } + c := o.client + o.client = nil + sysc := o.sysc + o.sysc = nil + o.stopAndClearPtmr() + stopAndClearTimer(&o.dtmr) + stopAndClearTimer(&o.gwdtmr) + delivery := o.cfg.DeliverSubject + o.waiting = nil + // Break us out of the readLoop. + if doSignal { + o.signalNewMessages() + } + n := o.node + qgroup := o.cfg.DeliverGroup + o.ackMsgs.unregister() + if o.nextMsgReqs != nil { + o.nextMsgReqs.unregister() + } + + // For cleaning up the node assignment. + var ca *consumerAssignment + if dflag { + ca = o.ca + } + js := o.js + o.mu.Unlock() + + if c != nil { + c.closeConnection(ClientClosed) + } + if sysc != nil { + sysc.closeConnection(ClientClosed) + } + + if delivery != _EMPTY_ { + a.sl.clearNotification(delivery, qgroup, o.inch) + } + + var rp RetentionPolicy + if mset != nil { + mset.mu.Lock() + mset.removeConsumer(o) + // No need for cfgMu's lock since mset.mu.Lock superseeds it. + rp = mset.cfg.Retention + mset.mu.Unlock() + } + + // Cleanup messages that lost interest. + if dflag && rp == InterestPolicy { + o.cleanupNoInterestMessages(mset, true) + } + + // Cluster cleanup. + if n != nil { + if dflag { + n.Delete() + } else { + n.Stop() + } + } + + if ca != nil { + js.mu.Lock() + if ca.Group != nil { + ca.Group.node = nil + } + js.mu.Unlock() + } + + // Clean up our store. + var err error + if store != nil { + if dflag { + if sdflag { + err = store.StreamDelete() + } else { + err = store.Delete() + } + } else { + err = store.Stop() + } + } else if dflag { + // If there's no store (for example, when it's offline), manually delete the directories. + o.mu.RLock() + stream, consumer := o.stream, o.name + o.mu.RUnlock() + accDir := filepath.Join(js.config.StoreDir, a.GetName()) + consumersDir := filepath.Join(accDir, streamsDir, stream, consumerDir) + os.RemoveAll(filepath.Join(consumersDir, consumer)) + } + + return err +} + +// We need to optionally remove all messages since we are interest based retention. +// We will do this consistently on all replicas. Note that if in clustered mode the non-leader +// consumers will need to restore state first. +// ignoreInterest marks whether the consumer should be ignored when determining interest. +// No lock held on entry. +func (o *consumer) cleanupNoInterestMessages(mset *stream, ignoreInterest bool) { + o.mu.Lock() + if !o.isLeader() { + o.readStoredState() + } + start := o.asflr + o.mu.Unlock() + + // Make sure we start at worst with first sequence in the stream. + state := mset.state() + if start < state.FirstSeq { + start = state.FirstSeq + } + stop := state.LastSeq + + // Consumer's interests are ignored by default. If we should not ignore interest, unset. + co := o + if !ignoreInterest { + co = nil + } + + var rmseqs []uint64 + mset.mu.RLock() + + // If over this amount of messages to check, optimistically call to checkInterestState(). + // It will not always do the right thing in removing messages that lost interest, but ensures + // we don't degrade performance by doing a linear scan through the whole stream. + // Messages might need to expire based on limits to be cleaned up. + // TODO(dlc) - Better way? + const bailThresh = 100_000 + + // Check if we would be spending too much time here and defer to separate go routine. + if len(mset.consumers) == 0 { + mset.mu.RUnlock() + mset.mu.Lock() + defer mset.mu.Unlock() + mset.store.Purge() + var state StreamState + mset.store.FastState(&state) + mset.lseq = state.LastSeq + // Also make sure we clear any pending acks. + mset.clearAllPreAcksBelowFloor(state.FirstSeq) + return + } else if stop-start > bailThresh { + mset.mu.RUnlock() + go mset.checkInterestState() + return + } + + mset.mu.RUnlock() + mset.mu.Lock() + for seq := start; seq <= stop; seq++ { + if mset.noInterest(seq, co) { + rmseqs = append(rmseqs, seq) + } + } + mset.mu.Unlock() + + // These can be removed. + for _, seq := range rmseqs { + mset.store.RemoveMsg(seq) + } +} + +// Check that we do not form a cycle by delivering to a delivery subject +// that is part of the interest group. +func deliveryFormsCycle(cfg *StreamConfig, deliverySubject string) bool { + for _, subject := range cfg.Subjects { + if subjectIsSubsetMatch(deliverySubject, subject) { + return true + } + } + return false +} + +// switchToEphemeral is called on startup when recovering ephemerals. +func (o *consumer) switchToEphemeral() { + o.mu.Lock() + o.cfg.Durable = _EMPTY_ + store := o.store + interest := o.acc.sl.HasInterest(o.cfg.DeliverSubject) + // Setup dthresh. + o.updateInactiveThreshold(&o.cfg) + o.updatePauseState(&o.cfg) + cfg := o.cfg + o.mu.Unlock() + + // Update interest + o.updateDeliveryInterest(interest) + // Write out new config + switch store := store.(type) { + case *consumerFileStore: + store.updateConfig(cfg) + case *consumerMemStore: + store.UpdateConfig(&cfg) + } +} + +// RequestNextMsgSubject returns the subject to request the next message when in pull or worker mode. +// Returns empty otherwise. +func (o *consumer) requestNextMsgSubject() string { + return o.nextMsgSubj +} + +func (o *consumer) decStreamPending(sseq uint64, subj string) { + o.mu.Lock() + + // Update our cached num pending only if we think deliverMsg has not done so. + if sseq >= o.sseq && o.isFilteredMatch(subj) { + o.npc-- + } + + // Check if this message was pending. + p, wasPending := o.pending[sseq] + var rdc uint64 + if wasPending { + rdc = o.deliveryCount(sseq) + } else if _, ok := o.rdc[sseq]; ok && o.isLeader() { + delete(o.rdc, sseq) + // Pass 0 as the delivered sequence to only remove the redelivered state. + o.updateAcks(0, sseq, _EMPTY_) + } + + o.mu.Unlock() + + // If it was pending process it like an ack. + if wasPending { + // We could have the lock for the stream so do this in a go routine. + // TODO(dlc) - We should do this with ipq vs naked go routines. + go o.processTerm(sseq, p.Sequence, rdc, ackTermUnackedLimitsReason, _EMPTY_) + } +} + +func (o *consumer) account() *Account { + o.mu.RLock() + a := o.acc + o.mu.RUnlock() + return a +} + +// Creates a sublist for consumer. +// All subjects share the same callback. +func (o *consumer) signalSubs() []string { + o.mu.Lock() + defer o.mu.Unlock() + + if o.sigSubs != nil { + return o.sigSubs + } + + if len(o.subjf) == 0 { + subs := []string{fwcs} + o.sigSubs = subs + return subs + } + + subs := make([]string, 0, len(o.subjf)) + for _, filter := range o.subjf { + subs = append(subs, filter.subject) + } + o.sigSubs = subs + return subs +} + +// This is what will be called when our parent stream wants to kick us regarding a new message. +// We know that this subject matches us by how the parent handles registering us with the signaling sublist, +// but we must check if we are leader. +// We do need the sequence of the message however and we use the msg as the encoded seq. +func (o *consumer) processStreamSignal(seq uint64) { + // We can get called here now when not leader, so bail fast + // and without acquiring any locks. + if !o.leader.Load() { + return + } + o.mu.Lock() + defer o.mu.Unlock() + if o.mset == nil { + return + } + if seq > o.npf { + o.npc++ + } + if seq < o.sseq { + return + } + if o.isPushMode() && o.active || o.isPullMode() && !o.waiting.isEmpty() { + o.signalNewMessages() + } +} + +// Used to compare if two multiple filtered subject lists are equal. +func subjectSliceEqual(slice1 []string, slice2 []string) bool { + if len(slice1) != len(slice2) { + return false + } + set2 := make(map[string]struct{}, len(slice2)) + for _, val := range slice2 { + set2[val] = struct{}{} + } + for _, val := range slice1 { + if _, ok := set2[val]; !ok { + return false + } + } + return true +} + +// Utility for simpler if conditions in Consumer config checks. +// In future iteration, we can immediately create `o.subjf` and +// use it to validate things. +func gatherSubjectFilters(filter string, filters []string) []string { + if filter != _EMPTY_ { + filters = append(filters, filter) + } + // list of filters should never contain non-empty filter. + return filters +} + +// shouldStartMonitor will return true if we should start a monitor +// goroutine or will return false if one is already running. +func (o *consumer) shouldStartMonitor() bool { + // monitorMu is held across the monitorWg.Add below so that it cannot race + // a concurrent monitorWg.Wait in stopMonitoring. It is taken before o.mu to + // keep a consistent lock ordering. + o.monitorMu.Lock() + defer o.monitorMu.Unlock() + + o.mu.Lock() + if o.inMonitor { + o.mu.Unlock() + return false + } + o.inMonitor = true + o.mu.Unlock() + + o.monitorWg.Add(1) + return true +} + +// Clear the monitor running state. The monitor goroutine should +// call this in a defer to clean up on exit. +func (o *consumer) clearMonitorRunning() { + o.mu.Lock() + defer o.mu.Unlock() + + if o.inMonitor { + o.monitorWg.Done() + o.inMonitor = false + } +} + +// stopMonitoring signals any running monitor goroutine to quit and waits for +// it to fully exit. +func (o *consumer) stopMonitoring() { + // monitorMu is held across both the quit signal and the wait so that a + // concurrent shouldStartMonitor cannot slip a new monitor generation in + // between. + o.monitorMu.Lock() + defer o.monitorMu.Unlock() + o.signalMonitorQuit() + o.monitorWg.Wait() +} + +// Test whether we are in the monitor routine. +func (o *consumer) isMonitorRunning() bool { + o.mu.RLock() + defer o.mu.RUnlock() + return o.inMonitor +} + +// If we detect that our ackfloor is higher than the stream's last sequence, return this error. +var errAckFloorHigherThanLastSeq = errors.New("consumer ack floor is higher than streams last sequence") +var errAckFloorInvalid = errors.New("consumer ack floor is invalid") + +// If we are a consumer of an interest or workqueue policy stream, process that state and make sure consistent. +func (o *consumer) checkStateForInterestStream(ss *StreamState) error { + o.mu.RLock() + // See if we need to process this update if our parent stream is not a limits policy stream. + mset := o.mset + shouldProcessState := mset != nil && o.retention != LimitsPolicy + if o.closed || !shouldProcessState || o.store == nil || ss == nil { + o.mu.RUnlock() + return nil + } + store := mset.store + state, err := o.store.State() + + filters, subjf, filter := o.filters, o.subjf, _EMPTY_ + var wc bool + if filters == nil && subjf != nil { + filter, wc = subjf[0].subject, subjf[0].hasWildcard + } + chkfloor := o.chkflr + o.mu.RUnlock() + + if err != nil { + return err + } + + asflr := state.AckFloor.Stream + // Protect ourselves against rolling backwards. + if asflr&(1<<63) != 0 { + return errAckFloorInvalid + } + dflr := asflr + if len(state.Pending) > 0 && state.Delivered.Stream > dflr { + dflr = state.Delivered.Stream + } + + // Check if the underlying stream's last sequence is less than our floor. + // This can happen if the stream has been reset and has not caught up yet. + if asflr > ss.LastSeq { + return errAckFloorHigherThanLastSeq + } + + var smv StoreMsg + var seq, nseq uint64 + // Start at first stream seq or a previous check floor, whichever is higher. + // Note this will really help for interest retention, with WQ the loadNextMsg + // gets us a long way already since it will skip deleted msgs not for our filter. + fseq := ss.FirstSeq + if chkfloor > fseq { + fseq = chkfloor + } + + var retryAsflr uint64 + for seq = fseq; dflr > 0 && seq <= dflr; seq++ { + if filters != nil { + _, nseq, err = store.LoadNextMsgMulti(filters, seq, &smv) + } else { + _, nseq, err = store.LoadNextMsg(filter, wc, seq, &smv) + } + // if we advanced sequence update our seq. This can be on no error and EOF. + if nseq > seq { + seq = nseq + } + if err == nil { + // Only ack though if no error and seq <= ack floor. + if seq <= asflr { + didRemove := mset.ackMsg(o, seq) + // Removing the message could fail. For example if clustered since we need to propose it. + // Overwrite retry floor (only the first time) to allow us to check next time if the removal was successful. + if didRemove && retryAsflr == 0 { + retryAsflr = seq + } + } else if seq <= dflr { + // Store the first entry above our ack floor, so we don't need to look it up again on retryAsflr=0. + if retryAsflr == 0 { + retryAsflr = seq + } + // If we have pending, we will need to walk through to delivered in case we missed any of those acks as well. + if _, ok := state.Pending[seq]; !ok { + // The filters are already taken into account, + mset.ackMsg(o, seq) + } + } + } else if err == ErrStoreEOF { + break + } + } + // If retry floor was not overwritten, set to ack floor+1, we don't need to account for any retries below it. + // However, our ack floor may be lower than the next message we can receive, so we correct it upward if needed. + if retryAsflr == 0 { + if filters != nil { + _, nseq, err = store.LoadNextMsgMulti(filters, asflr+1, &smv) + } else { + _, nseq, err = store.LoadNextMsg(filter, wc, asflr+1, &smv) + } + if err == nil { + retryAsflr = max(asflr+1, nseq) + } else if err == ErrStoreEOF { + retryAsflr = ss.LastSeq + 1 + } + } + + o.mu.Lock() + // Update our check floor. + // Check floor must never be greater than ack floor+1, otherwise subsequent calls to this function would skip work. + if retryAsflr > o.chkflr { + o.chkflr = retryAsflr + } + o.mu.Unlock() + return nil +} + +func (o *consumer) resetPtmr(delay time.Duration) { + // A delay of zero means it should be stopped. + if delay == 0 { + o.stopAndClearPtmr() + return + } + + if o.ptmr == nil { + o.ptmr = time.AfterFunc(delay, o.checkPending) + } else { + o.ptmr.Reset(delay) + } + o.ptmrEnd = time.Now().Add(delay) +} + +func (o *consumer) stopAndClearPtmr() { + // If the end time is unset, short-circuit since the timer will already be stopped. + if o.ptmrEnd.IsZero() { + return + } + stopAndClearTimer(&o.ptmr) + o.ptmrEnd = time.Time{} +} + +func (o *consumer) resetPendingDeliveries() { + for _, pmsg := range o.pendingDeliveries { + pmsg.returnToPool() + } + o.pendingDeliveries = nil + for _, wd := range o.waitingDeliveries { + wd.recycle() + } + o.waitingDeliveries = nil +} diff --git a/server/filestore.go b/server/filestore.go new file mode 100644 index 0000000..3d2eccf --- /dev/null +++ b/server/filestore.go @@ -0,0 +1,14716 @@ +// 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" + "cmp" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "iter" + "math" + mrand "math/rand/v2" + "net" + "os" + "path/filepath" + "runtime" + "runtime/debug" + "slices" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/antithesishq/antithesis-sdk-go/assert" + "github.com/klauspost/compress/s2" + "github.com/minio/highwayhash" + "github.com/nats-io/nats-server/v2/server/ats" + "github.com/nats-io/nats-server/v2/server/avl" + "github.com/nats-io/nats-server/v2/server/elastic" + "github.com/nats-io/nats-server/v2/server/gsl" + "github.com/nats-io/nats-server/v2/server/stree" + "github.com/nats-io/nats-server/v2/server/thw" + "golang.org/x/crypto/chacha20" + "golang.org/x/crypto/chacha20poly1305" +) + +type FileStoreConfig struct { + // Where the parent directory for all storage will be located. + StoreDir string + // BlockSize is the file block size. This also represents the maximum overhead size. + BlockSize uint64 + // CacheExpire is how long with no activity until we expire the cache. + CacheExpire time.Duration + // SubjectStateExpire is how long with no activity until we expire a msg block's subject state. + SubjectStateExpire time.Duration + // SyncInterval is how often we sync to disk in the background. + SyncInterval time.Duration + // SyncAlways is when the stream should sync all data writes. + SyncAlways bool + // SyncOnFlush relaxes SyncAlways to flush writes on FlushAllPending. + SyncOnFlush bool + // AsyncFlush allows async flush to batch write operations. + AsyncFlush bool + // Cipher is the cipher to use when encrypting. + Cipher StoreCipher + // Compression is the algorithm to use when compressing. + Compression StoreCompression + + // Internal reference to our server. + srv *Server +} + +// FileStreamInfo allows us to remember created time. +type FileStreamInfo struct { + Created time.Time + StreamConfig +} + +type StoreCipher int + +const ( + ChaCha StoreCipher = iota + AES + NoCipher +) + +func (cipher StoreCipher) String() string { + switch cipher { + case ChaCha: + return "ChaCha20-Poly1305" + case AES: + return "AES-GCM" + case NoCipher: + return "None" + default: + return "Unknown StoreCipher" + } +} + +type StoreCompression uint8 + +const ( + NoCompression StoreCompression = iota + S2Compression +) + +func (alg StoreCompression) String() string { + switch alg { + case NoCompression: + return "None" + case S2Compression: + return "S2" + default: + return "Unknown StoreCompression" + } +} + +func (alg StoreCompression) MarshalJSON() ([]byte, error) { + var str string + switch alg { + case S2Compression: + str = "s2" + case NoCompression: + str = "none" + default: + return nil, fmt.Errorf("unknown compression algorithm") + } + return json.Marshal(str) +} + +func (alg *StoreCompression) UnmarshalJSON(b []byte) error { + var str string + if err := json.Unmarshal(b, &str); err != nil { + return err + } + switch str { + case "s2": + *alg = S2Compression + case "none": + *alg = NoCompression + default: + return fmt.Errorf("unknown compression algorithm") + } + return nil +} + +// File ConsumerInfo is used for creating consumer stores. +type FileConsumerInfo struct { + Created time.Time + Name string + ConsumerConfig +} + +// Default file and directory permissions. +const ( + defaultDirPerms = os.FileMode(0700) + defaultFilePerms = os.FileMode(0600) +) + +type psi struct { + total uint64 + fblk uint32 + lblk uint32 +} + +type fileStore struct { + srv *Server + mu sync.RWMutex + state StreamState + tombs []uint64 + ld *LostStreamData + scb StorageUpdateHandler + rmcb StorageRemoveMsgHandler + pmsgcb ProcessJetStreamMsgHandler + ageChk *time.Timer // Timer to expire messages. + ageChkRun bool // Whether message expiration is currently running. + ageChkTime int64 // When the message expiration is scheduled to run. + recovering bool // Timers, schedules, TTLs etc won't fire while set. + syncTmr *time.Timer + syncMu sync.Mutex // Serializes background and explicit block syncs. + cfg FileStreamInfo + fcfg FileStoreConfig + syncAlways atomic.Bool // Effective SyncAlways behavior for lock-free reads. + syncOnFlush atomic.Bool // Effective sync on flush behavior. True only if SyncAlways and replicas > 1. + prf keyGen + oldprf keyGen + aek cipher.AEAD + lmb *msgBlock + blks []*msgBlock + bim map[uint32]*msgBlock + psim *stree.SubjectTree[psi] + tsl int + wfsmu sync.Mutex // Only one writeFullState at a time to protect from overwrites. + wfsrun atomic.Int64 // Is writeFullState already running? For timer check only + wfsadml int // writeFullState average dmap length, protected by wfsmu. + hh *highwayhash.Digest64 + qch chan struct{} + fsld chan struct{} + cmu sync.RWMutex + cfs []ConsumerStore + werr error + sips int + dirty int + closing bool + closed atomic.Bool // Atomic to reduce contention on ConsumerStores. + fip bool + receivedAny bool + firstMoved bool + ttls *thw.HashWheel + scheduling *MsgScheduling + sdm *SDMMeta + lpex time.Time // Last PurgeEx call. + dios *diskIOSemaphore + sources map[string]*StreamSourceState +} + +// Represents a message store block and its data. +type msgBlock struct { + // Here for 32bit systems and atomic. + first msgId + last msgId + mu sync.RWMutex + fs *fileStore + aek cipher.AEAD + bek cipher.Stream + seed []byte + nonce []byte + mfn string + mfd *os.File + cmp StoreCompression // Effective compression at the time of loading the block + liwsz int64 + index uint32 + bytes uint64 // User visible bytes count. + rbytes uint64 // Total bytes (raw) including deleted. Used for rolling to new blk. + cbytes uint64 // Bytes count after last compaction. 0 if no compaction happened yet. + msgs uint64 // User visible message count. + fss *stree.SubjectTree[SimpleState] + kfn string + lwts int64 + llts int64 + lrts int64 + lsts int64 + llseq uint64 + hh *highwayhash.Digest64 + ecache elastic.Pointer[cache] + cache *cache + cloads uint64 + cexp time.Duration + fexp time.Duration + ctmr *time.Timer + werr error + dmap avl.SequenceSet + fch chan struct{} + qch chan struct{} + lchk [8]byte + loading bool + flusher bool + noTrack bool + needSync bool + needKeySync bool // Key file is written once and immutable, cleared after its one sync. + noCompact bool + closed bool + ttls uint64 // How many msgs have TTLs? + schedules uint64 // How many msgs have schedules? + + // Used to mock write failures. + mockWriteErr bool +} + +// Write through caching layer that is also used on loading messages. +type cache struct { + buf []byte + wp int + idx []uint32 + fseq uint64 + nra bool + // When GC collects this cache due to it being a weak pointer, + // recycle the buf back into the block buffer pools. + clean runtime.Cleanup +} + +type msgId struct { + seq uint64 + ts int64 +} + +const ( + // Magic is used to identify the file store files. + magic = uint8(22) + // Version + version = uint8(1) + // New IndexInfo Version + newVersion = uint8(2) + // hdrLen + hdrLen = 2 + // This is where we keep the streams. + streamsDir = "streams" + // This is where we keep inflight batches for streams. + batchesDir = "batches" + // This is where we keep the message store blocks. + msgDir = "msgs" + // This is where we temporarily move the messages dir. + purgeDir = "__msgs__" + // This is where we temporarily move the new message block during purge. + newMsgDir = "__new_msgs__" + // used to scan blk file names. + blkScan = "%d.blk" + // suffix of a block file + blkSuffix = ".blk" + // used for compacted blocks that are staged. + newScan = "%d.new" + // used to scan index file names. + indexScan = "%d.idx" + // used to store our block encryption key. + keyScan = "%d.key" + // to look for orphans + keyScanAll = "*.key" + // This is where we keep state on consumers. + consumerDir = "obs" + // Index file for a consumer. + consumerState = "o.dat" + // The suffix that will be given to a new temporary block for compression or when rewriting the full file. + blkTmpSuffix = ".tmp" + // default cache buffer expiration + defaultCacheBufferExpiration = 10 * time.Second + // default sync interval + defaultSyncInterval = 2 * time.Minute + // default idle timeout to close FDs. + closeFDsIdle = 30 * time.Second + // default expiration time for mb.fss when idle. + defaultFssExpiration = 2 * time.Minute + // coalesceMinimum + coalesceMinimum = 16 * 1024 + // maxFlushWait is maximum we will wait to gather messages to flush. + maxFlushWait = 8 * time.Millisecond + + // Metafiles for streams and consumers. + JetStreamMetaFile = "meta.inf" + JetStreamMetaFileSum = "meta.sum" + JetStreamMetaFileKey = "meta.key" + + // This is the full snapshotted state for the stream. + streamStreamStateFile = "index.db" + + // This is the encoded time hash wheel for TTLs. + ttlStreamStateFile = "thw.db" + + // This is the encoded message scheduling file. + msgSchedulingStreamStateFile = "sched.db" + + // This is the encoded sources state file. Unlike the other state files it + // lives at the FileStoreConfig.StoreDir root rather than under msgDir, so + // a full purge does not also purge this file. + sourcesStreamStateFile = "sources.db" + + // AEK key sizes + minMetaKeySize = 64 + minBlkKeySize = 64 + + // Default stream block size. + defaultLargeBlockSize = 8 * 1024 * 1024 // 8MB + // Default for workqueue or interest based. + defaultMediumBlockSize = 4 * 1024 * 1024 // 4MB + // For smaller reuse buffers. Usually being generated during contention on the lead write buffer. + // E.g. mirrors/sources etc. + defaultSmallBlockSize = 1 * 1024 * 1024 // 1MB + // NOT an actual block size, but used for the sync.Pools, so that we don't allocate huge buffers + // unnecessarily until there are enough writes to justify it. + defaultTinyBlockSize = 1 * 1024 * 256 // 256KB + // Maximum size for the encrypted head block. + maximumEncryptedBlockSize = 2 * 1024 * 1024 // 2MB + // Default for KV based + defaultKVBlockSize = defaultMediumBlockSize + // max block size for now. + maxBlockSize = defaultLargeBlockSize + // Compact minimum threshold. + compactMinimum = 2 * 1024 * 1024 // 2MB + // FileStoreMinBlkSize is minimum size we will do for a blk size. + FileStoreMinBlkSize = 32 * 1000 // 32kib + // FileStoreMaxBlkSize is maximum size we will do for a blk size. + FileStoreMaxBlkSize = maxBlockSize + // Check for bad record length value due to corrupt data. + rlBadThresh = 32 * 1024 * 1024 + // Checksum size for hash for msg records. + recordHashSize = 8 + + // Above this number of subjects, index.db may not be written regularly anymore, and + // certain psim optimisations may not be used. + highCardinalityThreshold = 1_000_000 +) + +func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) { + return newFileStoreWithCreated(fcfg, cfg, time.Now().UTC(), nil, nil) +} + +func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created time.Time, prf, oldprf keyGen) (fs *fileStore, err error) { + return newFileStoreWithCreatedAndMode(fcfg, cfg, created, prf, oldprf, false) +} + +func newFileStoreWithCreatedAndMode(fcfg FileStoreConfig, cfg StreamConfig, created time.Time, prf, oldprf keyGen, recovering bool) (fs *fileStore, err error) { + if cfg.Name == _EMPTY_ { + return nil, fmt.Errorf("name required") + } + if cfg.Storage != FileStorage { + return nil, fmt.Errorf("fileStore requires file storage type in config") + } + // Default values. + if fcfg.BlockSize == 0 { + fcfg.BlockSize = dynBlkSize(cfg.Retention, cfg.MaxBytes, prf != nil) + } + if fcfg.BlockSize > maxBlockSize { + return nil, fmt.Errorf("filestore max block size is %s", friendlyBytes(maxBlockSize)) + } + if fcfg.CacheExpire == 0 { + fcfg.CacheExpire = defaultCacheBufferExpiration + } + if fcfg.SubjectStateExpire == 0 { + fcfg.SubjectStateExpire = defaultFssExpiration + } + if fcfg.SyncInterval == 0 { + fcfg.SyncInterval = defaultSyncInterval + } + dios := fcfg.srv.diskIOSemaphore() + + // Check the directory + if stat, err := os.Stat(fcfg.StoreDir); os.IsNotExist(err) { + if err := os.MkdirAll(fcfg.StoreDir, defaultDirPerms); err != nil { + return nil, fmt.Errorf("could not create storage directory - %v", err) + } + } else if stat == nil || !stat.IsDir() { + return nil, fmt.Errorf("storage directory is not a directory") + } + tmpfile, err := os.CreateTemp(fcfg.StoreDir, "_test_") + if err != nil { + return nil, fmt.Errorf("storage directory is not writable") + } + + tmpfile.Close() + dios.acquire() + os.Remove(tmpfile.Name()) + dios.release() + + fs = &fileStore{ + fcfg: fcfg, + dios: dios, + psim: stree.NewSubjectTree[psi](), + bim: make(map[uint32]*msgBlock), + cfg: FileStreamInfo{Created: created, StreamConfig: cfg}, + prf: prf, + oldprf: oldprf, + qch: make(chan struct{}), + fsld: make(chan struct{}), + srv: fcfg.srv, + recovering: recovering, + } + fs.syncAlways.Store(fcfg.SyncAlways && !fcfg.SyncOnFlush) + fs.syncOnFlush.Store(fcfg.SyncOnFlush) + + // Register with access time service. + ats.Register() + + // If we error before completion make sure to cleanup. + defer func() { + if err != nil { + ats.Unregister() + } + }() + + // Set flush in place to AsyncFlush which by default is false. + fs.fip = !fcfg.AsyncFlush + + // Check if this is a new setup. + mdir := filepath.Join(fcfg.StoreDir, msgDir) + odir := filepath.Join(fcfg.StoreDir, consumerDir) + if err := os.MkdirAll(mdir, defaultDirPerms); err != nil { + return nil, fmt.Errorf("could not create message storage directory - %v", err) + } + if err := os.MkdirAll(odir, defaultDirPerms); err != nil { + return nil, fmt.Errorf("could not create consumer storage directory - %v", err) + } + + // Create highway hash for message blocks. Use sha256 of directory as key. + key := sha256.Sum256([]byte(cfg.Name)) + fs.hh, err = highwayhash.NewDigest64(key[:]) + if err != nil { + return nil, fmt.Errorf("could not create hash: %v", err) + } + + keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey) + _, err = os.Stat(keyFile) + // Either the file should exist (err=nil), or it shouldn't. Any other error is reported. + if err != nil && !os.IsNotExist(err) { + return nil, err + } + // Make sure we do not have an encrypted store underneath of us but no main key. + if fs.prf == nil && err == nil { + return nil, errNoMainKey + } else if fs.prf != nil && err == nil { + // If encryption is configured and the key file exists, recover our keys. + if err = fs.recoverAEK(); err != nil { + return nil, err + } + } + + // Attempt to recover our state. + err = fs.recoverFullState() + if err != nil { + if !os.IsNotExist(err) { + fs.warn("Recovering stream state from index errored: %v", err) + } + // Hold onto state + prior := fs.state + // Reset anything that could have been set from above. + fs.state = StreamState{} + fs.psim, fs.tsl = fs.psim.Empty(), 0 + fs.bim = make(map[uint32]*msgBlock) + fs.blks = nil + fs.tombs = nil + + // Recover our message state the old way + if err := fs.recoverMsgs(); err != nil { + return nil, err + } + + fs.mu.Lock() + // Check if our prior state remembers a last sequence past where we can see. + // Unless we're async flushing, in which case this can happen if some blocks weren't flushed. + if prior.LastSeq > fs.state.LastSeq && !fs.fcfg.AsyncFlush { + if mb, err := fs.newMsgBlockForWrite(); err != nil { + fs.mu.Unlock() + return nil, err + } else if err = mb.writeTombstone(prior.LastSeq, prior.LastTime.UnixNano()); err != nil { + fs.mu.Unlock() + return nil, err + } + fs.state.LastSeq, fs.state.LastTime = prior.LastSeq, prior.LastTime + if fs.state.Msgs == 0 { + fs.state.FirstSeq = fs.state.LastSeq + 1 + fs.state.FirstTime = time.Time{} + } + } + // Since we recovered here, make sure to kick ourselves to write out our stream state. + fs.dirty++ + fs.mu.Unlock() + } + + // Lock during the remainder of the recovery. + fs.mu.Lock() + // Use defer to ensure the lock is released if any of the enforcement operations + // run into issues to avoid potential deadlocks on exit. + unlocked := false + defer func() { + if !unlocked { + fs.mu.Unlock() + } + }() + + // Recover per-message state like TTLs and schedules. + if err = fs.recoverPerMessageState(); err != nil { + fs.mu.Unlock() + return nil, err + } + + // Also make sure we get rid of old idx and fss files on return. + // Do this in separate go routine vs inline and at end of processing. + defer func() { + if fs != nil { + go fs.cleanupOldMeta() + } + }() + + // Check if we have any left over tombstones to process. + if len(fs.tombs) > 0 { + for _, seq := range fs.tombs { + _, err = fs.removeMsg(seq, false, true, false) + if err != nil && err != ErrStoreEOF && err != ErrStoreMsgNotFound { + return nil, err + } + fs.removeFromLostData(seq) + } + // Not needed after this phase. + fs.tombs = nil + } + + // Limits checks and enforcement. + if err = fs.enforceMsgLimit(); err != nil { + return nil, err + } + if err = fs.enforceBytesLimit(); err != nil { + return nil, err + } + + // Do age checks too, make sure to call in place. + if fs.cfg.MaxAge != 0 { + if err = fs.expireMsgsOnRecover(); err != nil { + return nil, err + } + fs.startAgeChk() + } + + // If we have max msgs per subject make sure the is also enforced. + if fs.cfg.MaxMsgsPer > 0 { + if err = fs.enforceMsgPerSubjectLimit(false); err != nil { + return nil, err + } + } + + // Grab first sequence for check below while we have lock. + firstSeq := fs.state.FirstSeq + fs.mu.Unlock() + unlocked = true + + // If the stream has an initial sequence number then make sure we + // have purged up until that point. We will do this only if the + // recovered first sequence number is before our configured first + // sequence. Need to do this locked as by now the age check timer + // has started. + if cfg.FirstSeq > 0 && firstSeq < cfg.FirstSeq { + if _, err := fs.purge(cfg.FirstSeq); err != nil { + return nil, err + } + } + + // Write our meta data if it does not exist or is zero'd out. + meta := filepath.Join(fcfg.StoreDir, JetStreamMetaFile) + fi, err := os.Stat(meta) + if err != nil && os.IsNotExist(err) || fi != nil && fi.Size() == 0 { + if err := fs.writeStreamMeta(); err != nil { + return nil, err + } + } + + // If we expect to be encrypted check that what we are restoring is not plaintext. + // This can happen on snapshot restores or conversions. + if fs.prf != nil { + if _, err := os.Stat(keyFile); err != nil && os.IsNotExist(err) { + if err := fs.writeStreamMeta(); err != nil { + return nil, err + } + } + } + + // Setup our sync timer. + fs.setSyncTimer() + + // Spin up the go routine that will write out our full state stream index. + go fs.flushStreamStateLoop(fs.qch, fs.fsld) + + return fs, nil +} + +// Lock all existing message blocks. +// Lock held on entry. +func (fs *fileStore) lockAllMsgBlocks() { + for _, mb := range fs.blks { + mb.mu.Lock() + } +} + +// Unlock all existing message blocks. +// Lock held on entry. +func (fs *fileStore) unlockAllMsgBlocks() { + for _, mb := range fs.blks { + mb.mu.Unlock() + } +} + +func (fs *fileStore) setCreatedTime(created time.Time) { + fs.mu.Lock() + fs.cfg.Created = created + fs.mu.Unlock() +} + +func (fs *fileStore) updateDurabilitySettingsLocked(syncOnFlush bool) { + if !fs.fcfg.SyncAlways { + return + } + // If the stream is backed by a Raft WAL we relax the SyncAlways + // setting in favor of SyncOnFlush. Enable the new sync mode + // before disabling the old one so writes are never processed + // with both modes disabled. + if syncOnFlush { + fs.syncOnFlush.Store(true) + fs.syncAlways.Store(false) + } else { + fs.syncAlways.Store(true) + fs.syncOnFlush.Store(false) + } +} + +func (fs *fileStore) resetDurabilitySettingsLocked() { + fs.updateDurabilitySettingsLocked(fs.cfg.Replicas > 1) +} + +// flushForScaleDown transitions a SyncOnFlush store back to SyncAlways. +func (fs *fileStore) flushForScaleDown() error { + fs.mu.Lock() + if !fs.fcfg.SyncAlways { + fs.mu.Unlock() + return fs.FlushAllPending() + } + // Turn off syncOnFlush, and enable sync after every write. + // First enable sync after every write, so that any inflight + // or subsequent write gets synced + fs.updateDurabilitySettingsLocked(false) + fs.mu.Unlock() + + // Sync all dirty blocks, so that we no longer have to rely + // on the backing Raft WAL. + fs.syncBlocks() + + fs.mu.RLock() + defer fs.mu.RUnlock() + return fs.werr +} + +func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error { + start := time.Now() + defer func() { + if took := time.Since(start); took > time.Minute { + fs.warn("UpdateConfig took %v", took.Round(time.Millisecond)) + } + }() + + if cfg.Name == _EMPTY_ { + return fmt.Errorf("name required") + } + if cfg.Storage != FileStorage { + return fmt.Errorf("fileStore requires file storage type in config") + } + if cfg.MaxMsgsPer < -1 { + cfg.MaxMsgsPer = -1 + } + + fs.mu.Lock() + if fs.isClosed() { + fs.mu.Unlock() + return ErrStoreClosed + } + new_cfg := FileStreamInfo{Created: fs.cfg.Created, StreamConfig: *cfg} + old_cfg := fs.cfg + // The reference story has changed here, so this full msg block lock + // may not be needed. + fs.lockAllMsgBlocks() + fs.cfg = new_cfg + fs.unlockAllMsgBlocks() + if err := fs.writeStreamMeta(); err != nil { + fs.lockAllMsgBlocks() + fs.cfg = old_cfg + fs.unlockAllMsgBlocks() + fs.mu.Unlock() + return err + } + + // Recover per-message state like TTLs and schedules. + if err := fs.recoverPerMessageState(); err != nil { + fs.mu.Unlock() + return err + } + // Limits checks and enforcement. + if err := fs.enforceMsgLimit(); err != nil { + fs.mu.Unlock() + return err + } + if err := fs.enforceBytesLimit(); err != nil { + fs.mu.Unlock() + return err + } + + // Do age timers. + if fs.ageChk == nil && fs.cfg.MaxAge != 0 { + fs.startAgeChk() + } + if fs.ageChk != nil && fs.cfg.MaxAge == 0 { + fs.ageChk.Stop() + fs.ageChk = nil + fs.ageChkTime = 0 + } + + if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer <= 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) { + if err := fs.enforceMsgPerSubjectLimit(true); err != nil { + fs.mu.Unlock() + return err + } + } + + fs.resetDurabilitySettingsLocked() + + if lmb := fs.lmb; lmb != nil { + // Enable/disable async flush depending on if it's supported and already initialized. + supportsAsyncFlush := !fs.fcfg.SyncAlways && cfg.Replicas > 1 + + // Async persist mode opts in to async flushing, + // sync always would also be disabled if it was configured. + if cfg.PersistMode == AsyncPersistMode { + supportsAsyncFlush = true + fs.fcfg.SyncAlways = false + fs.syncAlways.Store(false) + } + + if supportsAsyncFlush && !fs.fcfg.AsyncFlush { + fs.fcfg.AsyncFlush = true + lmb.spinUpFlushLoop() + } else if !supportsAsyncFlush && fs.fcfg.AsyncFlush { + fs.fcfg.AsyncFlush = false + lmb.mu.Lock() + // Quit the flush loop. + if lmb.qch != nil { + close(lmb.qch) + lmb.qch = nil + } + _, err := lmb.flushPendingMsgsLocked() + lmb.mu.Unlock() + if err != nil { + fs.mu.Unlock() + return err + } + } + // Set flush in place to AsyncFlush which by default is false. + fs.fip = !fs.fcfg.AsyncFlush + } + fs.mu.Unlock() + + if cfg.MaxAge != 0 || cfg.AllowMsgTTL { + fs.expireMsgs() + } + if cfg.AllowMsgSchedules { + fs.runMsgScheduling() + } + return nil +} + +func dynBlkSize(retention RetentionPolicy, maxBytes int64, encrypted bool) uint64 { + if maxBytes > 0 { + blkSize := (maxBytes / 4) + 1 // (25% overhead) + // Round up to nearest 100 + if m := blkSize % 100; m != 0 { + blkSize += 100 - m + } + if blkSize <= FileStoreMinBlkSize { + blkSize = FileStoreMinBlkSize + } else if blkSize >= FileStoreMaxBlkSize { + blkSize = FileStoreMaxBlkSize + } else { + blkSize = defaultMediumBlockSize + } + if encrypted && blkSize > maximumEncryptedBlockSize { + // Notes on this below. + blkSize = maximumEncryptedBlockSize + } + return uint64(blkSize) + } + + switch { + case encrypted: + // In the case of encrypted stores, large blocks can result in worsened perf + // since many writes on disk involve re-encrypting the entire block. For now, + // we will enforce a cap on the block size when encryption is enabled to avoid + // this. + return maximumEncryptedBlockSize + case retention == LimitsPolicy: + // TODO(dlc) - Make the blocksize relative to this if set. + return defaultLargeBlockSize + default: + // TODO(dlc) - Make the blocksize relative to this if set. + return defaultMediumBlockSize + } +} + +func genEncryptionKey(sc StoreCipher, seed []byte) (ek cipher.AEAD, err error) { + if sc == ChaCha { + ek, err = chacha20poly1305.NewX(seed) + } else if sc == AES { + block, e := aes.NewCipher(seed) + if e != nil { + return nil, e + } + ek, err = cipher.NewGCMWithNonceSize(block, block.BlockSize()) + } else { + err = errUnknownCipher + } + return ek, err +} + +// Generate an asset encryption key from the context and server PRF. +func (fs *fileStore) genEncryptionKeys(context string) (aek cipher.AEAD, bek cipher.Stream, seed, encrypted []byte, err error) { + if fs.prf == nil { + return nil, nil, nil, nil, errNoEncryption + } + // Generate key encryption key. + rb, err := fs.prf([]byte(context)) + if err != nil { + return nil, nil, nil, nil, err + } + + sc := fs.fcfg.Cipher + + kek, err := genEncryptionKey(sc, rb) + if err != nil { + return nil, nil, nil, nil, err + } + // Generate random asset encryption key seed. + + const seedSize = 32 + seed = make([]byte, seedSize) + if n, err := rand.Read(seed); err != nil { + return nil, nil, nil, nil, err + } else if n != seedSize { + return nil, nil, nil, nil, fmt.Errorf("not enough seed bytes read (%d != %d", n, seedSize) + } + + aek, err = genEncryptionKey(sc, seed) + if err != nil { + return nil, nil, nil, nil, err + } + + // Generate our nonce. Use same buffer to hold encrypted seed. + nonce := make([]byte, kek.NonceSize(), kek.NonceSize()+len(seed)+kek.Overhead()) + if n, err := rand.Read(nonce); err != nil { + return nil, nil, nil, nil, err + } else if n != len(nonce) { + return nil, nil, nil, nil, fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce)) + } + + bek, err = genBlockEncryptionKey(sc, seed[:], nonce) + if err != nil { + return nil, nil, nil, nil, err + } + + return aek, bek, seed, kek.Seal(nonce, nonce, seed, nil), nil +} + +// Will generate the block encryption key. +func genBlockEncryptionKey(sc StoreCipher, seed, nonce []byte) (cipher.Stream, error) { + if sc == ChaCha { + return chacha20.NewUnauthenticatedCipher(seed, nonce) + } else if sc == AES { + block, err := aes.NewCipher(seed) + if err != nil { + return nil, err + } + return cipher.NewCTR(block, nonce), nil + } + return nil, errUnknownCipher +} + +// Lock should be held. +func (fs *fileStore) recoverAEK() error { + if fs.prf != nil && fs.aek == nil { + ekey, err := os.ReadFile(filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)) + if err != nil { + return err + } + rb, err := fs.prf([]byte(fs.cfg.Name)) + if err != nil { + return err + } + kek, err := genEncryptionKey(fs.fcfg.Cipher, rb) + if err != nil { + return err + } + ns := kek.NonceSize() + seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil) + if err != nil { + return err + } + aek, err := genEncryptionKey(fs.fcfg.Cipher, seed) + if err != nil { + return err + } + fs.aek = aek + } + return nil +} + +// Lock should be held. +func (fs *fileStore) setupAEK() error { + if fs.prf != nil && fs.aek == nil { + key, _, _, encrypted, err := fs.genEncryptionKeys(fs.cfg.Name) + if err != nil { + return err + } + keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey) + if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) { + return err + } + err = fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms) + if err != nil { + return err + } + // Set our aek. + fs.aek = key + } + return nil +} + +// Write out meta and the checksum. +// Lock should be held. +func (fs *fileStore) writeStreamMeta() error { + if err := fs.setupAEK(); err != nil { + return err + } + + meta := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFile) + if _, err := os.Stat(meta); err != nil && !os.IsNotExist(err) { + return err + } + b, err := json.Marshal(fs.cfg) + if err != nil { + return err + } + // Encrypt if needed. + if fs.aek != nil { + nonce := make([]byte, fs.aek.NonceSize(), fs.aek.NonceSize()+len(b)+fs.aek.Overhead()) + if n, err := rand.Read(nonce); err != nil { + return err + } else if n != len(nonce) { + return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce)) + } + b = fs.aek.Seal(nonce, nonce, b, nil) + } + + err = fs.writeFileWithOptionalSync(meta, b, defaultFilePerms) + if err != nil { + return err + } + fs.hh.Reset() + fs.hh.Write(b) + var hb [highwayhash.Size64]byte + checksum := hex.EncodeToString(fs.hh.Sum(hb[:0])) + sum := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileSum) + err = fs.writeFileWithOptionalSync(sum, []byte(checksum), defaultFilePerms) + if err != nil { + return err + } + return nil +} + +// Pools to recycle the blocks to help with memory pressure. +var blkPoolTiny = &sync.Pool{ + New: func() any { + b := [defaultTinyBlockSize]byte{} + return &b + }, +} +var blkPoolSmall = &sync.Pool{ + New: func() any { + b := [defaultSmallBlockSize]byte{} + return &b + }, +} +var blkPoolMedium = &sync.Pool{ + New: func() any { + b := [defaultMediumBlockSize]byte{} + return &b + }, +} +var blkPoolBig = &sync.Pool{ + New: func() any { + b := [defaultLargeBlockSize]byte{} + return &b + }, +} + +// Get a new msg block based on sz estimate. +func getMsgBlockBuf(sz int) (buf []byte) { + switch { + case sz <= defaultTinyBlockSize: + return blkPoolTiny.Get().(*[defaultTinyBlockSize]byte)[:0] + case sz <= defaultSmallBlockSize: + return blkPoolSmall.Get().(*[defaultSmallBlockSize]byte)[:0] + case sz <= defaultMediumBlockSize: + return blkPoolMedium.Get().(*[defaultMediumBlockSize]byte)[:0] + case sz <= defaultLargeBlockSize: + return blkPoolBig.Get().(*[defaultLargeBlockSize]byte)[:0] + default: + // Ideally this should not happen, once we return a buffer that's + // larger than defaultLargeBlockSize then we will refuse to recycle + // it to stop the pools from bloating excessively. + return make([]byte, 0, sz) + } +} + +// registerRecycle arranges for the cache's buffer to be returned to the +// block buffer pools when the cache itself is garbage collected due to it +// remaining as a weak pointer. Without it, subsequent block loads would +// allocate fresh buffers, which could result in GC re-running, which is +// pathological under sustained block loads. +func (c *cache) registerRecycle() { + if c == nil { + return + } + // Any previous registration is canceled first. + c.stopRecycle() + // Skip if recycle isn't allowed or nothing to recycle. + if c.nra || cap(c.buf) == 0 { + return + } + // Don't make this cleanup conditional (e.g. skip if under memory pressure), GC + // will drain the block buffer pools separately, so this doesn't pin memory. + // Skipping would be worse, we'd delay freeing the buffer until the next GC cycle. + c.clean = runtime.AddCleanup(c, recycleMsgBlockBuf, c.buf) +} + +// stopRecycle cancels a pending recycle registration. Must be called before +// the buffer is recycled or handed off explicitly, otherwise the cleanup +// could return the same buffer to the pool a second time while it is in use. +func (c *cache) stopRecycle() { + if c.clean != (runtime.Cleanup{}) { + c.clean.Stop() + c.clean = runtime.Cleanup{} + } +} + +// Recycle the msg block. +func recycleMsgBlockBuf(buf []byte) { + switch cap(buf) { + case defaultTinyBlockSize: + b := (*[defaultTinyBlockSize]byte)(buf[0:defaultTinyBlockSize]) + blkPoolTiny.Put(b) + case defaultSmallBlockSize: + b := (*[defaultSmallBlockSize]byte)(buf[0:defaultSmallBlockSize]) + blkPoolSmall.Put(b) + case defaultMediumBlockSize: + b := (*[defaultMediumBlockSize]byte)(buf[0:defaultMediumBlockSize]) + blkPoolMedium.Put(b) + case defaultLargeBlockSize: + b := (*[defaultLargeBlockSize]byte)(buf[0:defaultLargeBlockSize]) + blkPoolBig.Put(b) + default: + // Too large, let the GC collect it instead. + } +} + +const ( + msgHdrSize = 22 + checksumSize = 8 + emptyRecordLen = msgHdrSize + checksumSize +) + +// Lock should be held. +func (fs *fileStore) noTrackSubjects() bool { + return !(fs.psim.Size() > 0 || len(fs.cfg.Subjects) > 0 || fs.cfg.Mirror != nil || len(fs.cfg.Sources) > 0) +} + +// Will init the basics for a message block. +func (fs *fileStore) initMsgBlock(index uint32) *msgBlock { + mb := &msgBlock{ + fs: fs, + index: index, + cexp: fs.fcfg.CacheExpire, + fexp: fs.fcfg.SubjectStateExpire, + noTrack: fs.noTrackSubjects(), + } + + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + mb.mfn = filepath.Join(mdir, fmt.Sprintf(blkScan, index)) + + if mb.hh == nil { + key := sha256.Sum256(fs.hashKeyForBlock(index)) + mb.hh, _ = highwayhash.NewDigest64(key[:]) + } + return mb +} + +// Check for encryption, we do not load keys on startup anymore so might need to load them here. +// Lock for fs should be held. +func (mb *msgBlock) checkAndLoadEncryption() error { + if mb.fs != nil && mb.fs.prf != nil && (mb.aek == nil || mb.bek == nil) { + if err := mb.fs.loadEncryptionForMsgBlock(mb); err != nil { + return err + } + } + return nil +} + +// Lock for fs should be held. +func (fs *fileStore) loadEncryptionForMsgBlock(mb *msgBlock) error { + if fs.prf == nil { + return nil + } + + var createdKeys bool + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + ekey, err := os.ReadFile(filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index))) + if err != nil { + // We do not seem to have keys even though we should. Could be a plaintext conversion. + // Create the keys and we will double check below. + if err := fs.genEncryptionKeysForBlock(mb); err != nil { + return err + } + createdKeys = true + } else { + if len(ekey) < minBlkKeySize { + return errBadKeySize + } + // Recover key encryption key. + rb, err := fs.prf([]byte(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index))) + if err != nil { + return err + } + + sc := fs.fcfg.Cipher + kek, err := genEncryptionKey(sc, rb) + if err != nil { + return err + } + ns := kek.NonceSize() + seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil) + if err != nil { + // We may be here on a cipher conversion, so attempt to convert. + if err = mb.convertCipher(); err != nil { + return err + } + } else { + mb.seed, mb.nonce = seed, ekey[:ns] + } + mb.aek, err = genEncryptionKey(sc, mb.seed) + if err != nil { + return err + } + if mb.bek, err = genBlockEncryptionKey(sc, mb.seed, mb.nonce); err != nil { + return err + } + } + + // If we created keys here, let's check the data and if it is plaintext convert here. + if createdKeys { + if err := mb.convertToEncrypted(); err != nil { + return err + } + } + + return nil +} + +// Load a last checksum if needed from the block file. +// Lock should be held. +func (mb *msgBlock) ensureLastChecksumLoaded() { + var empty [8]byte + if mb.lchk != empty { + return + } + copy(mb.lchk[0:], mb.lastChecksum()) +} + +// Lock held on entry +func (fs *fileStore) recoverMsgBlock(index uint32) (*msgBlock, error) { + mb := fs.initMsgBlock(index) + // Open up the message file, but we will try to recover from the index file. + // We will check that the last checksums match. + file, err := mb.openBlock() + if err != nil { + return nil, err + } + defer file.Close() + + if fi, err := file.Stat(); fi != nil { + mb.rbytes = uint64(fi.Size()) + } else { + return nil, err + } + + // Make sure encryption loaded if needed. + if err = fs.loadEncryptionForMsgBlock(mb); err != nil { + // If the encryption key is truncated or unrecoverable, return the block so it can be deleted. + if err == errBadKeySize || err == errKeyInvalid { + return mb, err + } + return nil, err + } + + // Grab last checksum from main block file. + var lchk [8]byte + if mb.rbytes >= checksumSize { + if mb.bek != nil { + // We pass nil, so get a buf from the block pool, we'll need to recycle it afterward. + buf, _ := mb.loadBlock(nil) + if len(buf) >= checksumSize { + mb.bek.XORKeyStream(buf, buf) + copy(lchk[0:], buf[len(buf)-checksumSize:]) + } + // We can recycle it now. + recycleMsgBlockBuf(buf) + } else if _, err = file.ReadAt(lchk[:], int64(mb.rbytes)-checksumSize); err != nil { + return nil, err + } + } + + if err = file.Close(); err != nil { + return nil, err + } + + // Read our index file. Use this as source of truth if possible. + // This not applicable in >= 2.10 servers. Here for upgrade paths from < 2.10. + if err := mb.readIndexInfo(); err == nil { + // Quick sanity check here. + // Note this only checks that the message blk file is not newer then this file, or is empty and we expect empty. + if (mb.rbytes == 0 && mb.msgs == 0) || bytes.Equal(lchk[:], mb.lchk[:]) { + if mb.msgs > 0 && !mb.noTrack && fs.psim != nil { + if err = fs.populateGlobalPerSubjectInfo(mb); err != nil { + return nil, err + } + // Try to dump any state we needed on recovery. + mb.tryForceExpireCacheLocked() + } + fs.addMsgBlock(mb) + return mb, nil + } + } + + // If we get data loss rebuilding the message block state record that with the fs itself. + ld, tombs, err := mb.rebuildState() + if err != nil { + return nil, err + } else if ld != nil { + fs.addLostData(ld) + } + // Collect all tombstones. + if len(tombs) > 0 { + fs.tombs = append(fs.tombs, tombs...) + } + + if mb.msgs > 0 && !mb.noTrack && fs.psim != nil { + if err = fs.populateGlobalPerSubjectInfo(mb); err != nil { + return nil, err + } + // Try to dump any state we needed on recovery. + mb.tryForceExpireCacheLocked() + } + + if err = mb.closeFDs(); err != nil { + return nil, err + } + fs.addMsgBlock(mb) + + return mb, nil +} + +func (fs *fileStore) lostData() *LostStreamData { + fs.mu.RLock() + defer fs.mu.RUnlock() + if fs.ld == nil { + return nil + } + nld := *fs.ld + return &nld +} + +// Lock should be held. +func (fs *fileStore) addLostData(ld *LostStreamData) { + if ld == nil { + return + } + if fs.ld != nil { + var added bool + for _, seq := range ld.Msgs { + if _, found := fs.ld.exists(seq); !found { + fs.ld.Msgs = append(fs.ld.Msgs, seq) + added = true + } + } + if added { + msgs := fs.ld.Msgs + slices.Sort(msgs) + fs.ld.Bytes += ld.Bytes + } + } else { + fs.ld = ld + } +} + +// Helper to see if we already have this sequence reported in our lost data. +func (ld *LostStreamData) exists(seq uint64) (int, bool) { + i := slices.IndexFunc(ld.Msgs, func(i uint64) bool { + return i == seq + }) + return i, i > -1 +} + +func (fs *fileStore) removeFromLostData(seq uint64) { + if fs.ld == nil { + return + } + if i, found := fs.ld.exists(seq); found { + fs.ld.Msgs = append(fs.ld.Msgs[:i], fs.ld.Msgs[i+1:]...) + if len(fs.ld.Msgs) == 0 { + fs.ld = nil + } + } +} + +func (fs *fileStore) rebuildState(ld *LostStreamData) { + fs.mu.Lock() + defer fs.mu.Unlock() + fs.rebuildStateLocked(ld) +} + +// Lock should be held. +func (fs *fileStore) rebuildStateLocked(ld *LostStreamData) { + fs.addLostData(ld) + + fs.state.Msgs, fs.state.Bytes = 0, 0 + fs.state.FirstSeq, fs.state.LastSeq = 0, 0 + + for _, mb := range fs.blks { + mb.mu.RLock() + fs.state.Msgs += mb.msgs + fs.state.Bytes += mb.bytes + fseq := atomic.LoadUint64(&mb.first.seq) + if fs.state.FirstSeq == 0 || (fseq < fs.state.FirstSeq && mb.first.ts != 0) { + fs.state.FirstSeq = fseq + if mb.first.ts == 0 { + fs.state.FirstTime = time.Time{} + } else { + fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC() + } + } + // Preserve last time, could have erased the last message in one block, and then + // have a tombstone with the proper timestamp afterward in another block + if lseq := atomic.LoadUint64(&mb.last.seq); lseq >= fs.state.LastSeq { + fs.state.LastSeq = lseq + if mb.last.ts == 0 { + fs.state.LastTime = time.Time{} + } else { + fs.state.LastTime = time.Unix(0, mb.last.ts).UTC() + } + } + mb.mu.RUnlock() + } +} + +// Attempt to convert the cipher used for this message block. +func (mb *msgBlock) convertCipher() error { + fs := mb.fs + sc := fs.fcfg.Cipher + + var osc StoreCipher + switch sc { + case ChaCha: + osc = AES + case AES: + osc = ChaCha + } + + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + ekey, err := os.ReadFile(filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index))) + if err != nil { + return err + } + if len(ekey) < minBlkKeySize { + return errBadKeySize + } + type prfWithCipher struct { + keyGen + StoreCipher + } + var prfs []prfWithCipher + if fs.prf != nil { + prfs = append(prfs, prfWithCipher{fs.prf, sc}) + prfs = append(prfs, prfWithCipher{fs.prf, osc}) + } + if fs.oldprf != nil { + prfs = append(prfs, prfWithCipher{fs.oldprf, sc}) + prfs = append(prfs, prfWithCipher{fs.oldprf, osc}) + } + + for _, prf := range prfs { + // Recover key encryption key. + rb, err := prf.keyGen([]byte(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index))) + if err != nil { + continue + } + kek, err := genEncryptionKey(prf.StoreCipher, rb) + if err != nil { + continue + } + ns := kek.NonceSize() + seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil) + if err != nil { + continue + } + nonce := ekey[:ns] + bek, err := genBlockEncryptionKey(prf.StoreCipher, seed, nonce) + if err != nil { + return err + } + + buf, _ := mb.loadBlock(nil) + bek.XORKeyStream(buf, buf) + // Check for compression, and make sure we can parse with old cipher and key file. + if nbuf, err := mb.decompressIfNeeded(buf); err != nil { + return err + } else if _, _, err = mb.rebuildStateFromBufLocked(nbuf, false); err != nil { + return err + } else if err = mb.indexCacheBuf(nbuf); err != nil { + return err + } + + // Reset the cache since we just read everything in. + mb.cache.stopRecycle() + mb.cache = nil + mb.ecache.Set(nil) + + // Generate new keys. If we error for some reason then we will put + // the old keyfile back. + if err := fs.genEncryptionKeysForBlock(mb); err != nil { + keyFile := filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index)) + writeFileWithSync(fs.dios, keyFile, ekey, defaultFilePerms) + return err + } + mb.bek.XORKeyStream(buf, buf) + mb.fs.dios.acquire() + err = os.WriteFile(mb.mfn, buf, defaultFilePerms) + mb.fs.dios.release() + if err != nil { + return err + } + return nil + } + return errKeyInvalid +} + +// Convert a plaintext block to encrypted. +func (mb *msgBlock) convertToEncrypted() error { + if mb.bek == nil { + return nil + } + buf, err := mb.loadBlock(nil) + if err != nil { + return err + } + // Check for compression. + if buf, err = mb.decompressIfNeeded(buf); err != nil { + return err + } else if _, _, err = mb.rebuildStateFromBufLocked(buf, false); err != nil { + return err + } else if err = mb.indexCacheBuf(buf); err != nil { + // This likely indicates this was already encrypted or corrupt. + mb.cache = nil + mb.ecache.Set(nil) + return err + } + // Undo cache from above for later. + mb.cache.stopRecycle() + mb.cache = nil + mb.ecache.Set(nil) + // Regenerate mb.bek so that the keystream offset is at zero. This matches + // what encryptOrDecryptIfNeeded does on read-back, otherwise re-entering + // convertToEncrypted with a previously-used mb.bek would write ciphertext at + // the wrong stream offset and silently corrupt the block. + if mb.bek, err = genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce); err != nil { + return err + } + mb.bek.XORKeyStream(buf, buf) + mb.fs.dios.acquire() + err = os.WriteFile(mb.mfn, buf, defaultFilePerms) + mb.fs.dios.release() + if err != nil { + return err + } + return nil +} + +// Return the mb's index. +func (mb *msgBlock) getIndex() uint32 { + mb.mu.RLock() + defer mb.mu.RUnlock() + return mb.index +} + +// Rebuild the state of the blk based on what we have on disk in the N.blk file. +// We will return any lost data, and we will return any delete tombstones we encountered. +func (mb *msgBlock) rebuildState() (*LostStreamData, []uint64, error) { + mb.mu.Lock() + defer mb.mu.Unlock() + return mb.rebuildStateLocked() +} + +// Rebuild the state of the blk based on what we have on disk in the N.blk file. +// Lock should be held. +func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, []uint64, error) { + // Remove the .fss file and clear any cache we have set. + mb.clearCacheAndOffset() + + buf, err := mb.loadBlock(nil) + defer recycleMsgBlockBuf(buf) + + if err != nil || len(buf) == 0 { + // Only allow continuing to mark lost data if the file itself doesn't exist, or was empty. + if err != nil && err != errNoBlkData { + return nil, nil, err + } + var ld *LostStreamData + // No data to rebuild from here. + if mb.msgs > 0 { + // We need to declare lost data here. + ld = &LostStreamData{Msgs: make([]uint64, 0, mb.msgs), Bytes: mb.bytes} + firstSeq, lastSeq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq) + for seq := firstSeq; seq <= lastSeq; seq++ { + if !mb.dmap.Exists(seq) { + ld.Msgs = append(ld.Msgs, seq) + } + } + // Clear invalid state. We will let this blk be added in here. + mb.msgs, mb.bytes, mb.rbytes, mb.fss = 0, 0, 0, nil + mb.dmap.Empty() + atomic.StoreUint64(&mb.first.seq, atomic.LoadUint64(&mb.last.seq)+1) + } + return ld, nil, nil + } + + // Check if we need to decrypt. + if err = mb.encryptOrDecryptIfNeeded(buf); err != nil { + return nil, nil, err + } + // Check for compression. + if buf, err = mb.decompressIfNeeded(buf); err != nil { + return nil, nil, err + } + return mb.rebuildStateFromBufLocked(buf, true) +} + +// Lock should be held. +func (mb *msgBlock) rebuildStateFromBufLocked(buf []byte, allowTruncate bool) (*LostStreamData, []uint64, error) { + var err error + startLastSeq := atomic.LoadUint64(&mb.last.seq) + + // Clear state we need to rebuild. + mb.msgs, mb.bytes, mb.rbytes, mb.fss = 0, 0, 0, nil + atomic.StoreUint64(&mb.last.seq, 0) + mb.last.ts = 0 + firstNeedsSet := true + + mb.rbytes = uint64(len(buf)) + + addToDmap := func(seq uint64) { + if seq == 0 { + return + } + mb.dmap.Insert(seq) + } + + // For tombstones that we find and collect. + var ( + tombstones []uint64 + maxTombstoneSeq uint64 + maxTombstoneTs int64 + ) + + defer func() { + // For empty msg blocks make sure we recover last seq correctly based off of first. + // Or if we seem to have no messages but had a tombstone, which we use to remember + // sequences and timestamps now, use that to properly setup the first and last. + if mb.msgs == 0 { + fseq := atomic.LoadUint64(&mb.first.seq) + if fseq > 0 { + atomic.StoreUint64(&mb.last.seq, fseq-1) + } else if fseq == 0 && maxTombstoneSeq > 0 { + atomic.StoreUint64(&mb.first.seq, maxTombstoneSeq+1) + mb.first.ts = 0 + if mb.last.seq == 0 { + atomic.StoreUint64(&mb.last.seq, maxTombstoneSeq) + mb.last.ts = maxTombstoneTs + } + } + } + }() + + var le = binary.LittleEndian + + // There are cases where we're not allowed to truncate, like for an encrypted or compressed + // block since the index will be the decrypted and decompressed index. + truncate := func(index uint32) error { + var fd *os.File + if mb.mfd != nil { + fd = mb.mfd + } else { + mb.fs.dios.acquire() + fd, err = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms) + mb.fs.dios.release() + if err == nil { + defer fd.Close() + } + } + if fd == nil { + return nil + } + if err := fd.Truncate(int64(index)); err != nil { + return err + } + + // Update our checksum. + if index >= 8 { + var lchk [8]byte + if _, err = fd.ReadAt(lchk[:], int64(index-8)); err != nil { + return err + } + copy(mb.lchk[0:], lchk[:]) + } + if err = fd.Sync(); err != nil { + return err + } + return nil + } + + gatherLost := func(lb uint32) *LostStreamData { + var ld LostStreamData + for seq := atomic.LoadUint64(&mb.last.seq) + 1; seq <= startLastSeq; seq++ { + ld.Msgs = append(ld.Msgs, seq) + } + ld.Bytes = uint64(lb) + return &ld + } + + // To detect gaps from compaction, and to ensure the sequence keeps moving up. + var last uint64 + var hb [highwayhash.Size64]byte + + updateLast := func(seq uint64, ts int64) { + // The sequence needs to only ever move up. + if seq <= last { + return + } + + // Check for any gaps from compaction, meaning no ebit entry. + if last > 0 && seq != last+1 && mb.msgs != 0 { + for dseq := last + 1; dseq < seq; dseq++ { + addToDmap(dseq) + } + } + last = seq + atomic.StoreUint64(&mb.last.seq, last) + mb.last.ts = ts + } + + for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; { + if index+msgHdrSize > lbuf { + err = errBadMsg{mb.mfn, fmt.Sprintf("message overrun (index %d lbuf %d)", index, lbuf)} + if allowTruncate { + if err = truncate(index); err != nil { + return nil, nil, err + } + } + return gatherLost(lbuf - index), tombstones, err + } + + hdr := buf[index : index+msgHdrSize] + rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:])) + + hasHeaders := rl&hbit != 0 + // Clear any headers bit that could be set. + rl &^= hbit + shlen := slen + if hasHeaders { + shlen += 4 + } + dlen := int(rl) - msgHdrSize + // Do some quick sanity checks here. + if dlen < 0 || shlen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh { + err = errBadMsg{mb.mfn, fmt.Sprintf("sanity check failed (dlen %d slen %d rl %d index %d lbuf %d)", dlen, slen, rl, index, lbuf)} + if allowTruncate { + if err = truncate(index); err != nil { + return nil, nil, err + } + } + return gatherLost(lbuf - index), tombstones, err + } + + // Check for checksum failures before additional processing. + data := buf[index+msgHdrSize : index+rl] + subj := data[:slen] + if hh := mb.hh; hh != nil { + hh.Reset() + hh.Write(hdr[4:20]) + hh.Write(subj) + if hasHeaders { + hh.Write(data[slen+4 : dlen-recordHashSize]) + } else { + hh.Write(data[slen : dlen-recordHashSize]) + } + checksum := hh.Sum(hb[:0]) + if !bytes.Equal(checksum, data[len(data)-recordHashSize:]) { + err = errBadMsg{mb.mfn, "invalid checksum"} + if allowTruncate { + if err = truncate(index); err != nil { + return nil, nil, err + } + } + return gatherLost(lbuf - index), tombstones, err + } + copy(mb.lchk[0:], checksum) + } + + // Grab our sequence and timestamp. + seq := le.Uint64(hdr[4:]) + ts := int64(le.Uint64(hdr[12:])) + + // Check if this is a delete tombstone. + if seq&tbit != 0 { + seq = seq &^ tbit + // Need to process this here and make sure we have accounted for this properly. + tombstones = append(tombstones, seq) + if maxTombstoneSeq == 0 || seq > maxTombstoneSeq { + maxTombstoneSeq, maxTombstoneTs = seq, ts + } + index += rl + continue + } + + fseq := atomic.LoadUint64(&mb.first.seq) + // This is an old erased message, or a new one that we can track. + if seq == 0 || seq&ebit != 0 || seq < fseq { + seq = seq &^ ebit + if seq >= fseq { + updateLast(seq, ts) + if mb.msgs == 0 { + atomic.StoreUint64(&mb.first.seq, seq+1) + mb.first.ts = 0 + } else if seq != 0 { + // Only add to dmap if past recorded first seq and non-zero. + addToDmap(seq) + } + } + index += rl + continue + } + + // This is for when we have index info that adjusts for deleted messages + // at the head. So the first.seq will be already set here. If this is larger + // replace what we have with this seq. + if firstNeedsSet && seq >= fseq { + atomic.StoreUint64(&mb.first.seq, seq) + firstNeedsSet, mb.first.ts = false, ts + } + + // The sequence needs to only ever move up. + if seq <= last { + // Advance to next record. + // We've already accounted for this sequence and marked it as deleted. + index += rl + continue + } + if !mb.dmap.Exists(seq) { + mb.msgs++ + mb.bytes += uint64(rl) + } + + updateLast(seq, ts) + + // Advance to next record. + index += rl + } + + // For empty msg blocks make sure we recover last seq correctly based off of first. + // Or if we seem to have no messages but had a tombstone, which we use to remember + // sequences and timestamps now, use that to properly setup the first and last. + if mb.msgs == 0 { + fseq := atomic.LoadUint64(&mb.first.seq) + if fseq > 0 { + atomic.StoreUint64(&mb.last.seq, fseq-1) + } else if fseq == 0 && maxTombstoneSeq > 0 { + atomic.StoreUint64(&mb.first.seq, maxTombstoneSeq+1) + mb.first.ts = 0 + if lseq := atomic.LoadUint64(&mb.last.seq); lseq == 0 { + atomic.StoreUint64(&mb.last.seq, maxTombstoneSeq) + mb.last.ts = maxTombstoneTs + } + } + } + + return nil, tombstones, nil +} + +// For doing warn logging. +// Lock should be held. +func (fs *fileStore) warn(format string, args ...any) { + // No-op if no server configured. + if fs.srv == nil { + return + } + fs.srv.Warnf(fmt.Sprintf("Filestore [%s] %s", fs.cfg.Name, format), args...) +} + +// For doing rate-limited warn logging. +// Lock should be held. +func (fs *fileStore) rateLimitWarn(format string, args ...any) { + // No-op if no server configured. + if fs.srv == nil { + return + } + fs.srv.RateLimitWarnf(fmt.Sprintf("Filestore [%s] %s", fs.cfg.Name, format), args...) +} + +// For doing error logging. +// Lock should be held. +func (fs *fileStore) error(format string, args ...any) { + // No-op if no server configured. + if fs.srv == nil { + return + } + fs.srv.Errorf(fmt.Sprintf("Filestore [%s] %s", fs.cfg.Name, format), args...) +} + +// For doing debug logging. +// Lock should be held. +func (fs *fileStore) debug(format string, args ...any) { + // No-op if no server configured. + if fs.srv == nil { + return + } + fs.srv.Debugf(fmt.Sprintf("Filestore [%s] %s", fs.cfg.Name, format), args...) +} + +// Track local state but ignore timestamps here. +func updateTrackingState(state *StreamState, mb *msgBlock) { + first := atomic.LoadUint64(&mb.first.seq) + last := atomic.LoadUint64(&mb.last.seq) + if state.FirstSeq == 0 { + state.FirstSeq = first + } else if first < state.FirstSeq && mb.first.ts != 0 { + state.FirstSeq = first + } + if last > state.LastSeq { + state.LastSeq = last + } + state.Msgs += mb.msgs + state.Bytes += mb.bytes +} + +// Determine if our tracking states are the same. +func trackingStatesEqual(fs, mb *StreamState) bool { + // When a fs is brand new the fs state will have first seq of 0, but tracking mb may have 1. + // If either has a first sequence that is not 0 or 1 we will check if they are the same, otherwise skip. + if (fs.FirstSeq > 1 && mb.FirstSeq > 1) || mb.FirstSeq > 1 { + return fs.Msgs == mb.Msgs && fs.FirstSeq == mb.FirstSeq && fs.LastSeq == mb.LastSeq && fs.Bytes == mb.Bytes + } + return fs.Msgs == mb.Msgs && fs.LastSeq == mb.LastSeq && fs.Bytes == mb.Bytes +} + +// recoverFullState will attempt to receover our last full state and re-process any state changes +// that happened afterwards. +func (fs *fileStore) recoverFullState() (rerr error) { + fs.mu.Lock() + defer fs.mu.Unlock() + + // Check for any left over purged messages. + fs.dios.acquire() + if err := fs.recoverPartialPurge(); err != nil { + fs.dios.release() + return err + } + // Grab our stream state file and load it in. + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile) + buf, err := os.ReadFile(fn) + fs.dios.release() + + if err != nil { + if !os.IsNotExist(err) { + fs.warn("Could not read stream state file: %v", err) + } + return err + } + + const minLen = 32 + if len(buf) < minLen { + _ = os.Remove(fn) + fs.warn("Stream state too short (%d bytes)", len(buf)) + return errCorruptState + } + + // The highwayhash will be on the end. Check that it still matches. + h := buf[len(buf)-highwayhash.Size64:] + buf = buf[:len(buf)-highwayhash.Size64] + fs.hh.Reset() + fs.hh.Write(buf) + var hb [highwayhash.Size64]byte + if !bytes.Equal(h, fs.hh.Sum(hb[:0])) { + _ = os.Remove(fn) + fs.warn("Stream state checksum did not match") + return errCorruptState + } + + // Decrypt if needed. + // We can be setup for encryption but if this is a snapshot restore we will be missing the keyfile + // since snapshots strip encryption. + if fs.prf != nil && fs.aek != nil { + ns := fs.aek.NonceSize() + buf, err = fs.aek.Open(nil, buf[:ns], buf[ns:], nil) + if err != nil { + fs.warn("Stream state error reading encryption key: %v", err) + return err + } + } + + version := buf[1] + if buf[0] != fullStateMagic || version < fullStateMinVersion || version > fullStateVersion { + _ = os.Remove(fn) + fs.warn("Stream state magic and version mismatch") + return errCorruptState + } + + bi := hdrLen + + readU64 := func() uint64 { + if bi < 0 { + return 0 + } + v, n := binary.Uvarint(buf[bi:]) + if n <= 0 { + bi = -1 + return 0 + } + bi += n + return v + } + readI64 := func() int64 { + if bi < 0 { + return 0 + } + v, n := binary.Varint(buf[bi:]) + if n <= 0 { + bi = -1 + return -1 + } + bi += n + return v + } + + setTime := func(t *time.Time, ts int64) { + if ts == 0 { + *t = time.Time{} + } else { + *t = time.Unix(0, ts).UTC() + } + } + + var state StreamState + state.Msgs = readU64() + state.Bytes = readU64() + state.FirstSeq = readU64() + baseTime := readI64() + setTime(&state.FirstTime, baseTime) + state.LastSeq = readU64() + setTime(&state.LastTime, readI64()) + + // Check for per subject info. + if numSubjects := int(readU64()); numSubjects > 0 { + fs.psim, fs.tsl = fs.psim.Empty(), 0 + for i := 0; i < numSubjects; i++ { + if lsubj := int(readU64()); lsubj > 0 { + if bi+lsubj > len(buf) { + _ = os.Remove(fn) + fs.warn("Stream state bad subject len (%d)", lsubj) + return errCorruptState + } + // If we have lots of subjects this will alloc for each one. + // We could reference the underlying buffer, but we could guess wrong if + // number of blocks is large and subjects is low, since we would reference buf. + subj := buf[bi : bi+lsubj] + // We had a bug that could cause memory corruption in the PSIM that could have gotten stored to disk. + // Only would affect subjects, so do quick check. + if !isValidSubject(bytesToString(subj), true) { + _ = os.Remove(fn) + fs.warn("Stream state corrupt subject detected") + return errCorruptState + } + bi += lsubj + psi := psi{total: readU64(), fblk: uint32(readU64())} + if psi.total > 1 || version >= 4 { + psi.lblk = uint32(readU64()) + } else { + psi.lblk = psi.fblk + } + fs.psim.Insert(subj, psi) + fs.tsl += lsubj + } + } + } + + // Track the state as represented by the blocks themselves. + var mstate StreamState + + if numBlocks := readU64(); numBlocks > 0 { + lastIndex := int(numBlocks - 1) + fs.blks = make([]*msgBlock, 0, numBlocks) + for i := 0; i < int(numBlocks); i++ { + index, nbytes, fseq, fts, lseq, lts, numDeleted := uint32(readU64()), readU64(), readU64(), readI64(), readU64(), readI64(), readU64() + var ttls uint64 + if version >= 2 { + ttls = readU64() + } + var schedules uint64 + if version >= 3 { + schedules = readU64() + } + if bi < 0 { + _ = os.Remove(fn) + return errCorruptState + } + mb := fs.initMsgBlock(index) + atomic.StoreUint64(&mb.first.seq, fseq) + atomic.StoreUint64(&mb.last.seq, lseq) + mb.msgs, mb.bytes = lseq-fseq+1, nbytes + mb.first.ts, mb.last.ts = fts+baseTime, lts+baseTime + mb.ttls = ttls + mb.schedules = schedules + if numDeleted > 0 { + dmap, n, err := avl.Decode(buf[bi:]) + if err != nil { + _ = os.Remove(fn) + fs.warn("Stream state error decoding avl dmap: %v", err) + return errCorruptState + } + mb.dmap = *dmap + if mb.msgs > numDeleted { + mb.msgs -= numDeleted + } else { + mb.msgs = 0 + } + bi += n + } + + // Pre-emptively mark block as closed, we'll confirm this block + // still exists on disk and report it as lost if not. + mb.closed = true + + // Only add in if not empty or the lmb. + if mb.msgs > 0 || i == lastIndex { + fs.addMsgBlock(mb) + updateTrackingState(&mstate, mb) + } else { + // Mark dirty to cleanup. + fs.dirty++ + } + } + } + + // Pull in last block index for the block that had last checksum when we wrote the full state. + blkIndex := uint32(readU64()) + var lchk [8]byte + if bi+len(lchk) > len(buf) { + bi = -1 + } else { + copy(lchk[0:], buf[bi:bi+len(lchk)]) + } + + // Check if we had any errors. + if bi < 0 { + _ = os.Remove(fn) + fs.warn("Stream state has no checksum present") + return errCorruptState + } + + // Move into place our state, msgBlks and subject info. + fs.state = state + + // First let's check the happy path, open the blk file that was the lmb when we created the full state. + // See if we have the last block available. + var matched bool + mb := fs.lmb + if mb == nil || mb.index != blkIndex { + _ = os.Remove(fn) + fs.warn("Stream state block does not exist or index mismatch") + return errCorruptState + } + if _, err := os.Stat(mb.mfn); err != nil && os.IsNotExist(err) { + // If our saved state is past what we see on disk, fallback and rebuild. + if ld, _, _ := mb.rebuildState(); ld != nil { + fs.addLostData(ld) + } + fs.warn("Stream state detected prior state, could not locate msg block %d", blkIndex) + return errPriorState + } + if matched = bytes.Equal(mb.lastChecksum(), lchk[:]); !matched { + // Detected a stale index.db, we didn't write it upon shutdown so can't rely on it being correct. + fs.warn("Stream state outdated, last block has additional entries, will rebuild") + return errPriorState + } + + // We need to see if any blocks exist after our last one even though we matched the last record exactly. + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + var dirs []os.DirEntry + + fs.dios.acquire() + if f, err := os.Open(mdir); err == nil { + dirs, _ = f.ReadDir(-1) + f.Close() + } + fs.dios.release() + + var index uint32 + for _, fi := range dirs { + // Ensure it's actually a block file, otherwise fmt.Sscanf also matches %d.blk.tmp + if !strings.HasSuffix(fi.Name(), blkSuffix) { + continue + } + if n, err := fmt.Sscanf(fi.Name(), blkScan, &index); err == nil && n == 1 { + if index > blkIndex { + fs.warn("Stream state outdated, found extra blocks, will rebuild") + return errPriorState + } else if mb, ok := fs.bim[index]; ok { + mb.closed = false + } + } + } + + var rebuild bool + for _, mb := range fs.blks { + if mb.closed { + rebuild = true + if ld, _, _ := mb.rebuildState(); ld != nil { + fs.addLostData(ld) + } + fs.warn("Stream state detected prior state, could not locate msg block %d", mb.index) + } + } + if rebuild { + return errPriorState + } + + // We check first and last seq and number of msgs and bytes. If there is a difference, + // return and error so we rebuild from the message block state on disk. + if !trackingStatesEqual(&fs.state, &mstate) { + _ = os.Remove(fn) + fs.warn("Stream state encountered internal inconsistency on recover") + return errCorruptState + } + + return nil +} + +// Recover per-message state like TTLs and schedules. Doing a linear scan through the stream +// only once if both states need to be recovered from the same sequences. +// Lock should be held. +func (fs *fileStore) recoverPerMessageState() error { + scanSeq := uint64(math.MaxUint64) + var ttlSeq, schedSeq, sourcesSeq uint64 + var ttlRecover, schedRecover, sourcesRecover bool + allowMsgTTL, allowMsgSchedules := fs.cfg.AllowMsgTTL, fs.cfg.AllowMsgSchedules + + var sources map[string]*StreamSource + if len(fs.cfg.Sources) > 0 { + sources = make(map[string]*StreamSource, len(fs.cfg.Sources)) + for _, ssi := range fs.cfg.Sources { + sources[ssi.composeIName()] = ssi + } + } + + // Create or delete the THW if needed. + if allowMsgTTL && fs.ttls == nil { + ttlSeq = fs.recoverTTLState() + ttlRecover = true + scanSeq = min(scanSeq, ttlSeq) + } else if !allowMsgTTL && fs.ttls != nil { + fs.ttls = nil + } + + // Create or delete the message scheduling state if needed. + if allowMsgSchedules && fs.scheduling == nil { + schedSeq = fs.recoverMsgSchedulingState() + schedRecover = true + scanSeq = min(scanSeq, schedSeq) + } else if !allowMsgSchedules && fs.scheduling != nil { + fs.scheduling = nil + } + + // Create or delete the source tracking if needed. + var sourcesScan map[string]*StreamSource + if sources != nil && fs.sources == nil { + sourcesSeq = fs.recoverSourcesState() + sourcesRecover = true + scanSeq = min(scanSeq, sourcesSeq) + if sourcesSeq <= fs.state.LastSeq { + sourcesScan = sources + } + } + if len(sources) > 0 { + // Ensure the map exists even on a fresh stream that has no persisted + // index yet, so storeRawMsg can validate headers against the configured + // set (zero sequence == configured but not yet observed). + if fs.sources == nil { + fs.sources = make(map[string]*StreamSourceState, len(sources)) + } + // Remove sources that are tracked but no longer exist. + for k := range fs.sources { + if _, ok := sources[k]; !ok { + delete(fs.sources, k) + } + } + // Seed sources that aren't in the map yet. + for k, v := range sources { + if _, ok := fs.sources[k]; !ok { + fs.sources[k] = &StreamSourceState{} + // Since we're adding a new source, we'll need to recover its sequence (if any). + sourcesSeq = fs.state.FirstSeq + sourcesRecover = true + scanSeq = min(scanSeq, sourcesSeq) + if sourcesScan == nil { + sourcesScan = make(map[string]*StreamSource) + } + if _, ok = sourcesScan[k]; !ok { + sourcesScan[k] = v + } + } + } + } + // No sources configured anymore, clear and remove the index from disk. + if fs.sources != nil && len(sources) == 0 { + fs.sources = nil + _ = os.Remove(fs.sourcesStatePath()) + } + + // Short-circuit if no recovering is required. + if !ttlRecover && !schedRecover && !sourcesRecover { + return nil + } + + if allowMsgTTL { + defer fs.resetAgeChk(0) + } + if allowMsgSchedules { + defer fs.scheduling.resetTimer() + } + + // If only sources need recovering, then we can optimize to a backward scan. + if sourcesRecover && !ttlRecover && !schedRecover { + if fs.state.Msgs > 0 && sourcesSeq <= fs.state.LastSeq { + fs.warn("Sourcing state is outdated; attempting to recover using backward scan (seq %d to %d)", sourcesSeq, fs.state.LastSeq) + if err := fs.recoverSourcesBackwardScan(sourcesSeq, sourcesScan); err != nil { + return err + } + } + return nil + } + + if fs.state.Msgs > 0 && scanSeq <= fs.state.LastSeq { + if ttlRecover && ttlSeq <= fs.state.LastSeq { + fs.warn("TTL state is outdated; attempting to recover using linear scan (seq %d to %d)", ttlSeq, fs.state.LastSeq) + } + if schedRecover && schedSeq <= fs.state.LastSeq { + fs.warn("Message scheduling state is outdated; attempting to recover using linear scan (seq %d to %d)", schedSeq, fs.state.LastSeq) + } + if sourcesRecover && sourcesSeq <= fs.state.LastSeq { + fs.warn("Sourcing state is outdated; attempting to recover using linear scan (seq %d to %d)", sourcesSeq, fs.state.LastSeq) + } + // Only consider sources that need recovering. + update := func(iName string, sseq uint64, ident string) { + if _, ok := sourcesScan[iName]; !ok { + return + } + if sss, ok := fs.sources[iName]; ok { + sss.Seq, sss.Ident = sseq, ident + } + } + var ( + mb *msgBlock + sm StoreMsg + mblseq uint64 + ) + for seq := scanSeq; seq <= fs.state.LastSeq; seq++ { + retry: + if mb == nil { + if mb = fs.selectMsgBlock(seq); mb == nil { + // Selecting the message block should return a block that contains this sequence, + // or a later block if it can't be found. + // It's an error if we can't find any block within the bounds of first and last seq. + fs.warn("Error loading msg block with seq %d for recovering per-message state", seq) + continue + } + seq = atomic.LoadUint64(&mb.first.seq) + mblseq = atomic.LoadUint64(&mb.last.seq) + } + // Check if this block can be skipped. + ttlBlock := ttlRecover && mblseq >= ttlSeq + schedBlock := schedRecover && mblseq >= schedSeq + sourcesBlock := sourcesRecover && mblseq >= sourcesSeq + if !sourcesBlock && (!ttlBlock || mb.ttls == 0) && (!schedBlock || mb.schedules == 0) { + // None of the messages in the block have state that needs recovering, so + // don't bother doing anything further with this block, skip to the end. + seq = mblseq + 1 + } + if seq > mblseq { + // We've reached the end of the loaded block, so let's go back to the + // beginning and process the next block. + mb.tryForceExpireCache() + mb = nil + if seq <= fs.state.LastSeq { + goto retry + } + // Done. + break + } + mb.mu.Lock() + msg, _, err := mb.fetchMsgNoCopyLocked(seq, &sm) + if err != nil { + mb.finishedWithCache() + mb.mu.Unlock() + if err == ErrStoreMsgNotFound || err == errDeletedMsg { + continue + } + fs.warn("Error loading msg seq %d for recovering per-message state: %s", seq, err) + return err + } + if len(msg.hdr) > 0 { + if ttlRecover && seq >= ttlSeq { + if ttl, _ := getMessageTTL(msg.hdr); ttl > 0 { + expires := time.Duration(msg.ts) + (time.Second * time.Duration(ttl)) + fs.ttls.Add(seq, int64(expires)) + } + } + if schedRecover && seq >= schedSeq { + if schedule, apiErr := nextMessageSchedule(msg.hdr, msg.ts); apiErr == nil && !schedule.IsZero() { + // Copy the subject, as it's stored in the scheduling maps and the backing cache could be reused in the meantime. + fs.scheduling.init(seq, copyString(msg.subj), schedule.UnixNano()) + } + } + if sourcesRecover && seq >= sourcesSeq { + if ss := sliceHeader(JSStreamSource, msg.hdr); len(ss) > 0 { + streamName, iName, sseq, ident := streamAndSeq(bytesToString(ss)) + if iName == _EMPTY_ { + // Pre-2.10 message header means it's a match for any source using that stream name. + // Such headers carry no identity, so it is left unset. + for _, ssi := range fs.cfg.Sources { + if streamName == ssi.Name || (ssi.External != nil && streamName == ssi.Name+":"+getHash(ssi.External.ApiPrefix)) { + update(ssi.composeIName(), sseq, _EMPTY_) + } + } + } else { + update(iName, sseq, ident) + } + } + } + } + mb.finishedWithCache() + mb.mu.Unlock() + } + } + return nil +} + +// Lock should be held. +func (fs *fileStore) recoverTTLState() uint64 { + // See if we have a timed hash wheel for TTLs. + fs.dios.acquire() + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, ttlStreamStateFile) + buf, err := os.ReadFile(fn) + fs.dios.release() + + if err != nil && !os.IsNotExist(err) { + fs.warn("Recovering TTL state from index errored: %v", err) + } + + fs.ttls = thw.NewHashWheel() + + var ttlseq uint64 + if err == nil { + ttlseq, err = fs.ttls.Decode(buf) + if err != nil { + fs.warn("Error decoding TTL state: %s", err) + // Remove the file, and reset collected state (if any). + _ = os.Remove(fn) + fs.ttls = thw.NewHashWheel() + } + } + + if ttlseq < fs.state.FirstSeq { + ttlseq = fs.state.FirstSeq + } + return ttlseq +} + +// Lock should be held. +func (fs *fileStore) recoverMsgSchedulingState() uint64 { + // See if we have a schedule index file. + fs.dios.acquire() + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, msgSchedulingStreamStateFile) + buf, err := os.ReadFile(fn) + fs.dios.release() + + if err != nil && !os.IsNotExist(err) { + fs.warn("Recovering message scheduling state from index errored: %v", err) + } + + fs.scheduling = newMsgScheduling(fs.runMsgScheduling) + fs.scheduling.paused = fs.recovering + + var schedSeq uint64 + if err == nil { + schedSeq, err = fs.scheduling.decode(buf) + if err != nil { + fs.warn("Error decoding message scheduling state: %s", err) + // Remove the file, and reset collected state (if any). + _ = os.Remove(fn) + fs.scheduling = newMsgScheduling(fs.runMsgScheduling) + fs.scheduling.paused = fs.recovering + } + } + + if schedSeq < fs.state.FirstSeq { + schedSeq = fs.state.FirstSeq + } + return schedSeq +} + +// Grabs last checksum for the named block file. +// Takes into account encryption etc. +func (mb *msgBlock) lastChecksum() []byte { + f, err := mb.openBlock() + if err != nil { + return nil + } + defer f.Close() + + var lchk [8]byte + if fi, _ := f.Stat(); fi != nil { + mb.rbytes = uint64(fi.Size()) + } + if mb.rbytes < checksumSize { + return lchk[:] + } + // Encrypted? + if err = mb.checkAndLoadEncryption(); err != nil { + return nil + } + if mb.bek != nil { + if buf, _ := mb.loadBlock(nil); len(buf) >= checksumSize { + if err = mb.encryptOrDecryptIfNeeded(buf); err != nil { + return nil + } + copy(lchk[0:], buf[len(buf)-checksumSize:]) + } else { + return nil + } + } else if _, err = f.ReadAt(lchk[:], int64(mb.rbytes)-checksumSize); err != nil { + return nil + } + return lchk[:] +} + +// This will make sure we clean up old idx and fss files. +func (fs *fileStore) cleanupOldMeta() { + fs.mu.RLock() + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + fs.mu.RUnlock() + + fs.dios.acquire() + f, err := os.Open(mdir) + fs.dios.release() + if err != nil { + return + } + + dirs, _ := f.ReadDir(-1) + f.Close() + + const ( + minLen = 4 + idxSuffix = ".idx" + fssSuffix = ".fss" + ) + for _, fi := range dirs { + if name := fi.Name(); strings.HasSuffix(name, idxSuffix) || strings.HasSuffix(name, fssSuffix) { + os.Remove(filepath.Join(mdir, name)) + } + } +} + +func (fs *fileStore) recoverMsgs() error { + fs.mu.Lock() + defer fs.mu.Unlock() + + // Check for any left over purged messages. + fs.dios.acquire() + if err := fs.recoverPartialPurge(); err != nil { + fs.dios.release() + return err + } + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + f, err := os.Open(mdir) + if err != nil { + fs.dios.release() + return errNotReadable + } + dirs, err := f.ReadDir(-1) + f.Close() + fs.dios.release() + + if err != nil { + return errNotReadable + } + + indices := make(sort.IntSlice, 0, len(dirs)) + var index int + for _, fi := range dirs { + // Ensure it's actually a block file, otherwise fmt.Sscanf also matches %d.blk.tmp + if !strings.HasSuffix(fi.Name(), blkSuffix) { + continue + } + if n, err := fmt.Sscanf(fi.Name(), blkScan, &index); err == nil && n == 1 { + indices = append(indices, index) + } + } + indices.Sort() + + // Recover all of the msg blocks. + // We now guarantee they are coming in order. + for _, index := range indices { + if mb, err := fs.recoverMsgBlock(uint32(index)); err == nil && mb != nil { + // This is a truncate block with possibly no index. If the OS got shutdown + // out from underneath of us this is possible. + mb.mu.Lock() + if atomic.LoadUint64(&mb.first.seq) == 0 { + if err := mb.dirtyCloseWithRemove(true); err != nil { + mb.mu.Unlock() + return err + } + fs.removeMsgBlockFromList(mb) + mb.mu.Unlock() + continue + } + // If the stream is empty, reset the first/last sequences so these can + // properly move up based purely on tombstones spread over multiple blocks. + if fs.state.Msgs == 0 { + fs.state.FirstSeq, fs.state.LastSeq = 0, 0 + fs.state.FirstTime, fs.state.LastTime = time.Time{}, time.Time{} + } + fseq := atomic.LoadUint64(&mb.first.seq) + if fs.state.FirstSeq == 0 || (fseq < fs.state.FirstSeq && mb.first.ts != 0) { + fs.state.FirstSeq = fseq + if mb.first.ts == 0 { + fs.state.FirstTime = time.Time{} + } else { + fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC() + } + } + // Preserve last time, could have erased the last message in one block, and then + // have a tombstone with the proper timestamp afterward in another block + if lseq := atomic.LoadUint64(&mb.last.seq); lseq >= fs.state.LastSeq { + fs.state.LastSeq = lseq + if mb.last.ts == 0 { + fs.state.LastTime = time.Time{} + } else { + fs.state.LastTime = time.Unix(0, mb.last.ts).UTC() + } + } + fs.state.Msgs += mb.msgs + fs.state.Bytes += mb.bytes + // If the block is empty, correct the sequences to be aligned with the current filestore state. + if mb.msgs == 0 { + atomic.StoreUint64(&mb.first.seq, fs.state.LastSeq+1) + mb.first.ts = 0 + atomic.StoreUint64(&mb.last.seq, fs.state.LastSeq) + mb.last.ts = fs.state.LastTime.UnixNano() + } + mb.mu.Unlock() + } else if (err == errBadKeySize || err == errKeyInvalid) && mb != nil { + // If we can't load the encryption key, we can't decrypt the block's data. + // We'll revert to deleting this block until there is peer-based recovery. This still + // catches up from the leader if it happened in the stream's tail. + mb.mu.Lock() + if err := mb.dirtyCloseWithRemove(true); err != nil { + mb.mu.Unlock() + return err + } + fs.removeMsgBlockFromList(mb) + mb.mu.Unlock() + continue + } else { + return err + } + } + + if len(fs.blks) > 0 { + fs.lmb = fs.blks[len(fs.blks)-1] + } else if _, err = fs.newMsgBlockForWrite(); err != nil { + return err + } + + // Check if we encountered any lost data. + if fs.ld != nil { + var emptyBlks []*msgBlock + for _, mb := range fs.blks { + if mb.msgs == 0 && mb.rbytes == 0 && mb != fs.lmb { + emptyBlks = append(emptyBlks, mb) + } + } + for _, mb := range emptyBlks { + // Need the mb lock here. + mb.mu.Lock() + err = fs.forceRemoveMsgBlock(mb) + mb.mu.Unlock() + if err != nil { + return err + } + } + } + + // Check for keyfiles orphans. + if kms, err := filepath.Glob(filepath.Join(mdir, keyScanAll)); err == nil && len(kms) > 0 { + valid := make(map[uint32]bool) + for _, mb := range fs.blks { + valid[mb.index] = true + } + for _, fn := range kms { + var index uint32 + shouldRemove := true + if n, err := fmt.Sscanf(filepath.Base(fn), keyScan, &index); err == nil && n == 1 && valid[index] { + shouldRemove = false + } + if shouldRemove { + os.Remove(fn) + } + } + } + + return nil +} + +// Will expire msgs that have aged out on restart. +// We will treat this differently in case we have a recovery +// that will expire alot of messages on startup. +// Should only be called on startup. +func (fs *fileStore) expireMsgsOnRecover() error { + if fs.state.Msgs == 0 { + return nil + } + + // If subject delete markers is configured, can't expire on recover. + // When clustered we need to go through proposals. + if fs.cfg.SubjectDeleteMarkerTTL > 0 { + return nil + } + + var minAge = time.Now().UnixNano() - int64(fs.cfg.MaxAge) + var purged, bytes uint64 + var deleted int + var nts int64 + + // If we expire all make sure to write out a tombstone. Need to be done by hand here, + // usually taken care of by fs.removeMsgBlock() but we do not call that here. + var last msgId + + deleteEmptyBlock := func(mb *msgBlock) error { + // If we are the last keep state to remember first/last sequence. + // Do this part by hand since not deleting one by one. + if mb == fs.lmb { + last.seq = atomic.LoadUint64(&mb.last.seq) + last.ts = mb.last.ts + } + // Make sure we do subject cleanup as well. + if err := mb.ensurePerSubjectInfoLoaded(); err != nil { + return err + } + mb.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool { + subj := bytesToString(bsubj) + for i := uint64(0); i < ss.Msgs; i++ { + fs.removePerSubject(subj) + } + return true + }) + if err := mb.dirtyCloseWithRemove(true); err != nil { + return err + } + deleted++ + return nil + } + + for _, mb := range fs.blks { + mb.mu.Lock() + if minAge < mb.first.ts { + nts = mb.first.ts + mb.mu.Unlock() + break + } + // Can we remove whole block here? + if mb.last.ts <= minAge { + purged += mb.msgs + bytes += mb.bytes + err := deleteEmptyBlock(mb) + mb.mu.Unlock() + if err != nil { + return err + } + continue + } + + // If we are here we have to process the interior messages of this blk. + // This will load fss as well. + if err := mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return err + } + + var smv StoreMsg + var needNextFirst bool + + // Walk messages and remove if expired. + fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq) + for seq := fseq; seq <= lseq; seq++ { + sm, err := mb.cacheLookupNoCopy(seq, &smv) + // Process interior deleted msgs. + if err == errDeletedMsg { + // Update dmap. + if mb.dmap.Exists(seq) { + mb.dmap.Delete(seq) + } + // Keep this updated just in case since we are removing dmap entries. + atomic.StoreUint64(&mb.first.seq, seq) + needNextFirst = true + continue + } + // Break on other errors. + if err != nil || sm == nil { + atomic.StoreUint64(&mb.first.seq, seq) + needNextFirst = true + break + } + + // No error and sm != nil from here onward. + + // Check for done. + if minAge < sm.ts { + atomic.StoreUint64(&mb.first.seq, sm.seq) + mb.first.ts = sm.ts + needNextFirst = false + nts = sm.ts + break + } + + // Delete the message here. + if mb.msgs > 0 { + atomic.StoreUint64(&mb.first.seq, seq) + needNextFirst = true + sz := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg) + if sz > mb.bytes { + sz = mb.bytes + } + mb.bytes -= sz + bytes += sz + mb.msgs-- + purged++ + } + // Update fss + // Make sure we have fss loaded. + if _, err = mb.removeSeqPerSubject(sm.subj, seq); err != nil { + mb.finishedWithCache() + mb.mu.Unlock() + return err + } + fs.removePerSubject(sm.subj) + } + // Make sure we have a proper next first sequence. + if needNextFirst { + mb.selectNextFirst() + } + // Check if empty after processing, could happen if tail of messages are all deleted. + var err error + if mb.msgs == 0 { + err = deleteEmptyBlock(mb) + } + mb.finishedWithCache() + mb.mu.Unlock() + if err != nil { + return err + } + break + } + + if nts > 0 { + // Make sure to set age check based on this value. + fs.resetAgeChk(nts - minAge) + } + + if deleted > 0 { + // Update block map. + if fs.bim != nil { + for _, mb := range fs.blks[:deleted] { + delete(fs.bim, mb.index) + } + } + // Update blks slice. + fs.blks = copyMsgBlocks(fs.blks[deleted:]) + if lb := len(fs.blks); lb == 0 { + fs.lmb = nil + } else { + fs.lmb = fs.blks[lb-1] + } + } + // Update top level accounting. + if purged < fs.state.Msgs { + fs.state.Msgs -= purged + } else { + fs.state.Msgs = 0 + } + if bytes < fs.state.Bytes { + fs.state.Bytes -= bytes + } else { + fs.state.Bytes = 0 + } + // Make sure to we properly set the fs first sequence and timestamp. + if err := fs.selectNextFirst(); err != nil { + return err + } + + // Check if we have no messages and blocks left. + + if fs.lmb == nil && last.seq != 0 { + if lmb, err := fs.newMsgBlockForWrite(); err != nil || lmb == nil { + if err == nil { + err = errors.New("lmb missing") + } + return err + } else if err = fs.writeTombstone(last.seq, last.ts); err != nil { + return err + } + // Clear any global subject state. + fs.psim, fs.tsl = fs.psim.Empty(), 0 + } + + // If we purged anything, make sure we kick flush state loop. + if purged > 0 { + fs.dirty++ + } + return nil +} + +func copyMsgBlocks(src []*msgBlock) []*msgBlock { + if src == nil { + return nil + } + dst := make([]*msgBlock, len(src)) + copy(dst, src) + return dst +} + +// GetSeqFromTime looks for the first sequence number that has +// the message with >= timestamp. +func (fs *fileStore) GetSeqFromTime(t time.Time) uint64 { + fs.mu.RLock() + lastSeq := fs.state.LastSeq + closed := fs.isClosed() + fs.mu.RUnlock() + + if closed { + return 0 + } + + mb := fs.selectMsgBlockForStart(t) + if mb == nil { + return lastSeq + 1 + } + + fseq := atomic.LoadUint64(&mb.first.seq) + lseq := atomic.LoadUint64(&mb.last.seq) + + var ( + smv StoreMsg + cts int64 + cseq uint64 + off uint64 + ) + ts := t.UnixNano() + + // Using a binary search, but need to be aware of interior deletes in the block. + seq := lseq + 1 + mb.mu.Lock() +loop: + for fseq <= lseq { + mid := fseq + (lseq-fseq)/2 + off = 0 + // Potentially skip over gaps. We keep the original middle but keep track of a + // potential delete range with an offset. + for { + sm, _, err := mb.fetchMsgNoCopyLocked(mid+off, &smv) + if err != nil || sm == nil { + off++ + if mid+off <= lseq { + continue + } else { + // Continue search to the left. Purposely ignore the skipped deletes here. + lseq = mid - 1 + continue loop + } + } + cts = sm.ts + cseq = sm.seq + break + } + if cts >= ts { + seq = cseq + if mid == fseq { + break + } + // Continue search to the left. + lseq = mid - 1 + } else { + // Continue search to the right (potentially skipping over interior deletes). + fseq = mid + off + 1 + } + } + mb.finishedWithCache() + mb.mu.Unlock() + return seq +} + +// Find the first matching message against a sublist. +func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) { + mb.mu.Lock() + var didLoad bool + var updateLLTS bool + defer func() { + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.finishedWithCache() + mb.mu.Unlock() + }() + + if mb.fssNotLoaded() { + // Make sure we have fss loaded. + if err := mb.loadMsgsWithLock(); err != nil { + return nil, false, err + } + didLoad = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + + // Make sure to start at mb.first.seq if fseq < mb.first.seq + if seq := atomic.LoadUint64(&mb.first.seq); seq > start { + start = seq + } + lseq := atomic.LoadUint64(&mb.last.seq) + + // If the FSS state has fewer entries than sequences in the linear scan, + // then use intersection instead as likely going to be cheaper. This will + // often be the case with high numbers of deletes, as well as a smaller + // number of subjects in the block. + if uint64(mb.fss.Size()) < lseq-start { + // If there are no subject matches then this is effectively no-op. + hseq := uint64(math.MaxUint64) + var ierr error + stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) bool { + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + // mb is already loaded into the cache so should be fast-ish. + if ierr = mb.recalculateForSubj(bytesToString(subj), ss); ierr != nil { + return false + } + } + first := max(start, ss.First) + if first > ss.Last || first >= hseq { + // The start cutoff is after the last sequence for this subject, + // or we think we already know of a subject with an earlier msg + // than our first seq for this subject. + return true + } + // Need messages loaded from here on out. + if mb.cacheNotLoaded() { + if ierr = mb.loadMsgsWithLock(); ierr != nil { + return false + } + didLoad = true + } + if sm == nil { + sm = new(StoreMsg) + } + if first == ss.First { + // If the start floor is below where this subject starts then we can + // short-circuit, avoiding needing to scan for the next message. + if fsm, err := mb.cacheLookup(ss.First, sm); err == nil { + sm = fsm + hseq = ss.First + } + return true + } + for seq := first; seq <= ss.Last; seq++ { + // Otherwise we have a start floor that intersects where this subject + // has messages in the block, so we need to walk up until we find a + // message matching the subject. + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + // Instead we will update it only once in a defer. + updateLLTS = true + continue + } + llseq := mb.llseq + fsm, err := mb.cacheLookup(seq, sm) + if err != nil { + continue + } + updateLLTS = false // cacheLookup already updated it. + if sl.HasInterest(fsm.subj) { + hseq = seq + sm = fsm + break + } + // If we are here we did not match, so put the llseq back. + mb.llseq = llseq + } + return true + }) + if ierr != nil { + return nil, false, ierr + } + if hseq < uint64(math.MaxUint64) && sm != nil { + return sm, didLoad && start == lseq, nil + } + } else { + // Need messages loaded from here on out. + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return nil, false, err + } + didLoad = true + } + if sm == nil { + sm = new(StoreMsg) + } + + for seq := start; seq <= lseq; seq++ { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + // Instead we will update it only once in a defer. + updateLLTS = true + continue + } + llseq := mb.llseq + fsm, err := mb.cacheLookup(seq, sm) + if err != nil { + continue + } + expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq + updateLLTS = false // cacheLookup already updated it. + if sl.HasInterest(fsm.subj) { + return fsm, expireOk, nil + } + // If we are here we did not match, so put the llseq back. + mb.llseq = llseq + } + } + + return nil, didLoad, ErrStoreMsgNotFound +} + +// Find the first matching message. +// fs lock should be held. +func (mb *msgBlock) firstMatching(filter string, wc bool, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) { + mb.mu.Lock() + var updateLLTS bool + defer func() { + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.finishedWithCache() + mb.mu.Unlock() + }() + + fseq, isAll := start, filter == _EMPTY_ || filter == fwcs + + var didLoad bool + if mb.fssNotLoaded() { + // Make sure we have fss loaded. + if err := mb.loadMsgsWithLock(); err != nil { + return nil, false, err + } + didLoad = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + + if filter == _EMPTY_ { + filter = fwcs + wc = true + } + + // If we only have 1 subject currently and it matches our filter we can also set isAll. + if !isAll && mb.fss.Size() == 1 { + if !wc { + _, isAll = mb.fss.Find(stringToBytes(filter)) + } else { + // Since mb.fss.Find won't work if filter is a wildcard, need to use Match instead. + mb.fss.Match(stringToBytes(filter), func(subject []byte, _ *SimpleState) { + isAll = true + }) + } + // If the only subject in this block isn't our filter, can simply short-circuit. + if !isAll { + return nil, didLoad, ErrStoreMsgNotFound + } + } + // Make sure to start at mb.first.seq if fseq < mb.first.seq + fseq = max(fseq, atomic.LoadUint64(&mb.first.seq)) + lseq := atomic.LoadUint64(&mb.last.seq) + + // Optionally build the isMatch for wildcard filters. + var isMatch func(subj string) bool + // Decide to build. + if wc { + _tsa, _fsa := [32]string{}, [32]string{} + tsa, fsa := _tsa[:0], tokenizeSubjectIntoSlice(_fsa[:0], filter) + isMatch = func(subj string) bool { + tsa = tokenizeSubjectIntoSlice(tsa[:0], subj) + return isSubsetMatchTokenized(tsa, fsa) + } + } + + subjs := mb.fs.cfg.Subjects + // If isAll or our single filter matches the filter arg do linear scan. + doLinearScan := isAll || (wc && len(subjs) == 1 && subjs[0] == filter) + // If we do not think we should do a linear scan check how many fss we + // would need to scan vs the full range of the linear walk. Optimize for + // 25th quantile of a match in a linear walk. Filter should be a wildcard. + // We should consult fss if our cache is not loaded and we only have fss loaded. + if !doLinearScan && wc && mb.cacheAlreadyLoaded() { + doLinearScan = mb.fss.Size()*4 > int(lseq-fseq) + } + + if !doLinearScan { + // If we have a wildcard match against all tracked subjects we know about. + fseq = lseq + 1 + if bfilter := stringToBytes(filter); wc { + var ierr error + mb.fss.Match(bfilter, func(bsubj []byte, ss *SimpleState) { + if ierr != nil { + return + } + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if ierr = mb.recalculateForSubj(bytesToString(bsubj), ss); ierr != nil { + return + } + } + if start <= ss.Last { + fseq = min(fseq, max(start, ss.First)) + } + }) + if ierr != nil { + return nil, false, ierr + } + } else if ss, _ := mb.fss.Find(bfilter); ss != nil { + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if err := mb.recalculateForSubj(filter, ss); err != nil { + return nil, false, err + } + } + if start <= ss.Last { + fseq = min(fseq, max(start, ss.First)) + } + } + } + + if fseq > lseq { + return nil, didLoad, ErrStoreMsgNotFound + } + + // Need messages loaded from here on out. + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return nil, false, err + } + didLoad = true + } + + if sm == nil { + sm = new(StoreMsg) + } + + for seq := fseq; seq <= lseq; seq++ { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + // Instead we will update it only once in a defer. + updateLLTS = true + continue + } + llseq := mb.llseq + fsm, err := mb.cacheLookup(seq, sm) + if err != nil { + if err == errPartialCache || err == errNoCache { + return nil, false, err + } + continue + } + updateLLTS = false // cacheLookup already updated it. + expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq + if isAll { + return fsm, expireOk, nil + } + if wc && isMatch(sm.subj) { + return fsm, expireOk, nil + } else if !wc && fsm.subj == filter { + return fsm, expireOk, nil + } + // If we are here we did not match, so put the llseq back. + mb.llseq = llseq + } + + return nil, didLoad, ErrStoreMsgNotFound +} + +// Find the previous matching message against a sublist, working BACKWARDS from start. +func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) { + mb.mu.Lock() + var didLoad bool + var updateLLTS bool + defer func() { + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.finishedWithCache() + mb.mu.Unlock() + }() + + // Need messages loaded from here on out. + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return nil, false, err + } + didLoad = true + } + + // Make sure to start at mb.last.seq if lseq < mb.last.seq + if seq := atomic.LoadUint64(&mb.last.seq); start > seq { + start = seq + } + lseq := atomic.LoadUint64(&mb.first.seq) + + if sm == nil { + sm = new(StoreMsg) + } + + // If the FSS state has fewer entries than sequences in the linear scan, + // then use intersection instead as likely going to be cheaper. This will + // often be the case with high numbers of deletes, as well as a smaller + // number of subjects in the block. + if uint64(mb.fss.Size()) < start-lseq { + // If there are no subject matches then this is effectively no-op. + hseq := uint64(0) + var ierr error + stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) bool { + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + // mb is already loaded into the cache so should be fast-ish. + if ierr = mb.recalculateForSubj(bytesToString(subj), ss); ierr != nil { + return false + } + } + first := min(start, ss.Last) + // Skip if cutoff is before this subject's first, or if we already + // have a higher-or-equal candidate (hseq holds the highest found). + if first < ss.First || first <= hseq { + // The start cutoff is before the first sequence for this subject, + // or we already know of a subject with a later-or-equal msg. + return true + } + if first == ss.Last { + // If the start floor is above where this subject starts then we can + // short-circuit, avoiding needing to scan for the next message. + if fsm, err := mb.cacheLookup(ss.Last, sm); err == nil { + sm = fsm + hseq = ss.Last + } + return true + } + for seq := first; seq >= ss.First; seq-- { + // Otherwise we have a start floor that intersects where this subject + // has messages in the block, so we need to walk up until we find a + // message matching the subject. + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + // Instead we will update it only once in a defer. + updateLLTS = true + continue + } + llseq := mb.llseq + fsm, err := mb.cacheLookup(seq, sm) + if err != nil { + continue + } + updateLLTS = false // cacheLookup already updated it. + if sl.HasInterest(fsm.subj) { + hseq = seq + sm = fsm + break + } + // If we are here we did not match, so put the llseq back. + mb.llseq = llseq + } + return true + }) + if ierr != nil { + return nil, false, ierr + } + if hseq > 0 && sm != nil { + return sm, didLoad && start == lseq, nil + } + } else { + for seq := start; seq >= lseq; seq-- { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + // Instead we will update it only once in a defer. + updateLLTS = true + continue + } + llseq := mb.llseq + fsm, err := mb.cacheLookup(seq, sm) + if err != nil { + continue + } + expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq + updateLLTS = false // cacheLookup already updated it. + if sl.HasInterest(fsm.subj) { + return fsm, expireOk, nil + } + // If we are here we did not match, so put the llseq back. + mb.llseq = llseq + } + } + + return nil, didLoad, ErrStoreMsgNotFound +} + +// This will traverse a message block and generate the filtered pending. +func (mb *msgBlock) filteredPending(subj string, wc bool, seq uint64) (total, first, last uint64, err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + return mb.filteredPendingLocked(subj, wc, seq) +} + +// This will traverse a message block and generate the filtered pending. +// Lock should be held. +func (mb *msgBlock) filteredPendingLocked(filter string, wc bool, sseq uint64) (total, first, last uint64, err error) { + isAll := filter == _EMPTY_ || filter == fwcs + + // First check if we can optimize this part. + // This means we want all and the starting sequence was before this block. + if isAll { + if fseq := atomic.LoadUint64(&mb.first.seq); sseq <= fseq { + return mb.msgs, fseq, atomic.LoadUint64(&mb.last.seq), nil + } + } + + needsCleanup := mb.cache == nil + defer func() { + if needsCleanup { + mb.finishedWithCache() + } + }() + + if filter == _EMPTY_ { + filter, wc = fwcs, true + } + + update := func(ss *SimpleState) { + total += ss.Msgs + if first == 0 || ss.First < first { + first = ss.First + } + if ss.Last > last { + last = ss.Last + } + } + + // Make sure we have fss loaded. + if err = mb.ensurePerSubjectInfoLoaded(); err != nil { + return 0, 0, 0, err + } + + var havePartial bool + + // If we are not a wildcard just use Find() here. Avoids allocations. + if !wc { + if ss, ok := mb.fss.Find(stringToBytes(filter)); ok && ss != nil { + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if err = mb.recalculateForSubj(filter, ss); err != nil { + return 0, 0, 0, err + } + } + if sseq <= ss.First { + update(ss) + } else if sseq <= ss.Last { + // We matched but its a partial. + havePartial = true + } + } + } else { + mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) { + if err != nil { + return + } + if havePartial { + // If we already found a partial then don't do anything else. + return + } + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if err = mb.recalculateForSubj(bytesToString(bsubj), ss); err != nil { + return + } + } + if sseq <= ss.First { + update(ss) + } else if sseq <= ss.Last { + // We matched but its a partial. + havePartial = true + } + }) + if err != nil { + return 0, 0, 0, err + } + } + + // If we did not encounter any partials we can return here. + if !havePartial { + return total, first, last, nil + } + + // If we are here we need to scan the msgs. + // Clear what we had. + total, first, last = 0, 0, 0 + + // If we load the cache for a linear scan we want to expire that cache upon exit. + var shouldExpire bool + if mb.cacheNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + return 0, 0, 0, err + } + shouldExpire = true + } + + _tsa, _fsa := [32]string{}, [32]string{} + tsa, fsa := _tsa[:0], _fsa[:0] + var isMatch func(subj string) bool + + if !wc { + isMatch = func(subj string) bool { return subj == filter } + } else { + fsa = tokenizeSubjectIntoSlice(fsa[:0], filter) + isMatch = func(subj string) bool { + tsa = tokenizeSubjectIntoSlice(tsa[:0], subj) + return isSubsetMatchTokenized(tsa, fsa) + } + } + + // 1. See if we match any subs from fss. + // 2. If we match and the sseq is past ss.Last then we can use meta only. + // 3. If we match and we need to do a partial, break and clear any totals and do a full scan like num pending. + + var smv StoreMsg + for seq, lseq := sseq, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ { + sm, _ := mb.cacheLookupNoCopy(seq, &smv) + if sm == nil { + continue + } + if isAll || isMatch(sm.subj) { + total++ + if first == 0 || seq < first { + first = seq + } + if seq > last { + last = seq + } + } + } + // If we loaded this block for this operation go ahead and expire it here. + if shouldExpire { + mb.tryForceExpireCacheLocked() + } + + return total, first, last, nil +} + +// FilteredState will return the SimpleState associated with the filtered subject and a proposed starting sequence. +func (fs *fileStore) FilteredState(sseq uint64, subj string) (SimpleState, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + lseq := fs.state.LastSeq + if sseq < fs.state.FirstSeq { + sseq = fs.state.FirstSeq + } + + // Returned state. + var ss SimpleState + + // If past the end no results. + if sseq > lseq { + // Make sure we track sequences + ss.First = fs.state.FirstSeq + ss.Last = fs.state.LastSeq + return ss, nil + } + + // If we want all msgs that match we can shortcircuit. + // TODO(dlc) - This can be extended for all cases but would + // need to be careful on total msgs calculations etc. + if sseq == fs.state.FirstSeq { + if err := fs.numFilteredPending(subj, &ss); err != nil { + return ss, err + } + } else { + wc := subjectHasWildcard(subj) + // Tracking subject state. + // TODO(dlc) - Optimize for 2.10 with avl tree and no atomics per block. + for _, mb := range fs.blks { + // Skip blocks that are less than our starting sequence. + if sseq > atomic.LoadUint64(&mb.last.seq) { + continue + } + t, f, l, err := mb.filteredPending(subj, wc, sseq) + if err != nil { + return ss, err + } + ss.Msgs += t + if ss.First == 0 || (f > 0 && f < ss.First) { + ss.First = f + } + if l > ss.Last { + ss.Last = l + } + } + } + + return ss, nil +} + +// This is used to see if we can selectively jump start blocks based on filter subject and a starting block index. +// Will return -1 and ErrStoreEOF if no matches at all or no more from where we are. +func (fs *fileStore) checkSkipFirstBlock(filter string, wc bool, bi int) (int, error) { + // If we match everything, just move to next blk. + if filter == _EMPTY_ || filter == fwcs { + return bi + 1, nil + } + // Move through psim to gather start and stop bounds. + start, stop := uint32(math.MaxUint32), uint32(0) + if wc { + if fs.psim.Size() > highCardinalityThreshold { + return bi + 1, nil + } + fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) { + if psi.fblk < start { + start = psi.fblk + } + if psi.lblk > stop { + stop = psi.lblk + } + }) + } else if psi, ok := fs.psim.Find(stringToBytes(filter)); ok { + start, stop = psi.fblk, psi.lblk + } + // Nothing was found. + if start == uint32(math.MaxUint32) { + return -1, ErrStoreEOF + } + return fs.selectSkipFirstBlock(bi, start, stop) +} + +// This is used to see if we can selectively jump start blocks based on filter subjects and a starting block index. +// Will return -1 and ErrStoreEOF if no matches at all or no more from where we are. +func (fs *fileStore) checkSkipFirstBlockMulti(sl *gsl.SimpleSublist, bi int) (int, error) { + // Don't bother if full wildcard. + if sl.MatchesFullWildcard() || fs.psim.Size() > highCardinalityThreshold { + return bi + 1, nil + } + // Move through psim to gather start and stop bounds. + start, stop := uint32(math.MaxUint32), uint32(0) + guard := fs.blks[bi].getIndex() + 1 + stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool { + if psi.fblk < start { + start = psi.fblk + } + if start == guard { + // One of the subjects matches the next block, so there's no point in carrying on trying to skip. + return false + } + if psi.lblk > stop { + stop = psi.lblk + } + return true + }) + if start == guard { + return bi + 1, nil + } + // Nothing was found. + if start == uint32(math.MaxUint32) { + return -1, ErrStoreEOF + } + return fs.selectSkipFirstBlock(bi, start, stop) +} + +func (fs *fileStore) selectSkipFirstBlock(bi int, start, stop uint32) (int, error) { + // Can not be nil so ok to inline dereference. + mbi := fs.blks[bi].getIndex() + // All matching msgs are behind us. + // Less than AND equal is important because we were called because we missed searching bi. + if stop <= mbi { + return -1, ErrStoreEOF + } + // If start is > index return dereference of fs.blks index. + if start > mbi { + if mb := fs.bim[start]; mb != nil { + ni, _ := fs.selectMsgBlockWithIndex(atomic.LoadUint64(&mb.last.seq)) + return ni, nil + } + } + // Otherwise just bump to the next one. + return bi + 1, nil +} + +// Optimized way for getting all num pending matching a filter subject. +// Lock should be held. +func (fs *fileStore) numFilteredPending(filter string, ss *SimpleState) error { + return fs.numFilteredPendingWithLast(filter, true, ss) +} + +// Optimized way for getting all num pending matching a filter subject and first sequence only. +// Lock should be held. +func (fs *fileStore) numFilteredPendingNoLast(filter string, ss *SimpleState) error { + return fs.numFilteredPendingWithLast(filter, false, ss) +} + +// Optimized way for getting all num pending matching a filter subject. +// Optionally look up last sequence. Sometimes do not need last and this avoids cost. +// Read lock should be held. +func (fs *fileStore) numFilteredPendingWithLast(filter string, last bool, ss *SimpleState) error { + isAll := filter == _EMPTY_ || filter == fwcs + + // If isAll we do not need to do anything special to calculate the first and last and total. + if isAll { + ss.First = fs.state.FirstSeq + ss.Last = fs.state.LastSeq + ss.Msgs = fs.state.Msgs + return nil + } + // Always reset. + ss.First, ss.Last, ss.Msgs = 0, 0, 0 + + // We do need to figure out the first and last sequences. + wc := subjectHasWildcard(filter) + start, stop := uint32(math.MaxUint32), uint32(0) + + if wc { + fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) { + ss.Msgs += psi.total + // Keep track of start and stop indexes for this subject. + if psi.fblk < start { + start = psi.fblk + } + if psi.lblk > stop { + stop = psi.lblk + } + }) + } else if psi, ok := fs.psim.Find(stringToBytes(filter)); ok { + ss.Msgs += psi.total + start, stop = psi.fblk, psi.lblk + } + + // Did not find anything. + if stop == 0 { + return nil + } + + // Do start + mb := fs.bim[start] + if mb != nil { + _, f, _, err := mb.filteredPending(filter, wc, 0) + if err != nil { + return err + } + ss.First = f + } + + if ss.First == 0 { + // This is a miss. This can happen since psi.fblk is lazy. + // We will make sure to update fblk. + + // Hold this outside loop for psim fblk updates when done. + i := start + 1 + for ; i <= stop; i++ { + mb := fs.bim[i] + if mb == nil { + continue + } + if _, f, _, err := mb.filteredPending(filter, wc, 0); err != nil { + return err + } else if f > 0 { + ss.First = f + break + } + } + // Update fblk since fblk was outdated. + // We only require read lock here as that is desirable, + // so we need to do this in a go routine to acquire write lock. + go func() { + fs.mu.Lock() + defer fs.mu.Unlock() + if !wc { + if info, ok := fs.psim.Find(stringToBytes(filter)); ok { + if i > info.fblk { + info.fblk = i + } + } + } else { + fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) { + if i > psi.fblk { + psi.fblk = i + } + }) + } + }() + } + // Now gather last sequence if asked to do so. + if last { + if mb = fs.bim[stop]; mb != nil { + _, _, l, err := mb.filteredPending(filter, wc, 0) + if err != nil { + return err + } + ss.Last = l + } + } + return nil +} + +// SubjectsState returns a map of SimpleState for all matching subjects. +func (fs *fileStore) SubjectsState(subject string) map[string]SimpleState { + fs.mu.RLock() + defer fs.mu.RUnlock() + + if fs.state.Msgs == 0 || fs.noTrackSubjects() { + return nil + } + + if subject == _EMPTY_ { + subject = fwcs + } + + start, stop := fs.blks[0], fs.lmb + // We can short circuit if not a wildcard using psim for start and stop. + if !subjectHasWildcard(subject) { + info, ok := fs.psim.Find(stringToBytes(subject)) + if !ok { + return nil + } + if f := fs.bim[info.fblk]; f != nil { + start = f + } + if l := fs.bim[info.lblk]; l != nil { + stop = l + } + } + + // Aggregate fss. + fss := make(map[string]SimpleState) + var startFound bool + + for _, mb := range fs.blks { + if !startFound { + if mb != start { + continue + } + startFound = true + } + + mb.mu.Lock() + var shouldExpire bool + if mb.fssNotLoaded() { + // Make sure we have fss loaded. + if err := mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return nil + } + shouldExpire = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + var ierr error + mb.fss.Match(stringToBytes(subject), func(bsubj []byte, ss *SimpleState) { + if ierr != nil { + return + } + subj := string(bsubj) + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if ierr = mb.recalculateForSubj(subj, ss); ierr != nil { + return + } + } + oss := fss[subj] + if oss.First == 0 { // New + fss[subj] = *ss + } else { + // Merge here. + oss.Last, oss.Msgs = ss.Last, oss.Msgs+ss.Msgs + fss[subj] = oss + } + }) + if shouldExpire { + // Expire this cache before moving on. + mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() + } + mb.mu.Unlock() + if ierr != nil { + return nil + } + if mb == stop { + break + } + } + + return fss +} + +// AllLastSeqs will return a sorted list of last sequences for all subjects. +func (fs *fileStore) AllLastSeqs() ([]uint64, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + return fs.allLastSeqsLocked() +} + +// allLastSeqsLocked will return a sorted list of last sequences for all +// subjects, but won't take the lock to do it, to avoid the issue of compounding +// read locks causing a deadlock with a write lock. +func (fs *fileStore) allLastSeqsLocked() ([]uint64, error) { + if fs.state.Msgs == 0 || fs.noTrackSubjects() { + return nil, nil + } + + numSubjects := fs.psim.Size() + seqs := make([]uint64, 0, numSubjects) + subs := make(map[string]struct{}, numSubjects) + + for i := len(fs.blks) - 1; i >= 0; i-- { + if len(subs) == numSubjects { + break + } + mb := fs.blks[i] + mb.mu.Lock() + + var shouldExpire bool + if mb.fssNotLoaded() { + // Make sure we have fss loaded. + if err := mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return nil, err + } + shouldExpire = true + } + + var ierr error + mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool { + // Check if already been processed and accounted. + if _, ok := subs[string(bsubj)]; !ok { + // Check if we need to recalculate. We only care about the last sequence. + if ss.lastNeedsUpdate { + // mb is already loaded into the cache so should be fast-ish. + if ierr = mb.recalculateForSubj(bytesToString(bsubj), ss); ierr != nil { + return false + } + } + seqs = append(seqs, ss.Last) + subs[string(bsubj)] = struct{}{} + } + return true + }) + if shouldExpire { + // Expire this cache before moving on. + mb.tryForceExpireCacheLocked() + } + mb.finishedWithCache() + mb.mu.Unlock() + if ierr != nil { + return nil, ierr + } + } + + slices.Sort(seqs) + return seqs, nil +} + +// Helper to determine if the filter(s) represent all the subjects. +// Most clients send in subjects even if they match the stream's ingest subjects. +// Lock should be held. +func (fs *fileStore) filterIsAll(filters []string) bool { + if len(filters) == 1 && filters[0] == fwcs { + return true + } + if len(filters) != len(fs.cfg.Subjects) { + return false + } + // Sort so we can compare. + subjects := copyStrings(fs.cfg.Subjects) + slices.Sort(filters) + slices.Sort(subjects) + for i, subj := range filters { + if !subjectIsSubsetMatch(subjects[i], subj) { + return false + } + } + return true +} + +// MultiLastSeqs will return a sorted list of sequences that match all subjects presented in filters. +// We will not exceed the maxSeq, which if 0 becomes the store's last sequence. +func (fs *fileStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed int) ([]uint64, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + return fs.multiLastSeqsLocked(filters, maxSeq, maxAllowed) +} + +// Lock should be held. +func (fs *fileStore) multiLastSeqsLocked(filters []string, maxSeq uint64, maxAllowed int) ([]uint64, error) { + if fs.state.Msgs == 0 || fs.noTrackSubjects() { + return nil, nil + } + + // See if we can short circuit if we think they are asking for all last sequences and have no maxSeq or maxAllowed set. + if maxSeq == 0 && maxAllowed <= 0 && fs.filterIsAll(filters) { + return fs.allLastSeqsLocked() + } + + lastBlkIndex := len(fs.blks) - 1 + lastMB := fs.blks[lastBlkIndex] + + // Implied last sequence. + if maxSeq == 0 { + maxSeq = fs.state.LastSeq + } else { + // Udate last mb index if not last seq. + lastBlkIndex, lastMB = fs.selectMsgBlockWithIndex(maxSeq) + } + // Make sure non-nil + if lastMB == nil { + return nil, nil + } + + // Grab our last mb index (not same as blk index). + lastMB.mu.RLock() + lastMBIndex := lastMB.index + lastMB.mu.RUnlock() + + subs := make(map[string]*psi) + var numLess int + var maxBlk uint32 + + for _, filter := range filters { + fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) { + subs[string(subj)] = psi + if psi.lblk < lastMBIndex { + numLess++ + if psi.lblk > maxBlk { + maxBlk = psi.lblk + } + } + }) + } + + // If all subjects have a lower last index, select the largest for our walk backwards. + if numLess == len(subs) { + lastMB = fs.bim[maxBlk] + } + + // Collect all sequences needed. + seqs := make([]uint64, 0, len(subs)) + + // If covering the whole stream, check fast path of visiting only last blocks. + if maxSeq >= fs.state.LastSeq { + if maxAllowed > 0 && len(subs) > maxAllowed { + return nil, ErrTooManyResults + } + if err := fs.multiLastSeqsByLastBlockLocked(subs, &seqs); err != nil { + return nil, err + } + } + + for i, lnf := lastBlkIndex, false; i >= 0; i-- { + if len(subs) == 0 { + break + } + mb := fs.blks[i] + if !lnf { + if mb != lastMB { + continue + } + lnf = true + } + // We can start properly looking here. + mb.mu.Lock() + var ierr error + if ierr = mb.ensurePerSubjectInfoLoaded(); ierr != nil { + mb.mu.Unlock() + return nil, ierr + } + + // Iterate the fss and check against our subs. We will delete from subs as we add. + // Once len(subs) == 0 we are done. + mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool { + // Already been processed and accounted for was not matched in the first place. + if subs[string(bsubj)] == nil { + return true + } + // Check if we need to recalculate. We only care about the last sequence. + if ss.lastNeedsUpdate { + // mb is already loaded into the cache so should be fast-ish. + if ierr = mb.recalculateForSubj(bytesToString(bsubj), ss); ierr != nil { + return false + } + } + // If we are equal or below just add to seqs slice. + if ss.Last <= maxSeq { + seqs = append(seqs, ss.Last) + delete(subs, bytesToString(bsubj)) + } else { + // Need to search for the real last since recorded last is > maxSeq. + needsCleanup := mb.cache == nil + if mb.cacheNotLoaded() { + if ierr = mb.loadMsgsWithLock(); ierr != nil { + if needsCleanup { + mb.finishedWithCache() + } + return false + } + } + var smv StoreMsg + fseq := atomic.LoadUint64(&mb.first.seq) + lseq := min(atomic.LoadUint64(&mb.last.seq), maxSeq) + ssubj := bytesToString(bsubj) + for seq := lseq; seq >= fseq; seq-- { + sm, _ := mb.cacheLookupNoCopy(seq, &smv) + if sm == nil || sm.subj != ssubj { + continue + } + seqs = append(seqs, sm.seq) + delete(subs, ssubj) + break + } + if needsCleanup { + mb.finishedWithCache() + } + } + return true + }) + mb.mu.Unlock() + if ierr != nil { + return nil, ierr + } + + // If maxAllowed was sepcified check that we will not exceed that. + if maxAllowed > 0 && len(seqs) > maxAllowed { + return nil, ErrTooManyResults + } + } + if len(seqs) == 0 { + return nil, nil + } + slices.Sort(seqs) + return seqs, nil +} + +// MultiLastMsgs delivers the last message per subject matching the filters, +// up to maxSeq and skipping sequences below minSeq, in ascending sequence +// order through cb, all within a single read section so the batch is a +// point-in-time snapshot of the store. +func (fs *fileStore) MultiLastMsgs(filters []string, minSeq, maxSeq uint64, maxAllowed int, cb func(sm *StoreMsg, np uint64) bool) (uint64, uint64, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + seqs, err := fs.multiLastSeqsLocked(filters, maxSeq, maxAllowed) + if err != nil || len(seqs) == 0 { + return 0, 0, err + } + total := uint64(len(seqs)) + np := total + for _, seq := range seqs { + if np > 0 { + np-- + } + if seq < minSeq { + continue + } + var svp StoreMsg + sm, err := fs.msgForSeqLocked(seq, &svp, false) + if err != nil { + return total, np, err + } + if !cb(sm, np) { + break + } + } + return total, np, nil +} + +// multiLastSeqsByLastBlockLocked resolves last sequences via each subject's +// last block (psi.lblk), moving resolved subjects from subs into seqs and +// leaving unresolved ones (e.g. stale lblk) in subs for the caller. +// Lock should be held. +func (fs *fileStore) multiLastSeqsByLastBlockLocked(subs map[string]*psi, seqs *[]uint64) error { + // Sort requested subjects by their last block so each block is visited + // once and in order, one flat slice instead of per-block map bookkeeping. + type lblkSubj struct { + lblk uint32 + subj string + info *psi + } + pairs := make([]lblkSubj, 0, len(subs)) + for subj, info := range subs { + pairs = append(pairs, lblkSubj{info.lblk, subj, info}) + } + slices.SortFunc(pairs, func(a, b lblkSubj) int { + return cmp.Compare(a.lblk, b.lblk) + }) + + var unresolved []lblkSubj + for i := 0; i < len(pairs); { + blkIdx := pairs[i].lblk + j := i + 1 + for j < len(pairs) && pairs[j].lblk == blkIdx { + j++ + } + mb := fs.bim[blkIdx] + if mb == nil { + unresolved = append(unresolved, pairs[i:j]...) + i = j + continue + } + mb.mu.Lock() + if err := mb.ensurePerSubjectInfoLoaded(); err != nil { + mb.mu.Unlock() + return err + } + for ; i < j; i++ { + subj := pairs[i].subj + ss, ok := mb.fss.Find(stringToBytes(subj)) + if !ok || ss == nil { + unresolved = append(unresolved, pairs[i]) + continue + } + // Check if we need to recalculate. We only care about the last sequence. + if ss.lastNeedsUpdate { + if err := mb.recalculateForSubj(subj, ss); err != nil { + mb.mu.Unlock() + return err + } + } + *seqs = append(*seqs, ss.Last) + } + mb.mu.Unlock() + } + + // Shrink subs down to only the unresolved subjects, clearing wholesale + // rather than deleting per resolved subject. + if len(unresolved) < len(subs) { + clear(subs) + for _, p := range unresolved { + subs[p.subj] = p.info + } + } + return nil +} + +// NumPending will return the number of pending messages matching the filter subject starting at sequence. +// Optimized for stream num pending calculations for consumers. +func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64, err error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + // This can always be last for these purposes. + validThrough = fs.state.LastSeq + + if fs.state.Msgs == 0 || sseq > fs.state.LastSeq { + return 0, validThrough, nil + } + + // If sseq is less then our first set to first. + if sseq < fs.state.FirstSeq { + sseq = fs.state.FirstSeq + } + // Track starting for both block for the sseq and staring block that matches any subject. + var seqStart int + // See if we need to figure out starting block per sseq. + if sseq > fs.state.FirstSeq { + // This should not, but can return -1, so make sure we check to avoid panic below. + if seqStart, _ = fs.selectMsgBlockWithIndex(sseq); seqStart < 0 { + seqStart = 0 + } + } + + isAll := filter == _EMPTY_ || filter == fwcs + if isAll && filter == _EMPTY_ { + filter = fwcs + } + wc := subjectHasWildcard(filter) + + // See if filter was provided but its the only subject. + if !isAll && !wc && fs.psim.Size() == 1 { + _, isAll = fs.psim.Find(stringToBytes(filter)) + } + // If we are isAll and have no deleted we can do a simpler calculation. + if !lastPerSubject && isAll && (fs.state.LastSeq-fs.state.FirstSeq+1) == fs.state.Msgs { + if sseq == 0 { + return fs.state.Msgs, validThrough, nil + } + return fs.state.LastSeq - sseq + 1, validThrough, nil + } + + _tsa, _fsa := [32]string{}, [32]string{} + tsa, fsa := _tsa[:0], _fsa[:0] + if wc { + fsa = tokenizeSubjectIntoSlice(fsa[:0], filter) + } + + isMatch := func(subj string) bool { + if isAll { + return true + } + if !wc { + return subj == filter + } + tsa = tokenizeSubjectIntoSlice(tsa[:0], subj) + return isSubsetMatchTokenized(tsa, fsa) + } + + // Use the per-subject index when at most one message per subject should be counted. + // This applies to last-per-subject consumers and streams limited to one message per subject. + // We will scan PSIM since we accurately track the last block we have seen the subject in. This + // allows us to only need to load at most one block now. + // For the last block, we need to track the subjects that we know are in that block, and track seen + // while in the block itself, but complexity there worth it. + if lastPerSubject || fs.cfg.MaxMsgsPer == 1 { + // If we want all and our start sequence is equal or less than first return number of subjects. + if isAll && sseq <= fs.state.FirstSeq { + return uint64(fs.psim.Size()), validThrough, nil + } + // If we are here we need to scan. We are going to scan the PSIM looking for lblks that are >= seqStart. + // This will build up a list of all subjects from the selected block onward. + lbm := make(map[string]bool) + mb := fs.blks[seqStart] + bi := mb.index + + fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) { + // If the select blk start is greater than entry's last blk skip. + if bi > psi.lblk { + return + } + total++ + // We will track the subjects that are an exact match to the last block. + // This is needed for last block processing. + if psi.lblk == bi { + lbm[string(subj)] = true + } + }) + + // Now check if we need to inspect the seqStart block. + // Grab write lock in case we need to load in msgs. + mb.mu.Lock() + var updateLLTS bool + var shouldExpire bool + // We need to walk this block to correct accounting from above. + if sseq > mb.first.seq { + // Track the ones we add back in case more than one. + seen := make(map[string]bool) + // We need to discount the total by subjects seen before sseq, but also add them right back in if they are >= sseq for this blk. + // This only should be subjects we know have the last blk in this block. + if mb.cacheNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + var smv StoreMsg + for seq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + updateLLTS = true + continue + } + sm, _ := mb.cacheLookupNoCopy(seq, &smv) + if sm == nil || sm.subj == _EMPTY_ || !lbm[sm.subj] { + continue + } + updateLLTS = false // cacheLookup already updated it. + if isMatch(sm.subj) { + // If less than sseq adjust off of total as long as this subject matched the last block. + if seq < sseq { + if !seen[sm.subj] { + total-- + seen[sm.subj] = true + } + } else if seen[sm.subj] { + // This is equal or more than sseq, so add back in. + total++ + // Make sure to not process anymore. + delete(seen, sm.subj) + } + } + } + } + // If we loaded the block try to force expire. + if shouldExpire { + mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() + } + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.mu.Unlock() + return total, validThrough, nil + } + + // If we would need to scan more from the beginning, revert back to calculating directly here. + // TODO(dlc) - Redo properly with sublists etc for subject-based filtering. + if seqStart >= (len(fs.blks) / 2) { + for i := seqStart; i < len(fs.blks); i++ { + var shouldExpire bool + mb := fs.blks[i] + // Hold write lock in case we need to load cache. + mb.mu.Lock() + if isAll && sseq <= atomic.LoadUint64(&mb.first.seq) { + total += mb.msgs + mb.mu.Unlock() + continue + } + // If we are here we need to at least scan the subject fss. + // Make sure we have fss loaded. + if mb.fssNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + + var t uint64 + var ierr error + var havePartial bool + mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) { + if ierr != nil { + return + } + if havePartial { + // If we already found a partial then don't do anything else. + return + } + subj := bytesToString(bsubj) + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if ierr = mb.recalculateForSubj(subj, ss); ierr != nil { + return + } + } + if sseq <= ss.First { + t += ss.Msgs + } else if sseq <= ss.Last { + // We matched but its a partial. + havePartial = true + } + }) + if ierr != nil { + mb.mu.Unlock() + return 0, 0, ierr + } + + // See if we need to scan msgs here. + if havePartial { + // Make sure we have the cache loaded. + if mb.cacheNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + // Clear on partial. + t = 0 + start := sseq + if fseq := atomic.LoadUint64(&mb.first.seq); fseq > start { + start = fseq + } + var smv StoreMsg + for seq, lseq := start, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ { + if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && isMatch(sm.subj) { + t++ + } + } + } + // If we loaded this block for this operation go ahead and expire it here. + if shouldExpire { + mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() + } + mb.mu.Unlock() + total += t + } + return total, validThrough, nil + } + + // If we are here it's better to calculate totals from psim and adjust downward by scanning less blocks. + // TODO(dlc) - Eventually when sublist uses generics, make this sublist driven instead. + start := uint32(math.MaxUint32) + fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) { + total += psi.total + // Keep track of start index for this subject. + if psi.fblk < start { + start = psi.fblk + } + }) + // See if we were asked for all, if so we are done. + if sseq <= fs.state.FirstSeq { + return total, validThrough, nil + } + + // If we are here we need to calculate partials for the first blocks. + firstSubjBlk := fs.bim[start] + var firstSubjBlkFound bool + // Adjust in case not found. + if firstSubjBlk == nil { + firstSubjBlkFound = true + } + + // Track how many we need to adjust against the total. + var adjust uint64 + for i := 0; i <= seqStart; i++ { + mb := fs.blks[i] + // We can skip blks if we know they are below the first one that has any subject matches. + if !firstSubjBlkFound { + if firstSubjBlkFound = (mb == firstSubjBlk); !firstSubjBlkFound { + continue + } + } + // We need to scan this block. + var shouldExpire bool + var updateLLTS bool + mb.mu.Lock() + // Check if we should include all of this block in adjusting. If so work with metadata. + if sseq > atomic.LoadUint64(&mb.last.seq) { + if isAll { + adjust += mb.msgs + } else { + // We need to adjust for all matches in this block. + // Make sure we have fss loaded. This loads whole block now. + if mb.fssNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + + mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) { + adjust += ss.Msgs + }) + } + } else { + // This is the last block. We need to scan per message here. + if mb.cacheNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + var last = atomic.LoadUint64(&mb.last.seq) + if sseq < last { + last = sseq + } + // We need to walk all messages in this block + var smv StoreMsg + for seq := atomic.LoadUint64(&mb.first.seq); seq < last; seq++ { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + updateLLTS = true + continue + } + sm, _ := mb.cacheLookupNoCopy(seq, &smv) + if sm == nil || sm.subj == _EMPTY_ { + continue + } + updateLLTS = false // cacheLookup already updated it. + // Check if it matches our filter. + if sm.seq < sseq && isMatch(sm.subj) { + adjust++ + } + } + } + // If we loaded the block try to force expire. + if shouldExpire { + mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() + } + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.mu.Unlock() + } + // Make final adjustment. + if adjust > total { + total = 0 + } else { + total -= adjust + } + + return total, validThrough, nil +} + +// NumPending will return the number of pending messages matching any subject in the sublist starting at sequence. +// Optimized for stream num pending calculations for consumers with lots of filtered subjects. +// Subjects should not overlap, this property is held when doing multi-filtered consumers. +func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerSubject bool) (total, validThrough uint64, err error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + // This can always be last for these purposes. + validThrough = fs.state.LastSeq + + if fs.state.Msgs == 0 || sseq > fs.state.LastSeq { + return 0, validThrough, nil + } + + // If sseq is less then our first set to first. + if sseq < fs.state.FirstSeq { + sseq = fs.state.FirstSeq + } + // Track starting for both block for the sseq and staring block that matches any subject. + var seqStart int + // See if we need to figure out starting block per sseq. + if sseq > fs.state.FirstSeq { + // This should not, but can return -1, so make sure we check to avoid panic below. + if seqStart, _ = fs.selectMsgBlockWithIndex(sseq); seqStart < 0 { + seqStart = 0 + } + } + + isAll := sl == nil + + // See if filter was provided but its the only subject. + if !isAll && fs.psim.Size() == 1 { + fs.psim.IterFast(func(subject []byte, _ *psi) bool { + isAll = sl.HasInterest(bytesToString(subject)) + return true + }) + } + // If we are isAll and have no deleted we can do a simpler calculation. + if !lastPerSubject && isAll && (fs.state.LastSeq-fs.state.FirstSeq+1) == fs.state.Msgs { + if sseq == 0 { + return fs.state.Msgs, validThrough, nil + } + return fs.state.LastSeq - sseq + 1, validThrough, nil + } + // Setup the isMatch function. + isMatch := func(subj string) bool { + if isAll { + return true + } + return sl.HasInterest(subj) + } + + // Use the per-subject index when at most one message per subject should be counted. + // This applies to last-per-subject consumers and streams limited to one message per subject. + // We will scan PSIM since we accurately track the last block we have seen the subject in. This + // allows us to only need to load at most one block now. + // For the last block, we need to track the subjects that we know are in that block, and track seen + // while in the block itself, but complexity there worth it. + if lastPerSubject || fs.cfg.MaxMsgsPer == 1 { + // If we want all and our start sequence is equal or less than first return number of subjects. + if isAll && sseq <= fs.state.FirstSeq { + return uint64(fs.psim.Size()), validThrough, nil + } + // If we are here we need to scan. We are going to scan the PSIM looking for lblks that are >= seqStart. + // This will build up a list of all subjects from the selected block onward. + lbm := make(map[string]bool) + mb := fs.blks[seqStart] + bi := mb.index + + stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool { + // If the select blk start is greater than entry's last blk skip. + if bi > psi.lblk { + return true + } + total++ + // We will track the subjects that are an exact match to the last block. + // This is needed for last block processing. + if psi.lblk == bi { + lbm[string(subj)] = true + } + return true + }) + + // Now check if we need to inspect the seqStart block. + // Grab write lock in case we need to load in msgs. + mb.mu.Lock() + var shouldExpire bool + var updateLLTS bool + // We need to walk this block to correct accounting from above. + if sseq > atomic.LoadUint64(&mb.first.seq) { + // Track the ones we add back in case more than one. + seen := make(map[string]bool) + // We need to discount the total by subjects seen before sseq, but also add them right back in if they are >= sseq for this blk. + // This only should be subjects we know have the last blk in this block. + if mb.cacheNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + var smv StoreMsg + for seq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + updateLLTS = true + continue + } + sm, _ := mb.cacheLookupNoCopy(seq, &smv) + if sm == nil || sm.subj == _EMPTY_ || !lbm[sm.subj] { + continue + } + updateLLTS = false // cacheLookup already updated it. + if isMatch(sm.subj) { + // If less than sseq adjust off of total as long as this subject matched the last block. + if seq < sseq { + if !seen[sm.subj] { + total-- + seen[sm.subj] = true + } + } else if seen[sm.subj] { + // This is equal or more than sseq, so add back in. + total++ + // Make sure to not process anymore. + delete(seen, sm.subj) + } + } + } + } + // If we loaded the block try to force expire. + if shouldExpire { + mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() + } + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.mu.Unlock() + return total, validThrough, nil + } + + // If we would need to scan more from the beginning, revert back to calculating directly here. + if seqStart >= (len(fs.blks) / 2) { + for i := seqStart; i < len(fs.blks); i++ { + var shouldExpire bool + mb := fs.blks[i] + // Hold write lock in case we need to load cache. + mb.mu.Lock() + if isAll && sseq <= atomic.LoadUint64(&mb.first.seq) { + total += mb.msgs + mb.mu.Unlock() + continue + } + // If we are here we need to at least scan the subject fss. + // Make sure we have fss loaded. + if mb.fssNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + + var t uint64 + var ierr error + var havePartial bool + var updateLLTS bool + stree.IntersectGSL[SimpleState](mb.fss, sl, func(bsubj []byte, ss *SimpleState) bool { + subj := bytesToString(bsubj) + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if ierr = mb.recalculateForSubj(subj, ss); ierr != nil { + return false + } + } + if sseq <= ss.First { + t += ss.Msgs + } else if sseq <= ss.Last { + // We matched but its a partial. + havePartial = true + return false + } + return true + }) + if ierr != nil { + mb.mu.Unlock() + return 0, 0, ierr + } + + // See if we need to scan msgs here. + if havePartial { + // Make sure we have the cache loaded. + if mb.cacheNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + // Clear on partial. + t = 0 + start := sseq + if fseq := atomic.LoadUint64(&mb.first.seq); fseq > start { + start = fseq + } + var smv StoreMsg + for seq, lseq := start, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + updateLLTS = true + continue + } + if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && isMatch(sm.subj) { + t++ + updateLLTS = false // cacheLookup already updated it. + } + } + } + // If we loaded this block for this operation go ahead and expire it here. + if shouldExpire { + mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() + } + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.mu.Unlock() + total += t + } + return total, validThrough, nil + } + + // If we are here it's better to calculate totals from psim and adjust downward by scanning less blocks. + start := uint32(math.MaxUint32) + stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool { + total += psi.total + // Keep track of start index for this subject. + if psi.fblk < start { + start = psi.fblk + } + return true + }) + + // See if we were asked for all, if so we are done. + if sseq <= fs.state.FirstSeq { + return total, validThrough, nil + } + + // If we are here we need to calculate partials for the first blocks. + firstSubjBlk := fs.bim[start] + var firstSubjBlkFound bool + // Adjust in case not found. + if firstSubjBlk == nil { + firstSubjBlkFound = true + } + + // Track how many we need to adjust against the total. + var adjust uint64 + for i := 0; i <= seqStart; i++ { + mb := fs.blks[i] + // We can skip blks if we know they are below the first one that has any subject matches. + if !firstSubjBlkFound { + if firstSubjBlkFound = (mb == firstSubjBlk); !firstSubjBlkFound { + continue + } + } + // We need to scan this block. + var shouldExpire bool + var updateLLTS bool + mb.mu.Lock() + // Check if we should include all of this block in adjusting. If so work with metadata. + if sseq > atomic.LoadUint64(&mb.last.seq) { + if isAll { + adjust += mb.msgs + } else { + // We need to adjust for all matches in this block. + // Make sure we have fss loaded. This loads whole block now. + if mb.fssNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + stree.IntersectGSL(mb.fss, sl, func(bsubj []byte, ss *SimpleState) bool { + adjust += ss.Msgs + return true + }) + } + } else { + // This is the last block. We need to scan per message here. + if mb.cacheNotLoaded() { + if err = mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + shouldExpire = true + } + var last = atomic.LoadUint64(&mb.last.seq) + if sseq < last { + last = sseq + } + // We need to walk all messages in this block + var smv StoreMsg + for seq := atomic.LoadUint64(&mb.first.seq); seq < last; seq++ { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + updateLLTS = true + continue + } + sm, _ := mb.cacheLookupNoCopy(seq, &smv) + if sm == nil || sm.subj == _EMPTY_ { + continue + } + updateLLTS = false // cacheLookup already updated it. + // Check if it matches our filter. + if sm.seq < sseq && isMatch(sm.subj) { + adjust++ + } + } + } + // If we loaded the block try to force expire. + if shouldExpire { + mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() + } + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.mu.Unlock() + } + // Make final adjustment. + if adjust > total { + total = 0 + } else { + total -= adjust + } + + return total, validThrough, nil +} + +// SubjectsTotals return message totals per subject. +func (fs *fileStore) SubjectsTotals(filter string) map[string]uint64 { + fs.mu.RLock() + defer fs.mu.RUnlock() + return fs.subjectsTotalsLocked(filter) +} + +// Lock should be held. +func (fs *fileStore) subjectsTotalsLocked(filter string) map[string]uint64 { + if fs.psim.Size() == 0 { + return nil + } + // Match all if no filter given. + if filter == _EMPTY_ { + filter = fwcs + } + fst := make(map[string]uint64) + fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) { + fst[string(subj)] = psi.total + }) + return fst +} + +// ApplySourcesState replaces the tracked sourcing state with the leader's. +func (fs *fileStore) ApplySourcesState(sources map[string]StreamSourceState) { + if len(sources) == 0 { + return + } + fs.mu.Lock() + defer fs.mu.Unlock() + // Nil means no sources are configured, so there is nothing to track. + if fs.sources == nil { + return + } + clear(fs.sources) + for name, ss := range sources { + fs.sources[name] = &StreamSourceState{Seq: ss.Seq, Ident: ss.Ident} + } + // Keep every configured source present, seeded as not yet observed, which is + // the invariant recoverPerMessageState and storeRawMsg expect. + for _, ssi := range fs.cfg.Sources { + if iname := ssi.composeIName(); fs.sources[iname] == nil { + fs.sources[iname] = &StreamSourceState{} + } + } + fs.dirty++ +} + +// SourcesState return sources keys and their last known state. +func (fs *fileStore) SourcesState() map[string]StreamSourceState { + fs.mu.RLock() + defer fs.mu.RUnlock() + if len(fs.sources) == 0 { + return nil + } + dst := make(map[string]StreamSourceState, len(fs.sources)) + for k, ss := range fs.sources { + dst[k] = *ss + } + return dst +} + +// RegisterStorageUpdates registers a callback for updates to storage changes. +// It will present number of messages and bytes as a signed integer and an +// optional sequence number of the message if a single. +func (fs *fileStore) RegisterStorageUpdates(cb StorageUpdateHandler) { + fs.mu.Lock() + fs.scb = cb + bsz := fs.state.Bytes + fs.mu.Unlock() + if cb != nil && bsz > 0 { + cb(0, int64(bsz), 0, _EMPTY_) + } +} + +// RegisterStorageRemoveMsg registers a callback to remove messages. +// Replicated streams should propose removals, R1 can remove inline. +func (fs *fileStore) RegisterStorageRemoveMsg(cb StorageRemoveMsgHandler) { + fs.mu.Lock() + fs.rmcb = cb + fs.mu.Unlock() +} + +// RegisterProcessJetStreamMsg registers a callback to process new JetStream messages. +func (fs *fileStore) RegisterProcessJetStreamMsg(cb ProcessJetStreamMsgHandler) { + fs.mu.Lock() + fs.pmsgcb = cb + fs.mu.Unlock() +} + +// Helper to get hash key for specific message block. +// Lock should be held +func (fs *fileStore) hashKeyForBlock(index uint32) []byte { + return []byte(fmt.Sprintf("%s-%d", fs.cfg.Name, index)) +} + +func (mb *msgBlock) setupWriteCache(buf []byte) error { + // Not safe to use mb.cacheAlreadyLoaded() here as the heuristic + // is wrong for writing to the beginning of an empty block cache. + if mb.cache != nil { + return nil + } + if mb.cache = mb.ecache.Value(); mb.cache != nil { + return nil + } + + // If there's an existing block on disk then we'll load it in. + var fi os.FileInfo + if mb.mfd != nil { + fi, _ = mb.mfd.Stat() + } else if mb.mfn != _EMPTY_ { + fi, _ = os.Stat(mb.mfn) + } + if fi != nil { + return mb.loadMsgsWithLock() + } + + // Looks like there isn't an existing file on disk, mint a new cache. + mb.cache = &cache{buf: buf} + mb.cache.registerRecycle() + mb.ecache.Set(mb.cache) + mb.llts = ats.AccessTime() + mb.startCacheExpireTimer() + return nil +} + +// finishedWithCache removes the strong reference to the cache, instead +// returning it to the elastic pointer. Note that the elastic pointer can +// be either weak or strong, they are strengthened when there are pending +// writes and weak otherwise. +// Lock must be held. +func (mb *msgBlock) finishedWithCache() { + if mb.cache != nil && mb.pendingWriteSizeLocked() == 0 { + mb.cache = nil + } +} + +// This rolls to a new append msg block. +// Lock should be held. +func (fs *fileStore) newMsgBlockForWrite() (*msgBlock, error) { + index := uint32(1) + var rbuf []byte + + if lmb := fs.lmb; lmb != nil { + lmb.mu.Lock() + index = lmb.index + 1 + + // Quit our loops. + if lmb.qch != nil { + close(lmb.qch) + lmb.qch = nil + } + // If we had a write error before, don't allow continuing into a new block. + if err := lmb.werr; err != nil { + lmb.mu.Unlock() + return nil, err + } + // Flush any pending messages. + if _, err := lmb.flushPendingMsgsLocked(); err != nil { + lmb.mu.Unlock() + return nil, err + } + // Determine if we can reclaim any resources here. + lmb.closeFDsLockedNoCheck() + if lmb.cache != nil { + // Reset write timestamp and see if we can expire this cache. + rbuf = lmb.tryExpireWriteCache() + } + lmb.mu.Unlock() + + if fs.fcfg.Compression != NoCompression { + // We've now reached the end of this message block, if we want + // to compress blocks then now's the time to do it. + go func() { + lmb.mu.Lock() + defer lmb.mu.Unlock() + // Might error, but we can't handle it here anyway. + _ = lmb.recompressOnDiskIfNeeded() + }() + } + } + + mb := fs.initMsgBlock(index) + // Lock should be held to quiet race detector. + mb.mu.Lock() + if err := mb.setupWriteCache(rbuf); err != nil { + mb.mu.Unlock() + return nil, err + } + mb.fss = stree.NewSubjectTree[SimpleState]() + + // Set cache time to creation time to start. + mb.llts, mb.lwts = 0, ats.AccessTime() + // Remember our last sequence number. + atomic.StoreUint64(&mb.first.seq, fs.state.LastSeq+1) + atomic.StoreUint64(&mb.last.seq, fs.state.LastSeq) + mb.mu.Unlock() + + // Now do local hash. + key := sha256.Sum256(fs.hashKeyForBlock(index)) + hh, err := highwayhash.NewDigest64(key[:]) + if err != nil { + return nil, fmt.Errorf("could not create hash: %v", err) + } + mb.hh = hh + + fs.dios.acquire() + mfd, err := os.OpenFile(mb.mfn, os.O_CREATE|os.O_RDWR, defaultFilePerms) + fs.dios.release() + + if err != nil { + if isPermissionError(err) { + return nil, err + } + _ = mb.dirtyCloseWithRemove(true) + return nil, fmt.Errorf("Error creating msg block file: %v", err) + } + mb.mfd = mfd + + // Check if encryption is enabled. + if fs.prf != nil { + if err := fs.genEncryptionKeysForBlock(mb); err != nil { + return nil, err + } + } + + // If we know we will need this so go ahead and spin up. + if !fs.fip { + mb.spinUpFlushLoop() + } + + // Add to our list of blocks and mark as last. + fs.addMsgBlock(mb) + + return mb, nil +} + +// Generate the keys for this message block and write them out. +func (fs *fileStore) genEncryptionKeysForBlock(mb *msgBlock) error { + if mb == nil { + return nil + } + key, bek, seed, encrypted, err := fs.genEncryptionKeys(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index)) + if err != nil { + return err + } + mb.aek, mb.bek, mb.seed, mb.nonce = key, bek, seed, encrypted[:key.NonceSize()] + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + keyFile := filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index)) + if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) { + return err + } + sync := fs.syncAlways.Load() || fs.syncOnFlush.Load() + err = writeAtomically(fs.dios, keyFile, encrypted, defaultFilePerms, sync) + if err != nil { + return err + } + mb.kfn = keyFile + // If we did not sync the key file above, mark it to be synced on the next syncBlocks pass. + mb.needKeySync = !sync + return nil +} + +// Stores a raw message with expected sequence number and timestamp. +// Lock should be held. +func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64, discardNewCheck bool) (err error) { + if fs.isClosed() { + return ErrStoreClosed + } + + // Per subject max check needed. + mmp := uint64(fs.cfg.MaxMsgsPer) + var psmc uint64 + psmax := mmp > 0 && len(subj) > 0 + var info *psi + if psmax { + if info, _ = fs.psim.Find(stringToBytes(subj)); info != nil { + // Take current total, but add 1 for the message we are about to store. + psmc = info.total + 1 + } + } + + var fseq uint64 + // Check if we are discarding new messages when we reach the limit. + // If we are clustered, we do the enforcement above and should not disqualify + // the message here since it could cause replicas to drift. + if discardNewCheck && fs.cfg.Discard == DiscardNew { + var asl bool + if psmax && psmc > mmp { + // If we are instructed to discard new per subject, this is an error. + // However, allow rollup messages through since they will purge old + // messages for the subject after storing, restoring the limit. + if fs.cfg.DiscardNewPer && len(sliceHeader(JSMsgRollup, hdr)) == 0 { + return ErrMaxMsgsPerSubject + } + if fseq, err = fs.firstSeqForSubj(subj); err != nil { + return err + } + // fs.firstSeqForSubj releases and re-acquires the lock, need to fetch the state again. + if info, _ = fs.psim.Find(stringToBytes(subj)); info != nil { + // Take current total, but add 1 for the message we are about to store. + psmc = info.total + 1 + } + asl = psmc > mmp + } + if fs.cfg.MaxMsgs > 0 && fs.state.Msgs >= uint64(fs.cfg.MaxMsgs) && !asl { + return ErrMaxMsgs + } + if fs.cfg.MaxBytes > 0 && fs.state.Bytes+fileStoreMsgSize(subj, hdr, msg) > uint64(fs.cfg.MaxBytes) { + if !asl || fs.sizeForSeq(fseq) < int(fileStoreMsgSize(subj, hdr, msg)) { + return ErrMaxBytes + } + } + } + + // Check sequence. + if seq != fs.state.LastSeq+1 { + if seq > 0 { + return ErrSequenceMismatch + } + seq = fs.state.LastSeq + 1 + } + + // Return previous write errors immediately. + if fs.werr != nil { + return fs.werr + } + // Persist any returned errors to be used in the future. + defer func() { + if err != nil { + fs.setWriteErr(err) + } + }() + + // Write msg record. + // Add expiry bit to sequence if needed. This is so that if we need to + // rebuild, we know which messages to look at more quickly. + n, err := fs.writeMsgRecord(seq, ts, subj, hdr, msg) + if err != nil { + return err + } + + // Adjust top level tracking of per subject msg counts. + if len(subj) > 0 && fs.psim != nil { + index := fs.lmb.index + if info == nil { + info, _ = fs.psim.Find(stringToBytes(subj)) + } + if info != nil { + info.total++ + if index > info.lblk { + info.lblk = index + } + } else { + info, _ = fs.psim.Insert(stringToBytes(subj), psi{total: 1, fblk: index, lblk: index}) + fs.tsl += len(subj) + } + } + + // Adjust first if needed. + now := time.Unix(0, ts).UTC() + if fs.state.Msgs == 0 { + fs.state.FirstSeq = seq + fs.state.FirstTime = now + } + + fs.state.Msgs++ + fs.state.Bytes += n + fs.state.LastSeq = seq + fs.state.LastTime = now + + // Enforce per message limits. + for psmax && psmc > mmp { + // We may have done this above. + if fseq == 0 { + fseq, err = fs.firstSeqForSubj(subj) + if err != nil { + return err + } else if fseq == 0 { + break + } + // fs.firstSeqForSubj releases and re-acquires the lock, need to fetch the state again. + if info, _ = fs.psim.Find(stringToBytes(subj)); info != nil { + psmc = info.total + } else { + break + } + // Re-check if we're at the limit. + continue + } + if _, err = fs.removeMsgViaLimits(fseq); err != nil && err != ErrStoreMsgNotFound { + return err + } + fseq = 0 + // fs.removeMsgViaLimits releases and re-acquires the lock, need to fetch the state again. + if info, _ = fs.psim.Find(stringToBytes(subj)); info != nil { + psmc = info.total + } else { + break + } + } + // If we only ever store one/last message for a subject, can correct the first block to where we've just written. + if info != nil && info.total == 1 && mmp == 1 { + info.fblk = info.lblk + } + + // Limits checks and enforcement. + // If they do any deletions they will update the + // byte count on their own, so no need to compensate. + if err = fs.enforceMsgLimit(); err != nil { + return err + } + if err = fs.enforceBytesLimit(); err != nil { + return err + } + + // Per-message TTL. + if ttl > 0 { + if fs.ttls != nil { + expires := time.Duration(ts) + (time.Second * time.Duration(ttl)) + fs.ttls.Add(seq, int64(expires)) + } + // Need to count these regardless as we might want to enable TTLs + // later via UpdateConfig. + fs.lmb.mu.Lock() + fs.lmb.ttls++ + fs.lmb.mu.Unlock() + } + + // Check if we have and need the age expiration timer running. + switch { + case fs.ttls != nil && ttl > 0: + fs.resetAgeChk(0) + case fs.ageChk == nil && (fs.cfg.MaxAge > 0 || fs.ttls != nil): + fs.startAgeChk() + } + + // Message scheduling. + if fs.scheduling != nil { + if schedule, apiErr := nextMessageSchedule(hdr, ts); apiErr == nil && !schedule.IsZero() { + fs.scheduling.add(seq, subj, schedule.UnixNano()) + fs.lmb.mu.Lock() + fs.lmb.schedules++ + fs.lmb.mu.Unlock() + } else if getMessageScheduler(hdr) == _EMPTY_ { + fs.scheduling.removeSubject(subj) + } + + // Check for a repeating schedule and update such that it triggers again. + if scheduleNext := bytesToString(sliceHeader(JSScheduleNext, hdr)); scheduleNext != _EMPTY_ && scheduleNext != JSScheduleNextPurge { + scheduler := getMessageScheduler(hdr) + if next, err := time.Parse(time.RFC3339Nano, scheduleNext); err == nil && scheduler != _EMPTY_ { + fs.scheduling.update(scheduler, next.UnixNano()) + } + } + } + + // Track sources. + if len(fs.cfg.Sources) > 0 { + if ss := sliceHeader(JSStreamSource, hdr); len(ss) > 0 { + _, indexName, sseq, ident := streamAndSeq(bytesToString(ss)) + if sss, ok := fs.sources[indexName]; ok { + sss.Seq, sss.Ident = sseq, ident + } + } + } + + return nil +} + +// isReadErr reports whether err originated from reading or interpreting +// existing on-disk data rather than from a failed write. These surface through +// cache/load paths and should not permanently disable writes. +func isReadErr(err error) bool { + var badMsg errBadMsg + return errors.Is(err, errNoCache) || + errors.Is(err, errDeletedMsg) || + errors.Is(err, errPartialCache) || + errors.Is(err, errCorruptState) || + errors.Is(err, errPriorState) || + errors.Is(err, io.ErrUnexpectedEOF) || + errors.Is(err, errMsgBlkTooBig) || + errors.As(err, &badMsg) +} + +// Lock should be held. +func (fs *fileStore) setWriteErr(err error) { + if fs.werr != nil { + return + } + // Ignore non-write errors. + if err == ErrStoreClosed { + return + } + // If this is a not found report but do not disable. + if os.IsNotExist(err) { + fs.warn("Resource not found: %v", err) + return + } + // Read/decode errors surfaced from existing on-disk data are not write failures. + // Log and continue instead of disabling writes. + if isReadErr(err) { + fs.rateLimitWarn("Ignoring non-write error: %v", err) + assert.Unreachable("Filestore encountered read error", map[string]any{ + "name": fs.cfg.Name, + "err": err, + "stack": string(debug.Stack()), + }) + return + } + fs.error("Critical write error: %v", err) + fs.werr = err + assert.Unreachable("Filestore encountered write error", map[string]any{ + "name": fs.cfg.Name, + "err": err, + "stack": string(debug.Stack()), + }) +} + +// StoreRawMsg stores a raw message with expected sequence number and timestamp. +func (fs *fileStore) StoreRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64, discardNewCheck bool) error { + fs.mu.Lock() + // Always return previous write errors. + if err := fs.werr; err != nil { + fs.mu.Unlock() + return err + } + err := fs.storeRawMsg(subj, hdr, msg, seq, ts, ttl, discardNewCheck) + cb := fs.scb + // Check if first message timestamp requires expiry + // sooner than initial replica expiry timer set to MaxAge when initializing. + if !fs.receivedAny && fs.cfg.MaxAge != 0 && ts > 0 { + fs.receivedAny = true + fs.resetAgeChk(0) + } + fs.mu.Unlock() + + if err == nil && cb != nil { + cb(1, int64(fileStoreMsgSize(subj, hdr, msg)), seq, subj) + } + + return err +} + +// Store stores a message. We hold the main filestore lock for any write operation. +func (fs *fileStore) StoreMsg(subj string, hdr, msg []byte, ttl int64) (uint64, int64, error) { + fs.mu.Lock() + // Always return previous write errors. + if err := fs.werr; err != nil { + fs.mu.Unlock() + return 0, 0, err + } + seq, ts := fs.state.LastSeq+1, time.Now().UnixNano() + // This is called for a R1 with no expected sequence number, so perform DiscardNew checks on the store-level. + err := fs.storeRawMsg(subj, hdr, msg, seq, ts, ttl, true) + cb := fs.scb + fs.mu.Unlock() + + if err != nil { + seq, ts = 0, 0 + } else if cb != nil { + cb(1, int64(fileStoreMsgSize(subj, hdr, msg)), seq, subj) + } + + return seq, ts, err +} + +// skipMsg will update this message block for a skipped message. +// If we do not have any messages, just update the metadata, otherwise +// we will place an empty record marking the sequence as used. The +// sequence will be marked erased. +// fs lock should be held. +func (mb *msgBlock) skipMsg(seq uint64, ts int64) error { + if mb == nil { + return nil + } + var needsRecord bool + + mb.mu.Lock() + if err := mb.werr; err != nil { + mb.mu.Unlock() + return err + } + // If we are empty can just do meta. + if mb.msgs == 0 { + atomic.StoreUint64(&mb.last.seq, seq) + mb.last.ts = ts + atomic.StoreUint64(&mb.first.seq, seq+1) + mb.first.ts = 0 + needsRecord = mb == mb.fs.lmb + } else { + needsRecord = true + mb.dmap.Insert(seq) + } + if needsRecord { + if err := mb.writeMsgRecordLocked(emptyRecordLen, seq|ebit, _EMPTY_, nil, nil, ts, true, true); err != nil { + mb.mu.Unlock() + return err + } + } + mb.mu.Unlock() + if !needsRecord { + mb.kickFlusher() + } + return nil +} + +// SkipMsg will use the next sequence number but not store anything. +func (fs *fileStore) SkipMsg(seq uint64) (uint64, error) { + return fs.skipMsg(seq, false) +} + +// SkipMsgNoInterest will use the next sequence number but not store anything. +// Unlike SkipMsg it also advances LastTime, which is used for Interest retention. +func (fs *fileStore) SkipMsgNoInterest(seq uint64) (uint64, error) { + return fs.skipMsg(seq, true) +} + +func (fs *fileStore) skipMsg(seq uint64, noInterest bool) (uint64, error) { + fs.mu.Lock() + defer fs.mu.Unlock() + + if fs.isClosed() { + return 0, ErrStoreClosed + } + // Always return previous write errors. + if err := fs.werr; err != nil { + return 0, err + } + + // Check sequence matches our last sequence. + if seq != fs.state.LastSeq+1 { + if seq > 0 { + return 0, ErrSequenceMismatch + } + seq = fs.state.LastSeq + 1 + } + + // Grab our current last message block. + mb, err := fs.checkLastBlock(emptyRecordLen) + if err != nil { + return 0, err + } + + // Only update time if not already set or for Interest retention. + var ts int64 + updateTime := noInterest || fs.state.LastTime.IsZero() + if updateTime { + ts = ats.AccessTime() + } else { + ts = fs.state.LastTime.UnixNano() + } + + // Write skip msg. + if err = mb.skipMsg(seq, ts); err != nil { + fs.setWriteErr(err) + return 0, err + } + + // Update fs state. + fs.state.LastSeq = seq + if updateTime { + fs.state.LastTime = time.Unix(0, ts).UTC() + } + if fs.state.Msgs == 0 { + fs.state.FirstSeq, fs.state.FirstTime = seq, time.Time{} + } + if seq == fs.state.FirstSeq { + fs.state.FirstSeq, fs.state.FirstTime = seq+1, time.Time{} + } + // Mark as dirty for stream state. + fs.dirty++ + + return seq, nil +} + +// SkipMsgs skips multiple msgs. We will determine if we can fit into current lmb or we need to create a new block. +func (fs *fileStore) SkipMsgs(seq uint64, num uint64) error { + fs.mu.Lock() + defer fs.mu.Unlock() + + if fs.isClosed() { + return ErrStoreClosed + } + // Always return previous write errors. + if err := fs.werr; err != nil { + return err + } + + // Check sequence matches our last sequence. + if seq != fs.state.LastSeq+1 { + if seq > 0 { + return ErrSequenceMismatch + } + seq = fs.state.LastSeq + 1 + } + // Make sure the last sequence leaves room for the next message. + if num == 0 || seq == 0 || num > math.MaxUint64-seq { + return ErrSequenceMismatch + } + + // Limit number of dmap entries + const maxDeletes = 64 * 1024 + mb := fs.lmb + + var msgs uint64 + numDeletes := num + if mb != nil { + mb.mu.RLock() + numDeletes = addSaturate(numDeletes, uint64(mb.dmap.Size())) + msgs = mb.msgs + mb.mu.RUnlock() + } + if mb == nil || numDeletes > maxDeletes && msgs > 0 || msgs > 0 && mb.blkSize()+emptyRecordLen > fs.fcfg.BlockSize { + var err error + if mb, err = fs.newMsgBlockForWrite(); err != nil { + return err + } + } + + // Insert into dmap all entries and place last as marker. + var ts int64 + if !fs.state.LastTime.IsZero() { + ts = fs.state.LastTime.UnixNano() + } else { + ts = ats.AccessTime() + } + lseq := seq + num - 1 + + mb.mu.Lock() + if err := mb.werr; err != nil { + mb.mu.Unlock() + return err + } + // If we are empty update meta directly. + if mb.msgs == 0 { + atomic.StoreUint64(&mb.last.seq, lseq) + mb.last.ts = ts + atomic.StoreUint64(&mb.first.seq, lseq+1) + mb.first.ts = 0 + } else { + for ; seq <= lseq; seq++ { + mb.dmap.Insert(seq) + } + } + // Write out our placeholder. + err := mb.writeMsgRecordLocked(emptyRecordLen, lseq|ebit, _EMPTY_, nil, nil, ts, true, true) + mb.mu.Unlock() + if err != nil { + fs.setWriteErr(err) + return err + } + + // Now update FS accounting. + // Update fs state. + fs.state.LastSeq = lseq + if fs.state.LastTime.IsZero() { + fs.state.LastTime = time.Unix(0, ts).UTC() + } + if fs.state.Msgs == 0 { + fs.state.FirstSeq, fs.state.FirstTime = lseq+1, time.Time{} + } + + // Mark as dirty for stream state. + fs.dirty++ + + return nil +} + +// FlushAllPending flushes all data that was still pending to be written. +func (fs *fileStore) FlushAllPending() error { + fs.mu.Lock() + defer fs.mu.Unlock() + // Return previous write errors immediately. + if fs.werr != nil { + return fs.werr + } + if fs.syncOnFlush.Load() { + fs.mu.Unlock() + fs.syncBlocks() + fs.mu.Lock() + return fs.werr + } + return fs.checkAndFlushLastBlock() +} + +// Lock should be held. +func (fs *fileStore) rebuildFirst() error { + if len(fs.blks) == 0 { + return nil + } + fmb := fs.blks[0] + if fmb == nil { + return nil + } + + ld, _, _ := fmb.rebuildState() + fmb.mu.RLock() + isEmpty := fmb.msgs == 0 + fmb.mu.RUnlock() + if isEmpty { + fmb.mu.Lock() + err := fs.forceRemoveMsgBlock(fmb) + fmb.mu.Unlock() + if err != nil { + return err + } + } + if err := fs.selectNextFirst(); err != nil { + return err + } + fs.rebuildStateLocked(ld) + return nil +} + +// Optimized helper function to return first sequence. +// subj will always be publish subject here, meaning non-wildcard. +// We assume a fast check that this subj even exists already happened. +// Write lock should be held. +func (fs *fileStore) firstSeqForSubj(subj string) (uint64, error) { + if len(fs.blks) == 0 { + return 0, nil + } + + // See if we can optimize where we start. + start, stop := fs.blks[0].index, fs.lmb.index + if info, ok := fs.psim.Find(stringToBytes(subj)); ok { + start, stop = info.fblk, info.lblk + } + + for i := start; i <= stop; i++ { + mb := fs.bim[i] + if mb == nil { + continue + } + // If we need to load msgs here and we need to walk multiple blocks this + // could tie up the upper fs lock, so release while dealing with the block. + fs.mu.Unlock() + + mb.mu.Lock() + // If marked closed, the block is already gone. + if mb.closed { + mb.mu.Unlock() + fs.mu.Lock() + continue + } + needsCleanup := mb.cache == nil + var shouldExpire bool + if mb.fssNotLoaded() { + // Make sure we have fss loaded. + if err := mb.loadMsgsWithLock(); err != nil { + if needsCleanup { + mb.finishedWithCache() + } + mb.mu.Unlock() + // Re-acquire fs lock + fs.mu.Lock() + return 0, err + } + shouldExpire = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + + bsubj := stringToBytes(subj) + if ss, ok := mb.fss.Find(bsubj); ok && ss != nil { + var err error + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + err = mb.recalculateForSubj(subj, ss) + } + if needsCleanup { + mb.finishedWithCache() + } + mb.mu.Unlock() + // Re-acquire fs lock + fs.mu.Lock() + // Adjust first if it was not where we thought it should be. + if i != start { + if info, ok := fs.psim.Find(bsubj); ok { + info.fblk = i + } + } + if err != nil { + return 0, err + } + return ss.First, nil + } + // If we did not find it and we loaded this msgBlock try to expire as long as not the last. + if shouldExpire { + // Expire this cache before moving on. + mb.tryForceExpireCacheLocked() + } else if needsCleanup { + mb.finishedWithCache() + } + mb.mu.Unlock() + // Re-acquire fs lock + fs.mu.Lock() + } + return 0, nil +} + +// Will check the msg limit and drop firstSeq msg if needed. +// Lock should be held. +func (fs *fileStore) enforceMsgLimit() error { + if fs.cfg.Discard != DiscardOld { + return nil + } + if fs.cfg.MaxMsgs <= 0 || fs.state.Msgs <= uint64(fs.cfg.MaxMsgs) { + return nil + } + for nmsgs := fs.state.Msgs; nmsgs > uint64(fs.cfg.MaxMsgs); nmsgs = fs.state.Msgs { + // If the first block can be removed fully, purge it entirely without needing to walk sequences. + if len(fs.blks) > 0 { + fmb := fs.blks[0] + fmb.mu.RLock() + msgs := fmb.msgs + fmb.mu.RUnlock() + if nmsgs-msgs > uint64(fs.cfg.MaxMsgs) { + if err := fs.purgeMsgBlock(fmb, nil); err != nil { + return err + } + continue + } + } + if removed, err := fs.deleteFirstMsg(); err != nil { + return err + } else if !removed { + return fs.rebuildFirst() + } + } + return nil +} + +// Will check the bytes limit and drop msgs if needed. +// Lock should be held. +func (fs *fileStore) enforceBytesLimit() error { + if fs.cfg.Discard != DiscardOld { + return nil + } + if fs.cfg.MaxBytes <= 0 || fs.state.Bytes <= uint64(fs.cfg.MaxBytes) { + return nil + } + for bs := fs.state.Bytes; bs > uint64(fs.cfg.MaxBytes); bs = fs.state.Bytes { + // If the first block can be removed fully, purge it entirely without needing to walk sequences. + if len(fs.blks) > 0 { + fmb := fs.blks[0] + fmb.mu.RLock() + bytes := fmb.bytes + fmb.mu.RUnlock() + if bs-bytes > uint64(fs.cfg.MaxBytes) { + if err := fs.purgeMsgBlock(fmb, nil); err != nil { + return err + } + continue + } + } + if removed, err := fs.deleteFirstMsg(); err != nil { + return err + } else if !removed { + return fs.rebuildFirst() + } + } + return nil +} + +// Will make sure we have limits honored for max msgs per subject on recovery or config update. +// We will make sure to go through all msg blocks etc. but in practice this +// will most likely only be the last one, so can take a more conservative approach. +// Lock should be held. +func (fs *fileStore) enforceMsgPerSubjectLimit(fireCallback bool) error { + start := time.Now() + defer func() { + if took := time.Since(start); took > time.Minute { + fs.warn("enforceMsgPerSubjectLimit took %v", took.Round(time.Millisecond)) + } + }() + + maxMsgsPer := uint64(fs.cfg.MaxMsgsPer) + + // We may want to suppress callbacks from remove during this process + // since these should have already been deleted and accounted for. + if !fireCallback { + cb := fs.scb + fs.scb = nil + defer func() { fs.scb = cb }() + } + + var numMsgs uint64 + + // collect all that are not correct. + needAttention := stree.NewSubjectTree[uint64]() + fblk, lblk := uint32(math.MaxUint32), uint32(0) + fs.psim.IterFast(func(subj []byte, psi *psi) bool { + numMsgs += psi.total + if psi.total > maxMsgsPer { + needAttention.Insert(subj, psi.total) + if psi.fblk < fblk { + fblk = psi.fblk + } + if psi.lblk > lblk { + lblk = psi.lblk + } + } + return true + }) + + // We had an issue with a use case where psim (and hence fss) were correct but idx was not and was not properly being caught. + // So do a quick sanity check here. If we detect a skew do a rebuild then re-check. + if numMsgs != fs.state.Msgs { + fs.warn("Detected skew in subject-based total (%d) vs raw total (%d), rebuilding", numMsgs, fs.state.Msgs) + // Clear any global subject state. + fs.psim, fs.tsl = fs.psim.Empty(), 0 + for _, mb := range fs.blks { + ld, _, err := mb.rebuildState() + if err != nil { + return err + } else if ld != nil { + fs.addLostData(ld) + } + if err = fs.populateGlobalPerSubjectInfo(mb); err != nil { + return err + } + } + // Rebuild fs state too. + fs.rebuildStateLocked(nil) + // Need to redo blocks that need attention. + needAttention.Empty() + fblk, lblk = uint32(math.MaxUint32), uint32(0) + fs.psim.IterFast(func(subj []byte, psi *psi) bool { + if psi.total > maxMsgsPer { + needAttention.Insert(subj, psi.total) + if psi.fblk < fblk { + fblk = psi.fblk + } + if psi.lblk > lblk { + lblk = psi.lblk + } + } + return true + }) + } + + // If nothing to do then stop. + if fblk == math.MaxUint32 { + return nil + } + + // Collect all the msgBlks we alter. + blks := make(map[*msgBlock]struct{}) + + // For re-use below. + var sm StoreMsg + var fss *stree.SubjectTree[*SimpleState] + for i := fblk; i <= lblk; i++ { + mb := fs.bim[i] + if mb == nil { + continue + } + mb.mu.Lock() + if err := mb.ensurePerSubjectInfoLoaded(); err != nil { + mb.mu.Unlock() + return err + } + // It isn't safe to intersect mb.fss directly, because removeMsgViaLimits modifies it + // during the iteration, which can cause us to miss keys. We won't copy the entire + // SimpleState structs though but rather just take pointers for speed. + fss = fss.Empty() + mb.fss.IterFast(func(subject []byte, val *SimpleState) bool { + fss.Insert(subject, val) + return true + }) + mb.mu.Unlock() + var ierr error + stree.LazyIntersect(needAttention, fss, func(subj []byte, total *uint64, ssptr **SimpleState) { + if ssptr == nil || total == nil || ierr != nil { + return + } + ss := *ssptr + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + mb.mu.Lock() + ierr = mb.recalculateForSubj(bytesToString(subj), ss) + mb.mu.Unlock() + if ierr != nil { + return + } + } + for first := ss.First; *total > maxMsgsPer && first <= ss.Last; { + m, _, err := mb.firstMatching(bytesToString(subj), false, first, &sm) + if err != nil { + break + } + first = m.seq + 1 + if removed, _ := fs.removeMsgViaLimits(m.seq); removed { + blks[mb] = struct{}{} + *total-- + } + } + }) + if ierr != nil { + return ierr + } + } + + // Expire the cache if we can. + for mb := range blks { + mb.mu.Lock() + if mb.msgs > 0 { + mb.tryForceExpireCacheLocked() + } + mb.mu.Unlock() + } + return nil +} + +// Lock should be held. +func (fs *fileStore) deleteFirstMsg() (bool, error) { + return fs.removeMsgViaLimits(fs.state.FirstSeq) +} + +// If we remove via limits that can always be recovered on a restart we +// do not force the system to update the index file. +// Lock should be held. +func (fs *fileStore) removeMsgViaLimits(seq uint64) (bool, error) { + return fs.removeMsg(seq, false, true, false) +} + +// RemoveMsg will remove the message from this store. +// Will return the number of bytes removed. +func (fs *fileStore) RemoveMsg(seq uint64) (bool, error) { + return fs.removeMsg(seq, false, false, true) +} + +func (fs *fileStore) EraseMsg(seq uint64) (bool, error) { + return fs.removeMsg(seq, true, false, true) +} + +// Convenience function to remove per subject tracking at the filestore level. +// Lock should be held. +func (fs *fileStore) removePerSubject(subj string) uint64 { + if len(subj) == 0 || fs.psim == nil { + return 0 + } + // We do not update sense of fblk here but will do so when we resolve during lookup. + bsubj := stringToBytes(subj) + if info, ok := fs.psim.Find(bsubj); ok { + info.total-- + if info.total == 0 { + if _, ok = fs.psim.Delete(bsubj); ok { + fs.tsl -= len(subj) + return 0 + } + } + return info.total + } + return 0 +} + +// Remove a message, optionally rewriting the mb file. +func (fs *fileStore) removeMsg(seq uint64, secure, viaLimits, needFSLock bool) (bool, error) { + if seq == 0 { + return false, ErrStoreMsgNotFound + } + fsLock := func() { + if needFSLock { + fs.mu.Lock() + } + } + fsUnlock := func() { + if needFSLock { + fs.mu.Unlock() + } + } + + fsLock() + defer fsUnlock() + + if fs.isClosed() { + return false, ErrStoreClosed + } + // Always return previous write errors. + if err := fs.werr; err != nil { + return false, err + } + // If in encrypted mode negate secure rewrite here. + if secure && fs.prf != nil { + secure = false + } + + mb := fs.selectMsgBlock(seq) + if mb == nil { + var err = ErrStoreEOF + if seq <= fs.state.LastSeq { + err = ErrStoreMsgNotFound + } + return false, err + } + + return fs.removeMsgFromBlock(mb, seq, secure, viaLimits) +} + +// Remove a message from the given block, optionally rewriting the mb file. +// fs lock should be held. +func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLimits bool) (removed bool, rerr error) { + mb.mu.Lock() + + // See if we are closed or the sequence number is still relevant or if we know its deleted. + if mb.closed || seq < atomic.LoadUint64(&mb.first.seq) || mb.dmap.Exists(seq) { + mb.mu.Unlock() + return false, nil + } + + // Persist any write errors. + defer func() { + if rerr != nil { + fs.setWriteErr(rerr) + } + }() + + fifo := seq == atomic.LoadUint64(&mb.first.seq) + isLastBlock := mb == fs.lmb + isEmpty := mb.msgs == 1 // ... about to be zero though. + + // We used to not have to load in the messages except with callbacks or the filtered subject state (which is now always on). + // Only load from disk if the cache is not already in memory, cacheNotLoaded will also revive a weakly held cache. + // TODO(dlc) - Figure out a way not to have to load it in, we need subject tracking outside main data block. + needsCleanup := mb.cache == nil + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + if needsCleanup { + mb.finishedWithCache() + } + mb.mu.Unlock() + return false, err + } + } + finishedWithCache := func() { + if needsCleanup { + mb.finishedWithCache() + } + } + + var ( + smv StoreMsg + subj string + ts int64 + lhdr, lmsg int + ttl int64 + ) + sm, err := mb.cacheLookupNoCopy(seq, &smv) + if err != nil { + finishedWithCache() + mb.mu.Unlock() + // Mimic err behavior from above check to dmap. No error returned if already removed. + if err == ErrStoreMsgNotFound || err == errDeletedMsg { + err = nil + } + return false, err + } else if sm != nil { + // subj aliases mb.cache.buf; copy now because the cache may be erased or + // recycled after we drop mb.mu. The rest are scalars stashed for later use. + subj = copyString(sm.subj) + ts = sm.ts + lhdr = len(sm.hdr) + lmsg = len(sm.msg) + ttl, _ = getMessageTTL(sm.hdr) + } + + // Check if we need to write a deleted record tombstone. + // This is for user initiated removes or to hold the first seq + // when the last block is empty. + // If not via limits and not empty (empty writes tombstone below if last) write tombstone. + if !viaLimits && !isEmpty && sm != nil { + mb.mu.Unlock() // Only safe way to checkLastBlock is to unlock here... + lmb, err := fs.checkLastBlock(emptyRecordLen) + if err != nil { + mb.mu.Lock() + finishedWithCache() + mb.mu.Unlock() + return false, err + } + if err := lmb.writeTombstone(seq, ts); err != nil { + mb.mu.Lock() + finishedWithCache() + mb.mu.Unlock() + return false, err + } + mb.mu.Lock() // We'll need the lock back to carry on safely. + } + + // Grab size + msz := fileStoreMsgSizeRaw(len(subj), lhdr, lmsg) + + // Set cache timestamp for last remove. + mb.lrts = ats.AccessTime() + + // Must always perform the erase, even if the block is empty as it could contain tombstones. + if secure { + // The cache may have been expired or recycled while we dropped mb.mu above. + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + finishedWithCache() + mb.mu.Unlock() + return false, err + } + } + // Grab record info, but use the pre-computed record length. + ri, _, _, err := mb.slotInfo(int(seq - mb.cache.fseq)) + if err != nil { + finishedWithCache() + mb.mu.Unlock() + return false, err + } + if err := mb.eraseMsg(seq, int(ri), int(msz), isLastBlock); err != nil { + finishedWithCache() + mb.mu.Unlock() + return false, err + } + } + + // Global stats + if fs.state.Msgs > 0 { + fs.state.Msgs-- + } + if msz < fs.state.Bytes { + fs.state.Bytes -= msz + } else { + fs.state.Bytes = 0 + } + + // Now local mb updates. + if mb.msgs > 0 { + mb.msgs-- + } + if msz < mb.bytes { + mb.bytes -= msz + } else { + mb.bytes = 0 + } + + // Allow us to check compaction again. + mb.noCompact = false + + // Mark as dirty for stream state. + fs.dirty++ + + // If we are tracking subjects here make sure we update that accounting. + if err = mb.ensurePerSubjectInfoLoaded(); err != nil { + finishedWithCache() + mb.mu.Unlock() + return false, err + } + + // If we are tracking multiple subjects here make sure we update that accounting. + if _, err = mb.removeSeqPerSubject(subj, seq); err != nil { + finishedWithCache() + mb.mu.Unlock() + return false, err + } + fs.removePerSubject(subj) + if fs.ttls != nil && ttl > 0 { + expires := time.Duration(ts) + (time.Second * time.Duration(ttl)) + fs.ttls.Remove(seq, int64(expires)) + } + + if fifo { + mb.selectNextFirst() + if !isEmpty { + // Can update this one in place. + if seq == fs.state.FirstSeq { + fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq) // new one. + if mb.first.ts == 0 { + fs.state.FirstTime = time.Time{} + } else { + fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC() + } + } + } + } else if !isEmpty { + // Out of order delete. + mb.dmap.Insert(seq) + // Make simple check here similar to Compact(). If we can save 50% and over a certain threshold do inline. + // All other more thorough cleanup will happen in syncBlocks logic. + // Note that we do not have to store empty records for the deleted, so don't use to calculate. + // TODO(dlc) - This should not be inline, should kick the sync routine. + if !isLastBlock && mb.shouldCompactInline() { + if err = mb.compact(); err != nil { + finishedWithCache() + mb.mu.Unlock() + return false, err + } + } + } + + if secure { + if ld, _ := mb.flushPendingMsgsLocked(); ld != nil { + // We have the mb lock here, this needs the mb locks so do in its own go routine. + go fs.rebuildState(ld) + } + } + + // If empty remove this block and check if we need to update first sequence. + // We will write a tombstone at the end. + var firstSeqNeedsUpdate bool + if isEmpty { + // This writes tombstone iff mb == lmb, so no need to do above. + if err = fs.removeMsgBlock(mb); err != nil { + finishedWithCache() + mb.mu.Unlock() + return false, err + } + firstSeqNeedsUpdate = seq == fs.state.FirstSeq + } + finishedWithCache() + mb.mu.Unlock() + + // If we emptied the current message block and the seq was state.FirstSeq + // then we need to jump message blocks. We will also write the index so + // we don't lose track of the first sequence. + if firstSeqNeedsUpdate { + if err = fs.selectNextFirst(); err != nil { + return false, err + } + } + + if cb := fs.scb; cb != nil { + // If we have a callback registered we need to release lock regardless since cb might need it to lookup msg, etc. + fs.mu.Unlock() + // Storage updates. + delta := int64(msz) + cb(-1, -delta, seq, subj) + + fs.mu.Lock() + } + + return true, nil +} + +// Remove all messages in the range [first, last] +// If agg is set the storage updates for fully removed blocks are aggregated. +// Lock should be held. +func (fs *fileStore) removeMsgsInRange(first, last uint64, viaLimits bool, agg *storeUpdateAgg) error { + last = min(last, fs.state.LastSeq) + if first > last { + return nil + } + + firstBlock := sort.Search(len(fs.blks), func(i int) bool { + return atomic.LoadUint64(&fs.blks[i].last.seq) >= first + }) + if firstBlock >= len(fs.blks) { + return nil + } + + for i := firstBlock; i < len(fs.blks); { + mb := fs.blks[i] + mbFirstSeq := atomic.LoadUint64(&mb.first.seq) + mbLastSeq := atomic.LoadUint64(&mb.last.seq) + if mbFirstSeq > last { + break + } + if mbFirstSeq >= first && mbLastSeq <= last && mb.numPriorTombs() == 0 { + // If this block stores no tombstones for previous blocks, + // and its sequences are within the range to be removed, + // we can get rid of the block entirely. To do that we use + // purgeMgsBlock, which also removes the block from fs.blks. + // After purgeMsgBlock, i will be the index of the following + // msgBlock, if any. Therefore, continue without incrementing i. + if err := fs.purgeMsgBlock(mb, agg); err != nil { + return err + } + } else { + from := max(first, mbFirstSeq) + to := min(last, mbLastSeq) + for seq := from; seq <= to; seq++ { + if _, err := fs.removeMsgFromBlock(mb, seq, false, viaLimits); err != nil { + return err + } + } + i++ + } + } + return nil +} + +// Tests whether we should try to compact this block while inline removing msgs. +// We will want rbytes to be over the minimum and have a 2x potential savings. +// If we compacted before but rbytes didn't improve much, guard against constantly compacting. +// Lock should be held. +func (mb *msgBlock) shouldCompactInline() bool { + return mb.rbytes > compactMinimum && mb.bytes*2 < mb.rbytes && (mb.cbytes == 0 || mb.bytes*2 < mb.cbytes) +} + +// Tests whether we should try to compact this block while running periodic sync. +// We will want rbytes to be over the minimum and have a 2x potential savings. +// Ignores 2MB minimum. +// Lock should be held. +func (mb *msgBlock) shouldCompactSync() bool { + return mb.bytes*2 < mb.rbytes && !mb.noCompact +} + +// This will compact and rewrite this block. This version will not process any tombstone cleanup. +// Write lock needs to be held. +func (mb *msgBlock) compact() error { + return mb.compactWithFloor(0, nil) +} + +// This will compact and rewrite this block. This should only be called when we know we want to rewrite this block. +// This should not be called on the lmb since we will prune tail deleted messages which could cause issues with +// writing new messages. We will silently bail on any issues with the underlying block and let someone else detect. +// if fseq > 0 we will attempt to cleanup stale tombstones. +// Write lock needs to be held. +func (mb *msgBlock) compactWithFloor(floor uint64, fsDmap *interiorDeletes) error { + wasLoaded := mb.cache != nil && mb.cacheAlreadyLoaded() + if !wasLoaded { + if err := mb.loadMsgsWithLock(); err != nil { + return err + } + } + defer mb.finishedWithCache() + + buf := mb.cache.buf + nbuf := getMsgBlockBuf(len(buf)) + // Recycle our nbuf when we are done. + defer recycleMsgBlockBuf(nbuf) + + var le = binary.LittleEndian + var firstSet bool + var last uint64 + var msgs uint64 + + fseq := atomic.LoadUint64(&mb.first.seq) + lseq := atomic.LoadUint64(&mb.last.seq) + isDeleted := func(seq uint64) bool { + return seq == 0 || seq&ebit != 0 || mb.dmap.Exists(seq) || seq < fseq + } + + for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; { + if index+msgHdrSize > lbuf { + return fmt.Errorf("message overrun") + } + hdr := buf[index : index+msgHdrSize] + rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:])) + hasHeaders := rl&hbit != 0 + // Clear any headers bit that could be set. + rl &^= hbit + shlen := slen + if hasHeaders { + shlen += 4 + } + dlen := int(rl) - msgHdrSize + // Do some quick sanity checks here. + if dlen < 0 || shlen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh { + return fmt.Errorf("sanity check failed") + } + // Only need to process non-deleted messages. + seq := le.Uint64(hdr[4:]) + ts := int64(le.Uint64(hdr[12:])) + + if !isDeleted(seq) { + // Check for tombstones. + if seq&tbit != 0 { + seq = seq &^ tbit + // If this entry is for a lower seq than ours then keep around. + // We also check that it is greater than our floor. Floor is zero on normal + // calls to compact. + // If the global delete map is set, check if a tombstone is still + // referencing a message in another block. If not, it can be removed. + if seq < fseq && seq >= floor && (fsDmap == nil || fsDmap.Exists(seq)) { + nbuf = append(nbuf, buf[index:index+rl]...) + } + } else { + // Normal message here. + msgs++ + nbuf = append(nbuf, buf[index:index+rl]...) + if !firstSet { + firstSet = true + atomic.StoreUint64(&mb.first.seq, seq) + } + if seq >= last { + last = seq + atomic.StoreUint64(&mb.last.seq, last) + mb.last.ts = ts + } + } + } + // Advance to next record. + index += rl + } + + // Handle compression + if mb.cmp != NoCompression && len(nbuf) > 0 { + originalSize := len(nbuf) + var err error + if nbuf, err = mb.cmp.Compress(nbuf); err != nil { + return err + } + meta := &CompressionInfo{ + Algorithm: mb.cmp, + OriginalSize: uint64(originalSize), + } + nbuf = append(meta.MarshalMetadata(), nbuf...) + } + + // Check for encryption. + if mb.bek != nil && len(nbuf) > 0 { + // Recreate to reset counter. + rbek, err := genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce) + if err != nil { + return err + } + rbek.XORKeyStream(nbuf, nbuf) + } + + // Close FDs first. + mb.closeFDsLocked() + + // We will write to a new file and mv/rename it in case of failure. + mfn := filepath.Join(mb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(newScan, mb.index)) + sync := mb.fs.syncAlways.Load() || mb.fs.syncOnFlush.Load() + if err := writeAtomicallyWithTemp(mb.fs.dios, mfn, mb.mfn, nbuf, defaultFilePerms, sync); err != nil { + return err + } + + // Make sure to sync if we have not done so yet + mb.needSync = !sync + + // Capture the updated rbytes. + if rbytes := uint64(len(nbuf)); rbytes == mb.rbytes { + // No change, so set our noCompact bool here to avoid attempting to continually compress in syncBlocks. + mb.noCompact = true + } else { + mb.rbytes = rbytes + } + mb.cbytes = mb.bytes + + // Remove any seqs from the beginning of the blk. + for seq, nfseq := fseq, atomic.LoadUint64(&mb.first.seq); seq < nfseq; seq++ { + mb.dmap.Delete(seq) + } + // Remove any seqs from the ending of the blk. + for seq, nlseq := lseq, atomic.LoadUint64(&mb.last.seq); seq > nlseq; seq-- { + mb.dmap.Delete(seq) + } + // If the block itself has no messages anymore (could still contain tombstones though), + // then we need to account for that by resetting the last sequence and timestamp. + if msgs == 0 { + atomic.StoreUint64(&mb.last.seq, fseq-1) + mb.last.ts = 0 + mb.dmap.Empty() + } + // Make sure we clear the cache since no longer valid. + mb.clearCacheAndOffset() + // If we entered with the msgs loaded make sure to reload them. + if wasLoaded { + if err := mb.loadMsgsWithLock(); err != nil { + return err + } + } + return nil +} + +// Grab info from a slot. +// Lock should be held. +func (mb *msgBlock) slotInfo(slot int) (uint32, uint32, bool, error) { + switch { + case mb.cache == nil: // Shouldn't be possible, but check it anyway. + return 0, 0, false, errNoCache + case slot < 0: + mb.fs.warn("Partial cache: offset slot index %d is less zero", slot) + return 0, 0, false, errPartialCache + case slot >= len(mb.cache.idx): + mb.fs.warn("Partial cache: offset slot index %d is greater than index len %d", slot, len(mb.cache.idx)) + return 0, 0, false, errPartialCache + } + + bi := mb.cache.idx[slot] + ri, hashChecked := (bi &^ cbit), (bi&cbit) != 0 + + // If this is a deleted slot return here. + if bi == dbit { + return 0, 0, false, errDeletedMsg + } + + // Determine record length + var rl uint32 + // Need to account for dbit markers in idx. + // So we will walk until we find valid idx slot to calculate rl. + for i := 1; slot+i < len(mb.cache.idx); i++ { + ni := mb.cache.idx[slot+i] &^ cbit + if ni == dbit { + continue + } + rl = ni - ri + break + } + // check if we had all trailing dbits. + // If so use len of cache buf minus ri. + if rl == 0 { + rl = uint32(len(mb.cache.buf)) - ri + } + if rl < msgHdrSize { + return 0, 0, false, errBadMsg{mb.mfn, fmt.Sprintf("length too short for slot %d", slot)} + } + return uint32(ri), rl, hashChecked, nil +} + +func (fs *fileStore) isClosed() bool { + return fs.closed.Load() +} + +// Will spin up our flush loop. +func (mb *msgBlock) spinUpFlushLoop() { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.spinUpFlushLoopLocked() +} + +// Will spin up our flush loop. +// Lock should be held. +func (mb *msgBlock) spinUpFlushLoopLocked() { + // Are we already running or closed? + if mb.flusher || mb.closed { + return + } + mb.flusher = true + mb.fch = make(chan struct{}, 1) + mb.qch = make(chan struct{}) + fch, qch := mb.fch, mb.qch + + go mb.flushLoop(fch, qch) +} + +// Raw low level kicker for flush loops. +func kickFlusher(fch chan struct{}) { + if fch != nil { + select { + case fch <- struct{}{}: + default: + } + } +} + +// Kick flusher for this message block. +func (mb *msgBlock) kickFlusher() { + mb.mu.RLock() + defer mb.mu.RUnlock() + kickFlusher(mb.fch) +} + +func (mb *msgBlock) setInFlusher() { + mb.mu.Lock() + mb.flusher = true + mb.mu.Unlock() +} + +func (mb *msgBlock) clearInFlusher() { + mb.mu.Lock() + mb.flusher = false + if mb.qch != nil { + close(mb.qch) + mb.qch = nil + } + if mb.fch != nil { + close(mb.fch) + mb.fch = nil + } + mb.mu.Unlock() +} + +// flushLoop watches for messages, index info, or recently closed msg block updates. +func (mb *msgBlock) flushLoop(fch, qch chan struct{}) { + mb.setInFlusher() + defer mb.clearInFlusher() + + for { + select { + case <-fch: + // If we have pending messages process them first. + if waiting := mb.pendingWriteSize(); waiting != 0 { + ts := 1 * time.Millisecond + var waited time.Duration + + for waiting < coalesceMinimum { + time.Sleep(ts) + select { + case <-qch: + return + default: + } + newWaiting := mb.pendingWriteSize() + if waited = waited + ts; waited > maxFlushWait || newWaiting <= waiting { + break + } + waiting = newWaiting + ts *= 2 + } + + // Ignore error here, the error is persisted as mb.werr and will be bubbled up later. + _ = mb.flushPendingMsgs() + } + + // Check if we are no longer the last message block. If we are + // not we can close FDs and exit. + mb.fs.mu.RLock() + notLast := mb != mb.fs.lmb + mb.fs.mu.RUnlock() + + if notLast { + if err := mb.closeFDs(); err == nil { + return + } + } + case <-qch: + return + } + } +} + +// Lock should be held. +func (mb *msgBlock) eraseMsg(seq uint64, ri, rl int, isLastBlock bool) error { + var le = binary.LittleEndian + var hdr [msgHdrSize]byte + + le.PutUint32(hdr[0:], uint32(rl)) + le.PutUint64(hdr[4:], seq|ebit) + le.PutUint64(hdr[12:], 0) + le.PutUint16(hdr[20:], 0) + + // Randomize record + data := make([]byte, rl-emptyRecordLen) + if n, err := rand.Read(data); err != nil { + return err + } else if n != len(data) { + return fmt.Errorf("not enough overwrite bytes read (%d != %d)", n, len(data)) + } + + // Now write to underlying buffer. + var b bytes.Buffer + b.Write(hdr[:]) + b.Write(data) + + // Calculate hash. + mb.hh.Reset() + mb.hh.Write(hdr[4:20]) + mb.hh.Write(data) + var hb [highwayhash.Size64]byte + checksum := mb.hh.Sum(hb[:0]) + // Write to msg record. + b.Write(checksum) + + // Update both cache and disk. + nbytes := b.Bytes() + + // Cache + if ri+rl <= len(mb.cache.buf) { + copy(mb.cache.buf[ri:ri+rl], nbytes) + } + + // Disk + if mb.cache.wp > ri { + if err := mb.atomicOverwriteFile(mb.cache.buf, !isLastBlock); err != nil { + return err + } + } + return nil +} + +// Truncate this message block to the tseq and ts. +// Lock should be held. +func (mb *msgBlock) truncate(tseq uint64, ts int64) (nmsgs, nbytes uint64, err error) { + // Make sure we are loaded to process messages etc. + if err := mb.loadMsgsWithLock(); err != nil { + return 0, 0, err + } + defer mb.finishedWithCache() + + // Calculate new eof using slot info from our new last sm. + ri, rl, _, err := mb.slotInfo(int(tseq - mb.cache.fseq)) + if err != nil { + return 0, 0, err + } + // Calculate new eof. + eof := int64(ri + rl) + + // FIXME(dlc) - We could be smarter here. + if buf, _ := mb.bytesPending(); len(buf) > 0 { + ld, err := mb.flushPendingMsgsLocked() + if ld != nil && mb.fs != nil { + // We do not know if fs is locked or not at this point. + // This should be an exceptional condition so do so in Go routine. + go mb.fs.rebuildState(ld) + } + if err != nil { + return 0, 0, err + } + } + + var purged, bytes uint64 + + checkDmap := mb.dmap.Size() > 0 + var smv StoreMsg + + for seq := atomic.LoadUint64(&mb.last.seq); seq > tseq; seq-- { + if checkDmap { + if mb.dmap.Exists(seq) { + // Delete and skip to next. + mb.dmap.Delete(seq) + checkDmap = !mb.dmap.IsEmpty() + continue + } + } + // We should have a valid msg to calculate removal stats. + if m, err := mb.cacheLookupNoCopy(seq, &smv); err == nil { + if mb.msgs > 0 { + rl := fileStoreMsgSize(m.subj, m.hdr, m.msg) + mb.msgs-- + if rl > mb.bytes { + rl = mb.bytes + } + mb.bytes -= rl + mb.rbytes -= rl + // For return accounting. + purged++ + bytes += uint64(rl) + } + } + } + + // If the block is compressed/encrypted then we have to load it into memory + // and decompress/decrypt it, truncate it and then write it back out. + // Otherwise, truncate the file itself and close the descriptor. + if mb.cmp != NoCompression || mb.bek != nil { + buf, err := mb.loadBlock(nil) + if err != nil { + return 0, 0, fmt.Errorf("failed to load block from disk: %w", err) + } + if err = mb.encryptOrDecryptIfNeeded(buf); err != nil { + return 0, 0, err + } + if buf, err = mb.decompressIfNeeded(buf); err != nil { + return 0, 0, fmt.Errorf("failed to decompress block: %w", err) + } + buf = buf[:eof] + copy(mb.lchk[0:], buf[len(buf)-checksumSize:]) + // We did decompress but don't recompress the truncated buffer here since we're the last block + // and would otherwise have compressed data and allow to write uncompressed data in the same block. + if err = mb.atomicOverwriteFile(buf, false); err != nil { + return 0, 0, err + } + } else if mb.mfd != nil { + if err = mb.mfd.Truncate(eof); err != nil { + return 0, 0, err + } + if err = mb.mfd.Sync(); err != nil { + return 0, 0, err + } + // Update our checksum. + var lchk [8]byte + if _, err = mb.mfd.ReadAt(lchk[:], eof-8); err != nil { + return 0, 0, err + } + copy(mb.lchk[0:], lchk[:]) + } else { + return 0, 0, fmt.Errorf("failed to truncate msg block %d, file not open", mb.index) + } + + // Update our last msg. + atomic.StoreUint64(&mb.last.seq, tseq) + mb.last.ts = ts + + // Clear our cache. + mb.clearCacheAndOffset() + + // Redo per subject info for this block. + if err = mb.resetPerSubjectInfo(); err != nil { + return purged, bytes, err + } + + // Load msgs again. + return purged, bytes, mb.loadMsgsWithLock() +} + +// Helper to determine if the mb is empty. +func (mb *msgBlock) isEmpty() bool { + return atomic.LoadUint64(&mb.first.seq) > atomic.LoadUint64(&mb.last.seq) +} + +// Lock should be held. +func (mb *msgBlock) selectNextFirst() { + var seq uint64 + fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq) + for seq = fseq + 1; seq <= lseq; seq++ { + if mb.dmap.Exists(seq) { + // We will move past this so we can delete the entry. + mb.dmap.Delete(seq) + } else { + break + } + } + // Set new first sequence. + atomic.StoreUint64(&mb.first.seq, seq) + + // Check if we are empty.. + if seq > lseq { + mb.first.ts = 0 + return + } + + // Need to get the timestamp. + // We will try the cache direct and fallback if needed. + var smv StoreMsg + sm, _ := mb.cacheLookupNoCopy(seq, &smv) + if sm == nil { + // Slow path, cache not loaded. + sm, _, _ = mb.fetchMsgNoCopyLocked(seq, &smv) + mb.finishedWithCache() + } + if sm != nil { + mb.first.ts = sm.ts + } else { + mb.first.ts = 0 + } +} + +// Select the next FirstSeq +// Also cleans up empty blocks at the start only containing tombstones. +// Lock should be held. +func (fs *fileStore) selectNextFirst() error { + if len(fs.blks) > 0 { + for len(fs.blks) > 1 { + mb := fs.blks[0] + mb.mu.Lock() + empty := mb.msgs == 0 + if !empty { + mb.mu.Unlock() + break + } + err := fs.forceRemoveMsgBlock(mb) + mb.mu.Unlock() + if err != nil { + return err + } + } + mb := fs.blks[0] + mb.mu.RLock() + fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq) + if mb.first.ts == 0 { + fs.state.FirstTime = time.Time{} + } else { + fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC() + } + mb.mu.RUnlock() + } else { + // Could not find anything, so treat like purge + fs.state.FirstSeq = fs.state.LastSeq + 1 + fs.state.FirstTime = time.Time{} + } + // Mark first as moved. Plays into tombstone cleanup for syncBlocks. + fs.firstMoved = true + return nil +} + +// Lock should be held. +func (mb *msgBlock) resetCacheExpireTimer(td time.Duration) { + if td == 0 { + td = mb.cexp + 100*time.Millisecond + } + if mb.ctmr == nil { + mb.ctmr = time.AfterFunc(td, mb.tryExpireCache) + } else { + mb.ctmr.Reset(td) + } +} + +// Lock should be held. +func (mb *msgBlock) startCacheExpireTimer() { + mb.resetCacheExpireTimer(0) +} + +// Used when we load in a message block. +// Lock should be held. +func (mb *msgBlock) clearCacheAndOffset() { + // Reset linear scan tracker. + mb.llseq = 0 + mb.clearCache() +} + +// Lock should be held. +func (mb *msgBlock) clearCache() { + if mb.ctmr != nil { + tsla := mb.sinceLastActivity() + if mb.fss == nil || tsla > mb.fexp { + // Force + mb.fss = nil + mb.ctmr.Stop() + mb.ctmr = nil + } else { + mb.resetCacheExpireTimer(mb.fexp - tsla) + } + } + + mbcache := mb.cache + if mbcache == nil { + mbcache = mb.ecache.Value() + } + if mbcache == nil { + return + } + + buf := mbcache.buf + mb.cache = nil + mb.ecache.Set(nil) + mbcache.stopRecycle() + recycleMsgBlockBuf(buf) +} + +// Called to possibly expire a message block cache. +func (mb *msgBlock) tryExpireCache() { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.tryExpireCacheLocked() +} + +func (mb *msgBlock) tryForceExpireCache() { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.tryForceExpireCacheLocked() +} + +// We will attempt to force expire this by temporarily clearing the last load time. +func (mb *msgBlock) tryForceExpireCacheLocked() { + llts := mb.llts + mb.llts = 0 + mb.tryExpireCacheLocked() + mb.llts = llts +} + +// This is for expiration of the write cache, which will be partial with fip. +// So we want to bypass the Pools here. +// Lock should be held. +func (mb *msgBlock) tryExpireWriteCache() []byte { + if mb.cache == nil { + return nil + } + lwts, buf, llts, nra := mb.lwts, mb.cache.buf, mb.llts, mb.cache.nra + mb.lwts, mb.cache.nra = 0, true + mb.tryExpireCacheLocked() + mb.lwts = lwts + if mb.cache != nil { + mb.cache.nra = nra + } + // We could check for a certain time since last load, but to be safe just reuse if no loads at all. + if llts == 0 && (mb.cache == nil || mb.cache.buf == nil) { + // Clear last write time since we now are about to move on to a new lmb. + mb.lwts = 0 + return buf[:0] + } + // The cache may have expired above without recycling the buffer. + if mb.cache == nil && !nra { + recycleMsgBlockBuf(buf) + } + return nil +} + +// Lock should be held. +func (mb *msgBlock) tryExpireCacheLocked() { + var strengthened bool + if mb.cache == nil { + mb.cache = mb.ecache.Value() + strengthened = true + } + if mb.cache == nil && mb.fss == nil { + if mb.ctmr != nil { + mb.ctmr.Stop() + mb.ctmr = nil + } + return + } + + // Can't expire if we still have pending. + if mb.cache != nil && len(mb.cache.buf)-int(mb.cache.wp) > 0 { + mb.resetCacheExpireTimer(mb.cexp) + if strengthened { + mb.finishedWithCache() + } + return + } + + // Grab timestamp to compare. + tns := ats.AccessTime() + + // For the core buffer of messages, we care about reads and writes, but not removes. + bufts := mb.llts + if mb.lwts > bufts { + bufts = mb.lwts + } + + // Check for activity on the cache that would prevent us from expiring. + // Both tns and bufts come from ats.AccessTime(), which means bufts can understate + // how recent the last activity actually was by up to one tick. + if delta := tns - bufts; delta <= int64(mb.cexp)+int64(ats.TickInterval) { + td := mb.cexp - time.Duration(delta) + if td <= 0 { + td = ats.TickInterval + } + mb.resetCacheExpireTimer(td) + if strengthened { + mb.finishedWithCache() + } + return + } + + // If we are here we will at least expire the core msg buffer. + // We need to capture offset in case we do a write next before a full load. + if mb.cache != nil { + mb.cache.stopRecycle() + if !mb.cache.nra { + recycleMsgBlockBuf(mb.cache.buf) + } + mb.cache.buf = nil + mb.cache.idx = mb.cache.idx[:0] + mb.cache.wp = 0 + } + + // Check if we can clear out our idx unless under force expire. + // fss we keep longer and expire under sync timer checks. + mb.clearCache() +} + +func (fs *fileStore) startAgeChk() { + if fs.recovering || fs.ageChk != nil { + return + } + if fs.cfg.MaxAge != 0 || fs.ttls != nil { + fs.ageChk = time.AfterFunc(fs.cfg.MaxAge, fs.expireMsgs) + } +} + +// Lock should be held. +func (fs *fileStore) resetAgeChk(delta int64) { + if fs.recovering { + return + } + // If we're already expiring messages, it will make sure to reset. + // Don't trigger again, as that could result in many expire goroutines. + if fs.ageChkRun { + return + } + + var next int64 = math.MaxInt64 + if fs.ttls != nil { + next = fs.ttls.GetNextExpiration(next) + } + + // If there's no MaxAge and there's nothing waiting to be expired then + // don't bother continuing. The next storeRawMsg() will wake us up if + // needs be. + if fs.cfg.MaxAge <= 0 && next == math.MaxInt64 { + clearTimer(&fs.ageChk) + return + } + + // Check to see if we should be firing sooner than MaxAge for an expiring TTL. + fireIn := fs.cfg.MaxAge + + // If delta for next-to-expire message is unset, but we still have messages to remove. + // Assume messages are removed through proposals, and we need to speed up subsequent age check. + if delta == 0 && fs.state.Msgs > 0 { + if until := 2 * time.Second; until < fireIn { + fireIn = until + } + } + + if next < math.MaxInt64 { + // Looks like there's a next expiration, use it either if there's no + // MaxAge set or if it looks to be sooner than MaxAge is. + if until := time.Until(time.Unix(0, next)); fireIn == 0 || until < fireIn { + fireIn = until + } + } + + // If not then look at the delta provided (usually gap to next age expiry). + if delta > 0 { + if fireIn == 0 || time.Duration(delta) < fireIn { + fireIn = time.Duration(delta) + } + } + + // Make sure we aren't firing too often either way, otherwise we can + // negatively impact stream ingest performance. + if fireIn < 250*time.Millisecond { + fireIn = 250 * time.Millisecond + } + + // If we want to kick the timer to run later than what was assigned before, don't reset it. + // Otherwise, we could get in a situation where the timer is continuously reset, and it never runs. + expires := ats.AccessTime() + fireIn.Nanoseconds() + if fs.ageChkTime > 0 && expires > fs.ageChkTime { + return + } + + fs.ageChkTime = expires + if fs.ageChk != nil { + fs.ageChk.Reset(fireIn) + } else { + fs.ageChk = time.AfterFunc(fireIn, fs.expireMsgs) + } +} + +// Lock should be held. +func (fs *fileStore) cancelAgeChk() { + if fs.ageChk != nil { + fs.ageChk.Stop() + fs.ageChk = nil + fs.ageChkTime = 0 + } +} + +// Will expire msgs that are too old. +func (fs *fileStore) expireMsgs() { + // We need to delete one by one here and can not optimize for the time being. + // Reason is that we need more information to adjust ack pending in consumers. + var smv StoreMsg + var sm *StoreMsg + + fs.mu.Lock() + if fs.recovering { + fs.cancelAgeChk() + fs.mu.Unlock() + return + } + maxAge := int64(fs.cfg.MaxAge) + minAge := ats.AccessTime() - maxAge + rmcb := fs.rmcb + pmsgcb := fs.pmsgcb + sdmTTL := int64(fs.cfg.SubjectDeleteMarkerTTL.Seconds()) + sdmEnabled := sdmTTL > 0 + + // If SDM is enabled, but handlers aren't set up yet. Try again later. + if sdmEnabled && (rmcb == nil || pmsgcb == nil) { + fs.resetAgeChk(0) + fs.mu.Unlock() + return + } + fs.ageChkRun = true + fs.mu.Unlock() + + if maxAge > 0 { + var seq uint64 + for sm, seq, _ = fs.LoadNextMsg(fwcs, true, 0, &smv); sm != nil && sm.ts <= minAge; sm, seq, _ = fs.LoadNextMsg(fwcs, true, seq+1, &smv) { + if len(sm.hdr) > 0 { + if ttl, err := getMessageTTL(sm.hdr); err == nil && ttl < 0 { + // The message has a negative TTL, therefore it must "never expire". + minAge = ats.AccessTime() - maxAge + continue + } + } + // Remove the message and then, if LimitsTTL is enabled, try and work out + // if it was the last message of that particular subject that we just deleted. + if sdmEnabled { + if last, ok := fs.shouldProcessSdm(seq, sm.subj); ok { + sdm := last && !isSubjectDeleteMarker(sm.hdr) + fs.handleRemovalOrSdm(seq, sm.subj, sdm, sdmTTL) + } + } else { + fs.mu.Lock() + fs.removeMsgViaLimits(sm.seq) + fs.mu.Unlock() + } + // Recalculate in case we are expiring a bunch. + minAge = ats.AccessTime() - maxAge + } + } + var ageDelta int64 + if sm != nil { + ageDelta = sm.ts - minAge + } + + fs.mu.Lock() + defer fs.mu.Unlock() + + // TODO: Not great that we're holding the lock here, but the timed hash wheel isn't thread-safe. + nextTTL := int64(math.MaxInt64) + var rmSeqs []thw.HashWheelEntry + if fs.ttls != nil { + fs.ttls.ExpireTasks(func(seq uint64, ts int64) bool { + rmSeqs = append(rmSeqs, thw.HashWheelEntry{Seq: seq, Expires: ts}) + // We might need to remove messages out of band, those can fail, and we can be shutdown halfway + // through so don't remove from THW just yet. + return false + }) + nextTTL = fs.ttls.GetNextExpiration(math.MaxInt64) + } + + // Remove messages collected by THW. + if !sdmEnabled { + for _, rm := range rmSeqs { + fs.removeMsg(rm.Seq, false, false, false) + } + } else { + // THW is unordered, so must sort by sequence and must not be holding the lock. + fs.mu.Unlock() + slices.SortFunc(rmSeqs, func(a, b thw.HashWheelEntry) int { + if a.Seq == b.Seq { + return 0 + } else if a.Seq < b.Seq { + return -1 + } else { + return 1 + } + }) + for _, rm := range rmSeqs { + // Need to grab subject for the specified sequence if for SDM, and check + // if the message hasn't been removed in the meantime. + // We need to grab the message and check if we should process SDM while holding the lock, + // otherwise we can race if a deletion of this message is in progress. + fs.mu.Lock() + sm, _ = fs.msgForSeqLocked(rm.Seq, &smv, false) + if sm == nil { + fs.ttls.Remove(rm.Seq, rm.Expires) + fs.mu.Unlock() + continue + } + last, ok := fs.shouldProcessSdmLocked(rm.Seq, sm.subj) + fs.mu.Unlock() + if ok { + sdm := last && !isSubjectDeleteMarker(sm.hdr) + fs.handleRemovalOrSdm(rm.Seq, sm.subj, sdm, sdmTTL) + } + } + fs.mu.Lock() + } + + // Only cancel if no message left, not on potential lookup error that would result in sm == nil. + fs.ageChkRun, fs.ageChkTime = false, 0 + if fs.state.Msgs == 0 && nextTTL == math.MaxInt64 { + fs.cancelAgeChk() + } else { + fs.resetAgeChk(ageDelta) + } +} + +func (fs *fileStore) shouldProcessSdm(seq uint64, subj string) (bool, bool) { + fs.mu.Lock() + defer fs.mu.Unlock() + return fs.shouldProcessSdmLocked(seq, subj) +} + +// Lock should be held. +func (fs *fileStore) shouldProcessSdmLocked(seq uint64, subj string) (bool, bool) { + if fs.sdm == nil { + fs.sdm = newSDMMeta() + } + + if p, ok := fs.sdm.pending[seq]; ok { + // Don't allow more proposals for the same sequence if we already did recently. + if time.Since(time.Unix(0, p.ts)) < 2*time.Second { + return p.last, false + } + // If we're about to use the cached value, and we knew it was last before, + // quickly check that we don't have more remaining messages for the subject now. + // Which means we are not the last anymore and must reset to not remove later data. + if p.last { + msgs := fs.subjectsTotalsLocked(subj)[subj] + numPending := fs.sdm.totals[subj] + if remaining := msgs - numPending; remaining > 0 { + p.last = false + } + } + fs.sdm.pending[seq] = SDMBySeq{p.last, time.Now().UnixNano()} + return p.last, true + } + + msgs := fs.subjectsTotalsLocked(subj)[subj] + if msgs == 0 { + return false, true + } + numPending := fs.sdm.totals[subj] + remaining := msgs - numPending + return fs.sdm.trackPending(seq, subj, remaining == 1), true +} + +func (fs *fileStore) handleRemovalOrSdm(seq uint64, subj string, sdm bool, sdmTTL int64) { + if sdm { + var _hdr [128]byte + hdr := fmt.Appendf( + _hdr[:0], + "NATS/1.0\r\n%s: %s\r\n%s: %s\r\n%s: %s\r\n\r\n", + JSMarkerReason, JSMarkerReasonMaxAge, + JSMessageTTL, time.Duration(sdmTTL)*time.Second, + JSMsgRollup, JSMsgRollupSubject, + ) + msg := &inMsg{ + subj: subj, + hdr: hdr, + } + fs.pmsgcb(msg) + } else { + fs.rmcb(seq) + } +} + +// Will run through scheduled messages. +func (fs *fileStore) runMsgScheduling() { + // TODO: Not great that we're holding the lock here, but the timed hash wheel and message scheduling isn't thread-safe. + fs.mu.Lock() + defer fs.mu.Unlock() + + // If scheduling is enabled, but handler isn't set up yet. Try again later. + if fs.scheduling == nil || fs.scheduling.paused { + return + } + if fs.pmsgcb == nil { + fs.scheduling.resetTimer() + return + } + fs.scheduling.running = true + + scheduledMsgs := fs.scheduling.getScheduledMessages( + func(seq uint64, smv *StoreMsg) *StoreMsg { + sm, _ := fs.msgForSeqLocked(seq, smv, false) + return sm + }, + func(subj string, smv *StoreMsg) *StoreMsg { + sm, _ := fs.loadLastLocked(subj, smv) + return sm + }, + ) + if len(scheduledMsgs) > 0 { + fs.mu.Unlock() + for _, msg := range scheduledMsgs { + fs.pmsgcb(msg) + } + fs.mu.Lock() + } + + fs.scheduling.running, fs.scheduling.deadline = false, 0 + fs.scheduling.resetTimer() +} + +// Lock should be held. +func (fs *fileStore) checkAndFlushLastBlock() error { + lmb := fs.lmb + if lmb == nil { + return nil + } + lmb.mu.Lock() + if err := lmb.werr; err != nil { + lmb.mu.Unlock() + return err + } + + if lmb.pendingWriteSizeLocked() == 0 { + lmb.mu.Unlock() + return nil + } + ld, err := lmb.flushPendingMsgsLocked() + lmb.mu.Unlock() + if err != nil { + return err + } + // Since fs lock is held need to unlock the mb in case we need to rebuild state. + if ld != nil { + fs.rebuildStateLocked(ld) + } + return nil +} + +// This will check all the checksums on messages and report back any sequence numbers with errors. +func (fs *fileStore) checkMsgs() (*LostStreamData, error) { + fs.mu.Lock() + defer fs.mu.Unlock() + + var firstErr error + storeErr := func(err error) error { + fs.warn("checkMsgs: %v", err) + if firstErr == nil { + firstErr = err + } + return err + } + + if err := fs.checkAndFlushLastBlock(); err != nil { + return nil, storeErr(fmt.Errorf("flush of last block failed: %w", err)) + } + + // Clear any global subject state. + fs.psim, fs.tsl = fs.psim.Empty(), 0 + + for _, mb := range fs.blks { + // Make sure encryption loaded if needed for the block. + if err := fs.loadEncryptionForMsgBlock(mb); err != nil { + _ = storeErr(fmt.Errorf("loading encryption for block %d failed: %w", mb.index, err)) + continue + } + // FIXME(dlc) - check tombstones here too? + ld, _, err := mb.rebuildState() + if err != nil { + _ = storeErr(fmt.Errorf("rebuildState for block %d failed: %w", mb.index, err)) + continue + } + if ld != nil { + // Rebuild fs state too. + fs.rebuildStateLocked(ld) + } + if err = fs.populateGlobalPerSubjectInfo(mb); err != nil { + _ = storeErr(fmt.Errorf("populating per-subject info for block %d failed: %w", mb.index, err)) + continue + } + } + + return fs.ld, firstErr +} + +// Lock should be held. +func (mb *msgBlock) enableForWriting(fip bool) error { + if mb == nil { + return errNoMsgBlk + } + if mb.mfd != nil { + return nil + } + mb.fs.dios.acquire() + mfd, err := os.OpenFile(mb.mfn, os.O_CREATE|os.O_RDWR, defaultFilePerms) + mb.fs.dios.release() + if err != nil { + return fmt.Errorf("error opening msg block file [%q]: %v", mb.mfn, err) + } + mb.mfd = mfd + + // Spin up our flusher loop if needed. + if !fip { + mb.spinUpFlushLoopLocked() + } + + return nil +} + +// Helper function to place a delete tombstone. +func (mb *msgBlock) writeTombstone(seq uint64, ts int64) error { + return mb.writeMsgRecord(emptyRecordLen, seq|tbit, _EMPTY_, nil, nil, ts, true) +} + +// Helper function to place a delete tombstone without flush. +// Lock should not be held. +func (mb *msgBlock) writeTombstoneNoFlush(seq uint64, ts int64) error { + mb.mu.Lock() + defer mb.mu.Unlock() + return mb.writeMsgRecordLocked(emptyRecordLen, seq|tbit, _EMPTY_, nil, nil, ts, false, false) +} + +// Will write the message record to the underlying message block. +// filestore lock will be held. +func (mb *msgBlock) writeMsgRecord(rl, seq uint64, subj string, mhdr, msg []byte, ts int64, flush bool) error { + mb.mu.Lock() + defer mb.mu.Unlock() + return mb.writeMsgRecordLocked(rl, seq, subj, mhdr, msg, ts, flush, true) +} + +// Will write the message record to the underlying message block. +// filestore lock will be held. +// mb lock should be held. +func (mb *msgBlock) writeMsgRecordLocked(rl, seq uint64, subj string, mhdr, msg []byte, ts int64, flush, kick bool) (rerr error) { + // Return previous write errors immediately. + if mb.werr != nil { + return mb.werr + } + // Persist any returned errors to be used in the future. + defer func() { + if rerr != nil && mb.werr == nil { + // Read/decode errors surfaced from existing on-disk data are not write failures. + // Log and continue instead of disabling writes. + if isReadErr(rerr) { + mb.fs.rateLimitWarn("Ignoring non-write error: %v", rerr) + assert.Unreachable("Filestore msg block encountered read error", map[string]any{ + "name": mb.fs.cfg.Name, + "mb.index": mb.index, + "err": rerr, + "stack": string(debug.Stack()), + }) + return + } + mb.werr = rerr + assert.Unreachable("Filestore msg block encountered write error", map[string]any{ + "name": mb.fs.cfg.Name, + "mb.index": mb.index, + "err": rerr, + "stack": string(debug.Stack()), + }) + } + }() + + // Enable for writing if our mfd is not open. + if mb.mfd == nil { + if err := mb.enableForWriting(flush && kick); err != nil { + return err + } + } + + // Check if we are tracking per subject for our simple state. + // Do this before changing the cache that would trigger a flush pending msgs call + // if we needed to regenerate the per subject info. + // Note that tombstones have no subject so will not trigger here. + if len(subj) > 0 && !mb.noTrack { + if err := mb.ensurePerSubjectInfoLoaded(); err != nil { + return err + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + if ss, ok := mb.fss.Find(stringToBytes(subj)); ok && ss != nil { + ss.Msgs++ + ss.Last = seq + ss.lastNeedsUpdate = false + } else { + mb.fss.Insert(stringToBytes(subj), SimpleState{Msgs: 1, First: seq, Last: seq}) + } + } + + // Make sure we have a cache setup. Do so after ensurePerSubjectInfoLoaded as it may + // have already brought in a cache for us. + if err := mb.setupWriteCache(nil); err != nil { + return err + } + + // Make sure that the GC can't take away our writes by strengthening the elastic + // reference. It will now stay strong until the flusher decides it is time to weaken. + mb.ecache.Strengthen() + + // Make sure we have enough space to write into. If we don't then we can pull a buffer + // from the next pool size up to save us from reallocating in append() below. + if nsz := len(mb.cache.buf) + int(rl); cap(mb.cache.buf) < nsz { + prev := mb.cache.buf + mb.cache.stopRecycle() + mb.cache.buf = getMsgBlockBuf(nsz) + if prev != nil { + mb.cache.buf = mb.cache.buf[:copy(mb.cache.buf[:nsz], prev)] + recycleMsgBlockBuf(prev) + } + mb.cache.registerRecycle() + } + + // Indexing + index := len(mb.cache.buf) + + // Formats + // Format with no header + // total_len(4) sequence(8) timestamp(8) subj_len(2) subj msg hash(8) + // With headers, high bit on total length will be set. + // total_len(4) sequence(8) timestamp(8) subj_len(2) subj hdr_len(4) hdr msg hash(8) + + var le = binary.LittleEndian + + l := uint32(rl) + hasHeaders := len(mhdr) > 0 + if hasHeaders { + l |= hbit + } + + // Reserve space for the header on the underlying buffer. + mb.cache.buf = append(mb.cache.buf, make([]byte, msgHdrSize)...) + hdr := mb.cache.buf[len(mb.cache.buf)-msgHdrSize : len(mb.cache.buf)] + le.PutUint32(hdr[0:], l) + le.PutUint64(hdr[4:], seq) + le.PutUint64(hdr[12:], uint64(ts)) + le.PutUint16(hdr[20:], uint16(len(subj))) + + // Now write to underlying buffer. + mb.cache.buf = append(mb.cache.buf, subj...) + + if hasHeaders { + var hlen [4]byte + le.PutUint32(hlen[0:], uint32(len(mhdr))) + mb.cache.buf = append(mb.cache.buf, hlen[:]...) + mb.cache.buf = append(mb.cache.buf, mhdr...) + } + mb.cache.buf = append(mb.cache.buf, msg...) + + // Calculate hash. + mb.hh.Reset() + mb.hh.Write(hdr[4:20]) + mb.hh.Write(stringToBytes(subj)) + if hasHeaders { + mb.hh.Write(mhdr) + } + mb.hh.Write(msg) + checksum := mb.hh.Sum(mb.lchk[:0:highwayhash.Size64]) + copy(mb.lchk[0:], checksum) + + // Update write through cache. + // Write to msg record. + mb.cache.buf = append(mb.cache.buf, checksum...) + + // Set cache timestamp for last store. + mb.lwts = ts + + // Only update index and do accounting if not a delete tombstone. + if seq&tbit == 0 { + last := atomic.LoadUint64(&mb.last.seq) + // Accounting, do this before stripping ebit, it is ebit aware. + mb.updateAccounting(seq, ts, rl) + // Strip ebit if set. + seq = seq &^ ebit + // If we have a hole due to skipping many messages, fill it. + if len(mb.cache.idx) > 0 && last+1 < seq { + for dseq := last + 1; dseq < seq; dseq++ { + mb.cache.idx = append(mb.cache.idx, dbit) + } + } + // Write index + if mb.cache.idx = append(mb.cache.idx, uint32(index)|cbit); len(mb.cache.idx) == 1 { + mb.cache.fseq = seq + } + } else { + // If the block is empty, still adjust the accounting accordingly. + tseq := seq &^ tbit + if mb.msgs == 0 && tseq > atomic.LoadUint64(&mb.last.seq) { + atomic.StoreUint64(&mb.last.seq, tseq) + mb.last.ts = ts + atomic.StoreUint64(&mb.first.seq, tseq+1) + mb.first.ts = 0 + } + // Make sure to account for tombstones in rbytes. + mb.rbytes += rl + } + + fch, werr := mb.fch, mb.werr + if werr != nil { + return werr + } + + // If we should be flushing, or had a write error, do so here. + if flush && mb.fs.fip { + ld, err := mb.flushPendingMsgsLocked() + if ld != nil { + // We have the mb lock here, this needs the mb locks so do in its own go routine. + go mb.fs.rebuildState(ld) + } + if err != nil { + return err + } + } else if kick { + // Kick the flusher here. + kickFlusher(fch) + } + return nil +} + +// How many bytes pending to be written for this message block. +func (mb *msgBlock) pendingWriteSize() int { + if mb == nil { + return 0 + } + mb.mu.RLock() + defer mb.mu.RUnlock() + return mb.pendingWriteSizeLocked() +} + +// How many bytes pending to be written for this message block. +func (mb *msgBlock) pendingWriteSizeLocked() int { + if mb == nil { + return 0 + } + var pending int + if !mb.closed && mb.mfd != nil && mb.cache != nil { + pending = len(mb.cache.buf) - int(mb.cache.wp) + } + return pending +} + +// Try to close our FDs if we can. +func (mb *msgBlock) closeFDs() error { + mb.mu.Lock() + defer mb.mu.Unlock() + return mb.closeFDsLocked() +} + +func (mb *msgBlock) closeFDsLocked() error { + if buf, _ := mb.bytesPending(); len(buf) > 0 { + return errPendingData + } + mb.closeFDsLockedNoCheck() + return nil +} + +func (mb *msgBlock) closeFDsLockedNoCheck() { + if mb.mfd != nil { + _ = mb.mfd.Close() + mb.mfd = nil + } +} + +// bytesPending returns the buffer to be used for writing to the underlying file. +// This marks we are in flush and will return nil if asked again until cleared. +// Lock should be held. +func (mb *msgBlock) bytesPending() ([]byte, error) { + if mb == nil || mb.mfd == nil { + return nil, errNoPending + } + if mb.cache == nil { + return nil, errNoCache + } + if len(mb.cache.buf) <= mb.cache.wp { + return nil, errNoPending + } + buf := mb.cache.buf[mb.cache.wp:] + if len(buf) == 0 { + return nil, errNoPending + } + return buf, nil +} + +// Returns the current blkSize including deleted msgs etc. +func (mb *msgBlock) blkSize() uint64 { + if mb == nil { + return 0 + } + mb.mu.RLock() + nb := mb.rbytes + mb.mu.RUnlock() + return nb +} + +// Update accounting on a write msg. +// Lock should be held. +func (mb *msgBlock) updateAccounting(seq uint64, ts int64, rl uint64) { + isDeleted := seq&ebit != 0 + if isDeleted { + seq = seq &^ ebit + } + + fseq := atomic.LoadUint64(&mb.first.seq) + if (fseq == 0 || mb.first.ts == 0) && seq >= fseq { + atomic.StoreUint64(&mb.first.seq, seq) + mb.first.ts = ts + } + // Need atomics here for selectMsgBlock speed. + atomic.StoreUint64(&mb.last.seq, seq) + mb.last.ts = ts + mb.rbytes += rl + if !isDeleted { + mb.bytes += rl + mb.msgs++ + } +} + +// Helper to check last msg block and create new one if too big. +// Lock should be held. +func (fs *fileStore) checkLastBlock(rl uint64) (lmb *msgBlock, err error) { + // Grab our current last message block. + lmb = fs.lmb + rbytes := lmb.blkSize() + if lmb == nil || (rbytes > 0 && rbytes+rl > fs.fcfg.BlockSize) { + if lmb, err = fs.newMsgBlockForWrite(); err != nil { + return nil, err + } + } + return lmb, nil +} + +// Lock should be held. +func (fs *fileStore) writeMsgRecord(seq uint64, ts int64, subj string, hdr, msg []byte) (uint64, error) { + // Get size for this message. + rl := fileStoreMsgSize(subj, hdr, msg) + if isFileStoreMsgTooLarge(rl) { + return 0, ErrMsgTooLarge + } + // Grab our current last message block. + mb, err := fs.checkLastBlock(rl) + if err != nil { + return 0, err + } + + // Mark as dirty for stream state. + fs.dirty++ + + // Ask msg block to store in write through cache. + err = mb.writeMsgRecord(rl, seq, subj, hdr, msg, ts, fs.fip) + + return rl, err +} + +// For writing tombstones to our lmb. This version will enforce maximum block sizes. +// Lock should be held. +func (fs *fileStore) writeTombstone(seq uint64, ts int64) error { + // Grab our current last message block. + lmb, err := fs.checkLastBlock(emptyRecordLen) + if err != nil { + return err + } + return lmb.writeTombstone(seq, ts) +} + +// For writing tombstones to our lmb. This version will enforce maximum block sizes. +// This version does not flush contents. +// Lock should be held. +func (fs *fileStore) writeTombstoneNoFlush(seq uint64, ts int64) error { + lmb, err := fs.checkLastBlock(emptyRecordLen) + if err != nil { + return err + } + // Write tombstone without flush or kick. + return lmb.writeTombstoneNoFlush(seq, ts) +} + +// Lock should be held. +func (mb *msgBlock) recompressOnDiskIfNeeded() error { + // If the block has been closed in the meantime, skip. + if mb.closed { + return nil + } + + alg := mb.fs.fcfg.Compression + + // Open up the file block and read in the entire contents into memory. + // One of two things will happen: + // 1. The block will be compressed already and have a valid metadata + // header, in which case we do nothing. + // 2. The block will be uncompressed, in which case we will compress it + // and then write it back out to disk, re-encrypting if necessary. + mb.fs.dios.acquire() + origBuf, err := os.ReadFile(mb.mfn) + mb.fs.dios.release() + + if err != nil { + return fmt.Errorf("failed to read original block from disk: %w", err) + } + + // If the block is encrypted then we will need to decrypt it before + // doing anything. We always encrypt after compressing because then the + // compression can be as efficient as possible on the raw data, whereas + // the encrypted ciphertext will not compress anywhere near as well. + // The block encryption also covers the optional compression metadata. + if err = mb.encryptOrDecryptIfNeeded(origBuf); err != nil { + return err + } + + decoded, diskAlg, err := mb.decode(origBuf) + if err != nil { + return fmt.Errorf("failed to decode original block: %w", err) + } + if diskAlg == alg { + // The block is already compressed with the chosen algorithm so there + // is nothing else to do. This is not a common case, it is here only + // to ensure we don't do unnecessary work in case something asked us + // to recompress an already compressed block with the same algorithm. + return nil + } + + return mb.atomicOverwriteFile(decoded, true) +} + +// Lock should be held. +func (mb *msgBlock) atomicOverwriteFile(buf []byte, allowCompress bool) error { + if mb.mfd != nil { + mb.closeFDsLockedNoCheck() + defer mb.enableForWriting(mb.fs.fip) + } + + origFN := mb.mfn // The original message block on disk. + tmpFN := mb.mfn + blkTmpSuffix // The new block will be written here. + + // Rather than modifying the existing block on disk (which is a dangerous + // operation if something goes wrong), create a new temporary file. We will + // write out the new block here and then swap the files around afterwards + // once everything else has succeeded correctly. + mb.fs.dios.acquire() + tmpFD, err := os.OpenFile(tmpFN, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultFilePerms) + mb.fs.dios.release() + + if err != nil { + return fmt.Errorf("failed to create temporary file: %w", err) + } + + errorCleanup := func(err error) error { + _ = tmpFD.Close() + _ = os.Remove(tmpFN) + return err + } + + alg := NoCompression + if calg := mb.fs.fcfg.Compression; calg != NoCompression && allowCompress { + alg = calg + // The original buffer at this point is uncompressed, so we will now compress + // it if needed. Note that if the selected algorithm is NoCompression, the + // Compress function will just return the input buffer unmodified. + originalSize := len(buf) + if buf, err = alg.Compress(buf); err != nil { + return errorCleanup(fmt.Errorf("failed to compress block: %w", err)) + } + + // We only need to write out the metadata header if compression is enabled. + // If we're trying to uncompress the file on disk at this point, don't bother + // writing metadata. + meta := &CompressionInfo{ + Algorithm: alg, + OriginalSize: uint64(originalSize), + } + buf = append(meta.MarshalMetadata(), buf...) + } + + // Re-encrypt the block if necessary. + if err = mb.encryptOrDecryptIfNeeded(buf); err != nil { + return errorCleanup(err) + } + + // Write the new block data (which might be compressed or encrypted) to the + // temporary file. + if n, err := tmpFD.Write(buf); err != nil { + return errorCleanup(fmt.Errorf("failed to write to temporary file: %w", err)) + } else if n != len(buf) { + return errorCleanup(fmt.Errorf("short write to temporary file (%d != %d)", n, len(buf))) + } + if err := tmpFD.Sync(); err != nil { + return errorCleanup(fmt.Errorf("failed to sync temporary file: %w", err)) + } + if err := tmpFD.Close(); err != nil { + return errorCleanup(fmt.Errorf("failed to close temporary file: %w", err)) + } + + // Now replace the original file with the newly updated temp file. + if err := os.Rename(tmpFN, origFN); err != nil { + return fmt.Errorf("failed to move temporary file into place: %w", err) + } + + // Since the message block might be retained in memory, make sure the + // compression algorithm is up-to-date, since this will be needed when + // compacting or truncating. + mb.cmp = alg + + // Also update rbytes + mb.rbytes = uint64(len(buf)) + + return nil +} + +// Lock should be held. +func (mb *msgBlock) decode(buf []byte) ([]byte, StoreCompression, error) { + var meta CompressionInfo + if n, err := meta.UnmarshalMetadata(buf); err != nil { + // There was a problem parsing the metadata header of the block. + // If there's no metadata header, an error isn't returned here, + // we will instead just use default values of no compression. + return nil, meta.Algorithm, err + } else if n == 0 { + // There were no metadata bytes, so we assume the block is not + // compressed and return it as-is. + return buf, NoCompression, nil + } else { + // Metadata was present, so try to decompress the block. + decoded, err := meta.Algorithm.Decompress(buf[n:]) + if err == nil { + return decoded, meta.Algorithm, nil + } + // Decompression failed. Before returning the error, check if the + // buffer happens to store a valid uncompressed record. + // A record with length 24145251 without headers happens to + // collide with a valid compression header "cmp" and + // compression algorithm 1 (S2). + if meta.Algorithm == S2Compression && len(buf) >= 24145251 { + var sm StoreMsg + if _, msgErr := mb.msgFromBufNoCopy(buf, &sm, mb.hh); msgErr == nil { + return buf, NoCompression, nil + } + } + // Return the original error + return nil, meta.Algorithm, err + } +} + +// Lock should be held. +func (mb *msgBlock) decompressIfNeeded(buf []byte) ([]byte, error) { + decoded, _, err := mb.decode(buf) + return decoded, err +} + +// Lock should be held. +func (mb *msgBlock) encryptOrDecryptIfNeeded(buf []byte) error { + if mb.bek != nil && len(buf) > 0 { + bek, err := genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce) + if err != nil { + return err + } + mb.bek = bek + mb.bek.XORKeyStream(buf, buf) + } + return nil +} + +// Lock should be held. +func (mb *msgBlock) ensureRawBytesLoaded() error { + if mb.rbytes > 0 { + return nil + } + f, err := mb.openBlock() + if err != nil { + return err + } + defer f.Close() + if fi, err := f.Stat(); fi != nil && err == nil { + mb.rbytes = uint64(fi.Size()) + } else { + return err + } + return nil +} + +// Lock should be held. +func (mb *msgBlock) syncFile() error { + mb.fs.dios.acquire() + defer mb.fs.dios.release() + fd, didOpen := mb.mfd, false + if fd == nil { + var err error + fd, err = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + didOpen = true + } + if err := fd.Sync(); err != nil { + // Close fd if we opened it, but ignore its error since sync takes precedence. + if didOpen { + _ = fd.Close() + } + return err + } + if didOpen { + return fd.Close() + } + return nil +} + +// Sync msg and index files as needed. This is called from a timer. +func (fs *fileStore) syncBlocks() { + fs.syncMu.Lock() + defer fs.syncMu.Unlock() + + if fs.isClosed() { + return + } + fs.mu.Lock() + if err := fs.werr; err != nil { + fs.mu.Unlock() + return + } + blks := append([]*msgBlock(nil), fs.blks...) + lmb, firstMoved, firstSeq := fs.lmb, fs.firstMoved, fs.state.FirstSeq + // Clear first moved. + fs.firstMoved = false + fs.mu.Unlock() + + storeFsWerr := func(err error) { + // A file or directory that no longer exists was removed by a + // concurrent purge, compact or delete. Syncing it is moot, not a + // write failure. + if os.IsNotExist(err) { + return + } + fs.mu.Lock() + defer fs.mu.Unlock() + fs.setWriteErr(err) + } + + var fsDmap *interiorDeletes + + var markDirty bool + var needDirSync bool + for _, mb := range blks { + // Do actual sync. Hold lock for consistency. + mb.mu.Lock() + if mb.closed { + mb.mu.Unlock() + continue + } + // Bubble up an individual block error into the broader filestore. + if err := mb.werr; err != nil { + mb.mu.Unlock() + storeFsWerr(err) + continue + } + // See if we can close FDs due to being idle. + if mb.mfd != nil && mb.sinceLastWriteActivity() > closeFDsIdle && mb.pendingWriteSizeLocked() == 0 { + if err := mb.dirtyCloseWithRemove(false); err != nil { + mb.mu.Unlock() + storeFsWerr(err) + continue + } + } + // If our first has moved and we are set to noCompact (which is from tombstones), + // clear so that we might cleanup tombstones. + if firstMoved && mb.noCompact { + mb.noCompact = false + } + // Check if we should compact here as well. + // Do not compact last mb. + var needsCompact bool + if mb != lmb && mb.ensureRawBytesLoaded() == nil && mb.shouldCompactSync() { + needsCompact = true + markDirty = true + } + + // Flush anything that may be pending. + if _, err := mb.flushPendingMsgsLocked(); err != nil { + mb.mu.Unlock() + storeFsWerr(err) + continue + } + // Check if we need to sync. We will not hold lock during actual sync. + needSync := mb.needSync + needKeySync, kfn := mb.needKeySync, mb.kfn + + // Reset. Because we let go of the lock, we could write new data to this mb which might or + // might not be synced later if we would've reset after letting go of the lock. + mb.needSync = false + mb.needKeySync = false + mb.mu.Unlock() + + // Check if we should compact here. + // Need to hold fs lock in case we reference psim when loading in the mb and we may remove this block if truly empty. + if needsCompact { + // Load a delete map containing only interior deletes. + // This is used when compacting to know if tombstones are still relevant, + // and if not they can be compacted. + if fsDmap == nil { + fsDmap = deleteMap(blks) + } + fs.mu.RLock() + mb.mu.Lock() + // If the block has already been removed in the meantime, we can simply skip. + if _, ok := fs.bim[mb.index]; !ok { + mb.mu.Unlock() + fs.mu.RUnlock() + continue + } + err := mb.compactWithFloor(firstSeq, fsDmap) + // If this compact removed all raw bytes due to tombstone cleanup, schedule to remove. + shouldRemove := mb.rbytes == 0 + needSync = mb.needSync + mb.mu.Unlock() + fs.mu.RUnlock() + if err != nil { + storeFsWerr(err) + continue + } + needDirSync = needDirSync || needSync || shouldRemove + + // Check if we should remove. This will not be common, so we will re-take fs write lock here vs changing + // it above which we would prefer to be a readlock such that other lookups can occur while compacting this block. + if shouldRemove { + fs.mu.Lock() + mb.mu.Lock() + err = fs.removeMsgBlock(mb) + mb.mu.Unlock() + fs.mu.Unlock() + needSync = false + if err != nil { + storeFsWerr(err) + continue + } + } + } + + // Check if we need to sync this block's key file. + if needKeySync && kfn != _EMPTY_ { + if err := fs.syncFileAndDir(kfn); err != nil { + storeFsWerr(err) + continue + } + } + + // Check if we need to sync this block. + if needSync { + mb.mu.Lock() + err := mb.syncFile() + if err == nil { + mb.needSync = false + } + mb.mu.Unlock() + if err != nil { + storeFsWerr(err) + continue + } + } + } + if needDirSync && canFsyncDirectories { + fs.dios.acquire() + err := syncDir(filepath.Join(fs.fcfg.StoreDir, msgDir)) + fs.dios.release() + if err != nil { + storeFsWerr(err) + } + } + + if fs.isClosed() { + return + } + fs.mu.Lock() + defer fs.mu.Unlock() + fs.setSyncTimer() + if markDirty { + fs.dirty++ + } + + // Sync state file if we are not running with sync always. + if !fs.syncAlways.Load() { + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile) + var fd *os.File + var err error + fs.dios.acquire() + fd, err = os.OpenFile(fn, os.O_RDWR, defaultFilePerms) + fs.dios.release() + if err != nil && !os.IsNotExist(err) { + fs.setWriteErr(err) + return + } + if fd != nil { + if err = fd.Sync(); err != nil { + // Close fd, but ignore its error since sync takes precedence. + _ = fd.Close() + fs.setWriteErr(err) + return + } + if err = fd.Close(); err != nil { + fs.setWriteErr(err) + return + } + } + } +} + +// Select the message block where this message should be found. +// Return nil if not in the set. +// Read lock should be held. +func (fs *fileStore) selectMsgBlock(seq uint64) *msgBlock { + _, mb := fs.selectMsgBlockWithIndex(seq) + return mb +} + +// Lock should be held. +func (fs *fileStore) selectMsgBlockWithIndex(seq uint64) (int, *msgBlock) { + // Check for out of range. + if seq < fs.state.FirstSeq || seq > fs.state.LastSeq || fs.state.Msgs == 0 { + return -1, nil + } + + const linearThresh = 32 + nb := len(fs.blks) - 1 + + if nb < linearThresh { + for i, mb := range fs.blks { + if seq <= atomic.LoadUint64(&mb.last.seq) { + return i, mb + } + } + return -1, nil + } + + // Do traditional binary search here since we know the blocks are sorted by sequence first and last. + for low, high, mid := 0, nb, nb/2; low <= high; mid = (low + high) / 2 { + mb := fs.blks[mid] + // Right now these atomic loads do not factor in, so fine to leave. Was considering + // uplifting these to fs scope to avoid atomic load but not needed. + first, last := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq) + if seq > last { + low = mid + 1 + } else if seq < first { + // A message block's first sequence can change here meaning we could find a gap. + // We want to behave like above, which if inclusive (we check at start) should + // always return an index and a valid mb. + // If we have a gap then our seq would be > fs.blks[mid-1].last.seq + if mid == 0 || seq > atomic.LoadUint64(&fs.blks[mid-1].last.seq) { + return mid, mb + } + high = mid - 1 + } else { + return mid, mb + } + } + + return -1, nil +} + +// Select the message block where this message should be found. +// Return nil if not in the set. +func (fs *fileStore) selectMsgBlockForStart(minTime time.Time) *msgBlock { + fs.mu.RLock() + defer fs.mu.RUnlock() + + // Binary search for first block where last.ts >= t. + i, _ := slices.BinarySearchFunc(fs.blks, minTime.UnixNano(), func(mb *msgBlock, target int64) int { + mb.mu.RLock() + last := mb.last.ts + mb.mu.RUnlock() + switch { + case last < target: + return -1 + case last > target: + return 1 + default: + return 0 + } + }) + + // BinarySearchFunc returns an insertion point if not found. + // Either way, i is the index of the first mb where mb.last.ts >= t. + if i < len(fs.blks) { + return fs.blks[i] + } + return nil +} + +// Index a raw msg buffer. +// Lock should be held. +func (mb *msgBlock) indexCacheBuf(buf []byte) error { + var le = binary.LittleEndian + + var alloc bool + var idx []uint32 + var index uint32 + + mbFirstSeq := atomic.LoadUint64(&mb.first.seq) + mbLastSeq := atomic.LoadUint64(&mb.last.seq) + + // Sanity check here since we calculate size to allocate based on this. + if mbFirstSeq > (mbLastSeq + 1) { // Purged state first == last + 1 + mb.fs.warn("indexCacheBuf corrupt state in %s: mb.first %d mb.last %d", mb.mfn, mbFirstSeq, mbLastSeq) + // This would cause idxSz to wrap. + return errCorruptState + } + + idxSz := mbLastSeq - mbFirstSeq + 1 + + if mb.cache == nil { + mb.cache = mb.ecache.Value() + } + if mb.cache == nil { + mb.cache = &cache{} + alloc = true + } else { + // The buf arg already came from the pool probably, so there's + // no point in reusing mb.cache.buf's underlying capacity here. + // Just recycle it for the next block load. + mb.cache.stopRecycle() + recycleMsgBlockBuf(mb.cache.buf) + } + if idx = mb.cache.idx; uint64(cap(idx)) >= idxSz { + idx = idx[:0] + } else { + idx = make([]uint32, 0, idxSz) + } + if !alloc { + // We didn't allocate a new *cache so instead wipe the current + // one, we already have reused idx if possible. + *mb.cache = cache{} + } + + // Create FSS if we should track. + var popFss bool + if mb.fssNotLoaded() { + mb.fss = stree.NewSubjectTree[SimpleState]() + popFss = true + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + mb.ttls = 0 + mb.schedules = 0 + + lbuf := uint32(len(buf)) + var seq, ttls, schedules uint64 + var sm StoreMsg // Used for finding headers + + // To ensure the sequence keeps moving up. As well as confirming our index + // is aligned with the mb's first and last sequence. + var first uint64 + var last uint64 + + for index < lbuf { + if index+msgHdrSize > lbuf { + return errCorruptState + } + hdr := buf[index : index+msgHdrSize] + rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:])) + seq = le.Uint64(hdr[4:]) + + hasHeaders := rl&hbit != 0 + // Clear any headers bit that could be set. + rl &^= hbit + shlen := slen + if hasHeaders { + shlen += 4 + } + dlen := int(rl) - msgHdrSize + // Do some quick sanity checks here. + if dlen < 0 || shlen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh { + mb.fs.warn("indexCacheBuf corrupt record state in %s: dlen %d slen %d index %d rl %d lbuf %d", mb.mfn, dlen, slen, index, rl, lbuf) + // This means something is off. + // TODO(dlc) - Add into bad list? + return errCorruptState + } + + // Check for tombstones which we can skip in terms of indexing. + if seq&tbit != 0 { + index += rl + continue + } + + // Clear any erase bits. + erased := seq&ebit != 0 + seq = seq &^ ebit + + // The sequence needs to only ever move up. + if seq <= last { + // Advance to next record. + // We've already accounted for this sequence and marked it as deleted. + index += rl + continue + } + // We defer checksum checks to individual msg cache lookups to amortorize costs and + // not introduce latency for first message from a newly loaded block. + if seq >= mbFirstSeq { + last = seq + + // If the first sequence doesn't align with what we had in-memory, we need to rebuild. + if first == 0 { + first = seq + if mbFirstSeq != first { + return errCorruptState + } + } + + // Track that we do not have holes. + if slot := int(seq - mbFirstSeq); slot != len(idx) { + // If we have a hole fill it. + for dseq := mbFirstSeq + uint64(len(idx)); dseq < seq; dseq++ { + idx = append(idx, dbit) + if dseq != 0 { + mb.dmap.Insert(dseq) + } + } + } + // Add to our index. + idx = append(idx, index) + + // Make sure our dmap has this entry if it was erased. + // If not, that means this erased message was not accounted for in our in-memory state. + if erased && seq != 0 && !mb.dmap.Exists(seq) { + return errCorruptState + } + + // Handle FSS inline here. + if popFss && slen > 0 && !mb.noTrack && !erased && !mb.dmap.Exists(seq) { + bsubj := buf[index+msgHdrSize : index+msgHdrSize+uint32(slen)] + if ss, ok := mb.fss.Find(bsubj); ok && ss != nil { + ss.Msgs++ + ss.Last = seq + ss.lastNeedsUpdate = false + } else { + mb.fss.Insert(bsubj, SimpleState{ + Msgs: 1, + First: seq, + Last: seq, + }) + } + } + + // Count how many TTLs/schedules we think are in this message block. + // TODO(nat): Not terribly optimal... + if hasHeaders { + if fsm, err := mb.msgFromBufNoCopy(buf[index:], &sm, nil); err == nil && fsm != nil { + if ttl := sliceHeader(JSMessageTTL, fsm.hdr); len(ttl) > 0 { + ttls++ + } + if sched := sliceHeader(JSSchedulePattern, fsm.hdr); len(sched) > 0 { + schedules++ + } + } + } + } + index += rl + } + + // If we ended up with a smaller or larger index, or the first/last sequence + // doesn't align with what we had in-memory, we need to rebuild. + if len(idx) != int(idxSz) || (first > 0 && mbFirstSeq != first) || (last > 0 && mbLastSeq != last) { + return errCorruptState + } + + mb.cache.buf = buf + mb.cache.idx = idx + mb.cache.fseq = mbFirstSeq + mb.cache.wp = int(lbuf) + mb.ttls = ttls + mb.schedules = schedules + mb.cache.registerRecycle() + + return nil +} + +// flushPendingMsgs writes out any messages for this message block. +func (mb *msgBlock) flushPendingMsgs() error { + mb.mu.Lock() + fsLostData, err := mb.flushPendingMsgsLocked() + fs := mb.fs + mb.mu.Unlock() + + // Signals us that we need to rebuild filestore state. + if fsLostData != nil && fs != nil { + // Rebuild fs state too. + fs.rebuildState(fsLostData) + } + return err +} + +// Write function for actual data. +// mb.mfd should not be nil. +// Lock should held. +func (mb *msgBlock) writeAt(buf []byte, woff int64) (int, error) { + // Used to mock write failures. + if mb.mockWriteErr { + // Reset on trip. + mb.mockWriteErr = false + return 0, errors.New("mock write error") + } + mb.fs.dios.acquire() + n, err := mb.mfd.WriteAt(buf, woff) + mb.fs.dios.release() + return n, err +} + +// flushPendingMsgsLocked writes out any messages for this message block. +// Lock should be held. +func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) { + var weakenCache bool + if mb.cache == nil { + mb.cache = mb.ecache.Value() + weakenCache = mb.cache != nil + } + if mb.cache == nil || mb.mfd == nil || mb.werr != nil { + return nil, mb.werr + } + + buf, err := mb.bytesPending() + // If we got an error back return here. + if err != nil { + // No pending data to be written is not an error. + if err == errNoPending || err == errNoCache { + err = nil + } + return nil, err + } + + wp := int64(mb.cache.wp) + lob := len(buf) + + // TODO(dlc) - Normally we would not hold the lock across I/O so we can improve performance. + // We will hold to stabilize the code base, as we have had a few anomalies with partial cache errors + // under heavy load. + + // Check if we need to encrypt. + if err := mb.checkAndLoadEncryption(); err != nil { + return nil, err + } + if mb.bek != nil && lob > 0 { + // Need to leave original alone. + var dst []byte + if lob <= defaultLargeBlockSize { + dst = getMsgBlockBuf(lob)[:lob] + defer recycleMsgBlockBuf(dst) + } else { + dst = make([]byte, lob) + } + mb.bek.XORKeyStream(dst, buf) + buf = dst + } + + // Append new data to the message block file. + for lbb := lob; lbb > 0; lbb = len(buf) { + n, err := mb.writeAt(buf, wp) + if err != nil { + // Ignore the errors here, we'll try reloading just to figure out and return the lost data if we can. + _ = mb.dirtyCloseWithRemove(false) + ld, _, _ := mb.rebuildStateLocked() + mb.werr = err + assert.Unreachable("Filestore msg block encountered flush error", map[string]any{ + "name": mb.fs.cfg.Name, + "mb.index": mb.index, + "err": err, + "stack": string(debug.Stack()), + }) + return ld, err + } + // Update our write offset. + wp += int64(n) + buf = buf[n:] + } + + // Cache may be gone. + if mb.cache == nil || mb.mfd == nil { + return nil, mb.werr + } + + // Update write pointer. + mb.cache.wp = int(wp) + + // Check if we are in sync always mode. + if mb.fs.syncAlways.Load() { + if err = mb.mfd.Sync(); err != nil { + mb.werr = err + assert.Unreachable("Filestore msg block encountered sync error", map[string]any{ + "name": mb.fs.cfg.Name, + "mb.index": mb.index, + "err": err, + "stack": string(debug.Stack()), + }) + return nil, err + } + } else { + mb.needSync = true + } + + // Check for additional writes while we were writing to the disk. + // TODO(nat): This should not be possible at present since, as above, we are + // not releasing the lock during I/O operation. Therefore this will always + // return zero. + if mb.pendingWriteSizeLocked() > 0 { + return nil, mb.werr + } + + // Check last access time. If we think the block still has read interest + // then we will weaken the pointer but otherwise try to hold onto it. + if ts := ats.AccessTime(); ts < mb.llts || (ts-mb.llts) <= int64(mb.cexp) { + if weakenCache { + mb.cache = nil + mb.ecache.Weaken() + } + mb.resetCacheExpireTimer(0) + return nil, mb.werr + } + + // If not, we'll just drop the cache altogether & recycle the buffer. + mb.cache.nra = false + mb.tryExpireCacheLocked() + return nil, mb.werr +} + +// Lock should be held. +func (mb *msgBlock) clearLoading() { + mb.loading = false +} + +// Will load msgs from disk. +func (mb *msgBlock) loadMsgs() error { + // We hold the lock here the whole time by design. + mb.mu.Lock() + defer mb.mu.Unlock() + return mb.loadMsgsWithLock() +} + +// Lock should be held. +func (mb *msgBlock) cacheAlreadyLoaded() bool { + if mb.cache == nil { + mb.cache = mb.ecache.Value() + } + if mb.cache == nil || mb.cache.fseq == 0 || len(mb.cache.buf) == 0 { + return false + } + numEntries := mb.msgs + uint64(mb.dmap.Size()) + (atomic.LoadUint64(&mb.first.seq) - mb.cache.fseq) + return numEntries == uint64(len(mb.cache.idx)) +} + +// Lock should be held. +func (mb *msgBlock) cacheNotLoaded() bool { + return !mb.cacheAlreadyLoaded() +} + +// Report if our fss is not loaded. +// Lock should be held. +func (mb *msgBlock) fssNotLoaded() bool { + return mb.fss == nil && !mb.noTrack +} + +// Wrap openBlock for the gated semaphore processing. +// Lock should be held +func (mb *msgBlock) openBlock() (*os.File, error) { + // Gate with concurrent IO semaphore. + mb.fs.dios.acquire() + f, err := os.Open(mb.mfn) + mb.fs.dios.release() + return f, err +} + +// Used to load in the block contents. +// Lock should be held and all conditionals satisfied prior. +func (mb *msgBlock) loadBlock(buf []byte) ([]byte, error) { + var f *os.File + // Re-use if we have mfd open. + if mb.mfd != nil { + f = mb.mfd + if n, err := f.Seek(0, 0); n != 0 || err != nil { + f = nil + mb.closeFDsLockedNoCheck() + } + } + if f == nil { + var err error + f, err = mb.openBlock() + if err != nil { + if os.IsNotExist(err) { + err = errNoBlkData + } + return nil, err + } + defer f.Close() + } + + var sz int + if info, err := f.Stat(); err == nil { + sz64 := info.Size() + if int64(int(sz64)) == sz64 { + sz = int(sz64) + } else { + return nil, errMsgBlkTooBig + } + } + + if buf == nil || cap(buf) < sz { + // getMsgBlockBuf will try to return a slice that fits from the + // buffer pool, but if by chance `sz` is greater than the maximum + // large block size, it'll return a new slice to fit regardless. + buf = getMsgBlockBuf(sz) + } + + mb.fs.dios.acquire() + n, err := io.ReadFull(f, buf[:sz]) + mb.fs.dios.release() + // On success capture raw bytes size. + if err == nil { + mb.rbytes = uint64(n) + } + return buf[:n], err +} + +// Lock should be held. +func (mb *msgBlock) loadMsgsWithLock() error { + if err := mb.checkAndLoadEncryption(); err != nil { + return err + } + + // Check to see if we are loading already. + if mb.loading { + return nil + } + + // Set loading status. + mb.loading = true + defer mb.clearLoading() + + var nchecks int + +checkCache: + nchecks++ + if nchecks > 8 { + return errNoCache + } + + // Check to see if we have a full cache. + if mb.cacheAlreadyLoaded() { + return nil + } + + mb.llts = ats.AccessTime() + + // FIXME(dlc) - We could be smarter here. + if buf, _ := mb.bytesPending(); len(buf) > 0 { + ld, err := mb.flushPendingMsgsLocked() + if ld != nil && mb.fs != nil { + // We do not know if fs is locked or not at this point. + // This should be an exceptional condition so do so in Go routine. + go mb.fs.rebuildState(ld) + } + if err != nil { + return err + } + goto checkCache + } + + // Load in the whole block. + // We want to hold the mb lock here to avoid any changes to state. + buf, err := mb.loadBlock(nil) + if err != nil { + mb.fs.warn("loadBlock error: %v", err) + if err == errNoBlkData { + if ld, _, _ := mb.rebuildStateLocked(); ld != nil { + // Rebuild fs state too. + go mb.fs.rebuildState(ld) + } + } + return err + } + + // Check if we need to decrypt. + if err = mb.encryptOrDecryptIfNeeded(buf); err != nil { + return err + } + // Check for compression. + if buf, err = mb.decompressIfNeeded(buf); err != nil { + return err + } + + if err := mb.indexCacheBuf(buf); err != nil { + if err == errCorruptState { + var ld *LostStreamData + ld, _, err = mb.rebuildStateLocked() + // We do not know if fs is locked or not at this point. + // This should be an exceptional condition so do so in Go routine. + // Always rebuild the filestore's state if indexing fails, even if no data was lost, + // our in-memory state was stale in that case. + go mb.fs.rebuildState(ld) + } + if err != nil { + return err + } + goto checkCache + } + + if len(buf) > 0 { + mb.cloads++ + mb.startCacheExpireTimer() + } + + // Update the cache pointer since we cleared & rebuilt. + mb.ecache.Set(mb.cache) + + return nil +} + +// Fetch a message from this block, possibly reading in and caching the messages. +// We assume the block was selected and is correct, so we do not do range checks. +// Lock should not be held. +func (mb *msgBlock) fetchMsg(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) { + mb.mu.Lock() + defer mb.mu.Unlock() + defer mb.finishedWithCache() + return mb.fetchMsgEx(seq, sm, true) +} + +// Fetch a message from this block, possibly reading in and caching the messages. +// We assume the block was selected and is correct, so we do not do range checks. +// We will not copy the msg data, the returned StoreMsg's subj/hdr/msg/buf are aliased +// into mb.cache.buf and are only safe to read while mb.mu is held. +func (mb *msgBlock) fetchMsgNoCopyLocked(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) { + return mb.fetchMsgEx(seq, sm, false) +} + +// Fetch a message from this block, possibly reading in and caching the messages. +// We assume the block was selected and is correct, so we do not do range checks. +// We will copy the msg data based on doCopy boolean. +// Lock should be held. +func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg, bool, error) { + fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq) + if seq < fseq || seq > lseq { + return nil, false, ErrStoreMsgNotFound + } + + // See if we can short circuit if we already know msg deleted. + if mb.dmap.Exists(seq) { + // Update for scanning like cacheLookup would have. + llseq := mb.llseq + if mb.llseq == 0 || seq < mb.llseq || seq == mb.llseq+1 || seq == mb.llseq-1 { + mb.llseq = seq + } + expireOk := (seq == lseq && llseq == seq-1) || (seq == fseq && llseq == seq+1) + return nil, expireOk, errDeletedMsg + } + + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return nil, false, err + } + } + llseq := mb.llseq + + fsm, err := mb.cacheLookupEx(seq, sm, doCopy) + if err != nil { + return nil, false, err + } + expireOk := (seq == lseq && llseq == seq-1) || (seq == fseq && llseq == seq+1) + return fsm, expireOk, nil +} + +var ( + errNoCache = errors.New("no message cache") + errDeletedMsg = errors.New("deleted message") + errPartialCache = errors.New("partial cache") + errNoPending = errors.New("message block does not have pending data") + errNotReadable = errors.New("storage directory not readable") + errCorruptState = errors.New("corrupt state file") + errPriorState = errors.New("prior state file") + errPendingData = errors.New("pending data still present") + errNoEncryption = errors.New("encryption not enabled") + errBadKeySize = errors.New("encryption bad key size") + errKeyInvalid = errors.New("unable to recover keys") + errNoMsgBlk = errors.New("no message block") + errMsgBlkTooBig = errors.New("message block size exceeded int capacity") + errUnknownCipher = errors.New("unknown cipher") + errNoMainKey = errors.New("encrypted store encountered with no main key") + errNoBlkData = errors.New("message block data missing") + errStateTooBig = errors.New("store state too big for optional write") +) + +type ( + errBadMsg struct{ fn, detail string } +) + +func (e errBadMsg) Error() string { + if e.detail != _EMPTY_ { + return fmt.Sprintf("malformed or corrupt message in %s: %s", filepath.Base(e.fn), e.detail) + } + return fmt.Sprintf("malformed or corrupt message in %s", filepath.Base(e.fn)) +} + +const ( + // "Checksum bit" is used in "mb.cache.idx" for marking messages that have had their checksums checked. + cbit = 1 << 31 + // "Delete bit" is used in "mb.cache.idx" to mark an index as deleted and non-existent. + dbit = 1 << 30 + // "Header bit" is used in "rl" to signal a message record with headers. + hbit = 1 << 31 + // "Erase bit" is used in "seq" for marking erased messages sequences. + ebit = 1 << 63 + // "Tombstone bit" is used in "seq" for marking tombstone sequences. + tbit = 1 << 62 +) + +// Will do a lookup from cache. +// This will copy the msg from the cache. +// Lock should be held. +func (mb *msgBlock) cacheLookup(seq uint64, sm *StoreMsg) (*StoreMsg, error) { + return mb.cacheLookupEx(seq, sm, true) +} + +// Will do a lookup from cache. +// This will NOT copy the msg from the cache. +// Lock should be held. +func (mb *msgBlock) cacheLookupNoCopy(seq uint64, sm *StoreMsg) (*StoreMsg, error) { + return mb.cacheLookupEx(seq, sm, false) +} + +// Will do a lookup from cache. +// Lock should be held. +func (mb *msgBlock) cacheLookupEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg, error) { + fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq) + switch { + case lseq == fseq-1: + // The block is empty, no messages have been written yet. This works because + // newMsgBlockForWrite sets fseq=fs.State.LastSeq+1 and lseq=fs.State.LastSeq. + return nil, ErrStoreMsgNotFound + case seq < fseq || seq > lseq: + // Sequence is out of range for this block. + return nil, ErrStoreMsgNotFound + } + + // The llseq signals us when we can expire a cache at the end of a linear scan. + // We want to only update when we know the last reads (multiple consumers) are sequential. + // We want to account for forwards and backwards linear scans. + if mb.llseq == 0 || seq < mb.llseq || seq == mb.llseq+1 || seq == mb.llseq-1 { + mb.llseq = seq + } + + // If we have a delete map check it. + if mb.dmap.Exists(seq) { + mb.llts = ats.AccessTime() + return nil, errDeletedMsg + } + + // Detect no cache loaded. + if mb.cache == nil { + mb.cache = mb.ecache.Value() + } + if mb.cache == nil || mb.cache.fseq == 0 || len(mb.cache.idx) == 0 || len(mb.cache.buf) == 0 { + var reason string + if mb.cache == nil { + reason = "no cache" + } else if mb.cache.fseq == 0 { + reason = "fseq is 0" + } else if len(mb.cache.idx) == 0 { + reason = "no idx present" + } else { + reason = "cache buf empty" + } + mb.fs.warn("Cache lookup for sequence %d in block %d detected no cache: %s", seq, mb.index, reason) + if mb.cache != nil { + mb.tryForceExpireCacheLocked() + } + return nil, errNoCache + } + // Check partial cache status. + if seq < mb.cache.fseq { + mb.fs.warn("Partial cache: seq %d is less than cache fseq %d", seq, mb.cache.fseq) + return nil, errPartialCache + } + + bi, _, hashChecked, err := mb.slotInfo(int(seq - mb.cache.fseq)) + if err != nil { + return nil, err + } + + // Update cache activity. + mb.llts = ats.AccessTime() + + li := int(bi) + if li >= len(mb.cache.buf) { + mb.fs.warn("Partial cache: slot index %d is less than cache buffer len %d", li, len(mb.cache.buf)) + return nil, errPartialCache + } + buf := mb.cache.buf[li:] + + // We use the high bit to denote we have already checked the checksum. + var hh *highwayhash.Digest64 + if !hashChecked { + hh = mb.hh // This will force the hash check in msgFromBuf. + } + + // Parse from the raw buffer. + fsm, err := mb.msgFromBufEx(buf, sm, hh, doCopy) + if err != nil || fsm == nil { + return nil, err + } + + // Deleted messages that are decoded return a 0 for sequence. + if fsm.seq == 0 { + return nil, errDeletedMsg + } + + if seq != fsm.seq { // See TestFileStoreInvalidIndexesRebuilt. + mb.clearCacheAndOffset() + return nil, fmt.Errorf("sequence numbers for cache load did not match, %d vs %d", seq, fsm.seq) + } + + // Clear the check bit here after we know all is good. + if !hashChecked { + mb.cache.idx[seq-mb.cache.fseq] = (bi | cbit) + } + + return fsm, nil +} + +// Used when we are checking if discarding a message due to max msgs per subject will give us +// enough room for a max bytes condition. +// Lock should be already held. +func (fs *fileStore) sizeForSeq(seq uint64) int { + if seq == 0 { + return 0 + } + var smv StoreMsg + if mb := fs.selectMsgBlock(seq); mb != nil { + mb.mu.Lock() + sm, _, _ := mb.fetchMsgNoCopyLocked(seq, &smv) + var sz int + if sm != nil { + sz = int(fileStoreMsgSize(sm.subj, sm.hdr, sm.msg)) + } + mb.finishedWithCache() + mb.mu.Unlock() + return sz + } + return 0 +} + +// Will return message for the given sequence number. +// This will be returned to external callers. +func (fs *fileStore) msgForSeq(seq uint64, sm *StoreMsg) (*StoreMsg, error) { + return fs.msgForSeqLocked(seq, sm, true) +} + +// Will return message for the given sequence number. +func (fs *fileStore) msgForSeqLocked(seq uint64, sm *StoreMsg, needFSLock bool) (*StoreMsg, error) { + if fs.isClosed() { + return nil, ErrStoreClosed + } + // TODO(dlc) - Since Store, Remove, Skip all hold the write lock on fs this will + // be stalled. Need another lock if want to happen in parallel. + if needFSLock { + fs.mu.RLock() + } + // Indicates we want first msg. + if seq == 0 { + seq = fs.state.FirstSeq + } + // Make sure to snapshot here. + mb, lseq := fs.selectMsgBlock(seq), fs.state.LastSeq + if needFSLock { + fs.mu.RUnlock() + } + + if mb == nil { + var err = ErrStoreEOF + if seq <= lseq { + err = ErrStoreMsgNotFound + } + return nil, err + } + + fsm, expireOk, err := mb.fetchMsg(seq, sm) + if err != nil { + return nil, err + } + + // We detected a linear scan and access to the last message. + // If we are not the last message block we can try to expire the cache. + if expireOk { + mb.tryForceExpireCache() + } + + return fsm, nil +} + +// Internal function to return msg parts from a raw buffer. +// Raw buffer will be copied into sm. +// Lock should be held. +func (mb *msgBlock) msgFromBuf(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64) (*StoreMsg, error) { + return mb.msgFromBufEx(buf, sm, hh, true) +} + +// Internal function to return msg parts from a raw buffer. +// Raw buffer will NOT be copied into sm. +// Only use for internal use, any message that is passed to upper layers should use mb.msgFromBuf. +// Lock should be held. +func (mb *msgBlock) msgFromBufNoCopy(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64) (*StoreMsg, error) { + return mb.msgFromBufEx(buf, sm, hh, false) +} + +// Internal function to return msg parts from a raw buffer. +// copy boolean will determine if we make a copy or not. +// Lock should be held. +func (mb *msgBlock) msgFromBufEx(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64, doCopy bool) (*StoreMsg, error) { + if len(buf) < emptyRecordLen { + return nil, errBadMsg{mb.mfn, "record too short"} + } + var le = binary.LittleEndian + + hdr := buf[:msgHdrSize] + rl := le.Uint32(hdr[0:]) + hasHeaders := rl&hbit != 0 + rl &^= hbit // clear header bit + dlen := int(rl) - msgHdrSize + slen := int(le.Uint16(hdr[20:])) + + shlen := slen + if hasHeaders { + shlen += 4 + } + // Simple sanity check. + if dlen < 0 || shlen > (dlen-recordHashSize) || dlen > int(rl) || int(rl) > len(buf) || rl > rlBadThresh { + return nil, errBadMsg{mb.mfn, fmt.Sprintf("sanity check failed (dlen %d slen %d rl %d buf %d)", dlen, slen, rl, buf)} + } + data := buf[msgHdrSize : msgHdrSize+dlen] + // Do checksum tests here if requested. + if hh != nil { + hh.Reset() + hh.Write(hdr[4:20]) + hh.Write(data[:slen]) + if hasHeaders { + hh.Write(data[slen+4 : dlen-recordHashSize]) + } else { + hh.Write(data[slen : dlen-recordHashSize]) + } + var hb [highwayhash.Size64]byte + if !bytes.Equal(hh.Sum(hb[:0]), data[len(data)-8:]) { + return nil, errBadMsg{mb.mfn, "invalid checksum"} + } + } + seq := le.Uint64(hdr[4:]) + if seq&ebit != 0 { + seq = 0 + } + ts := int64(le.Uint64(hdr[12:])) + + // Create a StoreMsg if needed. + if sm == nil { + sm = new(StoreMsg) + } else { + sm.clear() + } + // To recycle the large blocks we can never pass back a reference, so need to copy for the upper + // layers and for us to be safe to expire, and recycle, the large msgBlocks. + end := dlen - 8 + if len(data) < end { + return nil, errBadMsg{mb.mfn, "invalid data length"} + } + + if hasHeaders { + if slen+4 > len(data) { + return nil, errBadMsg{mb.mfn, "invalid subject length greataer than data length"} + } + hl := le.Uint32(data[slen:]) + bi := slen + 4 + li := bi + int(hl) + if bi > end { + return nil, errBadMsg{mb.mfn, "invalid buffer index"} + } + if doCopy { + sm.buf = append(sm.buf, data[bi:end]...) + } else { + sm.buf = data[bi:end] + } + li, end = li-bi, end-bi + if li > len(sm.buf) || end > len(sm.buf) { + return nil, errBadMsg{mb.mfn, "invalid message length or end greater than buffer length"} + } + sm.hdr = sm.buf[0:li:li] + sm.msg = sm.buf[li:end] + } else { + if slen > end { + return nil, errBadMsg{mb.mfn, "invalid subject length greater than end"} + } + if doCopy { + sm.buf = append(sm.buf, data[slen:end]...) + } else { + sm.buf = data[slen:end] + } + mlen := end - slen + if mlen > len(sm.buf) { + return nil, errBadMsg{mb.mfn, "invalid message length greater than buffer length"} + } + sm.msg = sm.buf[0:mlen] + } + sm.seq, sm.ts = seq, ts + if slen > 0 { + if slen > len(data) { + return nil, errBadMsg{mb.mfn, "invalid subject length greater than data length"} + } + if doCopy { + // Make a copy since sm.subj lifetime may last longer. + sm.subj = string(data[:slen]) + } else { + sm.subj = bytesToString(data[:slen]) + } + } + + return sm, nil +} + +// SubjectForSeq will return what the subject is for this sequence if found. +func (fs *fileStore) SubjectForSeq(seq uint64) (string, error) { + fs.mu.RLock() + if seq < fs.state.FirstSeq { + fs.mu.RUnlock() + return _EMPTY_, ErrStoreMsgNotFound + } + var smv StoreMsg + mb := fs.selectMsgBlock(seq) + fs.mu.RUnlock() + if mb != nil { + mb.mu.Lock() + sm, _, _ := mb.fetchMsgNoCopyLocked(seq, &smv) + var subj string + if sm != nil { + // Copy the subject, as it's used elsewhere, and the backing cache could be reused in the meantime. + subj = copyString(sm.subj) + } + mb.finishedWithCache() + mb.mu.Unlock() + if sm != nil { + return subj, nil + } + } + return _EMPTY_, ErrStoreMsgNotFound +} + +// LoadMsg will lookup the message by sequence number and return it if found. +func (fs *fileStore) LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error) { + return fs.msgForSeq(seq, sm) +} + +// loadLast will load the last message for a subject. Subject should be non empty and not ">". +func (fs *fileStore) loadLast(subj string, sm *StoreMsg) (lsm *StoreMsg, err error) { + if fs.isClosed() { + return nil, ErrStoreClosed + } + + fs.mu.RLock() + defer fs.mu.RUnlock() + return fs.loadLastLocked(subj, sm) +} + +// Lock should be held. +func (fs *fileStore) loadLastLocked(subj string, sm *StoreMsg) (lsm *StoreMsg, err error) { + if fs.isClosed() || fs.lmb == nil { + return nil, ErrStoreClosed + } + + if len(fs.blks) == 0 { + return nil, ErrStoreMsgNotFound + } + + wc := subjectHasWildcard(subj) + var start, stop uint32 + + // If literal subject check for presence. + if wc { + start = fs.lmb.index + fs.psim.Match(stringToBytes(subj), func(_ []byte, psi *psi) { + // Keep track of start and stop indexes for this subject. + if psi.fblk < start { + start = psi.fblk + } + if psi.lblk > stop { + stop = psi.lblk + } + }) + // None matched. + if stop == 0 { + return nil, ErrStoreMsgNotFound + } + // These need to be swapped. + start, stop = stop, start + } else if info, ok := fs.psim.Find(stringToBytes(subj)); ok { + start, stop = info.lblk, info.fblk + } else { + return nil, ErrStoreMsgNotFound + } + + // Walk blocks backwards. + for i := start; i >= stop; i-- { + mb := fs.bim[i] + if mb == nil { + continue + } + mb.mu.Lock() + if err = mb.ensurePerSubjectInfoLoaded(); err != nil { + mb.mu.Unlock() + return nil, err + } + // Mark fss activity. + mb.lsts = ats.AccessTime() + + var l uint64 + // Optimize if subject is not a wildcard. + if !wc { + if ss, ok := mb.fss.Find(stringToBytes(subj)); ok && ss != nil { + // Check if we need to recalculate. We only care about the last sequence. + if ss.lastNeedsUpdate { + // mb is already loaded into the cache so should be fast-ish. + if err = mb.recalculateForSubj(subj, ss); err != nil { + mb.mu.Unlock() + return nil, err + } + } + l = ss.Last + } + } + if l == 0 { + _, _, l, err = mb.filteredPendingLocked(subj, wc, atomic.LoadUint64(&mb.first.seq)) + if err != nil { + mb.mu.Unlock() + return nil, err + } + } + needsCleanup := mb.cache == nil + if l > 0 { + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + if needsCleanup { + mb.finishedWithCache() + } + mb.mu.Unlock() + return nil, err + } + } + lsm, err = mb.cacheLookup(l, sm) + } + if needsCleanup { + mb.finishedWithCache() + } + mb.mu.Unlock() + if l > 0 { + break + } + } + return lsm, err +} + +// LoadLastMsg will return the last message we have that matches a given subject. +// The subject can be a wildcard. +func (fs *fileStore) LoadLastMsg(subject string, smv *StoreMsg) (sm *StoreMsg, err error) { + if subject == _EMPTY_ || subject == fwcs { + sm, err = fs.msgForSeq(fs.lastSeq(), smv) + } else { + sm, err = fs.loadLast(subject, smv) + } + if sm == nil || (err != nil && err != ErrStoreClosed) { + err = ErrStoreMsgNotFound + } + return sm, err +} + +// LoadNextMsgMulti will find the next message matching any entry in the sublist. +func (fs *fileStore) LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) { + if fs.isClosed() { + return nil, 0, ErrStoreClosed + } + if sl == nil || sl.MatchesFullWildcard() { + return fs.LoadNextMsg(_EMPTY_, false, start, smp) + } + if filter, ok := sl.MatchesSingleFilter(); ok { + return fs.LoadNextMsg(filter, subjectHasWildcard(filter), start, smp) + } + fs.mu.RLock() + defer fs.mu.RUnlock() + + if fs.state.Msgs == 0 || start > fs.state.LastSeq { + return nil, fs.state.LastSeq, ErrStoreEOF + } + if start < fs.state.FirstSeq { + start = fs.state.FirstSeq + } + + // If start is less than or equal to beginning of our stream, meaning our first call, + // let's check the psim to see if we can skip ahead. + if start <= fs.state.FirstSeq { + var total uint64 + blkStart := uint32(math.MaxUint32) + stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool { + total += psi.total + // Keep track of start index for this subject. + if psi.fblk < blkStart { + blkStart = psi.fblk + } + return true + }) + // Nothing available. + if total == 0 { + return nil, fs.state.LastSeq, ErrStoreEOF + } + // We can skip ahead. + if mb := fs.bim[blkStart]; mb != nil { + fseq := atomic.LoadUint64(&mb.first.seq) + if fseq > start { + start = fseq + } + } + } + + if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 { + for i := bi; i < len(fs.blks); i++ { + mb := fs.blks[i] + if sm, expireOk, err := mb.firstMatchingMulti(sl, start, smp); err == nil { + if expireOk { + mb.tryForceExpireCache() + } + return sm, sm.seq, nil + } else if err != ErrStoreMsgNotFound { + return nil, 0, err + } else { + // Nothing found in this block. We missed, if first block (bi) check psim. + // Similar to above if start <= first seq. + // TODO(dlc) - For v2 track these by filter subject since they will represent filtered consumers. + // We should not do this at all if we are already on the last block. + if i == bi && i < len(fs.blks)-1 { + nbi, err := fs.checkSkipFirstBlockMulti(sl, bi) + // Nothing available. + if err == ErrStoreEOF { + return nil, fs.state.LastSeq, ErrStoreEOF + } + // See if we can jump ahead here. + // Right now we can only spin on first, so if we have interior sparseness need to favor checking per block fss if loaded. + // For v2 will track all blocks that have matches for psim. + if nbi > i { + i = nbi - 1 // For the iterator condition i++ + } + } + // Check if we can expire. + if expireOk { + mb.tryForceExpireCache() + } + } + } + } + + return nil, fs.state.LastSeq, ErrStoreEOF + +} + +func (fs *fileStore) LoadNextMsg(filter string, wc bool, start uint64, sm *StoreMsg) (*StoreMsg, uint64, error) { + if fs.isClosed() { + return nil, 0, ErrStoreClosed + } + + fs.mu.RLock() + defer fs.mu.RUnlock() + + if fs.state.Msgs == 0 || start > fs.state.LastSeq { + return nil, fs.state.LastSeq, ErrStoreEOF + } + if start < fs.state.FirstSeq { + start = fs.state.FirstSeq + } + + // If start is less than or equal to beginning of our stream, meaning our first call, + // let's check the psim to see if we can skip ahead. + if start <= fs.state.FirstSeq { + var ss SimpleState + if err := fs.numFilteredPendingNoLast(filter, &ss); err != nil { + return nil, 0, err + } + // Nothing available. + if ss.Msgs == 0 { + return nil, fs.state.LastSeq, ErrStoreEOF + } + // We can skip ahead. + if ss.First > start { + start = ss.First + } + } + + if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 { + for i := bi; i < len(fs.blks); i++ { + mb := fs.blks[i] + if sm, expireOk, err := mb.firstMatching(filter, wc, start, sm); err == nil { + if expireOk { + mb.tryForceExpireCache() + } + return sm, sm.seq, nil + } else if err != ErrStoreMsgNotFound { + return nil, 0, err + } else { + // Nothing found in this block. We missed, if first block (bi) check psim. + // Similar to above if start <= first seq. + // TODO(dlc) - For v2 track these by filter subject since they will represent filtered consumers. + // We should not do this at all if we are already on the last block. + // Also if we are a wildcard do not check if large subject space. + const wcMaxSizeToCheck = 64 * 1024 + if i == bi && i < len(fs.blks)-1 && (!wc || fs.psim.Size() < wcMaxSizeToCheck) { + nbi, err := fs.checkSkipFirstBlock(filter, wc, bi) + // Nothing available. + if err == ErrStoreEOF { + return nil, fs.state.LastSeq, ErrStoreEOF + } + // See if we can jump ahead here. + // Right now we can only spin on first, so if we have interior sparseness need to favor checking per block fss if loaded. + // For v2 will track all blocks that have matches for psim. + if nbi > i { + i = nbi - 1 // For the iterator condition i++ + } + } + // Check if we can expire. + if expireOk { + mb.tryForceExpireCache() + } + } + } + } + + return nil, fs.state.LastSeq, ErrStoreEOF +} + +// Find the previous matching message. +// fs lock should be held. +func (mb *msgBlock) prevMatching(filter string, wc bool, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) { + mb.mu.Lock() + var updateLLTS bool + defer func() { + if updateLLTS { + mb.llts = ats.AccessTime() + } + mb.finishedWithCache() + mb.mu.Unlock() + }() + + end, isAll := start, filter == _EMPTY_ || filter == fwcs + + var didLoad bool + if mb.fssNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return nil, false, err + } + didLoad = true + } + mb.lsts = ats.AccessTime() + + if filter == _EMPTY_ { + filter = fwcs + wc = true + } + + if !isAll && mb.fss.Size() == 1 { + if !wc { + _, isAll = mb.fss.Find(stringToBytes(filter)) + } else { + mb.fss.Match(stringToBytes(filter), func(subject []byte, _ *SimpleState) { + isAll = true + }) + } + if !isAll { + return nil, didLoad, ErrStoreMsgNotFound + } + } + + lseq := atomic.LoadUint64(&mb.first.seq) + end = min(end, atomic.LoadUint64(&mb.last.seq)) + + var isMatch func(subj string) bool + if wc { + _tsa, _fsa := [32]string{}, [32]string{} + tsa, fsa := _tsa[:0], tokenizeSubjectIntoSlice(_fsa[:0], filter) + isMatch = func(subj string) bool { + tsa = tokenizeSubjectIntoSlice(tsa[:0], subj) + return isSubsetMatchTokenized(tsa, fsa) + } + } + + subjs := mb.fs.cfg.Subjects + doLinearScan := isAll || (wc && len(subjs) == 1 && subjs[0] == filter) + if !doLinearScan && wc && mb.cacheAlreadyLoaded() { + doLinearScan = mb.fss.Size()*4 > int(end-lseq) + } + + if !doLinearScan { + var found bool + var first, last uint64 + if bfilter := stringToBytes(filter); wc { + var ierr error + mb.fss.Match(bfilter, func(bsubj []byte, ss *SimpleState) { + if ierr != nil { + return + } + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if ierr = mb.recalculateForSubj(bytesToString(bsubj), ss); ierr != nil { + return + } + } + if end < ss.First { + return + } + if !found { + found = true + first, last = ss.First, min(end, ss.Last) + return + } + first = min(first, ss.First) + last = max(last, min(end, ss.Last)) + }) + if ierr != nil { + return nil, false, ierr + } + } else if ss, _ := mb.fss.Find(bfilter); ss != nil { + if ss.firstNeedsUpdate || ss.lastNeedsUpdate { + if err := mb.recalculateForSubj(filter, ss); err != nil { + return nil, false, err + } + } + if end >= ss.First { + found = true + first, last = ss.First, min(end, ss.Last) + } + } + if !found || first > last { + return nil, didLoad, ErrStoreMsgNotFound + } + lseq, end = max(lseq, first), last + } + + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return nil, false, err + } + didLoad = true + } + + if sm == nil { + sm = new(StoreMsg) + } + + for seq := end; seq >= lseq; seq-- { + if mb.dmap.Exists(seq) { + updateLLTS = true + continue + } + llseq := mb.llseq + fsm, err := mb.cacheLookup(seq, sm) + if err != nil { + if err == errPartialCache || err == errNoCache { + return nil, false, err + } + continue + } + updateLLTS = false + expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq + if isAll { + return fsm, expireOk, nil + } + if wc && isMatch(sm.subj) { + return fsm, expireOk, nil + } else if !wc && fsm.subj == filter { + return fsm, expireOk, nil + } + mb.llseq = llseq + } + + return nil, didLoad, ErrStoreMsgNotFound +} + +// Will load the previous message matching the filter subject, starting at the start sequence and walking backwards. +func (fs *fileStore) LoadPrevMsg(filter string, wc bool, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) { + if fs.isClosed() { + return nil, 0, ErrStoreClosed + } + + fs.mu.RLock() + defer fs.mu.RUnlock() + + if fs.state.Msgs == 0 || start < fs.state.FirstSeq { + return nil, fs.state.FirstSeq, ErrStoreEOF + } + + if start > fs.state.LastSeq { + start = fs.state.LastSeq + } + + if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 { + for i := bi; i >= 0; i-- { + mb := fs.blks[i] + if sm, expireOk, err := mb.prevMatching(filter, wc, start, smp); err == nil { + if expireOk { + mb.tryForceExpireCache() + } + return sm, sm.seq, nil + } else if err != ErrStoreMsgNotFound { + return nil, 0, err + } else if expireOk { + mb.tryForceExpireCache() + } + } + } + + return nil, fs.state.FirstSeq, ErrStoreEOF +} + +// LoadPrevMsgMulti will find the previous message matching any entry in the sublist. +func (fs *fileStore) LoadPrevMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) { + if fs.isClosed() { + return nil, 0, ErrStoreClosed + } + + if sl == nil || sl.MatchesFullWildcard() { + return fs.LoadPrevMsg(_EMPTY_, false, start, smp) + } + if filter, ok := sl.MatchesSingleFilter(); ok { + return fs.LoadPrevMsg(filter, subjectHasWildcard(filter), start, smp) + } + fs.mu.RLock() + defer fs.mu.RUnlock() + return fs.loadPrevMsgMultiLocked(sl, start, smp) +} + +// loadPrevMsgMultiLocked will find the previous message matching any entry in the sublist. +// Lock should be held. +func (fs *fileStore) loadPrevMsgMultiLocked(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) { + if fs.state.Msgs == 0 || start < fs.state.FirstSeq { + return nil, fs.state.FirstSeq, ErrStoreEOF + } + if start > fs.state.LastSeq { + start = fs.state.LastSeq + } + + if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 { + for i := bi; i >= 0; i-- { + mb := fs.blks[i] + if sm, expireOk, err := mb.prevMatchingMulti(sl, start, smp); err == nil { + if expireOk { + mb.tryForceExpireCache() + } + return sm, sm.seq, nil + } else if err != ErrStoreMsgNotFound { + return nil, 0, err + } else if expireOk { + mb.tryForceExpireCache() + } + } + } + + return nil, fs.state.FirstSeq, ErrStoreEOF +} + +// Type returns the type of the underlying store. +func (fs *fileStore) Type() StorageType { + return FileStorage +} + +// Returns number of subjects in this store. +// Lock should be held. +func (fs *fileStore) numSubjects() int { + return fs.psim.Size() +} + +// numConsumers uses new lock. +func (fs *fileStore) numConsumers() int { + fs.cmu.RLock() + defer fs.cmu.RUnlock() + return len(fs.cfs) +} + +// FastState will fill in state with only the following. +// Msgs, Bytes, First and Last Sequence and Time and NumDeleted. +func (fs *fileStore) FastState(state *StreamState) { + fs.mu.RLock() + state.Msgs = fs.state.Msgs + state.Bytes = fs.state.Bytes + state.FirstSeq = fs.state.FirstSeq + state.FirstTime = fs.state.FirstTime + state.LastSeq = fs.state.LastSeq + state.LastTime = fs.state.LastTime + // Make sure to reset if being re-used. + state.Deleted, state.NumDeleted = nil, 0 + if state.LastSeq > state.FirstSeq { + state.NumDeleted = int((state.LastSeq - state.FirstSeq + 1) - state.Msgs) + if state.NumDeleted < 0 { + state.NumDeleted = 0 + } + } + state.Consumers = fs.numConsumers() + state.NumSubjects = fs.numSubjects() + fs.mu.RUnlock() +} + +// State returns the current state of the stream. +func (fs *fileStore) State() StreamState { + fs.mu.RLock() + state := fs.state + state.Consumers = fs.numConsumers() + state.NumSubjects = fs.numSubjects() + state.Deleted = nil // make sure. + + if numDeleted := int((state.LastSeq - state.FirstSeq + 1) - state.Msgs); numDeleted > 0 { + state.Deleted = make([]uint64, 0, numDeleted) + cur := fs.state.FirstSeq + + for _, mb := range fs.blks { + mb.mu.Lock() + fseq := atomic.LoadUint64(&mb.first.seq) + // Account for messages missing from the head. + if fseq > cur { + for seq := cur; seq < fseq; seq++ { + state.Deleted = append(state.Deleted, seq) + } + } + // Only advance cur if we are increasing. We could have marker blocks with just tombstones. + if last := atomic.LoadUint64(&mb.last.seq); last >= cur { + cur = last + 1 // Expected next first. + } + // Add in deleted. + mb.dmap.Range(func(seq uint64) bool { + state.Deleted = append(state.Deleted, seq) + return true + }) + mb.mu.Unlock() + } + } + fs.mu.RUnlock() + + state.Lost = fs.lostData() + + // Can not be guaranteed to be sorted. + if len(state.Deleted) > 0 { + slices.Sort(state.Deleted) + state.NumDeleted = len(state.Deleted) + } + return state +} + +func (fs *fileStore) Utilization() (total, reported uint64, err error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + for _, mb := range fs.blks { + mb.mu.RLock() + reported += mb.bytes + total += mb.rbytes + mb.mu.RUnlock() + } + return total, reported, nil +} + +func fileStoreMsgSizeRaw(slen, hlen, mlen int) uint64 { + if hlen == 0 { + // length of the message record (4bytes) + seq(8) + ts(8) + subj_len(2) + subj + msg + hash(8) + return uint64(22 + slen + mlen + 8) + } + // length of the message record (4bytes) + seq(8) + ts(8) + subj_len(2) + subj + hdr_len(4) + hdr + msg + hash(8) + return uint64(22 + slen + 4 + hlen + mlen + 8) +} + +func fileStoreMsgSize(subj string, hdr, msg []byte) uint64 { + return fileStoreMsgSizeRaw(len(subj), len(hdr), len(msg)) +} + +// isFileStoreMsgTooLarge reports whether a message record cannot be represented +// safely by the file store. +func isFileStoreMsgTooLarge(rl uint64) bool { + return rl&hbit != 0 || rl > rlBadThresh +} + +func fileStoreMsgSizeEstimate(slen, maxPayload int) uint64 { + return uint64(emptyRecordLen + slen + 4 + maxPayload) +} + +// ResetState resets any state that's temporary. For example when changing leaders. +func (fs *fileStore) ResetState() { + fs.mu.Lock() + defer fs.mu.Unlock() + if fs.scheduling != nil { + fs.scheduling.clearInflight() + } +} + +func (fs *fileStore) Ready() { + fs.mu.Lock() + defer fs.mu.Unlock() + if !fs.recovering { + return + } + fs.recovering = false + var ageDelta int64 + if fs.cfg.MaxAge > 0 && !fs.state.FirstTime.IsZero() { + ageDelta = time.Until(fs.state.FirstTime.Add(fs.cfg.MaxAge)).Nanoseconds() + if ageDelta <= 0 { + ageDelta = 1 + } + } + fs.resetAgeChk(ageDelta) + if fs.scheduling != nil { + fs.scheduling.paused = false + fs.scheduling.resetTimer() + } +} + +// Determine time since any last activity, read/load, write or remove. +func (mb *msgBlock) sinceLastActivity() time.Duration { + if mb.closed { + return 0 + } + last := mb.lwts + if mb.lrts > last { + last = mb.lrts + } + if mb.llts > last { + last = mb.llts + } + if mb.lsts > last { + last = mb.lsts + } + return time.Since(time.Unix(0, last).UTC()) +} + +// Determine time since last write or remove of a message. +// Read lock should be held. +func (mb *msgBlock) sinceLastWriteActivity() time.Duration { + if mb.closed { + return 0 + } + last := mb.lwts + if mb.lrts > last { + last = mb.lrts + } + return time.Since(time.Unix(0, last).UTC()) +} + +func checkNewHeader(hdr []byte) error { + if len(hdr) < 2 || hdr[0] != magic || + (hdr[1] != version && hdr[1] != newVersion) { + return errCorruptState + } + return nil +} + +// readIndexInfo will read in the index information for the message block. +func (mb *msgBlock) readIndexInfo() error { + ifn := filepath.Join(mb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(indexScan, mb.index)) + buf, err := os.ReadFile(ifn) + if err != nil { + return err + } + + // Set if first time. + if mb.liwsz == 0 { + mb.liwsz = int64(len(buf)) + } + + // Decrypt if needed. + if mb.aek != nil { + buf, err = mb.aek.Open(buf[:0], mb.nonce, buf, nil) + if err != nil { + return err + } + } + + if err := checkNewHeader(buf); err != nil { + defer os.Remove(ifn) + return fmt.Errorf("bad index file") + } + + bi := hdrLen + + // Helpers, will set i to -1 on error. + readSeq := func() uint64 { + if bi < 0 { + return 0 + } + seq, n := binary.Uvarint(buf[bi:]) + if n <= 0 { + bi = -1 + return 0 + } + bi += n + return seq &^ ebit + } + readCount := readSeq + readTimeStamp := func() int64 { + if bi < 0 { + return 0 + } + ts, n := binary.Varint(buf[bi:]) + if n <= 0 { + bi = -1 + return -1 + } + bi += n + return ts + } + mb.msgs = readCount() + mb.bytes = readCount() + atomic.StoreUint64(&mb.first.seq, readSeq()) + mb.first.ts = readTimeStamp() + atomic.StoreUint64(&mb.last.seq, readSeq()) + mb.last.ts = readTimeStamp() + dmapLen := readCount() + + // Check if this is a short write index file. + if bi < 0 || bi+checksumSize > len(buf) { + os.Remove(ifn) + return fmt.Errorf("short index file") + } + + // Check for consistency if accounting. If something is off bail and we will rebuild. + if mb.msgs != (atomic.LoadUint64(&mb.last.seq)-atomic.LoadUint64(&mb.first.seq)+1)-dmapLen { + os.Remove(ifn) + return fmt.Errorf("accounting inconsistent") + } + + // Checksum + copy(mb.lchk[0:], buf[bi:bi+checksumSize]) + bi += checksumSize + + // Now check for presence of a delete map + if dmapLen > 0 { + // New version is encoded avl seqset. + if buf[1] == newVersion { + dmap, _, err := avl.Decode(buf[bi:]) + if err != nil { + return fmt.Errorf("could not decode avl dmap: %v", err) + } + mb.dmap = *dmap + } else { + // This is the old version. + for i, fseq := 0, atomic.LoadUint64(&mb.first.seq); i < int(dmapLen); i++ { + seq := readSeq() + if seq == 0 { + break + } + mb.dmap.Insert(seq + fseq) + } + } + } + + return nil +} + +// Will return total number of cache loads. +func (fs *fileStore) cacheLoads() uint64 { + var tl uint64 + fs.mu.RLock() + for _, mb := range fs.blks { + mb.mu.RLock() + tl += mb.cloads + mb.mu.RUnlock() + } + fs.mu.RUnlock() + return tl +} + +// Will return total number of cached bytes. +func (fs *fileStore) cacheSize() uint64 { + var sz uint64 + fs.mu.RLock() + for _, mb := range fs.blks { + mb.mu.Lock() + var needsCleanup bool + if mb.cache == nil { + mb.cache = mb.ecache.Value() + needsCleanup = mb.cache != nil + } + if mb.cache != nil { + sz += uint64(len(mb.cache.buf)) + } + if needsCleanup { + mb.finishedWithCache() + } + mb.mu.Unlock() + } + fs.mu.RUnlock() + return sz +} + +// Will return total number of dmapEntries for all msg blocks. +func (fs *fileStore) dmapEntries() int { + var total int + fs.mu.RLock() + for _, mb := range fs.blks { + total += mb.dmap.Size() + } + fs.mu.RUnlock() + return total +} + +// Fixed helper for iterating. +func subjectsEqual(a, b string) bool { + return a == b +} + +func subjectsAll(a, b string) bool { + return true +} + +func compareFn(subject string) func(string, string) bool { + if subject == _EMPTY_ || subject == fwcs { + return subjectsAll + } + if subjectHasWildcard(subject) { + return subjectIsSubsetMatch + } + return subjectsEqual +} + +// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep. +// Will return the number of purged messages. +func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) { + // sequence == 1 means "purge up to but not including 1", a no-op. + if sequence == 1 { + return 0, nil + } + if subject == _EMPTY_ || subject == fwcs { + if keep == 0 && sequence == 0 { + return fs.purge(0) + } + if sequence > 1 { + return fs.compact(sequence) + } + // Make sure to not leave subject if empty. + if subject == _EMPTY_ { + subject = fwcs + } + } + + // Persist any write errors. + defer func() { + if err != nil { + fs.mu.Lock() + fs.setWriteErr(err) + fs.mu.Unlock() + } + }() + + eq, wc := compareFn(subject), subjectHasWildcard(subject) + var firstSeqNeedsUpdate bool + var bytes uint64 + + // If we have a "keep" designation need to get full filtered state so we know how many to purge. + var maxp uint64 + if keep > 0 { + ss, err := fs.FilteredState(1, subject) + if err != nil || keep >= ss.Msgs { + return 0, err + } + maxp = ss.Msgs - keep + } + + var smv StoreMsg + var tombs []msgId + var lowSeq uint64 + + fs.mu.Lock() + if fs.isClosed() { + fs.mu.Unlock() + return purged, ErrStoreClosed + } + // Always return previous write errors. + if err := fs.werr; err != nil { + fs.mu.Unlock() + return purged, err + } + if len(fs.blks) == 0 || fs.lmb == nil { + fs.mu.Unlock() + return purged, nil + } + + var start, stop uint32 + + // If literal subject check for presence. + if wc { + start = fs.lmb.index + fs.psim.Match(stringToBytes(subject), func(_ []byte, psi *psi) { + // Keep track of start and stop indexes for this subject. + if psi.fblk < start { + start = psi.fblk + } + if psi.lblk > stop { + stop = psi.lblk + } + }) + // None matched. + if stop == 0 { + fs.mu.Unlock() + return purged, nil + } + } else if info, ok := fs.psim.Find(stringToBytes(subject)); ok { + start, stop = info.fblk, info.lblk + } else { + fs.mu.Unlock() + return purged, nil + } + + // We may remove blocks as we purge, so don't range directly on fs.blks + // otherwise we may jump over some (see https://github.com/nats-io/nats-server/issues/3528) + for i := 0; i < len(fs.blks); i++ { + mb := fs.blks[i] + // Skip if not within our range for the purge subject. + if mb.index < start || mb.index > stop { + continue + } + mb.mu.Lock() + + // If we do not have our fss, try to expire the cache if we have no items in this block. + shouldExpire := mb.fssNotLoaded() + + t, f, l, err := mb.filteredPendingLocked(subject, wc, atomic.LoadUint64(&mb.first.seq)) + if err != nil { + mb.mu.Unlock() + fs.mu.Unlock() + return purged, err + } + if t == 0 { + // Expire if we were responsible for loading. + if shouldExpire { + // Expire this cache before moving on. + mb.tryForceExpireCacheLocked() + } + mb.mu.Unlock() + continue + } + + // "Purge up to but not including sequence": sequence == 0 means no + // sequence filter; sequence >= 1 clamps the per-block upper bound to + // sequence-1 (so sequence == 1 leaves nothing to process). + if sequence >= 1 && sequence <= l { + l = sequence - 1 + } + + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + fs.mu.Unlock() + return 0, err + } + shouldExpire = true + } + + var nrg uint64 // Number of remaining messages globally after removal from psim. + + for seq, te := f, len(tombs); seq <= l; seq++ { + if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && eq(sm.subj, subject) { + rl := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg) + // Do fast in place remove. + // Stats + if mb.msgs > 0 { + // Msgs + fs.state.Msgs-- + mb.msgs-- + // Bytes, make sure to not go negative. + if rl > fs.state.Bytes { + rl = fs.state.Bytes + } + if rl > mb.bytes { + rl = mb.bytes + } + fs.state.Bytes -= rl + mb.bytes -= rl + // Totals + purged++ + bytes += rl + } + // PSIM and FSS updates. + nr, err := mb.removeSeqPerSubject(sm.subj, seq) + if err != nil { + mb.mu.Unlock() + fs.mu.Unlock() + return purged, err + } + nrg = fs.removePerSubject(sm.subj) + + // Track tombstones we need to write. + tombs = append(tombs, msgId{sm.seq, sm.ts}) + if sm.seq < lowSeq || lowSeq == 0 { + lowSeq = sm.seq + } + + // Check for first message. + if seq == atomic.LoadUint64(&mb.first.seq) { + mb.selectNextFirst() + if mb.isEmpty() { + // Since we are removing this block don't need to write tombstones. + tombs = tombs[:te] + if err = fs.removeMsgBlock(mb); err != nil { + mb.mu.Unlock() + fs.mu.Unlock() + return 0, err + } + i-- + // keep flag set, if set previously + firstSeqNeedsUpdate = firstSeqNeedsUpdate || seq == fs.state.FirstSeq + } else if seq == fs.state.FirstSeq { + fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq) // new one. + if mb.first.ts == 0 { + fs.state.FirstTime = time.Time{} + } else { + fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC() + } + } + } else { + // Out of order delete. + mb.dmap.Insert(seq) + } + // Break if we have emptied this block or if we set a maximum purge count. + if mb.isEmpty() || (maxp > 0 && purged >= maxp) { + break + } + // Also break if we know we have no more messages matching here. + // This is only applicable for non-wildcarded filters. + if !wc && nr == 0 { + break + } + } + } + // Expire if we were responsible for loading and we do not seem to be doing successive purgeEx calls. + // On successive calls - most likely from KV purge deletes, we want to keep the data loaded. + if shouldExpire && time.Since(fs.lpex) > time.Second { + // Expire this cache before moving on. + mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() + } + mb.mu.Unlock() + + // Check if we should break out of top level too. + if maxp > 0 && purged >= maxp { + break + } + // Also check if not wildcarded and we have no remaining matches. + if !wc && nrg == 0 { + break + } + } + if firstSeqNeedsUpdate { + if err = fs.selectNextFirst(); err != nil { + fs.mu.Unlock() + return purged, err + } + } + + // Update the last purgeEx call time. + defer func() { fs.lpex = time.Now() }() + + // Write any tombstones as needed. + // When writing multiple tombstones we will flush at the end. + if len(tombs) > 0 { + for _, tomb := range tombs { + if err = fs.writeTombstoneNoFlush(tomb.seq, tomb.ts); err != nil { + fs.mu.Unlock() + return purged, err + } + } + // Flush any pending. If we change blocks the newMsgBlockForWrite() will flush any pending for us. + if lmb := fs.lmb; lmb != nil { + if err = lmb.flushPendingMsgs(); err != nil { + fs.mu.Unlock() + return purged, err + } + } + } + + fs.dirty++ + cb := fs.scb + fs.mu.Unlock() + + if cb != nil { + if purged == 1 { + cb(-int64(purged), -int64(bytes), lowSeq, subject) + } else { + // FIXME(dlc) - Since we track lowSeq we could send to upper layer if they dealt with the condition properly. + cb(-int64(purged), -int64(bytes), 0, _EMPTY_) + } + } + + return purged, nil +} + +// Purge will remove all messages from this store. +// Will return the number of purged messages. +func (fs *fileStore) Purge() (uint64, error) { + return fs.purge(0) +} + +func (fs *fileStore) purge(fseq uint64) (uint64, error) { + fs.mu.Lock() + if fs.isClosed() { + fs.mu.Unlock() + return 0, ErrStoreClosed + } + cb := fs.scb + purged, bytes, err := fs.purgeLocked(fseq) + if err != nil { + fs.setWriteErr(err) + fs.mu.Unlock() + return purged, err + } + fs.mu.Unlock() + + // Force a new index.db to be written. + if purged > 0 { + fs.forceWriteFullState() + } + if cb != nil { + cb(-int64(purged), -int64(bytes), 0, _EMPTY_) + } + return purged, nil +} + +// Lock must be held. +func (fs *fileStore) purgeLocked(fseq uint64) (purged, bytes uint64, err error) { + // Always return previous write errors. + if err := fs.werr; err != nil { + return 0, 0, err + } + + purged, bytes = fs.state.Msgs, fs.state.Bytes + + fs.state.FirstSeq = fs.state.LastSeq + 1 + fs.state.FirstTime = time.Time{} + + fs.state.Bytes = 0 + fs.state.Msgs = 0 + + for _, mb := range fs.blks { + // These blocks are being discarded by the purge, so mark them closed. + mb.mu.Lock() + mb.dirtyCloseWithRemove(false) + mb.closed = true + mb.mu.Unlock() + } + + // Check if we need to set the first seq to a new number. + if fseq > fs.state.FirstSeq { + fs.state.FirstSeq = fseq + fs.state.LastSeq = fseq - 1 + } + + // Make sure we have a lmb to write to. + if _, err := fs.newMsgBlockForWrite(); err != nil { + return purged, bytes, err + } + + lmb := fs.lmb + atomic.StoreUint64(&lmb.first.seq, fs.state.FirstSeq) + atomic.StoreUint64(&lmb.last.seq, fs.state.LastSeq) + lmb.last.ts = fs.state.LastTime.UnixNano() + + if lseq := atomic.LoadUint64(&lmb.last.seq); lseq > 0 { + // Leave a tombstone so we can remember our starting sequence in case + // full state becomes corrupted. + if err := fs.writeTombstone(lseq, lmb.last.ts); err != nil { + return purged, bytes, err + } + } + // Close FDs since we'll move the file. We re-enable the FD after the purge is complete. + if err := lmb.flushPendingMsgs(); err != nil { + return purged, bytes, err + } + if err := lmb.closeFDs(); err != nil { + return purged, bytes, err + } + + fs.blks = nil + fs.lmb = nil + fs.bim = make(map[uint32]*msgBlock) + // Clear any per subject tracking. + fs.psim, fs.tsl = fs.psim.Empty(), 0 + fs.sdm.empty() + // Mark dirty. + fs.dirty++ + fs.addMsgBlock(lmb) + + // Move the msgs directory out of the way, will delete out of band. + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + ndir := filepath.Join(fs.fcfg.StoreDir, newMsgDir) + pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir) + fs.dios.acquire() + // If purge directory still exists then we need to wait + // in place and remove since rename would fail. + if _, err := os.Stat(ndir); err == nil { + if err = os.RemoveAll(ndir); err != nil { + fs.dios.release() + return purged, bytes, err + } + } else if !os.IsNotExist(err) { + fs.dios.release() + return purged, bytes, err + } + if _, err := os.Stat(pdir); err == nil { + if err = os.RemoveAll(pdir); err != nil { + fs.dios.release() + return purged, bytes, err + } + } else if !os.IsNotExist(err) { + fs.dios.release() + return purged, bytes, err + } + + // Create directory to move the new tombstone to. + if err := os.MkdirAll(ndir, defaultDirPerms); err != nil { + fs.dios.release() + return purged, bytes, err + } + // Move out the block containing the tombstone. Also move the key file if encrypted. + // The block file itself MUST be moved last to ensure we can assume the prior renames + // were successful during recovery. + for _, mbf := range []string{fmt.Sprintf(keyScan, lmb.index), fmt.Sprintf(blkScan, lmb.index)} { + b := filepath.Join(mdir, mbf) + a := filepath.Join(ndir, mbf) + if err := os.Rename(b, a); err != nil && !os.IsNotExist(err) { + fs.dios.release() + return purged, bytes, err + } + } + // Purge all remaining messages. + if err := os.Rename(mdir, pdir); err != nil { + fs.dios.release() + return purged, bytes, err + } + // Rename the directory back to be left only with the tombstone. + if err := os.Rename(ndir, mdir); err != nil { + fs.dios.release() + return purged, bytes, err + } + fs.dios.release() + + // Remove the purged messages directory asynchronously. + go func() { + fs.dios.acquire() + _ = os.RemoveAll(pdir) + fs.dios.release() + }() + + // Re-enable writing for the lmb. + lmb.mu.Lock() + err = lmb.enableForWriting(fs.fip) + lmb.mu.Unlock() + if err != nil { + return purged, bytes, err + } + + return purged, bytes, nil +} + +// Lock and dios should be held. +func (fs *fileStore) recoverPartialPurge() error { + mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) + ndir := filepath.Join(fs.fcfg.StoreDir, newMsgDir) + if entries, err := os.ReadDir(ndir); err != nil && !os.IsNotExist(err) { + return err + } else if err == nil { + hasBlk := slices.ContainsFunc(entries, func(e os.DirEntry) bool { + return strings.HasSuffix(e.Name(), blkSuffix) + }) + if hasBlk { + // We have a tombstone, we can purge the old messages. + if err = os.RemoveAll(mdir); err != nil { + return err + } + if err = os.Rename(ndir, mdir); err != nil { + return err + } + } else { + // No .blk means the purge did not complete, so clear + // any progress made by the partial purge. + for _, entry := range entries { + var index uint32 + if n, err := fmt.Sscanf(entry.Name(), keyScan, &index); err != nil || n != 1 { + continue + } + // Found a key file, remove the corresponding .blk, if any. + // Recovery may otherwise wrongly conclude that the .blk is + // is plaintext, and consider it corrupt when trying to open it. + err := os.Remove(filepath.Join(mdir, fmt.Sprintf(blkScan, index))) + if err != nil && !os.IsNotExist(err) { + return err + } + } + _ = os.RemoveAll(ndir) + return nil + } + } + pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir) + if _, err := os.Stat(pdir); err == nil { + _ = os.RemoveAll(pdir) + } + return nil +} + +// Compact will remove all messages from this store up to +// but not including the seq parameter. +// Will return the number of purged messages. +func (fs *fileStore) Compact(seq uint64) (uint64, error) { + return fs.compact(seq) +} + +func (fs *fileStore) compact(seq uint64) (uint64, error) { + var err error + var purged, bytes uint64 + fs.mu.Lock() + if fs.isClosed() { + fs.mu.Unlock() + return 0, ErrStoreClosed + } + if seq == 0 || seq > fs.state.LastSeq { + purged, bytes, err = fs.purgeLocked(seq) + } else { + purged, bytes, err = fs.compactLocked(seq) + } + if err != nil { + fs.setWriteErr(err) + fs.mu.Unlock() + return purged, err + } + cb := fs.scb + fs.mu.Unlock() + + // Force a new index.db to be written. + if purged > 0 { + fs.forceWriteFullState() + } + + if cb != nil && purged > 0 { + cb(-int64(purged), -int64(bytes), 0, _EMPTY_) + } + + return purged, nil +} + +// Lock must be held. +func (fs *fileStore) compactLocked(seq uint64) (purged, bytes uint64, err error) { + // Always return previous write errors. + if err := fs.werr; err != nil { + return 0, 0, err + } + + // Short-circuit if the store was already compacted past this point. + if fs.state.FirstSeq > seq { + return 0, 0, nil + } + // We have to delete interior messages. + smb := fs.selectMsgBlock(seq) + if smb == nil { + return 0, 0, nil + } + + // All msgblocks up to this one can be thrown away. + var deleted int + for _, mb := range fs.blks { + if mb == smb { + break + } + mb.mu.Lock() + purged += mb.msgs + bytes += mb.bytes + // Make sure we do subject cleanup as well. + if err := mb.ensurePerSubjectInfoLoaded(); err != nil { + mb.mu.Unlock() + return 0, 0, err + } + mb.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool { + subj := bytesToString(bsubj) + for i := uint64(0); i < ss.Msgs; i++ { + fs.removePerSubject(subj) + } + return true + }) + // Now close. + err := mb.dirtyCloseWithRemove(true) + mb.mu.Unlock() + if err != nil { + return purged, bytes, err + } + deleted++ + } + + var smv StoreMsg + var tombs []msgId + + smb.mu.Lock() + if atomic.LoadUint64(&smb.first.seq) == seq { + fs.state.FirstSeq = atomic.LoadUint64(&smb.first.seq) + fs.state.FirstTime = time.Unix(0, smb.first.ts).UTC() + goto SKIP + } + + // Make sure we have the messages loaded. + if smb.cacheNotLoaded() { + if err = smb.loadMsgsWithLock(); err != nil { + smb.mu.Unlock() + return purged, bytes, err + } + defer func() { + // The block lock is released once we get here, so need to re-acquire. + smb.mu.Lock() + smb.finishedWithCache() + smb.mu.Unlock() + }() + } + for mseq := atomic.LoadUint64(&smb.first.seq); mseq < seq; mseq++ { + sm, err := smb.cacheLookupNoCopy(mseq, &smv) + if err == errDeletedMsg { + // Update dmap. + if !smb.dmap.IsEmpty() { + smb.dmap.Delete(mseq) + } + } else if sm != nil { + sz := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg) + if smb.msgs > 0 { + smb.msgs-- + if sz > smb.bytes { + sz = smb.bytes + } + smb.bytes -= sz + bytes += sz + purged++ + } + // Update fss + if _, err := smb.removeSeqPerSubject(sm.subj, mseq); err != nil { + smb.mu.Unlock() + return purged, bytes, err + } + fs.removePerSubject(sm.subj) + tombs = append(tombs, msgId{sm.seq, sm.ts}) + } + } + + // Check if empty after processing, could happen if tail of messages are all deleted. + if isEmpty := smb.msgs == 0; isEmpty { + // Only remove if not the last block. + if smb != fs.lmb { + if err = smb.dirtyCloseWithRemove(true); err != nil { + smb.mu.Unlock() + return purged, bytes, err + } + deleted++ + } else { + // Make sure to sync changes. + smb.needSync = true + } + // Update fs first here as well. + fs.state.FirstSeq = atomic.LoadUint64(&smb.last.seq) + 1 + fs.state.FirstTime = time.Time{} + + } else { + // Make sure to sync changes. + smb.needSync = true + // Just for start condition for selectNextFirst. + if smb.first.seq < seq { + atomic.StoreUint64(&smb.first.seq, seq-1) + } else { + // selectNextFirst always adds 1, so need to subtract 1 here. + atomic.StoreUint64(&smb.first.seq, smb.first.seq-1) + } + smb.selectNextFirst() + + fs.state.FirstSeq = atomic.LoadUint64(&smb.first.seq) + fs.state.FirstTime = time.Unix(0, smb.first.ts).UTC() + + // Check if we should reclaim the head space from this block. + // This will be optimistic only, so don't continue if we encounter any errors here. + if smb.rbytes > compactMinimum && smb.bytes*2 < smb.rbytes { + var moff uint32 + moff, _, _, err = smb.slotInfo(int(atomic.LoadUint64(&smb.first.seq) - smb.cache.fseq)) + if err != nil { + smb.mu.Unlock() + return purged, bytes, err + } else if moff >= uint32(len(smb.cache.buf)) { + goto SKIP + } + buf := smb.cache.buf[moff:] + // Don't reuse, copy to new recycled buf. + nbuf := getMsgBlockBuf(len(buf)) + // Recycle our nbuf when we are done. + defer recycleMsgBlockBuf(nbuf) + nbuf = append(nbuf, buf...) + smb.closeFDsLockedNoCheck() + // Check for compression. + if smb.cmp != NoCompression && len(nbuf) > 0 { + originalSize := len(nbuf) + if nbuf, err = smb.cmp.Compress(nbuf); err != nil { + smb.mu.Unlock() + return purged, bytes, err + } + meta := &CompressionInfo{ + Algorithm: smb.cmp, + OriginalSize: uint64(originalSize), + } + nbuf = append(meta.MarshalMetadata(), nbuf...) + } + // Check for encryption. + if smb.bek != nil && len(nbuf) > 0 { + // Recreate to reset counter. + bek, err := genBlockEncryptionKey(smb.fs.fcfg.Cipher, smb.seed, smb.nonce) + if err != nil { + smb.mu.Unlock() + return purged, bytes, err + } + // For future writes make sure to set smb.bek to keep counter correct. + smb.bek = bek + smb.bek.XORKeyStream(nbuf, nbuf) + } + + // We will write to a new file and mv/rename it in case of failure. + mfn := filepath.Join(smb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(newScan, smb.index)) + fs.dios.acquire() + err = os.WriteFile(mfn, nbuf, defaultFilePerms) + fs.dios.release() + if err != nil { + _ = os.Remove(mfn) + smb.mu.Unlock() + return purged, bytes, err + } + if err = os.Rename(mfn, smb.mfn); err != nil { + _ = os.Remove(mfn) + smb.mu.Unlock() + return purged, bytes, err + } + + // Make sure to remove fss state. + smb.fss = nil + smb.clearCacheAndOffset() + smb.rbytes = uint64(len(nbuf)) + // Make sure we don't write any additional tombstones. + tombs = nil + } + } + +SKIP: + smb.mu.Unlock() + + // Write any tombstones as needed. + // When writing multiple tombstones we will flush at the end. + if len(tombs) > 0 { + for _, tomb := range tombs { + if err = fs.writeTombstoneNoFlush(tomb.seq, tomb.ts); err != nil { + return purged, bytes, err + } + } + // Flush any pending. If we change blocks the newMsgBlockForWrite() will flush any pending for us. + if lmb := fs.lmb; lmb != nil { + if err = lmb.flushPendingMsgs(); err != nil { + return purged, bytes, err + } + } + } + + if deleted > 0 { + // Update block map. + if fs.bim != nil { + for _, mb := range fs.blks[:deleted] { + delete(fs.bim, mb.index) + } + } + // Update blks slice. + fs.blks = copyMsgBlocks(fs.blks[deleted:]) + if lb := len(fs.blks); lb == 0 { + fs.lmb = nil + } else { + fs.lmb = fs.blks[lb-1] + } + } + + // Update top level accounting. + if purged > fs.state.Msgs { + purged = fs.state.Msgs + } + fs.state.Msgs -= purged + if fs.state.Msgs == 0 { + fs.state.FirstSeq = fs.state.LastSeq + 1 + fs.state.FirstTime = time.Time{} + } + + if bytes > fs.state.Bytes { + bytes = fs.state.Bytes + } + fs.state.Bytes -= bytes + + // Any existing state file no longer applicable. We will force write a new one + // after we release the lock. + os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)) + fs.dirty++ + + return purged, bytes, nil +} + +// Will completely reset our store. +func (fs *fileStore) reset() error { + fs.mu.Lock() + if fs.isClosed() { + fs.mu.Unlock() + return ErrStoreClosed + } + + var purged, bytes uint64 + cb := fs.scb + + for _, mb := range fs.blks { + mb.mu.Lock() + purged += mb.msgs + bytes += mb.bytes + err := mb.dirtyCloseWithRemove(true) + mb.mu.Unlock() + if err != nil { + fs.mu.Unlock() + return err + } + } + + // Reset + fs.state.FirstSeq = 0 + fs.state.FirstTime = time.Time{} + fs.state.LastSeq = 0 + fs.state.LastTime = time.Now().UTC() + // Update msgs and bytes. + fs.state.Msgs = 0 + fs.state.Bytes = 0 + + // Reset blocks. + fs.blks, fs.lmb = nil, nil + + // Reset subject mappings. + fs.psim, fs.tsl = fs.psim.Empty(), 0 + fs.sdm.empty() + fs.bim = make(map[uint32]*msgBlock) + + // If we purged anything, make sure we kick flush state loop. + if purged > 0 { + fs.dirty++ + } + + fs.mu.Unlock() + + if cb != nil { + cb(-int64(purged), -int64(bytes), 0, _EMPTY_) + } + + return nil +} + +// Return all active tombstones in this msgBlock. +func (mb *msgBlock) tombs() []msgId { + mb.mu.Lock() + defer mb.mu.Unlock() + return mb.tombsLocked() +} + +// Return all active tombstones in this msgBlock. +// Write lock should be held. +func (mb *msgBlock) tombsLocked() []msgId { + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return nil + } + } + defer mb.finishedWithCache() + + var tombs []msgId + var le = binary.LittleEndian + buf := mb.cache.buf + + for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; { + if index+msgHdrSize > lbuf { + return tombs + } + hdr := buf[index : index+msgHdrSize] + rl, seq := le.Uint32(hdr[0:]), le.Uint64(hdr[4:]) + // Clear any headers bit that could be set. + rl &^= hbit + // Check for tombstones. + if seq&tbit != 0 { + ts := int64(le.Uint64(hdr[12:])) + tombs = append(tombs, msgId{seq &^ tbit, ts}) + } + // Advance to next record. + index += rl + } + + return tombs +} + +// fs lock should be held. +func (mb *msgBlock) numPriorTombs() int { + mb.mu.Lock() + defer mb.mu.Unlock() + return mb.numPriorTombsLocked() +} + +// Return number of tombstones for messages prior to this msgBlock. +// Both locks should be held. +// Write lock should be held for block. +func (mb *msgBlock) numPriorTombsLocked() int { + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return 0 + } + } + defer mb.finishedWithCache() + + var fseq uint64 + var tombs int + var le = binary.LittleEndian + buf := mb.cache.buf + + for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; { + if index+msgHdrSize > lbuf { + return tombs + } + hdr := buf[index : index+msgHdrSize] + rl, seq := le.Uint32(hdr[0:]), le.Uint64(hdr[4:]) + // Clear any headers bit that could be set. + rl &^= hbit + // Check for tombstones. + if seq&tbit != 0 { + seq = seq &^ tbit + // Tombstones below the global first seq are irrelevant. + // And we only count tombstones below this block's first seq. + if seq >= mb.fs.state.FirstSeq && (fseq == 0 || seq < fseq) { + tombs++ + } + index += rl + continue + } + if seq == 0 || seq&ebit != 0 { + index += rl + continue + } + // Advance to next record. + index += rl + if fseq == 0 { + fseq = seq + } + } + + return tombs +} + +// Truncate will truncate a stream store up to seq. Sequence needs to be valid. +func (fs *fileStore) Truncate(seq uint64) (rerr error) { + // Check for request to reset. + if seq == 0 { + return fs.reset() + } + + fs.mu.Lock() + if fs.isClosed() { + fs.mu.Unlock() + return ErrStoreClosed + } + // Always return previous write errors. + if err := fs.werr; err != nil { + fs.mu.Unlock() + return err + } + + // Persist any write errors. + defer func() { + if rerr != nil { + fs.mu.Lock() + fs.setWriteErr(rerr) + fs.mu.Unlock() + } + }() + + // Any existing state file will no longer be applicable. We will force write a new one + // at the end, after we release the lock. + os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)) + + var hasLsm bool + var lastTime int64 + var lsm StoreMsg + smb := fs.selectMsgBlock(seq) + if smb != nil { + _, _, err := smb.prevMatching(fwcs, true, seq, &lsm) + if err == nil && lsm.seq == seq { + hasLsm = true + lastTime = lsm.ts + } + if err != nil && err != ErrStoreMsgNotFound { + fs.mu.Unlock() + return err + } + } + + // Reset last so new block doesn't contain truncated sequences/timestamps. + if !hasLsm { + if smb != nil { + lastTime = smb.last.ts + } else { + lastTime = fs.state.LastTime.UnixNano() + } + } + fs.state.LastSeq = seq + fs.state.LastTime = time.Unix(0, lastTime).UTC() + + // Always create a new write block for any tombstones. + // We'll truncate the selected message block as the last step, so can't write tombstones to it. + // If we end up not needing to write tombstones, this block will be cleaned up at the end. + tmb, err := fs.newMsgBlockForWrite() + if err != nil { + fs.mu.Unlock() + return err + } + + // The selected message block needs to be removed if it needs to be fully truncated. + var removeSmb bool + if smb != nil { + removeSmb = atomic.LoadUint64(&smb.first.seq) > seq + } + + // If the selected block is not found or the message was deleted, we'll need to write a tombstone + // at the truncated sequence so we don't roll backward on our last sequence and timestamp. + if !hasLsm || removeSmb { + if err = fs.writeTombstone(seq, lastTime); err != nil { + fs.mu.Unlock() + return err + } + } + + var purged, bytes uint64 + + // Remove any left over msg blocks. + getLastMsgBlock := func() *msgBlock { + // Start at one before last, tmb will be the last most of the time + // unless a new block gets added for tombstones. + for i := len(fs.blks) - 2; i >= 0; i-- { + if mb := fs.blks[i]; mb.index < tmb.index { + return mb + } + } + return nil + } + for mb := getLastMsgBlock(); mb != nil && mb != smb; mb = getLastMsgBlock() { + mb.mu.Lock() + purged += mb.msgs + bytes += mb.bytes + + // We could have tombstones for messages before the truncated sequence. + // Need to store those for blocks we're about to remove. + if tombs := mb.tombsLocked(); len(tombs) > 0 { + // Temporarily unlock while we write tombstones. + mb.mu.Unlock() + for _, tomb := range tombs { + if tomb.seq < seq { + if err = fs.writeTombstone(tomb.seq, tomb.ts); err != nil { + fs.mu.Unlock() + return err + } + } + } + mb.mu.Lock() + } + err = fs.forceRemoveMsgBlock(mb) + mb.mu.Unlock() + if err != nil { + fs.mu.Unlock() + return err + } + } + + hasWrittenTombstones := len(tmb.tombs()) > 0 + if smb != nil { + smb.mu.Lock() + if removeSmb { + purged += smb.msgs + bytes += smb.bytes + + // We could have tombstones for messages before the truncated sequence. + if tombs := smb.tombsLocked(); len(tombs) > 0 { + // Temporarily unlock while we write tombstones. + smb.mu.Unlock() + for _, tomb := range tombs { + if tomb.seq < seq { + if err = fs.writeTombstone(tomb.seq, tomb.ts); err != nil { + fs.mu.Unlock() + return err + } + } + } + smb.mu.Lock() + } + err = fs.forceRemoveMsgBlock(smb) + smb.mu.Unlock() + if err != nil { + fs.mu.Unlock() + return err + } + goto SKIP + } + + // Make sure writeable. + if err := smb.enableForWriting(fs.fip); err != nil { + smb.mu.Unlock() + fs.mu.Unlock() + return err + } + + // Truncate our selected message block. + nmsgs, nbytes, err := smb.truncate(lsm.seq, lsm.ts) + if err != nil { + smb.mu.Unlock() + fs.mu.Unlock() + return fmt.Errorf("smb.truncate: %w", err) + } + // Account for the truncated msgs and bytes. + purged += nmsgs + bytes += nbytes + + // The selected message block is not the last anymore, need to close down resources. + if hasWrittenTombstones { + // Quit our loops. + if smb.qch != nil { + close(smb.qch) + smb.qch = nil + } + smb.closeFDsLockedNoCheck() + smb.recompressOnDiskIfNeeded() + } + smb.mu.Unlock() + } + +SKIP: + // If no tombstones were written, we can remove the block and + // purely rely on the selected block as the last block. + if !hasWrittenTombstones { + fs.lmb = smb + tmb.mu.Lock() + err = fs.forceRemoveMsgBlock(tmb) + tmb.mu.Unlock() + if err != nil { + fs.mu.Unlock() + return err + } + } + + // Reset last. + fs.state.LastSeq = seq + fs.state.LastTime = time.Unix(0, lastTime).UTC() + // Update msgs and bytes. + if purged > fs.state.Msgs { + purged = fs.state.Msgs + } + fs.state.Msgs -= purged + if bytes > fs.state.Bytes { + bytes = fs.state.Bytes + } + fs.state.Bytes -= bytes + + // Reset our subject lookup info. + if err = fs.resetGlobalPerSubjectInfo(); err != nil { + fs.mu.Unlock() + return err + } + + fs.dirty++ + + cb := fs.scb + fs.mu.Unlock() + + // Force a new index.db to be written. + if purged > 0 { + fs.forceWriteFullState() + } + + if cb != nil { + cb(-int64(purged), -int64(bytes), 0, _EMPTY_) + } + + return nil +} + +func (fs *fileStore) lastSeq() uint64 { + fs.mu.RLock() + seq := fs.state.LastSeq + fs.mu.RUnlock() + return seq +} + +// Returns number of msg blks. +func (fs *fileStore) numMsgBlocks() int { + fs.mu.RLock() + defer fs.mu.RUnlock() + return len(fs.blks) +} + +// Will add a new msgBlock. +// Lock should be held. +func (fs *fileStore) addMsgBlock(mb *msgBlock) { + fs.blks = append(fs.blks, mb) + fs.lmb = mb + fs.bim[mb.index] = mb +} + +// Remove from our list of blks. +// Both locks should be held. +func (fs *fileStore) removeMsgBlockFromList(mb *msgBlock) { + // Remove from list. + for i, omb := range fs.blks { + if mb == omb { + fs.dirty++ + blks := append(fs.blks[:i], fs.blks[i+1:]...) + fs.blks = copyMsgBlocks(blks) + if fs.bim != nil { + delete(fs.bim, mb.index) + } + break + } + } +} + +// Removes the msgBlock +// Both locks should be held. +func (fs *fileStore) removeMsgBlock(mb *msgBlock) error { + // Check for us being last message block + lseq, lts := atomic.LoadUint64(&mb.last.seq), mb.last.ts + if mb == fs.lmb { + // Creating a new message write block requires that the lmb lock is not held. + mb.mu.Unlock() + // Write the tombstone to remember since this was last block. + if lmb, err := fs.newMsgBlockForWrite(); err != nil || lmb == nil { + if err != nil { + err = errors.New("lmb missing") + } + // Re-acquire mb lock + mb.mu.Lock() + return err + } else if err = fs.writeTombstone(lseq, lts); err != nil { + // Re-acquire mb lock + mb.mu.Lock() + return err + } + mb.mu.Lock() + } else if lseq == fs.state.LastSeq { + // Need to write a tombstone for the last sequence if we're removing the block containing it. + if err := fs.writeTombstone(lseq, lts); err != nil { + return err + } + } + // Only delete message block after (potentially) writing a tombstone. + // But only if it doesn't contain any tombstones for prior blocks. + if mb.numPriorTombsLocked() > 0 { + return nil + } + return fs.forceRemoveMsgBlock(mb) +} + +// Removes the msgBlock, without writing tombstones to ensure the last sequence is preserved. +// Both locks should be held. +func (fs *fileStore) forceRemoveMsgBlock(mb *msgBlock) error { + if err := mb.dirtyCloseWithRemove(true); err != nil { + return err + } + fs.removeMsgBlockFromList(mb) + return nil +} + +// Purges and removes the msgBlock from the store. +// If agg is set the storage updates for fully removed blocks are aggregated. +// Lock should be held. +func (fs *fileStore) purgeMsgBlock(mb *msgBlock, agg *storeUpdateAgg) error { + mb.mu.Lock() + // Adjust per-subject tracking if present. + if err := mb.ensurePerSubjectInfoLoaded(); err != nil { + mb.mu.Unlock() + return err + } else if mb.fss != nil { + mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool { + subj := bytesToString(bsubj) + for range ss.Msgs { + fs.removePerSubject(subj) + } + return true + }) + } + // Clean up scheduled message metadata if we know this block contained any. + if fs.scheduling != nil && mb.schedules > 0 { + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + mb.mu.Unlock() + return err + } + } + var smv StoreMsg + fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq) + for seq := fseq; seq <= lseq; seq++ { + sm, err := mb.cacheLookupNoCopy(seq, &smv) + if err != nil && err != errDeletedMsg { + mb.mu.Unlock() + return err + } + if sm == nil { + continue + } + if schedule, apiErr := getMessageSchedule(sm.hdr); apiErr == nil && !schedule.IsZero() { + fs.scheduling.remove(seq) + } + } + } + // Update top level accounting. + msgs, bytes := mb.msgs, mb.bytes + if msgs > fs.state.Msgs { + msgs = fs.state.Msgs + } + if bytes > fs.state.Bytes { + bytes = fs.state.Bytes + } + fs.state.Msgs -= msgs + fs.state.Bytes -= bytes + if err := fs.removeMsgBlock(mb); err != nil { + mb.mu.Unlock() + return err + } + mb.tryForceExpireCacheLocked() + mb.finishedWithCache() + mb.mu.Unlock() + if err := fs.selectNextFirst(); err != nil { + return err + } + + if agg != nil { + // Aggregating, so no callback and no need to release fs.mu. + agg.md, agg.bd = agg.md-int64(msgs), agg.bd-int64(bytes) + } else if cb := fs.scb; cb != nil { + // If we have a callback registered, we need to release lock regardless since consumers will recalculate pending. + fs.mu.Unlock() + // Storage updates. + cb(-int64(msgs), -int64(bytes), 0, _EMPTY_) + fs.mu.Lock() + } + return nil +} + +// Called by purge to simply get rid of the cache and close our fds. +// Lock should not be held. +func (mb *msgBlock) dirtyClose() { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.dirtyCloseWithRemove(false) +} + +// Should be called with lock held. +func (mb *msgBlock) dirtyCloseWithRemove(remove bool) error { + if mb == nil { + return nil + } + // Stop cache expiration timer. + if mb.ctmr != nil { + mb.ctmr.Stop() + mb.ctmr = nil + } + // Close cache + mb.clearCacheAndOffset() + // Quit our loops. + if mb.qch != nil { + close(mb.qch) + mb.qch = nil + } + if fd := mb.mfd; fd != nil { + mb.mfd = nil + if err := fd.Close(); err != nil && !os.IsNotExist(err) { + return err + } + } + if remove { + // The block is being destroyed, so mark it closed. + mb.closed = true + // Clear any tracking by subject if we are removing. + mb.fss = nil + if mb.mfn != _EMPTY_ { + if err := os.Remove(mb.mfn); err != nil && !os.IsNotExist(err) { + return err + } + mb.mfn = _EMPTY_ + } + if mb.kfn != _EMPTY_ { + if err := os.Remove(mb.kfn); err != nil && !os.IsNotExist(err) { + return err + } + } + } + return nil +} + +// Remove a seq from the fss and select new first. +// Lock should be held. +func (mb *msgBlock) removeSeqPerSubject(subj string, seq uint64) (uint64, error) { + if err := mb.ensurePerSubjectInfoLoaded(); err != nil { + return 0, err + } + if mb.fss == nil { + return 0, nil + } + bsubj := stringToBytes(subj) + ss, ok := mb.fss.Find(bsubj) + if !ok || ss == nil { + return 0, nil + } + + mb.fs.sdm.removeSeqAndSubject(seq, subj) + if ss.Msgs == 1 { + mb.fss.Delete(bsubj) + return 0, nil + } + + ss.Msgs-- + + // Only one left. + if ss.Msgs == 1 { + if !ss.lastNeedsUpdate && seq != ss.Last { + ss.First = ss.Last + ss.firstNeedsUpdate = false + return 1, nil + } + if !ss.firstNeedsUpdate && seq != ss.First { + ss.Last = ss.First + ss.lastNeedsUpdate = false + return 1, nil + } + } + + // We can lazily calculate the first/last sequence when needed. + ss.firstNeedsUpdate = seq == ss.First || ss.firstNeedsUpdate + ss.lastNeedsUpdate = seq == ss.Last || ss.lastNeedsUpdate + + return ss.Msgs, nil +} + +// Will recalculate the first and/or last sequence for this subject in this block. +// Will avoid slower path message lookups and scan the cache directly instead. +func (mb *msgBlock) recalculateForSubj(subj string, ss *SimpleState) error { + // Need to make sure messages are loaded. + needsCleanup := mb.cache == nil + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + return err + } + } + if needsCleanup { + defer mb.finishedWithCache() + } + + startSlot := int(ss.First - mb.cache.fseq) + if startSlot < 0 { + startSlot = 0 + } + if startSlot >= len(mb.cache.idx) { + ss.First = ss.Last + ss.firstNeedsUpdate = false + ss.lastNeedsUpdate = false + return nil + } + + endSlot := int(ss.Last - mb.cache.fseq) + if endSlot < 0 { + endSlot = 0 + } + if endSlot >= len(mb.cache.idx) || startSlot > endSlot { + return nil + } + + var le = binary.LittleEndian + if ss.firstNeedsUpdate { + // Mark first as updated. + ss.firstNeedsUpdate = false + + fseq := ss.First + 1 + if mbFseq := atomic.LoadUint64(&mb.first.seq); fseq < mbFseq { + fseq = mbFseq + } + for slot := startSlot; slot < len(mb.cache.idx); slot++ { + bi := mb.cache.idx[slot] &^ cbit + if bi == dbit { + // delete marker so skip. + continue + } + li := int(bi) + if li >= len(mb.cache.buf) { + ss.First = ss.Last + // Only need to reset ss.lastNeedsUpdate, ss.firstNeedsUpdate is already reset above. + ss.lastNeedsUpdate = false + return nil + } + buf := mb.cache.buf[li:] + hdr := buf[:msgHdrSize] + slen := int(le.Uint16(hdr[20:])) + if subj == bytesToString(buf[msgHdrSize:msgHdrSize+slen]) { + seq := le.Uint64(hdr[4:]) + if seq < fseq || seq&ebit != 0 || mb.dmap.Exists(seq) { + continue + } + ss.First = seq + if ss.Msgs == 1 { + ss.Last = seq + ss.lastNeedsUpdate = false + return nil + } + // Skip the start slot ahead, if we need to recalculate last we can stop early. + startSlot = slot + break + } + } + } + if ss.lastNeedsUpdate { + // Mark last as updated. + ss.lastNeedsUpdate = false + + lseq := ss.Last - 1 + if mbLseq := atomic.LoadUint64(&mb.last.seq); lseq > mbLseq { + lseq = mbLseq + } + for slot := endSlot; slot >= startSlot; slot-- { + bi := mb.cache.idx[slot] &^ cbit + if bi == dbit { + // delete marker so skip. + continue + } + li := int(bi) + if li >= len(mb.cache.buf) { + // Can't overwrite ss.Last, just skip. + return nil + } + buf := mb.cache.buf[li:] + hdr := buf[:msgHdrSize] + slen := int(le.Uint16(hdr[20:])) + if subj == bytesToString(buf[msgHdrSize:msgHdrSize+slen]) { + seq := le.Uint64(hdr[4:]) + if seq > lseq || seq&ebit != 0 || mb.dmap.Exists(seq) { + continue + } + // Sequence should never be lower, but guard against it nonetheless. + if seq < ss.First { + seq = ss.First + } + ss.Last = seq + if ss.Msgs == 1 { + ss.First = seq + ss.firstNeedsUpdate = false + } + return nil + } + } + } + return nil +} + +// Lock should be held. +func (fs *fileStore) resetGlobalPerSubjectInfo() error { + // Clear any global subject state. + fs.psim, fs.tsl = fs.psim.Empty(), 0 + if fs.noTrackSubjects() { + return nil + } + for _, mb := range fs.blks { + if err := fs.populateGlobalPerSubjectInfo(mb); err != nil { + return err + } + } + return nil +} + +// Lock should be held. +func (mb *msgBlock) resetPerSubjectInfo() error { + mb.fss = nil + return mb.generatePerSubjectInfo() +} + +// generatePerSubjectInfo will generate the per subject info via the raw msg block. +// Lock should be held. +func (mb *msgBlock) generatePerSubjectInfo() error { + // Check if this mb is empty. This can happen when its the last one and we are holding onto it for seq and timestamp info. + if mb.msgs == 0 { + return nil + } + + needsCleanup := mb.cache == nil + if mb.cacheNotLoaded() { + if err := mb.loadMsgsWithLock(); err != nil { + if needsCleanup { + mb.finishedWithCache() + } + return err + } + } + if needsCleanup { + defer mb.finishedWithCache() + } + // indexCacheBuf can produce fss now, so if non-nil we are good. + if mb.fss != nil { + return nil + } + + // Create new one regardless. + mb.fss = mb.fss.Empty() + + var smv StoreMsg + fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq) + for seq := fseq; seq <= lseq; seq++ { + if mb.dmap.Exists(seq) { + // Optimisation to avoid calling cacheLookup which hits time.Now(). + // It gets set later on if the fss is non-empty anyway. + continue + } + sm, err := mb.cacheLookupNoCopy(seq, &smv) + if err != nil { + // Since we are walking by sequence we can ignore some errors that are benign to rebuilding our state. + if err == ErrStoreMsgNotFound || err == errDeletedMsg { + continue + } + if err == errNoCache { + return nil + } + // Clear partially built fss so callers don't operate on incomplete state. + mb.fss = nil + mb.clearCacheAndOffset() + return err + } + if sm != nil && len(sm.subj) > 0 { + if ss, ok := mb.fss.Find(stringToBytes(sm.subj)); ok && ss != nil { + ss.Msgs++ + ss.Last = seq + ss.lastNeedsUpdate = false + } else { + mb.fss.Insert(stringToBytes(sm.subj), SimpleState{Msgs: 1, First: seq, Last: seq}) + } + } + } + + if mb.fss.Size() > 0 { + // Make sure we run the cache expire timer. + mb.llts = ats.AccessTime() + // Mark fss activity same as load time. + mb.lsts = mb.llts + mb.startCacheExpireTimer() + } + return nil +} + +// Helper to make sure fss loaded if we are tracking. +// Lock should be held +func (mb *msgBlock) ensurePerSubjectInfoLoaded() error { + if mb.fss != nil || mb.noTrack { + if mb.fss != nil { + // Mark fss activity. + mb.lsts = ats.AccessTime() + } + return nil + } + if mb.msgs == 0 { + mb.fss = stree.NewSubjectTree[SimpleState]() + return nil + } + return mb.generatePerSubjectInfo() +} + +// Called on recovery to populate the global psim state. +// Lock should be held. +func (fs *fileStore) populateGlobalPerSubjectInfo(mb *msgBlock) error { + mb.mu.Lock() + defer mb.mu.Unlock() + + if err := mb.ensurePerSubjectInfoLoaded(); err != nil { + return err + } + + // Now populate psim. + mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool { + if len(bsubj) > 0 { + if info, ok := fs.psim.Find(bsubj); ok { + info.total += ss.Msgs + if mb.index > info.lblk { + info.lblk = mb.index + } + } else { + fs.psim.Insert(bsubj, psi{total: ss.Msgs, fblk: mb.index, lblk: mb.index}) + fs.tsl += len(bsubj) + } + } + return true + }) + return nil +} + +// Calls os.RemoveAll on the given `dir` directory, but if an error occurs, +// retries up to one second. If that still fails, returns the last error +// that os.RemoveAll returned. +func removeAllWithRetry(dios *diskIOSemaphore, dir string) error { + dios.acquire() + err := os.RemoveAll(dir) + dios.release() + if err == nil { + return nil + } + ttl := time.Now().Add(time.Second) + for time.Now().Before(ttl) { + time.Sleep(10 * time.Millisecond) + dios.acquire() + err = os.RemoveAll(dir) + dios.release() + if err == nil { + return nil + } + } + return err +} + +// Close the message block. +func (mb *msgBlock) close(sync bool) { + if mb == nil { + return + } + mb.mu.Lock() + defer mb.mu.Unlock() + + if mb.closed { + return + } + + // Stop cache expiration timer. + if mb.ctmr != nil { + mb.ctmr.Stop() + mb.ctmr = nil + } + + // Clear fss. + mb.fss = nil + + // Flush pending writes. + mb.flushPendingMsgsLocked() + // Close cache + mb.clearCacheAndOffset() + // Quit our loops. + if mb.qch != nil { + close(mb.qch) + mb.qch = nil + } + if mb.mfd != nil { + if sync { + mb.mfd.Sync() + } + mb.mfd.Close() + } + mb.mfd = nil + // Mark as closed. + mb.closed = true +} + +func (fs *fileStore) closeAllMsgBlocks(sync bool) { + for _, mb := range fs.blks { + mb.close(sync) + } +} + +func (fs *fileStore) Delete(inline bool) error { + if fs.isClosed() { + // Always attempt to remove since we could have been closed beforehand. + os.RemoveAll(fs.fcfg.StoreDir) + // Since we did remove, if we did have anything remaining make sure to + // call into any storage updates that had been registered. + fs.mu.Lock() + cb, msgs, bytes := fs.scb, int64(fs.state.Msgs), int64(fs.state.Bytes) + // Guard against double accounting if called twice. + fs.state.Msgs, fs.state.Bytes = 0, 0 + fs.mu.Unlock() + if msgs > 0 && cb != nil { + cb(-msgs, -bytes, 0, _EMPTY_) + } + return ErrStoreClosed + } + + pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir) + if _, err := os.Stat(pdir); err == nil { + _ = os.RemoveAll(pdir) + } + + // Quickly close all blocks and simulate a purge w/o overhead an new write block. + fs.mu.Lock() + for _, mb := range fs.blks { + mb.dirtyClose() + } + dmsgs := fs.state.Msgs + dbytes := int64(fs.state.Bytes) + fs.state.Msgs, fs.state.Bytes = 0, 0 + fs.blks = nil + cb := fs.scb + fs.mu.Unlock() + + if cb != nil { + cb(-int64(dmsgs), -dbytes, 0, _EMPTY_) + } + + if err := fs.stop(true, false); err != nil { + return err + } + + // Make sure we will not try to recover if killed before removal below completes. + if err := os.Remove(filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFile)); err != nil { + return err + } + // Now move into different directory with "." prefix. + ndir := filepath.Join(filepath.Dir(fs.fcfg.StoreDir), tsep+filepath.Base(fs.fcfg.StoreDir)) + if err := os.Rename(fs.fcfg.StoreDir, ndir); err != nil { + return err + } + // Do this in separate Go routine in case lots of blocks. + // Purge above protects us as does the removal of meta artifacts above. + if inline { + if err := removeAllWithRetry(fs.dios, ndir); err != nil { + return err + } + } else { + go removeAllWithRetry(fs.dios, ndir) + } + return nil +} + +// Lock should be held. +func (fs *fileStore) setSyncTimer() { + if fs.syncTmr != nil { + fs.syncTmr.Reset(fs.fcfg.SyncInterval) + } else { + // First time this fires will be between SyncInterval/2 and SyncInterval, + // so that different stores are spread out, rather than having many of + // them trying to all sync at once, causing blips and contending dios. + start := (fs.fcfg.SyncInterval / 2) + (time.Duration(mrand.Int64N(int64(fs.fcfg.SyncInterval / 2)))) + fs.syncTmr = time.AfterFunc(start, fs.syncBlocks) + } +} + +// Lock should be held. +func (fs *fileStore) cancelSyncTimer() { + if fs.syncTmr != nil { + fs.syncTmr.Stop() + fs.syncTmr = nil + } +} + +// The full state file is versioned. +// - 0x1: original binary index.db format +// - 0x2: adds support for TTL count field after num deleted +const ( + fullStateMagic = uint8(11) + fullStateMinVersion = uint8(1) // What is the minimum version we know how to parse? + fullStateVersion = uint8(4) // What is the current version written out to index.db? +) + +// This go routine periodically writes out our full stream state index. +func (fs *fileStore) flushStreamStateLoop(qch, done chan struct{}) { + // Signal we are done on exit. + defer close(done) + + // Make sure we do not try to write these out too fast. + // Spread these out for large numbers on a server restart. + const writeThreshold = 2 * time.Minute + writeJitter := time.Duration(mrand.Int64N(int64(30 * time.Second))) + t := time.NewTicker(writeThreshold + writeJitter) + defer t.Stop() + + for { + select { + case <-t.C: + err := fs.writeFullState() + if isPermissionError(err) && fs.srv != nil { + fs.warn("File system permission denied when flushing stream state, disabling JetStream: %v", err) + // messages in block cache could be lost in the worst case. + // In the clustered mode it is very highly unlikely as a result of replication. + fs.srv.ShutdownJetStream() + return + } + + case <-qch: + return + } + } +} + +// Helper since unixnano of zero time undefined. +func timestampNormalized(t time.Time) int64 { + if t.IsZero() { + return 0 + } + return t.UnixNano() +} + +// writeFullState will proceed to write the full meta state iff not complex and time consuming. +// Since this is for quick recovery it is optional and should not block/stall normal operations. +func (fs *fileStore) writeFullState() error { + return fs._writeFullState(false) +} + +// forceWriteFullState will proceed to write the full meta state. +func (fs *fileStore) forceWriteFullState() error { + return fs._writeFullState(true) +} + +// This will write the full binary state for the stream. +// This plus everything new since last hash will be the total recovered state. +// This state dump will have the following. +// 1. Stream summary - Msgs, Bytes, First and Last (Sequence and Timestamp) +// 2. PSIM - Per Subject Index Map - Tracks first and last blocks with subjects present. +// 3. MBs - Index, Bytes, First and Last Sequence and Timestamps, and the deleted map (avl.seqset). +// 4. Last block index and hash of record inclusive to this stream state. +func (fs *fileStore) _writeFullState(force bool) error { + if fs.isClosed() { + return nil + } + + // If we aren't forcing an update then only queue this up if we aren't already + // running. This means we can keep waiting on shutdown if needed but not build up + // lots of waiting goroutines in a bad timer case. + if fs.wfsrun.Add(1) > 1 && !force { + fs.wfsrun.Add(-1) + return nil + } + defer fs.wfsrun.Add(-1) + + // Only allow one _writeFullState to take place at a time, otherwise we can + // have multiple goroutines trying to write the same file after we've released + // the store lock. + fs.wfsmu.Lock() + defer fs.wfsmu.Unlock() + + start := time.Now() + fs.mu.RLock() + if fs.dirty == 0 { + fs.mu.RUnlock() + return nil + } + + // Configure encryption if needed. + if fs.prf != nil { + // Re-acquire temporarily as write lock to set up AEK. + fs.mu.RUnlock() + fs.mu.Lock() + err := fs.setupAEK() + fs.mu.Unlock() + if err != nil { + return err + } + fs.mu.RLock() + } + + // For calculating size and checking time costs for non forced calls. + numSubjects := fs.numSubjects() + + // If we are not being forced to write out our state, check the complexity for time costs as to not + // block or stall normal operations. + // We will base off of number of subjects and interior deletes. A very large number of msg blocks could also + // be used, but for next server version will redo all meta handling to be disk based. So this is temporary. + if !force { + // Calculate interior deletes. + var numDeleted int + if fs.state.LastSeq > fs.state.FirstSeq { + numDeleted = int((fs.state.LastSeq - fs.state.FirstSeq + 1) - fs.state.Msgs) + } + if numSubjects > highCardinalityThreshold || numDeleted > highCardinalityThreshold { + fs.mu.RUnlock() + return errStateTooBig + } + } + + // We track this through subsequent runs to get an avg per blk used for subsequent runs. + avgDmapLen := fs.wfsadml + // If first time through could be 0 + if avgDmapLen == 0 && ((fs.state.LastSeq-fs.state.FirstSeq+1)-fs.state.Msgs) > 0 { + avgDmapLen = 1024 + } + + // Calculate and estimate of the uper bound on the size to avoid multiple allocations. + sz := hdrLen + // Magic and Version + (binary.MaxVarintLen64 * 6) + // FS data + binary.MaxVarintLen64 + fs.tsl + // NumSubjects + total subject length + numSubjects*(binary.MaxVarintLen64*4) + // psi record + binary.MaxVarintLen64 + // Num blocks. + len(fs.blks)*((binary.MaxVarintLen64*8)+avgDmapLen) + // msg blocks, avgDmapLen is est for dmaps + binary.MaxVarintLen64 + 8 + 8 // last index + record checksum + full state checksum + + // Do 4k on stack if possible. + const ssz = 4 * 1024 + var buf []byte + + if sz <= ssz { + var _buf [ssz]byte + buf, sz = _buf[0:hdrLen:ssz], ssz + } else { + buf = make([]byte, hdrLen, sz) + } + + buf[0], buf[1] = fullStateMagic, fullStateVersion + buf = binary.AppendUvarint(buf, fs.state.Msgs) + buf = binary.AppendUvarint(buf, fs.state.Bytes) + buf = binary.AppendUvarint(buf, fs.state.FirstSeq) + buf = binary.AppendVarint(buf, timestampNormalized(fs.state.FirstTime)) + buf = binary.AppendUvarint(buf, fs.state.LastSeq) + buf = binary.AppendVarint(buf, timestampNormalized(fs.state.LastTime)) + + // Do per subject information map if applicable. + buf = binary.AppendUvarint(buf, uint64(numSubjects)) + if numSubjects > 0 { + fs.psim.Match([]byte(fwcs), func(subj []byte, psi *psi) { + buf = binary.AppendUvarint(buf, uint64(len(subj))) + buf = append(buf, subj...) + buf = binary.AppendUvarint(buf, psi.total) + buf = binary.AppendUvarint(buf, uint64(psi.fblk)) + buf = binary.AppendUvarint(buf, uint64(psi.lblk)) + }) + } + + // Now walk all blocks and write out first and last and optional dmap encoding. + var lbi uint32 + var lchk [8]byte + + nb := len(fs.blks) + buf = binary.AppendUvarint(buf, uint64(nb)) + + // Use basetime to save some space. + baseTime := timestampNormalized(fs.state.FirstTime) + var scratch [8 * 1024]byte + + // Track the state as represented by the mbs. + var mstate StreamState + + var dmapTotalLen int + for _, mb := range fs.blks { + mb.mu.RLock() + buf = binary.AppendUvarint(buf, uint64(mb.index)) + buf = binary.AppendUvarint(buf, mb.bytes) + buf = binary.AppendUvarint(buf, atomic.LoadUint64(&mb.first.seq)) + buf = binary.AppendVarint(buf, mb.first.ts-baseTime) + buf = binary.AppendUvarint(buf, atomic.LoadUint64(&mb.last.seq)) + buf = binary.AppendVarint(buf, mb.last.ts-baseTime) + + numDeleted := mb.dmap.Size() + buf = binary.AppendUvarint(buf, uint64(numDeleted)) + buf = binary.AppendUvarint(buf, mb.ttls) // Field is new in version 2 + buf = binary.AppendUvarint(buf, mb.schedules) // Field is new in version 3 + if numDeleted > 0 { + dmap := mb.dmap.Encode(scratch[:0]) + dmapTotalLen += len(dmap) + buf = append(buf, dmap...) + } + // If this is the last one grab the last checksum and the block index, e.g. 22.blk, 22 is the block index. + // We use this to quickly open this file on recovery. + if mb == fs.lmb { + lbi = mb.index + mb.ensureLastChecksumLoaded() + copy(lchk[0:], mb.lchk[:]) + } + updateTrackingState(&mstate, mb) + mb.mu.RUnlock() + } + if dmapTotalLen > 0 { + fs.wfsadml = dmapTotalLen / len(fs.blks) + } + + // Place block index and hash onto the end. + buf = binary.AppendUvarint(buf, uint64(lbi)) + buf = append(buf, lchk[:]...) + + // Encrypt if needed. + if fs.prf != nil { + nonce := make([]byte, fs.aek.NonceSize(), fs.aek.NonceSize()+len(buf)+fs.aek.Overhead()) + if n, err := rand.Read(nonce); err != nil { + fs.mu.RUnlock() + return err + } else if n != len(nonce) { + fs.mu.RUnlock() + return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce)) + } + buf = fs.aek.Seal(nonce, nonce, buf, nil) + } + + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile) + + // Need to have our own hasher here, as under a read lock we can't mutate the + // fs.hh safely. + key := sha256.Sum256([]byte(fs.cfg.Name)) + hh, _ := highwayhash.NewDigest64(key[:]) + hh.Write(buf) + buf = hh.Sum(buf) + + // Snapshot prior dirty count. + priorDirty := fs.dirty + + statesEqual := trackingStatesEqual(&fs.state, &mstate) + // Release lock. + fs.mu.RUnlock() + + // Check consistency here. + if !statesEqual { + fs.warn("Stream state encountered internal inconsistency on write") + // Rebuild our fs state from the mb state. + fs.rebuildState(nil) + return errCorruptState + } + + if cap(buf) > sz { + fs.debug("WriteFullState reallocated from %d to %d", sz, cap(buf)) + } + + // Only warn about construction time since file write not holding any locks. + if took := time.Since(start); took > time.Minute { + fs.warn("WriteFullState took %v (%d bytes)", took.Round(time.Millisecond), len(buf)) + } + + // Write our update index.db + // Protect with dios. + fs.dios.acquire() + err := os.WriteFile(fn, buf, defaultFilePerms) + // if file system is not writable isPermissionError is set to true + fs.dios.release() + if err != nil { + return err + } + + // Update dirty if successful. + fs.mu.Lock() + fs.dirty -= priorDirty + fs.mu.Unlock() + + // Attempt to write other index files, an error in one should not prevent the other from being written. + ttlErr := fs.writeTTLState() + schedErr := fs.writeMsgSchedulingState() + sourcesErr := fs.writeSourcesState() + if ttlErr != nil { + return ttlErr + } else if schedErr != nil { + return schedErr + } else if sourcesErr != nil { + return sourcesErr + } + return nil +} + +func (fs *fileStore) writeTTLState() error { + fs.mu.RLock() + if fs.ttls == nil { + fs.mu.RUnlock() + return nil + } + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, ttlStreamStateFile) + // Must be lseq+1 to identify up to which sequence the TTLs are valid. + buf := fs.ttls.Encode(fs.state.LastSeq + 1) + fs.mu.RUnlock() + + return fs.writeFileWithOptionalSync(fn, buf, defaultFilePerms) +} + +func (fs *fileStore) writeMsgSchedulingState() error { + fs.mu.RLock() + if fs.scheduling == nil { + fs.mu.RUnlock() + return nil + } + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, msgSchedulingStreamStateFile) + // Must be lseq+1 to identify up to which sequence the schedules are valid. + buf := fs.scheduling.encode(fs.state.LastSeq + 1) + fs.mu.RUnlock() + + return fs.writeFileWithOptionalSync(fn, buf, defaultFilePerms) +} + +const sourcesHeaderLen = 17 // 1 byte magic + 2x uint64s + +func (fs *fileStore) sourcesStatePath() string { + return filepath.Join(fs.fcfg.StoreDir, sourcesStreamStateFile) +} + +func (fs *fileStore) writeSourcesState() error { + fs.mu.RLock() + if len(fs.sources) == 0 { + fs.mu.RUnlock() + return nil + } + fn := fs.sourcesStatePath() + + // Must be lseq+1 to identify up to which sequence the sources are valid. + highSeq := fs.state.LastSeq + 1 + + count := uint64(len(fs.sources)) + var sz uint64 + for source, ss := range fs.sources { + // Length prefix + source bytes + seq varint + + // length prefix + identity bytes. + sz += uint64(uvarintLen(uint64(len(source)))) + uint64(len(source)) + binary.MaxVarintLen64 + + uint64(uvarintLen(uint64(len(ss.Ident)))) + uint64(len(ss.Ident)) + } + buf := make([]byte, 0, sourcesHeaderLen+sz) + buf = append(buf, 1) // Magic version + buf = binary.LittleEndian.AppendUint64(buf, count) // Entry count + buf = binary.LittleEndian.AppendUint64(buf, highSeq) // Stamp + for source, ss := range fs.sources { + buf = binary.AppendUvarint(buf, uint64(len(source))) + buf = append(buf, source...) + buf = binary.AppendUvarint(buf, ss.Seq) + buf = binary.AppendUvarint(buf, uint64(len(ss.Ident))) + buf = append(buf, ss.Ident...) + } + fs.mu.RUnlock() + + return fs.writeFileWithOptionalSync(fn, buf, defaultFilePerms) +} + +// Lock should be held. +func (fs *fileStore) recoverSourcesState() uint64 { + // See if we have a sources index file. + fs.dios.acquire() + fn := fs.sourcesStatePath() + buf, err := os.ReadFile(fn) + fs.dios.release() + + if err != nil && !os.IsNotExist(err) { + fs.warn("Recovering sources state from index errored: %v", err) + } + + var sourcesSeq uint64 + if err == nil { + sourcesSeq, err = fs.decodeSourcesState(buf) + if err != nil { + fs.warn("Error decoding sources state: %s", err) + // Remove the file, and reset collected state (if any). + _ = os.Remove(fn) + fs.sources = nil + } + } + + if sourcesSeq < fs.state.FirstSeq { + sourcesSeq = fs.state.FirstSeq + } + return sourcesSeq +} + +var errSourcesInvalidVersion = errors.New("sources: encoded version not known") + +func (fs *fileStore) decodeSourcesState(b []byte) (uint64, error) { + if len(b) < sourcesHeaderLen { + return 0, io.ErrShortBuffer + } + if b[0] != 1 { + return 0, errSourcesInvalidVersion + } + + count := binary.LittleEndian.Uint64(b[1:]) + stamp := binary.LittleEndian.Uint64(b[9:]) + if count > 0 && fs.sources == nil { + fs.sources = make(map[string]*StreamSourceState) + } + b = b[sourcesHeaderLen:] + readStr := func() (string, bool) { + n, vn := binary.Uvarint(b) + if vn <= 0 || uint64(len(b)-vn) < n { + return _EMPTY_, false + } + b = b[vn:] + s := string(b[:n]) + b = b[n:] + return s, true + } + for i := uint64(0); i < count; i++ { + source, ok := readStr() + if !ok { + return 0, io.ErrUnexpectedEOF + } + seq, vn := binary.Uvarint(b) + if vn <= 0 { + return 0, io.ErrUnexpectedEOF + } + b = b[vn:] + ident, ok := readStr() + if !ok { + return 0, io.ErrUnexpectedEOF + } + fs.sources[source] = &StreamSourceState{Seq: seq, Ident: ident} + } + return stamp, nil +} + +// recoverSourcesBackwardScan recovers fs.sources by scanning the stream backwards. +// Lock should be held. +func (fs *fileStore) recoverSourcesBackwardScan(stopSeq uint64, sources map[string]*StreamSource) error { + 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, sseq uint64, ident string) { + // Only consider sources that are configured and not yet found. The backward + // scan visits the highest matching sequence first, so the first hit per source + // is the latest and overwrites any older decoded value. + if _, ok := sources[iName]; !ok { + return + } + if fs.sources == nil { + fs.sources = make(map[string]*StreamSourceState, 1) + } + fs.sources[iName] = &StreamSourceState{Seq: sseq, Ident: ident} + delete(sources, iName) + refreshSublist() + } + + var smv StoreMsg + for last := fs.state.LastSeq; ; { + sm, seq, err := fs.loadPrevMsgMultiLocked(sl, last, &smv) + if err == ErrStoreEOF { + // Done. + break + } + if err != nil { + return err + } + // Stop if we've gone below the point where decoded state is still valid. + if seq < stopSeq { + break + } + if len(sm.hdr) > 0 { + if ss := sliceHeader(JSStreamSource, sm.hdr); len(ss) > 0 { + streamName, iName, sseq, ident := streamAndSeq(bytesToString(ss)) + if iName == _EMPTY_ { + // Pre-2.10 message header means it's a match for any source using that stream name. + // Such headers carry no identity, so it is left unset. + for _, ssi := range fs.cfg.Sources { + if streamName == ssi.Name || (ssi.External != nil && streamName == ssi.Name+":"+getHash(ssi.External.ApiPrefix)) { + update(ssi.composeIName(), sseq, _EMPTY_) + } + } + } else { + update(iName, sseq, ident) + } + } + } + // Done once every source has been resolved, or we've reached the floor. + if len(sources) == 0 || seq <= stopSeq { + break + } + last = seq - 1 + } + return nil +} + +// Stop the current filestore. +func (fs *fileStore) Stop() error { + return fs.stop(false, true) +} + +// Stop the current filestore. +func (fs *fileStore) stop(delete, writeState bool) error { + if fs.isClosed() { + return ErrStoreClosed + } + + fs.mu.Lock() + if fs.closing { + fs.mu.Unlock() + return ErrStoreClosed + } + + // Mark as closing. Do before releasing the lock to wait on the state flush loop + // so we don't end up with this function running more than once. + fs.closing = true + + // Release the state flusher loop. + if fs.qch != nil { + close(fs.qch) + fs.qch = nil + } + + if writeState { + // Wait for the state flush loop to exit. + fsld := fs.fsld + fs.mu.Unlock() + <-fsld + fs.mu.Lock() + + fs.checkAndFlushLastBlock() + } + fs.closeAllMsgBlocks(false) + + fs.cancelSyncTimer() + fs.cancelAgeChk() + + if writeState { + // Write full state if needed. If not dirty this is a no-op. + fs.mu.Unlock() + fs.forceWriteFullState() + fs.mu.Lock() + } + + // Mark as closed. Last message block needs to be cleared after + // writeFullState has completed. + fs.closed.Store(true) + fs.lmb = nil + + // We should update the upper usage layer on a stop. + cb, bytes := fs.scb, int64(fs.state.Bytes) + fs.mu.Unlock() + + // Wait for any in-flight syncBlocks to finish now that we are marked + // closed. This ensures a Delete can't remove or rename directories from + // underneath a running sync. Any sync starting after this sees closed and + // returns immediately. Must not hold fs.mu here, syncBlocks acquires + // syncMu before fs.mu. + fs.syncMu.Lock() + fs.syncMu.Unlock() //nolint:staticcheck // Only waiting for in-flight syncBlocks. + + fs.cmu.Lock() + var _cfs [256]ConsumerStore + cfs := append(_cfs[:0], fs.cfs...) + fs.cfs = nil + fs.cmu.Unlock() + + for _, o := range cfs { + if delete { + o.StreamDelete() + } else { + o.Stop() + } + } + + if bytes > 0 && cb != nil { + cb(0, -bytes, 0, _EMPTY_) + } + + // Unregister from the access time service. + ats.Unregister() + + return nil +} + +const errFile = "errors.txt" + +// Stream our snapshot through S2 compression and tar. +func (fs *fileStore) streamSnapshot(w io.WriteCloser, includeConsumers bool, errCh chan error) { + defer close(errCh) + defer w.Close() + + enc := s2.NewWriter(w) + defer enc.Close() + + tw := tar.NewWriter(enc) + defer tw.Close() + + defer func() { + fs.mu.Lock() + fs.sips-- + fs.mu.Unlock() + }() + + modTime := time.Now().UTC() + + writeFile := func(name string, buf []byte) error { + hdr := &tar.Header{ + Name: name, + Mode: 0600, + ModTime: modTime, + Uname: "nats", + Gname: "nats", + Size: int64(len(buf)), + Format: tar.FormatPAX, + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err := tw.Write(buf); err != nil { + return err + } + return nil + } + + writeErr := func(err error) { + writeFile(errFile, []byte(err.Error())) + errCh <- err + } + + fs.mu.Lock() + blks := fs.blks + // Grab our general meta data. + // We do this now instead of pulling from files since they could be encrypted. + meta, err := json.Marshal(fs.cfg) + if err != nil { + fs.mu.Unlock() + writeErr(fmt.Errorf("could not gather stream meta file: %w", err)) + return + } + hh := fs.hh + hh.Reset() + hh.Write(meta) + var hb [highwayhash.Size64]byte + sum := []byte(hex.EncodeToString(fs.hh.Sum(hb[:0]))) + fs.mu.Unlock() + + // Meta first. + if writeFile(JetStreamMetaFile, meta) != nil { + return + } + if writeFile(JetStreamMetaFileSum, sum) != nil { + return + } + + // Can't use join path here, tar only recognizes relative paths with forward slashes. + msgPre := msgDir + "/" + var bbuf []byte + + // Now do messages themselves. + for _, mb := range blks { + if mb.pendingWriteSize() > 0 { + mb.flushPendingMsgs() + } + mb.mu.Lock() + // We could stream but don't want to hold the lock and prevent changes, so just read in and + // release the lock for now. + bbuf, err = mb.loadBlock(bbuf) + if err != nil { + mb.mu.Unlock() + writeErr(fmt.Errorf("could not read message block [%d]: %w", mb.index, err)) + return + } + // Check for encryption. + if mb.bek != nil && len(bbuf) > 0 { + rbek, err := genBlockEncryptionKey(fs.fcfg.Cipher, mb.seed, mb.nonce) + if err != nil { + mb.mu.Unlock() + writeErr(fmt.Errorf("could not create encryption key for message block [%d]: %w", mb.index, err)) + return + } + rbek.XORKeyStream(bbuf, bbuf) + } + // Check for compression. + if bbuf, err = mb.decompressIfNeeded(bbuf); err != nil { + mb.mu.Unlock() + writeErr(fmt.Errorf("could not decompress message block [%d]: %w", mb.index, err)) + return + } + mb.mu.Unlock() + + // Do this one unlocked. + if writeFile(msgPre+fmt.Sprintf(blkScan, mb.index), bbuf) != nil { + return + } + } + + // Do index.db last. We will force a write as well. + // Write out full state as well before proceeding. + if err := fs.forceWriteFullState(); err == nil { + const minLen = 32 + sfn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile) + if buf, err := os.ReadFile(sfn); err == nil && len(buf) >= minLen { + fs.mu.Lock() + if fs.aek != nil { + ns := fs.aek.NonceSize() + buf, err = fs.aek.Open(nil, buf[:ns], buf[ns:len(buf)-highwayhash.Size64], nil) + if err == nil { + // Redo hash checksum at end on plaintext. + hh.Reset() + hh.Write(buf) + buf = fs.hh.Sum(buf) + } + } + fs.mu.Unlock() + if err == nil && writeFile(msgPre+streamStreamStateFile, buf) != nil { + return + } + } + } + + // Bail if no consumers requested. + if !includeConsumers { + return + } + + // Do consumers' state last. + fs.cmu.RLock() + cfs := fs.cfs + fs.cmu.RUnlock() + + for _, cs := range cfs { + o, ok := cs.(*consumerFileStore) + if !ok { + continue + } + o.mu.Lock() + // Grab our general meta data. + // We do this now instead of pulling from files since they could be encrypted. + meta, err := json.Marshal(o.cfg) + if err != nil { + o.mu.Unlock() + writeErr(fmt.Errorf("could not gather consumer meta file for %q: %w", o.name, err)) + return + } + o.hh.Reset() + o.hh.Write(meta) + var hb [highwayhash.Size64]byte + sum := []byte(hex.EncodeToString(o.hh.Sum(hb[:0]))) + + // We can have the running state directly encoded now. + state, err := o.encodeState() + if err != nil { + o.mu.Unlock() + writeErr(fmt.Errorf("could not encode consumer state for %q: %w", o.name, err)) + return + } + odirPre := filepath.Join(consumerDir, o.name) + o.mu.Unlock() + + // Write all the consumer files. + if writeFile(filepath.Join(odirPre, JetStreamMetaFile), meta) != nil { + return + } + if writeFile(filepath.Join(odirPre, JetStreamMetaFileSum), sum) != nil { + return + } + writeFile(filepath.Join(odirPre, consumerState), state) + } +} + +// Create a snapshot of this stream and its consumer's state along with messages. +func (fs *fileStore) Snapshot(deadline time.Duration, checkMsgs, includeConsumers bool) (*SnapshotResult, error) { + if fs.isClosed() { + return nil, ErrStoreClosed + } + + fs.mu.Lock() + + // Only allow one at a time. + if fs.sips > 0 { + fs.mu.Unlock() + return nil, ErrStoreSnapshotInProgress + } + // Mark us as snapshotting + fs.sips += 1 + fs.mu.Unlock() + + if checkMsgs { + ld, err := fs.checkMsgs() + clearSips := func() { + fs.mu.Lock() + fs.sips-- + fs.mu.Unlock() + } + if err != nil { + clearSips() + return nil, fmt.Errorf("snapshot check failed: %w", err) + } + if ld != nil && len(ld.Msgs) > 0 { + clearSips() + return nil, fmt.Errorf("snapshot check detected %d bad messages", len(ld.Msgs)) + } + } + + pr, pw := net.Pipe() + + // Set a write deadline here to protect ourselves. + if deadline > 0 { + pw.SetWriteDeadline(time.Now().Add(deadline)) + } + + // We can add to our stream while snapshotting but not "user" delete anything. + var state StreamState + fs.FastState(&state) + + // Stream in separate Go routine. + errCh := make(chan error, 1) + go fs.streamSnapshot(pw, includeConsumers, errCh) + + return &SnapshotResult{pr, state, errCh}, nil +} + +// Helper to return the config. +func (fs *fileStore) fileStoreConfig() FileStoreConfig { + fs.mu.RLock() + defer fs.mu.RUnlock() + return fs.fcfg +} + +// Read lock all existing message blocks. +// Lock held on entry. +func (fs *fileStore) readLockAllMsgBlocks() { + for _, mb := range fs.blks { + mb.mu.RLock() + } +} + +// Read unlock all existing message blocks. +// Lock held on entry. +func (fs *fileStore) readUnlockAllMsgBlocks() { + for _, mb := range fs.blks { + mb.mu.RUnlock() + } +} + +// Binary encoded state snapshot, >= v2.10 server. +func (fs *fileStore) EncodedStreamState(failed uint64, withSources bool) ([]byte, error) { + fs.mu.RLock() + defer fs.mu.RUnlock() + + withSources = withSources && len(fs.sources) > 0 + version := streamStateVersion + + // Calculate deleted. + var numDeleted int64 + if fs.state.LastSeq > fs.state.FirstSeq { + numDeleted = int64(fs.state.LastSeq-fs.state.FirstSeq+1) - int64(fs.state.Msgs) + if numDeleted < 0 { + numDeleted = 0 + } + } + + // Encoded is Msgs, Bytes, FirstSeq, LastSeq, Failed, NumDeleted and optional DeletedBlocks. + // Calculate the exact encoded size up front so the buffer is allocated once. + total := hdrLen + uvarintLen(fs.state.Msgs) + uvarintLen(fs.state.Bytes) + + uvarintLen(fs.state.FirstSeq) + uvarintLen(fs.state.LastSeq) + + uvarintLen(failed) + uvarintLen(uint64(numDeleted)) + + var numSources uint64 + var sourcesLen int + if withSources { + for name, ss := range fs.sources { + if ss.Seq == 0 { + continue + } + numSources++ + // Length prefix + source bytes + seq varint + + // length prefix + identity bytes. + sourcesLen += uvarintLen(uint64(len(name))) + len(name) + uvarintLen(ss.Seq) + + uvarintLen(uint64(len(ss.Ident))) + len(ss.Ident) + } + // Nothing observed yet, then there is nothing to carry. + if withSources = numSources > 0; withSources { + total += uvarintLen(numSources) + sourcesLen + version = streamStateVersionSources + } + } + + var dbs DeleteBlocks + if numDeleted > 0 { + fs.readLockAllMsgBlocks() + defer fs.readUnlockAllMsgBlocks() + var sz int + dbs, sz = fs.deleteBlocks() + total += sz + } + + b := make([]byte, 0, total) + b = append(b, streamStateMagic, version) + b = binary.AppendUvarint(b, fs.state.Msgs) + b = binary.AppendUvarint(b, fs.state.Bytes) + b = binary.AppendUvarint(b, fs.state.FirstSeq) + b = binary.AppendUvarint(b, fs.state.LastSeq) + b = binary.AppendUvarint(b, failed) + if withSources { + b = binary.AppendUvarint(b, numSources) + for name, ss := range fs.sources { + if ss.Seq == 0 { + continue + } + b = binary.AppendUvarint(b, uint64(len(name))) + b = append(b, name...) + b = binary.AppendUvarint(b, ss.Seq) + b = binary.AppendUvarint(b, uint64(len(ss.Ident))) + b = append(b, ss.Ident...) + } + } + b = binary.AppendUvarint(b, uint64(numDeleted)) + + for _, db := range dbs { + switch db := db.(type) { + case *DeleteRange: + b = appendRunLength(b, db.First, db.Num) + case *avl.SequenceSet: + enc := db.Encode(b[len(b):]) + if n := len(b) + len(enc); n <= cap(b) { + b = b[:n] + } else { + // Fallback if the buffer didn't have spare capacity. + b = append(b, enc...) + } + default: + return nil, errors.New("no impl") + } + } + + if len(b) != total { + assert.Unreachable("Filestore EncodedStreamState size accounting mismatch", map[string]any{ + "name": fs.cfg.Name, + "total": total, + "length": len(b), + }) + } + return b, nil +} + +// deleteBlocks returns DeleteBlocks representing interior deletes +// and gaps between blocks, as well as their total binary encoded size. +// All blocks should be at least read locked. +func (fs *fileStore) deleteBlocks() (dbs DeleteBlocks, sz int) { + var prevLast uint64 + var prevRange *DeleteRange + var msgsSinceGap bool + + for _, mb := range fs.blks { + // Detect if we have a gap between these blocks. + fseq := atomic.LoadUint64(&mb.first.seq) + if prevLast > 0 && prevLast+1 != fseq { + gapSize := fseq - prevLast - 1 + // The previous DeleteRange can be extended + // to include this gap, if there are no + // blocks containing messages between the + // two gaps. + if prevRange != nil && !msgsSinceGap { + sz -= runLengthEncodeLen(prevRange.First, prevRange.Num) + prevRange.Num += gapSize + sz += runLengthEncodeLen(prevRange.First, prevRange.Num) + } else { + prevRange = &DeleteRange{ + First: prevLast + 1, + Num: gapSize, + } + sz += runLengthEncodeLen(prevRange.First, prevRange.Num) + msgsSinceGap = false + dbs = append(dbs, prevRange) + } + } + if mb.dmap.Size() > 0 { + dbs = append(dbs, &mb.dmap) + sz += mb.dmap.EncodeLen() + prevRange = nil + } + prevLast = atomic.LoadUint64(&mb.last.seq) + msgsSinceGap = msgsSinceGap || mb.msgs > 0 + } + return dbs, sz +} + +// interiorDeletes is a point-in-time view of the interior deletes tracked by +// the live message blocks, held as per-block clones of each mb.dmap. Blocks +// own disjoint ascending sequence ranges, so a lookup binary searches for the +// owning clone. Reads require no locks. +type interiorDeletes struct { + sets []*avl.SequenceSet // Per-block dmap clones, ascending disjoint ranges. + maxs []uint64 // Last sequence of the block owning each clone. + last int // Clone index of the previous lookup. +} + +// Exists returns whether the sequence was marked as an interior delete by a +// live block at the time the view was built. +// Not safe for concurrent use. +func (v *interiorDeletes) Exists(seq uint64) bool { + if v == nil { + return false + } + // Check the clone that answered the previous lookup first, sequences are + // mostly checked in ascending order and cluster per block. + if i := v.last; i < len(v.maxs) && seq <= v.maxs[i] && (i == 0 || v.maxs[i-1] < seq) { + return v.sets[i].Exists(seq) + } + // First clone whose max is >= seq is the only one that can contain it. + i, _ := slices.BinarySearch(v.maxs, seq) + if i == len(v.sets) { + return false + } + v.last = i + return v.sets[i].Exists(seq) +} + +// deleteMap returns a view of all interior deletes for each of the given blocks, +// based on the mb.dmap. Specifically, this will not contain any deletes for blocks +// that had already been removed. This is useful to know whether a tombstone is +// still relevant and marked as deleted by an active block. +// No locks should be held on entry. +func deleteMap(blks []*msgBlock) *interiorDeletes { + v := interiorDeletes{ + sets: make([]*avl.SequenceSet, 0, len(blks)), + maxs: make([]uint64, 0, len(blks)), + } + for _, mb := range blks { + mb.mu.RLock() + if !mb.closed && mb.dmap.Size() > 0 { + // The block's last sequence bounds all of its dmap entries and + // preserves the ascending disjoint ordering across clones. + v.sets = append(v.sets, mb.dmap.Clone()) + v.maxs = append(v.maxs, atomic.LoadUint64(&mb.last.seq)) + } + mb.mu.RUnlock() + } + return &v +} + +// storeUpdateAgg aggregates storage update deltas, so removing multiple blocks +// fires one storage callback instead of one per block. +// fs lock should be held for all access. +type storeUpdateAgg struct { + md, bd int64 +} + +// SyncDeleted will make sure this stream has same deleted state as dbs. +// This will only process deleted state within our current state. +func (fs *fileStore) SyncDeleted(dbs DeleteBlocks) error { + if len(dbs) == 0 { + return nil + } + + fs.mu.Lock() + defer fs.mu.Unlock() + + if fs.isClosed() { + return ErrStoreClosed + } + + // Always return previous write errors. + if err := fs.werr; err != nil { + return err + } + + // Removing a block has all consumers recalculate their pending state, so + // aggregate those into one callback at the end. Single message removals + // keep NumPending up to date incrementally, which is cheaper. + agg := &storeUpdateAgg{} + defer func() { + if agg.md == 0 && agg.bd == 0 { + return + } + if cb := fs.scb; cb != nil { + fs.mu.Unlock() + cb(agg.md, agg.bd, 0, _EMPTY_) + fs.mu.Lock() + } + }() + + lseq := fs.state.LastSeq + fs.readLockAllMsgBlocks() + mdbs, _ := fs.deleteBlocks() + // We'll release the locks below, so need to copy the ones that are references + // which are only safe while the locks are still held. + for i, db := range mdbs { + if ss, ok := db.(*avl.SequenceSet); ok { + mdbs[i] = ss.Clone() + } + } + fs.readUnlockAllMsgBlocks() + + for _, db := range dbs { + first, last, _ := db.State() + if first > lseq { + break + } + + var prune bool + if prune, mdbs = pruneDeleteBlock(db, mdbs); prune { + continue + } + + var err error + if _, ok := db.(*DeleteRange); ok { + err = fs.removeMsgsInRange(first, last, true, agg) + } else { + db.Range(func(dseq uint64) bool { + _, err = fs.removeMsg(dseq, false, true, false) + // Can continue safely if the message doesn't exist. + if err == ErrStoreEOF || err == ErrStoreMsgNotFound { + err = nil + } + return err == nil + }) + } + if err != nil { + return err + } + } + return nil +} + +// pruneDeleteBlock tries to find a delete block in the ordered blocks slice +// that matches db. It skips blocks that are already behind db and returns +// whether the next candidate matches exactly, along with the remaining +// suffix to use for the next comparison. +func pruneDeleteBlock(db DeleteBlock, blocks DeleteBlocks) (bool, DeleteBlocks) { + if len(blocks) == 0 { + return false, blocks + } + + aFirst, aLast, aNum := db.State() + bFirst, bLast, bNum := blocks[0].State() + + // Drop blocks that end before db starts. + for bLast < aFirst { + blocks = blocks[1:] + if len(blocks) == 0 { + return false, blocks + } + bFirst, bLast, bNum = blocks[0].State() + } + + if aFirst == bFirst && aLast == bLast && aNum == bNum { + // Matching state is only conclusive for a dense block; two sparse + // sequence sets can share the same state but differ in contents. + if aNum == aLast-aFirst+1 || deleteBlockContentsEqual(db, blocks[0]) { + return true, blocks[1:] + } + } + + return false, blocks +} + +// deleteBlockContentsEqual reports whether two sparse delete blocks with +// identical State() contain the same sequences. If neither block is a +// sequence set we can't compare cheaply and safely report unequal. +func deleteBlockContentsEqual(a, b DeleteBlock) bool { + ssa, aIsSet := a.(*avl.SequenceSet) + ssb, bIsSet := b.(*avl.SequenceSet) + if aIsSet && bIsSet { + return ssa.Equal(ssb) + } + // Use whichever side is a SequenceSet for fast Exists lookups. + var ss *avl.SequenceSet + var other DeleteBlock + if bIsSet { + ss, other = ssb, a + } else if aIsSet { + ss, other = ssa, b + } else { + return false + } + equal := true + other.Range(func(seq uint64) bool { + equal = ss.Exists(seq) + return equal + }) + return equal +} + +//////////////////////////////////////////////////////////////////////////////// +// Consumers +//////////////////////////////////////////////////////////////////////////////// + +type consumerFileStore struct { + mu sync.Mutex + fs *fileStore + cfg *FileConsumerInfo + prf keyGen + aek cipher.AEAD + name string + odir string + ifn string + hh *highwayhash.Digest64 + state ConsumerState + fch chan struct{} + qch chan struct{} + flusher bool + writing bool + dirty bool + closed bool +} + +func (fs *fileStore) ConsumerStore(name string, created time.Time, cfg *ConsumerConfig) (ConsumerStore, error) { + if fs == nil { + return nil, fmt.Errorf("filestore is nil") + } + if fs.isClosed() { + return nil, ErrStoreClosed + } + if cfg == nil || name == _EMPTY_ { + return nil, fmt.Errorf("bad consumer config") + } + + // We now allow overrides from a stream being a filestore type and forcing a consumer to be memory store. + if cfg.MemoryStorage { + // Create directly here. + o := &consumerMemStore{ms: fs, name: name, cfg: *cfg} + if err := fs.AddConsumer(o); err != nil { + return nil, err + } + return o, nil + } + + odir := filepath.Join(fs.fcfg.StoreDir, consumerDir, name) + if err := os.MkdirAll(odir, defaultDirPerms); err != nil { + return nil, fmt.Errorf("could not create consumer directory - %v", err) + } + csi := &FileConsumerInfo{Name: name, Created: created, ConsumerConfig: *cfg} + o := &consumerFileStore{ + fs: fs, + cfg: csi, + prf: fs.prf, + name: name, + odir: odir, + ifn: filepath.Join(odir, consumerState), + } + key := sha256.Sum256([]byte(fs.cfg.Name + "/" + name)) + hh, err := highwayhash.NewDigest64(key[:]) + if err != nil { + return nil, fmt.Errorf("could not create hash: %v", err) + } + o.hh = hh + + // Check for encryption. + if o.prf != nil { + if ekey, err := os.ReadFile(filepath.Join(odir, JetStreamMetaFileKey)); err == nil { + if len(ekey) < minBlkKeySize { + return nil, errBadKeySize + } + // Recover key encryption key. + rb, err := fs.prf([]byte(fs.cfg.Name + tsep + o.name)) + if err != nil { + return nil, err + } + + sc := fs.fcfg.Cipher + kek, err := genEncryptionKey(sc, rb) + if err != nil { + return nil, err + } + ns := kek.NonceSize() + nonce := ekey[:ns] + seed, err := kek.Open(nil, nonce, ekey[ns:], nil) + if err != nil { + // We may be here on a cipher conversion, so attempt to convert. + if err = o.convertCipher(); err != nil { + return nil, err + } + } else { + o.aek, err = genEncryptionKey(sc, seed) + } + if err != nil { + return nil, err + } + } + } + + // Track if we are creating the directory so that we can clean up if we encounter an error. + var didCreate bool + + // Write our meta data iff does not exist. + meta := filepath.Join(odir, JetStreamMetaFile) + if _, err := os.Stat(meta); err != nil && os.IsNotExist(err) { + didCreate = true + if err := o.writeConsumerMeta(); err != nil { + _ = os.RemoveAll(odir) + return nil, err + } + } + + // If we expect to be encrypted check that what we are restoring is not plaintext. + // This can happen on snapshot restores or conversions. + if o.prf != nil { + keyFile := filepath.Join(odir, JetStreamMetaFileKey) + if _, err := os.Stat(keyFile); err != nil && os.IsNotExist(err) { + if err := o.writeConsumerMeta(); err != nil { + if didCreate { + _ = os.RemoveAll(odir) + } + return nil, err + } + // Redo the state file as well here if we have one and we can tell it was plaintext. + if buf, err := os.ReadFile(o.ifn); err == nil { + if _, err := decodeConsumerState(buf); err == nil { + state, err := o.encryptState(buf) + if err != nil { + return nil, err + } + err = fs.writeFileWithOptionalSync(o.ifn, state, defaultFilePerms) + if err != nil { + if didCreate { + _ = os.RemoveAll(odir) + } + return nil, err + } + } + } + } + } + + // Create channels to control our flush go routine. + o.fch = make(chan struct{}, 1) + o.qch = make(chan struct{}) + + // Make sure to load in our state from disk if needed. + if err = o.loadState(); err != nil { + return nil, err + } + + // Assign to filestore. + if err = fs.AddConsumer(o); err != nil { + return nil, err + } + + go o.flushLoop(o.fch, o.qch) + return o, nil +} + +func (o *consumerFileStore) convertCipher() error { + fs := o.fs + odir := filepath.Join(fs.fcfg.StoreDir, consumerDir, o.name) + + ekey, err := os.ReadFile(filepath.Join(odir, JetStreamMetaFileKey)) + if err != nil { + return err + } + if len(ekey) < minBlkKeySize { + return errBadKeySize + } + // Recover key encryption key. + rb, err := fs.prf([]byte(fs.cfg.Name + tsep + o.name)) + if err != nil { + return err + } + + // Do these in reverse since converting. + sc := fs.fcfg.Cipher + osc := AES + if sc == AES { + osc = ChaCha + } + kek, err := genEncryptionKey(osc, rb) + if err != nil { + return err + } + ns := kek.NonceSize() + nonce := ekey[:ns] + seed, err := kek.Open(nil, nonce, ekey[ns:], nil) + if err != nil { + return err + } + aek, err := genEncryptionKey(osc, seed) + if err != nil { + return err + } + // Now read in and decode our state using the old cipher. + buf, err := os.ReadFile(o.ifn) + if err != nil { + return err + } + buf, err = aek.Open(nil, buf[:ns], buf[ns:], nil) + if err != nil { + return err + } + + // Since we are here we recovered our old state. + // Now write our meta, which will generate the new keys with the new cipher. + if err := o.writeConsumerMeta(); err != nil { + return err + } + + // Now write out or state with the new cipher. + return o.writeState(buf) +} + +// Kick flusher for this consumer. +// Lock should be held. +func (o *consumerFileStore) kickFlusher() { + if o.fch != nil { + select { + case o.fch <- struct{}{}: + default: + } + } + o.dirty = true +} + +// Set in flusher status +func (o *consumerFileStore) setInFlusher() { + o.mu.Lock() + o.flusher = true + o.mu.Unlock() +} + +// Clear in flusher status +func (o *consumerFileStore) clearInFlusher() { + o.mu.Lock() + o.flusher = false + o.mu.Unlock() +} + +// Report in flusher status +func (o *consumerFileStore) inFlusher() bool { + o.mu.Lock() + defer o.mu.Unlock() + return o.flusher +} + +// flushLoop watches for consumer updates and the quit channel. +func (o *consumerFileStore) flushLoop(fch, qch chan struct{}) { + + o.setInFlusher() + defer o.clearInFlusher() + + // Maintain approximately 10 updates per second per consumer under load. + const minTime = 100 * time.Millisecond + var lastWrite time.Time + var dt *time.Timer + + setDelayTimer := func(addWait time.Duration) { + if dt == nil { + dt = time.NewTimer(addWait) + return + } + if !dt.Stop() { + select { + case <-dt.C: + default: + } + } + dt.Reset(addWait) + } + + for { + select { + case <-fch: + if ts := time.Since(lastWrite); ts < minTime { + setDelayTimer(minTime - ts) + select { + case <-dt.C: + case <-qch: + return + } + } + o.mu.Lock() + if o.closed { + o.mu.Unlock() + return + } + buf, err := o.encodeState() + o.mu.Unlock() + if err != nil { + return + } + // TODO(dlc) - if we error should start failing upwards. + if err := o.writeState(buf); err == nil { + lastWrite = time.Now() + } + case <-qch: + return + } + } +} + +// SetStarting sets our starting stream sequence. +func (o *consumerFileStore) SetStarting(sseq uint64) error { + o.mu.Lock() + o.state.Delivered.Stream = sseq + o.state.AckFloor.Stream = sseq + buf := encodeConsumerState(&o.state) + o.mu.Unlock() + return o.writeState(buf) +} + +// UpdateStarting updates our starting stream sequence. +func (o *consumerFileStore) UpdateStarting(sseq uint64) { + o.mu.Lock() + defer o.mu.Unlock() + + if sseq > o.state.Delivered.Stream { + o.state.Delivered.Stream = sseq + // For AckNone just update delivered and ackfloor at the same time. + if o.cfg.AckPolicy == AckNone { + o.state.AckFloor.Stream = sseq + } + } + // Make sure we flush to disk. + o.kickFlusher() +} + +// Reset all values in the store, and reset the starting sequence. +func (o *consumerFileStore) Reset(sseq uint64) error { + o.mu.Lock() + o.state = ConsumerState{} + o.mu.Unlock() + return o.SetStarting(sseq) +} + +// HasState returns if this store has a recorded state. +func (o *consumerFileStore) HasState() bool { + o.mu.Lock() + // We have a running state, or stored on disk but not yet initialized. + if o.state.Delivered.Consumer != 0 || o.state.Delivered.Stream != 0 { + o.mu.Unlock() + return true + } + _, err := os.Stat(o.ifn) + o.mu.Unlock() + return err == nil +} + +// UpdateDelivered is called whenever a new message has been delivered. +func (o *consumerFileStore) UpdateDelivered(dseq, sseq, dc uint64, ts int64) error { + o.mu.Lock() + defer o.mu.Unlock() + + if dc != 1 && o.cfg.AckPolicy == AckNone { + return ErrNoAckPolicy + } + + // On restarts the old leader may get a replay from the raft logs that are old. + if dseq <= o.state.AckFloor.Consumer { + return nil + } + + // See if we expect an ack for this. + if o.cfg.AckPolicy != AckNone { + // Need to create pending records here. + if o.state.Pending == nil { + o.state.Pending = make(map[uint64]*Pending) + } + var p *Pending + // Check for an update to a message already delivered. + if sseq <= o.state.Delivered.Stream { + if p = o.state.Pending[sseq]; p != nil { + // Do not update p.Sequence, that should be the original delivery sequence. + p.Timestamp = ts + } + } else { + // Add to pending. + o.state.Pending[sseq] = &Pending{dseq, ts} + } + // Update delivered as needed. + if dseq > o.state.Delivered.Consumer { + o.state.Delivered.Consumer = dseq + } + if sseq > o.state.Delivered.Stream { + o.state.Delivered.Stream = sseq + } + + if dc > 1 { + if maxdc := uint64(o.cfg.MaxDeliver); maxdc > 0 && dc > maxdc { + // Make sure to remove from pending. + delete(o.state.Pending, sseq) + } + if o.state.Redelivered == nil { + o.state.Redelivered = make(map[uint64]uint64) + } + // Only update if greater than what we already have. + if o.state.Redelivered[sseq] < dc-1 { + o.state.Redelivered[sseq] = dc - 1 + } + } + } else { + // For AckNone just update delivered and ackfloor at the same time. + if dseq > o.state.Delivered.Consumer { + o.state.Delivered.Consumer = dseq + o.state.AckFloor.Consumer = dseq + } + if sseq > o.state.Delivered.Stream { + o.state.Delivered.Stream = sseq + o.state.AckFloor.Stream = sseq + } + } + // Make sure we flush to disk. + o.kickFlusher() + + return nil +} + +// UpdateAcks is called whenever a consumer with explicit ack or ack all acks a message. +func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error { + o.mu.Lock() + defer o.mu.Unlock() + + if o.cfg.AckPolicy == AckNone { + return ErrNoAckPolicy + } + + var kick bool + defer func() { + if kick { + o.kickFlusher() + } + }() + + // We do this regardless. + if _, ok := o.state.Redelivered[sseq]; ok { + delete(o.state.Redelivered, sseq) + kick = true + } + + // On restarts the old leader may get a replay from the raft logs that are old. + if dseq <= o.state.AckFloor.Consumer { + return nil + } + + if len(o.state.Pending) == 0 || o.state.Pending[sseq] == nil { + return ErrStoreMsgNotFound + } + + // Done with the consistency checks, we'll always kick for below updates. + kick = true + + // Check for AckAll here (or AckFlowControl which functions like AckAll). + if o.cfg.AckPolicy == AckAll || o.cfg.AckPolicy == AckFlowControl { + sgap := sseq - o.state.AckFloor.Stream + o.state.AckFloor.Consumer = dseq + o.state.AckFloor.Stream = sseq + if sgap > uint64(len(o.state.Pending)) { + for seq := range o.state.Pending { + if seq <= sseq { + delete(o.state.Pending, seq) + delete(o.state.Redelivered, seq) + } + } + } else { + for seq := sseq; seq > sseq-sgap && len(o.state.Pending) > 0; seq-- { + delete(o.state.Pending, seq) + delete(o.state.Redelivered, seq) + } + } + return nil + } + + // AckExplicit + + // First delete from our pending state. + if p, ok := o.state.Pending[sseq]; ok { + delete(o.state.Pending, sseq) + if dseq > p.Sequence && p.Sequence > 0 { + dseq = p.Sequence // Use the original. + } + } + if len(o.state.Pending) == 0 { + o.state.AckFloor.Consumer = o.state.Delivered.Consumer + o.state.AckFloor.Stream = o.state.Delivered.Stream + } else if dseq == o.state.AckFloor.Consumer+1 { + o.state.AckFloor.Consumer = dseq + o.state.AckFloor.Stream = sseq + + if o.state.Delivered.Consumer > dseq { + for ss := sseq + 1; ss <= o.state.Delivered.Stream; ss++ { + if p, ok := o.state.Pending[ss]; ok { + if p.Sequence > 0 { + o.state.AckFloor.Consumer = p.Sequence - 1 + o.state.AckFloor.Stream = ss - 1 + } + break + } + } + } + } + return nil +} + +func (o *consumerFileStore) RemoveRedeliveredBelow(seq uint64) { + if seq == 0 { + return + } + o.mu.Lock() + defer o.mu.Unlock() + var removed bool + for s := range o.state.Redelivered { + if s < seq { + delete(o.state.Redelivered, s) + removed = true + } + } + if removed { + o.kickFlusher() + } +} + +const seqsHdrSize = 6*binary.MaxVarintLen64 + hdrLen + +// Encode our consumer state, version 2. +// Lock should be held. + +func (o *consumerFileStore) EncodedState() ([]byte, error) { + o.mu.Lock() + defer o.mu.Unlock() + return o.encodeState() +} + +func (o *consumerFileStore) encodeState() ([]byte, error) { + // Grab reference to state, but make sure we load in if needed, so do not reference o.state directly. + state, err := o.stateWithCopyLocked(false) + if err != nil { + return nil, err + } + return encodeConsumerState(state), nil +} + +func (o *consumerFileStore) GetConfig() *ConsumerConfig { + o.mu.Lock() + defer o.mu.Unlock() + clone := o.cfg.clone() + clone.Name = o.name + return clone +} + +func (o *consumerFileStore) UpdateConfig(cfg *ConsumerConfig) error { + o.mu.Lock() + defer o.mu.Unlock() + + // This is mostly unchecked here. We are assuming the upper layers have done sanity checking. + csi := o.cfg + csi.ConsumerConfig = *cfg + + return o.writeConsumerMeta() +} + +func (o *consumerFileStore) Update(state *ConsumerState) error { + // Sanity checks. + if state.AckFloor.Consumer > state.Delivered.Consumer { + return fmt.Errorf("bad ack floor for consumer") + } + if state.AckFloor.Stream > state.Delivered.Stream { + return fmt.Errorf("bad ack floor for stream") + } + + // Copy to our state. + var pending map[uint64]*Pending + var redelivered map[uint64]uint64 + if len(state.Pending) > 0 { + pending = make(map[uint64]*Pending, len(state.Pending)) + for seq, p := range state.Pending { + pending[seq] = &Pending{p.Sequence, p.Timestamp} + if seq <= state.AckFloor.Stream || seq > state.Delivered.Stream { + return fmt.Errorf("bad pending entry, sequence [%d] out of range", seq) + } + } + } + if len(state.Redelivered) > 0 { + redelivered = make(map[uint64]uint64, len(state.Redelivered)) + for seq, dc := range state.Redelivered { + redelivered[seq] = dc + } + } + + // Replace our state. + o.mu.Lock() + defer o.mu.Unlock() + + // Check to see if this is an outdated update. + if state.Delivered.Consumer < o.state.Delivered.Consumer || state.AckFloor.Stream < o.state.AckFloor.Stream { + return ErrStoreOldUpdate + } + + o.state.Delivered = state.Delivered + o.state.AckFloor = state.AckFloor + o.state.Pending = pending + o.state.Redelivered = redelivered + + o.kickFlusher() + + return nil +} + +// ForceUpdate updates the consumer state without the backwards check. +// This is used during recovery when we need to reset the consumer to an earlier sequence. +func (o *consumerFileStore) ForceUpdate(state *ConsumerState) error { + // Sanity checks. + if state.AckFloor.Consumer > state.Delivered.Consumer { + return fmt.Errorf("bad ack floor for consumer") + } + if state.AckFloor.Stream > state.Delivered.Stream { + return fmt.Errorf("bad ack floor for stream") + } + + // Copy to our state. + var pending map[uint64]*Pending + var redelivered map[uint64]uint64 + if len(state.Pending) > 0 { + pending = make(map[uint64]*Pending, len(state.Pending)) + for seq, p := range state.Pending { + pending[seq] = &Pending{p.Sequence, p.Timestamp} + if seq <= state.AckFloor.Stream || seq > state.Delivered.Stream { + return fmt.Errorf("bad pending entry, sequence [%d] out of range", seq) + } + } + } + if len(state.Redelivered) > 0 { + redelivered = make(map[uint64]uint64, len(state.Redelivered)) + for seq, dc := range state.Redelivered { + redelivered[seq] = dc + } + } + + // Replace our state. + o.mu.Lock() + o.state.Delivered = state.Delivered + o.state.AckFloor = state.AckFloor + o.state.Pending = pending + o.state.Redelivered = redelivered + buf, err := o.encodeState() + o.mu.Unlock() + if err != nil { + return err + } + return o.writeState(buf) +} + +// Will encrypt the state with our asset key. Will be a no-op if encryption not enabled. +// Lock should be held. +func (o *consumerFileStore) encryptState(buf []byte) ([]byte, error) { + if o.aek == nil { + return buf, nil + } + // TODO(dlc) - Optimize on space usage a bit? + nonce := make([]byte, o.aek.NonceSize(), o.aek.NonceSize()+len(buf)+o.aek.Overhead()) + if n, err := rand.Read(nonce); err != nil { + return nil, err + } else if n != len(nonce) { + return nil, fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce)) + } + return o.aek.Seal(nonce, nonce, buf, nil), nil +} + +func (o *consumerFileStore) writeState(buf []byte) error { + // Check if we have the index file open. + o.mu.Lock() + if o.writing || len(buf) == 0 { + o.mu.Unlock() + return nil + } + + // Check on encryption. + if o.aek != nil { + var err error + if buf, err = o.encryptState(buf); err != nil { + o.mu.Unlock() + return err + } + } + + o.writing = true + o.dirty = false + ifn := o.ifn + o.mu.Unlock() + + // Lock not held here but we do limit number of outstanding calls that could block OS threads. + err := o.fs.writeFileWithOptionalSync(ifn, buf, defaultFilePerms) + + o.mu.Lock() + if err != nil { + o.dirty = true + } + o.writing = false + o.mu.Unlock() + + return err +} + +// Will upodate the config. Only used when recovering ephemerals. +func (o *consumerFileStore) updateConfig(cfg ConsumerConfig) error { + o.mu.Lock() + defer o.mu.Unlock() + o.cfg = &FileConsumerInfo{ConsumerConfig: cfg} + return o.writeConsumerMeta() +} + +// Write out the consumer meta data, i.e. state. +// Lock should be held. +func (cfs *consumerFileStore) writeConsumerMeta() error { + meta := filepath.Join(cfs.odir, JetStreamMetaFile) + if _, err := os.Stat(meta); err != nil && !os.IsNotExist(err) { + return err + } + + if cfs.prf != nil && cfs.aek == nil { + fs := cfs.fs + key, _, _, encrypted, err := fs.genEncryptionKeys(fs.cfg.Name + tsep + cfs.name) + if err != nil { + return err + } + cfs.aek = key + keyFile := filepath.Join(cfs.odir, JetStreamMetaFileKey) + if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) { + return err + } + err = cfs.fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms) + if err != nil { + return err + } + } + + b, err := json.Marshal(cfs.cfg) + if err != nil { + return err + } + // Encrypt if needed. + if cfs.aek != nil { + nonce := make([]byte, cfs.aek.NonceSize(), cfs.aek.NonceSize()+len(b)+cfs.aek.Overhead()) + if n, err := rand.Read(nonce); err != nil { + return err + } else if n != len(nonce) { + return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce)) + } + b = cfs.aek.Seal(nonce, nonce, b, nil) + } + + err = cfs.fs.writeFileWithOptionalSync(meta, b, defaultFilePerms) + if err != nil { + return err + } + cfs.hh.Reset() + cfs.hh.Write(b) + var hb [highwayhash.Size64]byte + checksum := hex.EncodeToString(cfs.hh.Sum(hb[:0])) + sum := filepath.Join(cfs.odir, JetStreamMetaFileSum) + + err = cfs.fs.writeFileWithOptionalSync(sum, []byte(checksum), defaultFilePerms) + if err != nil { + return err + } + return nil +} + +// Consumer version. +func checkConsumerHeader(hdr []byte) (uint8, error) { + if len(hdr) < 2 || hdr[0] != magic { + return 0, errCorruptState + } + version := hdr[1] + switch version { + case 1, 2: + return version, nil + } + return 0, fmt.Errorf("unsupported version: %d", version) +} + +func (o *consumerFileStore) copyPending() map[uint64]*Pending { + pending := make(map[uint64]*Pending, len(o.state.Pending)) + for seq, p := range o.state.Pending { + pending[seq] = &Pending{p.Sequence, p.Timestamp} + } + return pending +} + +func (o *consumerFileStore) copyRedelivered() map[uint64]uint64 { + redelivered := make(map[uint64]uint64, len(o.state.Redelivered)) + for seq, dc := range o.state.Redelivered { + redelivered[seq] = dc + } + return redelivered +} + +// Type returns the type of the underlying store. +func (o *consumerFileStore) Type() StorageType { return FileStorage } + +// State retrieves the state from the state file. +// This is not expected to be called in high performance code, only on startup. +func (o *consumerFileStore) State() (*ConsumerState, error) { + return o.stateWithCopy(true) +} + +// This will not copy pending or redelivered, so should only be done under the +// consumer owner's lock. +func (o *consumerFileStore) BorrowState() (*ConsumerState, error) { + return o.stateWithCopy(false) +} + +func (o *consumerFileStore) stateWithCopy(doCopy bool) (*ConsumerState, error) { + o.mu.Lock() + defer o.mu.Unlock() + return o.stateWithCopyLocked(doCopy) +} + +// Lock should be held. +func (o *consumerFileStore) stateWithCopyLocked(doCopy bool) (*ConsumerState, error) { + if o.closed { + return nil, ErrStoreClosed + } + + state := &ConsumerState{} + + // See if we have a running state or if we need to read in from disk. + if o.state.Delivered.Consumer != 0 || o.state.Delivered.Stream != 0 { + state.Delivered = o.state.Delivered + state.AckFloor = o.state.AckFloor + if len(o.state.Pending) > 0 { + if doCopy { + state.Pending = o.copyPending() + } else { + state.Pending = o.state.Pending + } + } + if len(o.state.Redelivered) > 0 { + if doCopy { + state.Redelivered = o.copyRedelivered() + } else { + state.Redelivered = o.state.Redelivered + } + } + return state, nil + } + + // Read the state in here from disk.. + o.fs.dios.acquire() + buf, err := os.ReadFile(o.ifn) + o.fs.dios.release() + + if err != nil && !os.IsNotExist(err) { + return nil, err + } + + if len(buf) == 0 { + return state, nil + } + + // Check on encryption. + if o.aek != nil { + ns := o.aek.NonceSize() + buf, err = o.aek.Open(nil, buf[:ns], buf[ns:], nil) + if err != nil { + return nil, err + } + } + + state, err = decodeConsumerState(buf) + if err != nil { + return nil, err + } + + // Copy this state into our own. + o.state.Delivered = state.Delivered + o.state.AckFloor = state.AckFloor + if len(state.Pending) > 0 { + if doCopy { + o.state.Pending = make(map[uint64]*Pending, len(state.Pending)) + for seq, p := range state.Pending { + o.state.Pending[seq] = &Pending{p.Sequence, p.Timestamp} + } + } else { + o.state.Pending = state.Pending + } + } + if len(state.Redelivered) > 0 { + if doCopy { + o.state.Redelivered = make(map[uint64]uint64, len(state.Redelivered)) + for seq, dc := range state.Redelivered { + o.state.Redelivered[seq] = dc + } + } else { + o.state.Redelivered = state.Redelivered + } + } + + return state, nil +} + +// Lock should be held. Called at startup. +func (o *consumerFileStore) loadState() error { + if _, err := os.Stat(o.ifn); err == nil { + // This will load our state in from disk. + _, err = o.stateWithCopyLocked(false) + return err + } else if os.IsNotExist(err) { + return nil + } else { + return err + } +} + +// Decode consumer state. +func decodeConsumerState(buf []byte) (*ConsumerState, error) { + version, err := checkConsumerHeader(buf) + if err != nil { + return nil, err + } + + bi := hdrLen + // Helpers, will set i to -1 on error. + readSeq := func() uint64 { + if bi < 0 { + return 0 + } + seq, n := binary.Uvarint(buf[bi:]) + if n <= 0 { + bi = -1 + return 0 + } + bi += n + return seq + } + readTimeStamp := func() int64 { + if bi < 0 { + return 0 + } + ts, n := binary.Varint(buf[bi:]) + if n <= 0 { + bi = -1 + return -1 + } + bi += n + return ts + } + // Just for clarity below. + readLen := readSeq + readCount := readSeq + + state := &ConsumerState{} + state.AckFloor.Consumer = readSeq() + state.AckFloor.Stream = readSeq() + state.Delivered.Consumer = readSeq() + state.Delivered.Stream = readSeq() + + if bi == -1 { + return nil, errCorruptState + } + if version == 1 { + // Adjust back. Version 1 also stored delivered as next to be delivered, + // so adjust that back down here. + if state.AckFloor.Consumer > 1 { + state.Delivered.Consumer += state.AckFloor.Consumer - 1 + } + if state.AckFloor.Stream > 1 { + state.Delivered.Stream += state.AckFloor.Stream - 1 + } + } + + // Protect ourselves against rolling backwards. + const hbit = 1 << 63 + if state.AckFloor.Stream&hbit != 0 || state.Delivered.Stream&hbit != 0 { + return nil, errCorruptState + } + + // We have additional stuff. + if numPending := readLen(); numPending > 0 { + mints := readTimeStamp() + state.Pending = make(map[uint64]*Pending, numPending) + for i := 0; i < int(numPending); i++ { + sseq := readSeq() + var dseq uint64 + if version == 2 { + dseq = readSeq() + } + ts := readTimeStamp() + // Check the state machine for corruption, not the value which could be -1. + if bi == -1 { + return nil, errCorruptState + } + // Adjust seq back. + sseq += state.AckFloor.Stream + if sseq == 0 { + return nil, errCorruptState + } + if version == 2 { + dseq += state.AckFloor.Consumer + } + // Adjust the timestamp back. + if version == 1 { + ts = (ts + mints) * int64(time.Second) + } else { + ts = (mints - ts) * int64(time.Second) + } + // Store in pending. + state.Pending[sseq] = &Pending{dseq, ts} + } + } + + // We have redelivered entries here. + if numRedelivered := readLen(); numRedelivered > 0 { + state.Redelivered = make(map[uint64]uint64, numRedelivered) + for i := 0; i < int(numRedelivered); i++ { + if seq, n := readSeq(), readCount(); seq > 0 && n > 0 { + // Adjust seq back. + seq += state.AckFloor.Stream + state.Redelivered[seq] = n + } + } + } + + return state, nil +} + +// Stop the processing of the consumers's state. +func (o *consumerFileStore) Stop() error { + o.mu.Lock() + if o.closed { + o.mu.Unlock() + return nil + } + if o.qch != nil { + close(o.qch) + o.qch = nil + } + + var err error + var buf []byte + + if o.dirty { + // Make sure to write this out.. + if buf, err = o.encodeState(); err == nil && len(buf) > 0 { + if o.aek != nil { + if buf, err = o.encryptState(buf); err != nil { + o.mu.Unlock() + return err + } + } + } + } + + o.odir = _EMPTY_ + o.closed = true + ifn, fs := o.ifn, o.fs + o.mu.Unlock() + + if err = fs.RemoveConsumer(o); err != nil { + return err + } + + if len(buf) > 0 { + o.waitOnFlusher() + err = o.fs.writeFileWithOptionalSync(ifn, buf, defaultFilePerms) + } + return err +} + +func (o *consumerFileStore) waitOnFlusher() { + if !o.inFlusher() { + return + } + + timeout := time.Now().Add(100 * time.Millisecond) + for time.Now().Before(timeout) { + if !o.inFlusher() { + return + } + time.Sleep(10 * time.Millisecond) + } +} + +// Delete the consumer. +func (o *consumerFileStore) Delete() error { + return o.delete(false) +} + +func (o *consumerFileStore) StreamDelete() error { + return o.delete(true) +} + +func (o *consumerFileStore) delete(streamDeleted bool) error { + o.mu.Lock() + if o.closed { + o.mu.Unlock() + return nil + } + if o.qch != nil { + close(o.qch) + o.qch = nil + } + + odir := o.odir + o.odir = _EMPTY_ + o.closed = true + fs := o.fs + o.mu.Unlock() + + // If our stream was not deleted this will remove the directories. + if odir != _EMPTY_ && !streamDeleted { + if err := removeAllWithRetry(o.fs.dios, odir); err != nil { + return err + } + } + + if !streamDeleted { + if err := fs.RemoveConsumer(o); err != nil { + return err + } + } + + return nil +} + +func (fs *fileStore) AddConsumer(o ConsumerStore) error { + fs.cmu.Lock() + defer fs.cmu.Unlock() + fs.cfs = append(fs.cfs, o) + return nil +} + +func (fs *fileStore) RemoveConsumer(o ConsumerStore) error { + fs.cmu.Lock() + defer fs.cmu.Unlock() + for i, cfs := range fs.cfs { + if o == cfs { + fs.cfs = append(fs.cfs[:i], fs.cfs[i+1:]...) + break + } + } + return nil +} + +func (fs *fileStore) Consumers() iter.Seq[ConsumerStore] { + return func(yield func(ConsumerStore) bool) { + fs.cmu.RLock() + defer fs.cmu.RUnlock() + + for _, v := range fs.cfs { + if !yield(v) { + return + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Compression +//////////////////////////////////////////////////////////////////////////////// + +type CompressionInfo struct { + Algorithm StoreCompression + OriginalSize uint64 +} + +func (c *CompressionInfo) MarshalMetadata() []byte { + b := make([]byte, 14) // 4 + potentially up to 10 for uint64 + b[0], b[1], b[2] = 'c', 'm', 'p' + b[3] = byte(c.Algorithm) + n := binary.PutUvarint(b[4:], c.OriginalSize) + return b[:4+n] +} + +func (c *CompressionInfo) UnmarshalMetadata(b []byte) (int, error) { + c.Algorithm = NoCompression + c.OriginalSize = 0 + if len(b) < 5 { // 4 + min 1 for uvarint uint64 + return 0, nil + } + // An uncompressed record with length 7,368,035 starts with "cmp", + // so we also check algorithm byte. + if b[0] != 'c' || b[1] != 'm' || b[2] != 'p' || StoreCompression(b[3]) != S2Compression { + return 0, nil + } + var n int + c.Algorithm = StoreCompression(b[3]) + c.OriginalSize, n = binary.Uvarint(b[4:]) + if n <= 0 { + return 0, fmt.Errorf("metadata incomplete") + } + return 4 + n, nil +} + +func (alg StoreCompression) Compress(buf []byte) ([]byte, error) { + if len(buf) < checksumSize { + return nil, fmt.Errorf("uncompressed buffer is too short") + } + bodyLen := int64(len(buf) - checksumSize) + var output bytes.Buffer + var writer io.WriteCloser + switch alg { + case NoCompression: + return buf, nil + case S2Compression: + writer = s2.NewWriter(&output) + default: + return nil, fmt.Errorf("compression algorithm not known") + } + + input := bytes.NewReader(buf[:bodyLen]) + checksum := buf[bodyLen:] + + // Compress the block content, but don't compress the checksum. + // We will preserve it at the end of the block as-is. + if n, err := io.CopyN(writer, input, bodyLen); err != nil { + return nil, fmt.Errorf("error writing to compression writer: %w", err) + } else if n != bodyLen { + return nil, fmt.Errorf("short write on body (%d != %d)", n, bodyLen) + } + if err := writer.Close(); err != nil { + return nil, fmt.Errorf("error closing compression writer: %w", err) + } + + // Now add the checksum back onto the end of the block. + if n, err := output.Write(checksum); err != nil { + return nil, fmt.Errorf("error writing checksum: %w", err) + } else if n != checksumSize { + return nil, fmt.Errorf("short write on checksum (%d != %d)", n, checksumSize) + } + + return output.Bytes(), nil +} + +func (alg StoreCompression) Decompress(buf []byte) ([]byte, error) { + if len(buf) < checksumSize { + return nil, fmt.Errorf("compressed buffer is too short") + } + bodyLen := int64(len(buf) - checksumSize) + input := bytes.NewReader(buf[:bodyLen]) + + var reader io.ReadCloser + switch alg { + case NoCompression: + return buf, nil + case S2Compression: + reader = io.NopCloser(s2.NewReader(input)) + default: + return nil, fmt.Errorf("compression algorithm not known") + } + + // Decompress the block content. The checksum isn't compressed so + // we can preserve it from the end of the block as-is. + checksum := buf[bodyLen:] + output, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("error reading compression reader: %w", err) + } + output = append(output, checksum...) + + return output, reader.Close() +} + +// writeFileWithOptionalSync is equivalent to os.WriteFile() but optionally +// sets O_SYNC on the open file if SyncAlways is set. The dios semaphore is +// handled automatically by this function, so don't wrap calls to it in dios. +func (fs *fileStore) writeFileWithOptionalSync(name string, data []byte, perm fs.FileMode) error { + sync := fs.syncAlways.Load() || fs.syncOnFlush.Load() + return writeAtomically(fs.dios, name, data, perm, sync) +} + +func writeFileWithSync(dios *diskIOSemaphore, name string, data []byte, perm fs.FileMode) error { + return writeAtomically(dios, name, data, perm, true) +} + +// Windows does not support fsyncing directory metadata, it results in a panic, so +// we need to skip doing this there. +const canFsyncDirectories = runtime.GOOS != "windows" + +func writeAtomically(dios *diskIOSemaphore, name string, data []byte, perm fs.FileMode, sync bool) error { + return writeAtomicallyWithTemp(dios, name+".tmp", name, data, perm, sync) +} + +func writeAtomicallyWithTemp(dios *diskIOSemaphore, tmp, name string, data []byte, perm fs.FileMode, sync bool) error { + flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC + if sync { + flags = flags | os.O_SYNC + } + dios.acquire() + defer dios.release() + f, err := os.OpenFile(tmp, flags, perm) + if err != nil { + return err + } + if _, err := f.Write(data); err != nil { + // Close fd, but ignore its error since write takes precedence. + _ = f.Close() + _ = os.Remove(tmp) + return err + } + if err := f.Close(); err != nil { + _ = os.Remove(tmp) + return err + } + if err := os.Rename(tmp, name); err != nil { + _ = os.Remove(tmp) + return err + } + if sync && canFsyncDirectories { + // To ensure that the file rename was persisted on all filesystems, + // also try to flush the directory metadata. + if err = syncDir(filepath.Dir(name)); err != nil { + return err + } + } + return nil +} + +func (fs *fileStore) syncFileAndDir(name string) error { + fs.dios.acquire() + defer fs.dios.release() + f, err := os.OpenFile(name, os.O_RDWR, defaultFilePerms) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if err = f.Sync(); err != nil { + // Close fd, but ignore its error since sync takes precedence. + _ = f.Close() + return err + } + if err = f.Close(); err != nil { + return err + } + if canFsyncDirectories { + if err = syncDir(filepath.Dir(name)); err != nil { + return err + } + } + return nil +} + +// Dios should already be held. +func syncDir(path string) error { + var d *os.File + var err error + if d, err = os.Open(path); err != nil { + return err + } + if err = d.Sync(); err != nil { + // Close fd, but ignore its error since sync takes precedence. + _ = d.Close() + return err + } + if err = d.Close(); err != nil { + return err + } + return nil +} diff --git a/server/jetstream_api.go b/server/jetstream_api.go new file mode 100644 index 0000000..907801a --- /dev/null +++ b/server/jetstream_api.go @@ -0,0 +1,6012 @@ +// Copyright 2020-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 ( + "bytes" + "cmp" + "encoding/json" + "errors" + "fmt" + "io" + "maps" + "os" + "path/filepath" + "runtime" + "slices" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/klauspost/compress/s2" + "github.com/nats-io/nats-server/v2/server/archive" + "github.com/nats-io/nuid" +) + +// Request API subjects for JetStream. +const ( + // All API endpoints. + jsAllAPI = "$JS.API.>" + + // For constructing JetStream domain prefixes. + jsDomainAPI = "$JS.%s.API.>" + + JSApiPrefix = "$JS.API" + + // JSApiAccountInfo is for obtaining general information about JetStream for this account. + // Will return JSON response. + JSApiAccountInfo = "$JS.API.INFO" + + // JSApiStreamCreate is the endpoint to create new streams. + // Will return JSON response. + JSApiStreamCreate = "$JS.API.STREAM.CREATE.*" + JSApiStreamCreateT = "$JS.API.STREAM.CREATE.%s" + + // JSApiStreamUpdate is the endpoint to update existing streams. + // Will return JSON response. + JSApiStreamUpdate = "$JS.API.STREAM.UPDATE.*" + JSApiStreamUpdateT = "$JS.API.STREAM.UPDATE.%s" + + // JSApiStreams is the endpoint to list all stream names for this account. + // Will return JSON response. + JSApiStreams = "$JS.API.STREAM.NAMES" + // JSApiStreamList is the endpoint that will return all detailed stream information + JSApiStreamList = "$JS.API.STREAM.LIST" + + // JSApiStreamInfo is for obtaining general information about a named stream. + // Will return JSON response. + JSApiStreamInfo = "$JS.API.STREAM.INFO.*" + JSApiStreamInfoT = "$JS.API.STREAM.INFO.%s" + + // JSApiStreamDelete is the endpoint to delete streams. + // Will return JSON response. + JSApiStreamDelete = "$JS.API.STREAM.DELETE.*" + JSApiStreamDeleteT = "$JS.API.STREAM.DELETE.%s" + + // JSApiStreamPurge is the endpoint to purge streams. + // Will return JSON response. + JSApiStreamPurge = "$JS.API.STREAM.PURGE.*" + JSApiStreamPurgeT = "$JS.API.STREAM.PURGE.%s" + + // JSApiStreamSnapshot is the endpoint to snapshot streams. + // Will return a stream of chunks with a nil chunk as EOF to + // the deliver subject. Caller should respond to each chunk + // with a nil body response for ack flow. + JSApiStreamSnapshot = "$JS.API.STREAM.SNAPSHOT.*" + JSApiStreamSnapshotT = "$JS.API.STREAM.SNAPSHOT.%s" + + // JSApiStreamRestore is the endpoint to restore a stream from a snapshot. + // Caller should respond to each chunk with a nil body response. + JSApiStreamRestore = "$JS.API.STREAM.RESTORE.*" + JSApiStreamRestoreT = "$JS.API.STREAM.RESTORE.%s" + + // JSApiMsgDelete is the endpoint to delete messages from a stream. + // Will return JSON response. + JSApiMsgDelete = "$JS.API.STREAM.MSG.DELETE.*" + JSApiMsgDeleteT = "$JS.API.STREAM.MSG.DELETE.%s" + + // JSApiMsgGet is the template for direct requests for a message by its stream sequence number. + // Will return JSON response. + JSApiMsgGet = "$JS.API.STREAM.MSG.GET.*" + JSApiMsgGetT = "$JS.API.STREAM.MSG.GET.%s" + + // JSDirectMsgGet is the template for non-api layer direct requests for a message by its stream sequence number or last by subject. + // Will return the message similar to how a consumer receives the message, no JSON processing. + // If the message can not be found we will use a status header of 404. If the stream does not exist the client will get a no-responders or timeout. + JSDirectMsgGet = "$JS.API.DIRECT.GET.*" + JSDirectMsgGetT = "$JS.API.DIRECT.GET.%s" + + // This is a direct version of get last by subject, which will be the dominant pattern for KV access once 2.9 is released. + // The stream and the key will be part of the subject to allow for no-marshal payloads and subject based security permissions. + JSDirectGetLastBySubject = "$JS.API.DIRECT.GET.*.>" + JSDirectGetLastBySubjectT = "$JS.API.DIRECT.GET.%s.%s" + + // jsDirectGetPre + jsDirectGetPre = "$JS.API.DIRECT.GET" + + // JSApiConsumerCreate is the endpoint to create consumers for streams. + // This was also the legacy endpoint for ephemeral consumers. + // It now can take consumer name and optional filter subject, which when part of the subject controls access. + // Will return JSON response. + JSApiConsumerCreate = "$JS.API.CONSUMER.CREATE.*" + JSApiConsumerCreateT = "$JS.API.CONSUMER.CREATE.%s" + JSApiConsumerCreateEx = "$JS.API.CONSUMER.CREATE.*.>" + JSApiConsumerCreateExT = "$JS.API.CONSUMER.CREATE.%s.%s.%s" + + // JSApiDurableCreate is the endpoint to create durable consumers for streams. + // You need to include the stream and consumer name in the subject. + JSApiDurableCreate = "$JS.API.CONSUMER.DURABLE.CREATE.*.*" + JSApiDurableCreateT = "$JS.API.CONSUMER.DURABLE.CREATE.%s.%s" + + // JSApiConsumers is the endpoint to list all consumer names for the stream. + // Will return JSON response. + JSApiConsumers = "$JS.API.CONSUMER.NAMES.*" + JSApiConsumersT = "$JS.API.CONSUMER.NAMES.%s" + + // JSApiConsumerList is the endpoint that will return all detailed consumer information + JSApiConsumerList = "$JS.API.CONSUMER.LIST.*" + JSApiConsumerListT = "$JS.API.CONSUMER.LIST.%s" + + // JSApiConsumerInfo is for obtaining general information about a consumer. + // Will return JSON response. + JSApiConsumerInfo = "$JS.API.CONSUMER.INFO.*.*" + JSApiConsumerInfoT = "$JS.API.CONSUMER.INFO.%s.%s" + + // JSApiConsumerDelete is the endpoint to delete consumers. + // Will return JSON response. + JSApiConsumerDelete = "$JS.API.CONSUMER.DELETE.*.*" + JSApiConsumerDeleteT = "$JS.API.CONSUMER.DELETE.%s.%s" + + // JSApiConsumerPause is the endpoint to pause or unpause consumers. + // Will return JSON response. + JSApiConsumerPause = "$JS.API.CONSUMER.PAUSE.*.*" + JSApiConsumerPauseT = "$JS.API.CONSUMER.PAUSE.%s.%s" + + // JSApiRequestNextT is the prefix for the request next message(s) for a consumer in worker/pull mode. + JSApiRequestNextT = "$JS.API.CONSUMER.MSG.NEXT.%s.%s" + + // JSApiConsumerResetT is the prefix for resetting a given consumer to a new starting sequence. + JSApiConsumerResetT = "$JS.API.CONSUMER.RESET.%s.%s" + + // JSApiConsumerUnpinT is the prefix for unpinning subscription for a given consumer. + JSApiConsumerUnpin = "$JS.API.CONSUMER.UNPIN.*.*" + JSApiConsumerUnpinT = "$JS.API.CONSUMER.UNPIN.%s.%s" + + // jsRequestNextPre + jsRequestNextPre = "$JS.API.CONSUMER.MSG.NEXT." + + // For snapshots and restores. The ack will have additional tokens. + jsSnapshotAckT = "$JS.SNAPSHOT.ACK.%s.%s" + jsRestoreDeliverT = "$JS.SNAPSHOT.RESTORE.%s.%s" + + // JSApiStreamRemovePeer is the endpoint to remove a peer from a clustered stream and its consumers. + // Will return JSON response. + JSApiStreamRemovePeer = "$JS.API.STREAM.PEER.REMOVE.*" + JSApiStreamRemovePeerT = "$JS.API.STREAM.PEER.REMOVE.%s" + + // JSApiStreamEvacuatePeer is the endpoint to evacuate a peer from a clustered stream and its consumers. + // Will return JSON response. + JSApiStreamEvacuatePeer = "$JS.API.STREAM.PEER.EVACUATE.*" + JSApiStreamEvacuatePeerT = "$JS.API.STREAM.PEER.EVACUATE.%s" + + // JSApiStreamCancelMove is the endpoint to cancel an in-progress stream reconfiguration, + // rolling the stream back to the config and peers it had before the reconfiguration + // started. This is not limited to moves, any in-flight desired state is rolled back, + // which includes a scale up/down or a retention change. + // Will return JSON response. + JSApiStreamCancelMove = "$JS.API.STREAM.CANCEL_MOVE.*" + JSApiStreamCancelMoveT = "$JS.API.STREAM.CANCEL_MOVE.%s" + + // JSApiStreamLeaderStepDown is the endpoint to have stream leader stepdown. + // Will return JSON response. + JSApiStreamLeaderStepDown = "$JS.API.STREAM.LEADER.STEPDOWN.*" + JSApiStreamLeaderStepDownT = "$JS.API.STREAM.LEADER.STEPDOWN.%s" + + // JSApiConsumerLeaderStepDown is the endpoint to have consumer leader stepdown. + // Will return JSON response. + JSApiConsumerLeaderStepDown = "$JS.API.CONSUMER.LEADER.STEPDOWN.*.*" + JSApiConsumerLeaderStepDownT = "$JS.API.CONSUMER.LEADER.STEPDOWN.%s.%s" + + // JSApiConsumerRemovePeer is the endpoint to remove a peer from a single clustered consumer. + // Will return JSON response. + JSApiConsumerRemovePeer = "$JS.API.CONSUMER.PEER.REMOVE.*.*" + JSApiConsumerRemovePeerT = "$JS.API.CONSUMER.PEER.REMOVE.%s.%s" + + // JSApiConsumerEvacuatePeer is the endpoint to evacuate a peer from a single clustered consumer. + // Will return JSON response. + JSApiConsumerEvacuatePeer = "$JS.API.CONSUMER.PEER.EVACUATE.*.*" + JSApiConsumerEvacuatePeerT = "$JS.API.CONSUMER.PEER.EVACUATE.%s.%s" + + // JSApiLeaderStepDown is the endpoint to have our metaleader stepdown. + // Only works from system account. + // Will return JSON response. + JSApiLeaderStepDown = "$JS.API.META.LEADER.STEPDOWN" + + // JSApiRemoveServer is the endpoint to remove a peer server from the cluster. + // Only works from system account. + // Will return JSON response. + JSApiRemoveServer = "$JS.API.SERVER.REMOVE" + + // JSApiEvacuateServer is the endpoint to evacuate a peer server from the cluster. + // Only works from system account. + // Will return JSON response. + JSApiEvacuateServer = "$JS.API.SERVER.EVACUATE" + + // JSApiMetaRescue is the endpoint to unsafely lower the meta group's quorum + // requirement on the surviving servers for disaster recovery. + // This is a broadcast subject, every online server evaluates and responds + // to the request independently. + // Only works from system account. + // Will return JSON response. + JSApiMetaRescue = "$JS.API.META.RESCUE" + + // JSApiAccountPurge is the endpoint to purge the js content of an account + // Only works from system account. + // Will return JSON response. + JSApiAccountPurge = "$JS.API.ACCOUNT.PURGE.*" + JSApiAccountPurgeT = "$JS.API.ACCOUNT.PURGE.%s" + + // JSApiServerStreamMove is the endpoint to move streams off a server + // Only works from system account. + // Will return JSON response. + JSApiServerStreamMove = "$JS.API.ACCOUNT.STREAM.MOVE.*.*" + JSApiServerStreamMoveT = "$JS.API.ACCOUNT.STREAM.MOVE.%s.%s" + + // JSApiServerStreamCancelMove is the endpoint to cancel an in-progress stream + // reconfiguration. Like JSApiStreamCancelMove this is not limited to moves, any + // in-flight desired state is rolled back to its origin. + // Only works from system account. + // Will return JSON response. + JSApiServerStreamCancelMove = "$JS.API.ACCOUNT.STREAM.CANCEL_MOVE.*.*" + JSApiServerStreamCancelMoveT = "$JS.API.ACCOUNT.STREAM.CANCEL_MOVE.%s.%s" + + // The prefix for system level account API. + jsAPIAccountPre = "$JS.API.ACCOUNT." + + // jsAckT is the template for the ack message stream coming back from a consumer + // when they ACK/NAK, etc a message. + jsAckT = "$JS.ACK.%s.%s" + jsAckTv2 = "$JS.ACK.%s.%s.%s.%s" + jsAckPre = "$JS.ACK." + jsAckPreLen = len(jsAckPre) + + // jsFlowControl is for flow control subjects. + jsFlowControlPre = "$JS.FC." + // jsFlowControl is for FC responses. + jsFlowControl = "$JS.FC.%s.%s.*" + jsFlowControlV2 = "$JS.FC.%s.%s.%s.%s.*" + + // JSAdvisoryPrefix is a prefix for all JetStream advisories. + JSAdvisoryPrefix = "$JS.EVENT.ADVISORY" + + // JSMetricPrefix is a prefix for all JetStream metrics. + JSMetricPrefix = "$JS.EVENT.METRIC" + + // JSMetricConsumerAckPre is a metric containing ack latency. + JSMetricConsumerAckPre = "$JS.EVENT.METRIC.CONSUMER.ACK" + + // JSAdvisoryConsumerMaxDeliveryExceedPre is a notification published when a message exceeds its delivery threshold. + JSAdvisoryConsumerMaxDeliveryExceedPre = "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES" + + // JSAdvisoryConsumerMsgNakPre is a notification published when a message has been naked + JSAdvisoryConsumerMsgNakPre = "$JS.EVENT.ADVISORY.CONSUMER.MSG_NAKED" + + // JSAdvisoryConsumerMsgTerminatedPre is a notification published when a message has been terminated. + JSAdvisoryConsumerMsgTerminatedPre = "$JS.EVENT.ADVISORY.CONSUMER.MSG_TERMINATED" + + // JSAdvisoryStreamCreatedPre notification that a stream was created. + JSAdvisoryStreamCreatedPre = "$JS.EVENT.ADVISORY.STREAM.CREATED" + + // JSAdvisoryStreamDeletedPre notification that a stream was deleted. + JSAdvisoryStreamDeletedPre = "$JS.EVENT.ADVISORY.STREAM.DELETED" + + // JSAdvisoryStreamUpdatedPre notification that a stream was updated. + JSAdvisoryStreamUpdatedPre = "$JS.EVENT.ADVISORY.STREAM.UPDATED" + + // JSAdvisoryConsumerCreatedPre notification that a consumer was created. + JSAdvisoryConsumerCreatedPre = "$JS.EVENT.ADVISORY.CONSUMER.CREATED" + + // JSAdvisoryConsumerDeletedPre notification that a consumer was deleted. + JSAdvisoryConsumerDeletedPre = "$JS.EVENT.ADVISORY.CONSUMER.DELETED" + + // JSAdvisoryConsumerPausePre notification that a consumer paused/unpaused. + JSAdvisoryConsumerPausePre = "$JS.EVENT.ADVISORY.CONSUMER.PAUSE" + + // JSAdvisoryConsumerPinnedPre notification that a consumer was pinned. + JSAdvisoryConsumerPinnedPre = "$JS.EVENT.ADVISORY.CONSUMER.PINNED" + + // JSAdvisoryConsumerUnpinnedPre notification that a consumer was unpinned. + JSAdvisoryConsumerUnpinnedPre = "$JS.EVENT.ADVISORY.CONSUMER.UNPINNED" + + // JSAdvisoryStreamSnapshotCreatePre notification that a snapshot was created. + JSAdvisoryStreamSnapshotCreatePre = "$JS.EVENT.ADVISORY.STREAM.SNAPSHOT_CREATE" + + // JSAdvisoryStreamSnapshotCompletePre notification that a snapshot was completed. + JSAdvisoryStreamSnapshotCompletePre = "$JS.EVENT.ADVISORY.STREAM.SNAPSHOT_COMPLETE" + + // JSAdvisoryStreamRestoreCreatePre notification that a restore was start. + JSAdvisoryStreamRestoreCreatePre = "$JS.EVENT.ADVISORY.STREAM.RESTORE_CREATE" + + // JSAdvisoryStreamRestoreCompletePre notification that a restore was completed. + JSAdvisoryStreamRestoreCompletePre = "$JS.EVENT.ADVISORY.STREAM.RESTORE_COMPLETE" + + // JSAdvisoryDomainLeaderElectedPre notification that a jetstream domain has elected a leader. + JSAdvisoryDomainLeaderElected = "$JS.EVENT.ADVISORY.DOMAIN.LEADER_ELECTED" + + // JSAdvisoryStreamLeaderElectedPre notification that a replicated stream has elected a leader. + JSAdvisoryStreamLeaderElectedPre = "$JS.EVENT.ADVISORY.STREAM.LEADER_ELECTED" + + // JSAdvisoryStreamQuorumLostPre notification that a stream and its consumers are stalled. + JSAdvisoryStreamQuorumLostPre = "$JS.EVENT.ADVISORY.STREAM.QUORUM_LOST" + + // JSAdvisoryStreamBatchAbandonedPre notification that a stream's batch was abandoned. + JSAdvisoryStreamBatchAbandonedPre = "$JS.EVENT.ADVISORY.STREAM.BATCH_ABANDONED" + + // JSAdvisoryConsumerLeaderElectedPre notification that a replicated consumer has elected a leader. + JSAdvisoryConsumerLeaderElectedPre = "$JS.EVENT.ADVISORY.CONSUMER.LEADER_ELECTED" + + // JSAdvisoryConsumerQuorumLostPre notification that a consumer is stalled. + JSAdvisoryConsumerQuorumLostPre = "$JS.EVENT.ADVISORY.CONSUMER.QUORUM_LOST" + + // JSAdvisoryServerOutOfStorage notification that a server has no more storage. + JSAdvisoryServerOutOfStorage = "$JS.EVENT.ADVISORY.SERVER.OUT_OF_STORAGE" + + // JSAdvisoryServerRemoved notification that a server has been removed from the system. + JSAdvisoryServerRemoved = "$JS.EVENT.ADVISORY.SERVER.REMOVED" + + // JSAdvisoryMetaRescue notification that a server has unsafely lowered the + // meta group's quorum requirement for disaster recovery. + JSAdvisoryMetaRescue = "$JS.EVENT.ADVISORY.SERVER.META_RESCUE" + + // JSAdvisoryAPILimitReached notification that a server has reached the JS API hard limit. + JSAdvisoryAPILimitReached = "$JS.EVENT.ADVISORY.API.LIMIT_REACHED" + + // JSAuditAdvisory is a notification about JetStream API access. + // FIXME - Add in details about who.. + JSAuditAdvisory = "$JS.EVENT.ADVISORY.API" +) + +// Headers used in $JS.API.> requests. +const ( + // JSRequiredApiLevel requires the API level of the responding server to have the specified minimum value. + JSRequiredApiLevel = "Nats-Required-Api-Level" + + // JSStreamIdentity is the stream's identity used to reject consumer create on a mismatch, and return + // the stream identity in the consumer create/reset responses. + JSStreamIdentity = "Nats-Stream-Identity" +) + +var denyAllClientJs = []string{jsAllAPI, "$KV.>", "$OBJ.>"} +var denyAllJs = []string{jscAllSubj, raftAllSubj, jsAllAPI, "$KV.>", "$OBJ.>"} + +func generateJSMappingTable(domain string) map[string]string { + mappings := map[string]string{} + // This set of mappings is very very very ugly. + // It is a consequence of what we defined the domain prefix to be "$JS.domain.API" and it's mapping to "$JS.API" + // For optics $KV and $OBJ where made to be independent subject spaces. + // As materialized views of JS, they did not simply extend that subject space to say "$JS.API.KV" "$JS.API.OBJ" + // This is very unfortunate!!! + // Furthermore, it seemed bad to require different domain prefixes for JS/KV/OBJ. + // Especially since the actual API for say KV, does use stream create from JS. + // To avoid overlaps KV and OBJ views append the prefix to their API. + // (Replacing $KV with the prefix allows users to create collisions with say the bucket name) + // This mapping therefore needs to have extra token so that the mapping can properly discern between $JS, $KV, $OBJ + for srcMappingSuffix, to := range map[string]string{ + "INFO": JSApiAccountInfo, + "STREAM.>": "$JS.API.STREAM.>", + "CONSUMER.>": "$JS.API.CONSUMER.>", + "DIRECT.>": "$JS.API.DIRECT.>", + "META.>": "$JS.API.META.>", + "SERVER.>": "$JS.API.SERVER.>", + "ACCOUNT.>": "$JS.API.ACCOUNT.>", + "$KV.>": "$KV.>", + "$OBJ.>": "$OBJ.>", + } { + mappings[fmt.Sprintf("$JS.%s.API.%s", domain, srcMappingSuffix)] = to + } + return mappings +} + +// JSMaxDescription is the maximum description length for streams and consumers. +const JSMaxDescriptionLen = 4 * 1024 + +// JSMaxMetadataLen is the maximum length for streams and consumers metadata map. +// It's calculated by summing length of all keys and values. +const JSMaxMetadataLen = 128 * 1024 + +// JSMaxNameLen is the maximum name lengths for streams, consumers and templates. +// Picked 255 as it seems to be a widely used file name limit +const JSMaxNameLen = 255 + +// JSDefaultRequestQueueLimit is the default number of entries that we will +// put on the global request queue before we react. +const JSDefaultRequestQueueLimit = 10_000 + +// JSDefaultMaxConsumersPerStream is the default maximum number of consumers per stream. +const JSDefaultMaxConsumersPerStream = 1_000 + +// Responses for API calls. + +// ApiResponse is a standard response from the JetStream JSON API +type ApiResponse struct { + Type string `json:"type"` + Error *ApiError `json:"error,omitempty"` +} + +const JSApiSystemResponseType = "io.nats.jetstream.api.v1.system_response" + +// When passing back to the clients generalize store failures. +var ( + errStreamStoreFailed = errors.New("error creating store for stream") + errConsumerStoreFailed = errors.New("error creating store for consumer") +) + +// 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 *ApiResponse) ToError() error { + if r.Error == nil { + return nil + } + + return r.Error +} + +const JSApiOverloadedType = "io.nats.jetstream.api.v1.system_overloaded" + +// ApiPaged includes variables used to create paged responses from the JSON API +type ApiPaged struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` +} + +// ApiPagedRequest includes parameters allowing specific pages to be requests from APIs responding with ApiPaged +type ApiPagedRequest struct { + Offset int `json:"offset"` +} + +// JSApiAccountInfoResponse reports back information on jetstream for this account. +type JSApiAccountInfoResponse struct { + ApiResponse + *JetStreamAccountStats +} + +const JSApiAccountInfoResponseType = "io.nats.jetstream.api.v1.account_info_response" + +// JSApiStreamCreateResponse stream creation. +type JSApiStreamCreateResponse struct { + ApiResponse + *StreamInfo + DidCreate bool `json:"did_create,omitempty"` +} + +const JSApiStreamCreateResponseType = "io.nats.jetstream.api.v1.stream_create_response" + +// JSApiStreamDeleteResponse stream removal. +type JSApiStreamDeleteResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiStreamDeleteResponseType = "io.nats.jetstream.api.v1.stream_delete_response" + +// JSMaxSubjectDetails The limit of the number of subject details we will send in a stream info response. +const JSMaxSubjectDetails = 100_000 + +type JSApiStreamInfoRequest struct { + ApiPagedRequest + DeletedDetails bool `json:"deleted_details,omitempty"` + SubjectsFilter string `json:"subjects_filter,omitempty"` +} + +type JSApiStreamInfoResponse struct { + ApiResponse + ApiPaged + *StreamInfo +} + +const JSApiStreamInfoResponseType = "io.nats.jetstream.api.v1.stream_info_response" + +// JSApiNamesLimit is the maximum entries we will return for streams or consumers lists. +// TODO(dlc) - with header or request support could request chunked response. +const JSApiNamesLimit = 1024 +const JSApiListLimit = 256 + +type JSApiStreamNamesRequest struct { + ApiPagedRequest + // These are filters that can be applied to the list. + Subject string `json:"subject,omitempty"` +} + +// JSApiStreamNamesResponse list of streams. +// A nil request is valid and means all streams. +type JSApiStreamNamesResponse struct { + ApiResponse + ApiPaged + Streams []string `json:"streams"` +} + +const JSApiStreamNamesResponseType = "io.nats.jetstream.api.v1.stream_names_response" + +type JSApiStreamListRequest struct { + ApiPagedRequest + // These are filters that can be applied to the list. + Subject string `json:"subject,omitempty"` +} + +// JSApiStreamListResponse list of detailed stream information. +// A nil request is valid and means all streams. +type JSApiStreamListResponse struct { + ApiResponse + ApiPaged + Streams []*StreamInfo `json:"streams"` + Missing []string `json:"missing,omitempty"` + Offline map[string]string `json:"offline,omitempty"` +} + +const JSApiStreamListResponseType = "io.nats.jetstream.api.v1.stream_list_response" + +// JSApiStreamPurgeRequest is optional request information to the purge API. +// Subject will filter the purge request to only messages that match the subject, which can have wildcards. +// Sequence will purge up to but not including this sequence and can be combined with subject filtering. +// Keep will specify how many messages to keep. This can also be combined with subject filtering. +// Note that Sequence and Keep are mutually exclusive, so both can not be set at the same time. +type JSApiStreamPurgeRequest struct { + // Purge up to but not including sequence. + Sequence uint64 `json:"seq,omitempty"` + // Subject to match against messages for the purge command. + Subject string `json:"filter,omitempty"` + // Number of messages to keep. + Keep uint64 `json:"keep,omitempty"` +} + +type JSApiStreamPurgeResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` + Purged uint64 `json:"purged"` +} + +const JSApiStreamPurgeResponseType = "io.nats.jetstream.api.v1.stream_purge_response" + +type JSApiConsumerUnpinRequest struct { + Group string `json:"group"` +} + +type JSApiConsumerUnpinResponse struct { + ApiResponse +} + +const JSApiConsumerUnpinResponseType = "io.nats.jetstream.api.v1.consumer_unpin_response" + +// JSApiStreamUpdateResponse for updating a stream. +type JSApiStreamUpdateResponse struct { + ApiResponse + *StreamInfo +} + +const JSApiStreamUpdateResponseType = "io.nats.jetstream.api.v1.stream_update_response" + +// JSApiMsgDeleteRequest delete message request. +type JSApiMsgDeleteRequest struct { + Seq uint64 `json:"seq"` + NoErase bool `json:"no_erase,omitempty"` +} + +type JSApiMsgDeleteResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiMsgDeleteResponseType = "io.nats.jetstream.api.v1.stream_msg_delete_response" + +type JSApiStreamSnapshotRequest struct { + // Subject to deliver the chunks to for the snapshot. + DeliverSubject string `json:"deliver_subject"` + // Do not include consumers in the snapshot. + NoConsumers bool `json:"no_consumers,omitempty"` + // Optional chunk size preference. Defaults to 128KB, + // automatically clamped to within the range 1KB to 1MB. + // A smaller chunk size means more in-flight messages + // and more acks needed. Links with good throughput + // but high latency may need to increase this. + ChunkSize int `json:"chunk_size,omitempty"` + // Optional window size preference. Defaults to 8MB, + // automatically clamped to within the range 1KB to 32MB. + // very slow connections may need to reduce this to + // avoid slow consumer issues. + WindowSize int `json:"window_size,omitempty"` + // Check all message's checksums prior to snapshot. + CheckMsgs bool `json:"jsck,omitempty"` +} + +// JSApiStreamSnapshotResponse is the direct response to the snapshot request. +type JSApiStreamSnapshotResponse struct { + ApiResponse + // Configuration of the given stream. + Config *StreamConfig `json:"config,omitempty"` + // Current State for the given stream. + State *StreamState `json:"state,omitempty"` +} + +const JSApiStreamSnapshotResponseType = "io.nats.jetstream.api.v1.stream_snapshot_response" + +// JSApiStreamRestoreRequest is the required restore request. +type JSApiStreamRestoreRequest struct { + // Configuration of the given stream. + Config StreamConfig `json:"config"` + // Current State for the given stream. + State StreamState `json:"state"` +} + +// JSApiStreamRestoreResponse is the direct response to the restore request. +type JSApiStreamRestoreResponse struct { + ApiResponse + // Subject to deliver the chunks to for the snapshot restore. + DeliverSubject string `json:"deliver_subject"` +} + +const JSApiStreamRestoreResponseType = "io.nats.jetstream.api.v1.stream_restore_response" + +// JSApiStreamRemovePeerRequest is the required remove peer request. +type JSApiStreamRemovePeerRequest struct { + // Server name or peer ID of the peer to be removed. + Peer string `json:"peer"` +} + +// JSApiStreamRemovePeerResponse is the response to a remove peer request. +type JSApiStreamRemovePeerResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiStreamRemovePeerResponseType = "io.nats.jetstream.api.v1.stream_remove_peer_response" + +// JSApiStreamEvacuatePeerRequest is the required evacuate peer request. +type JSApiStreamEvacuatePeerRequest struct { + // Server name or peer ID of the peer to be evacuated. + Peer string `json:"peer"` +} + +// JSApiStreamEvacuatePeerResponse is the response to an evacuate peer request. +type JSApiStreamEvacuatePeerResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiStreamEvacuatePeerResponseType = "io.nats.jetstream.api.v1.stream_evacuate_peer_response" + +// JSApiStreamLeaderStepDownResponse is the response to a leader stepdown request. +type JSApiStreamLeaderStepDownResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiStreamLeaderStepDownResponseType = "io.nats.jetstream.api.v1.stream_leader_stepdown_response" + +// JSApiConsumerLeaderStepDownResponse is the response to a consumer leader stepdown request. +type JSApiConsumerLeaderStepDownResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiConsumerLeaderStepDownResponseType = "io.nats.jetstream.api.v1.consumer_leader_stepdown_response" + +// JSApiConsumerRemovePeerRequest is the required remove peer request for a single consumer. +type JSApiConsumerRemovePeerRequest struct { + // Server name or peer ID of the peer to be removed. + Peer string `json:"peer"` +} + +// JSApiConsumerRemovePeerResponse is the response to a consumer remove peer request. +type JSApiConsumerRemovePeerResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiConsumerRemovePeerResponseType = "io.nats.jetstream.api.v1.consumer_remove_peer_response" + +// JSApiConsumerEvacuatePeerRequest is the required evacuate peer request for a single consumer. +type JSApiConsumerEvacuatePeerRequest struct { + // Server name or peer ID of the peer to be evacuated. + Peer string `json:"peer"` +} + +// JSApiConsumerEvacuatePeerResponse is the response to a consumer evacuate peer request. +type JSApiConsumerEvacuatePeerResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiConsumerEvacuatePeerResponseType = "io.nats.jetstream.api.v1.consumer_evacuate_peer_response" + +// JSApiLeaderStepdownRequest allows placement control over the meta leader placement. +type JSApiLeaderStepdownRequest struct { + Placement *Placement `json:"placement,omitempty"` +} + +// JSApiLeaderStepDownResponse is the response to a meta leader stepdown request. +type JSApiLeaderStepDownResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiLeaderStepDownResponseType = "io.nats.jetstream.api.v1.meta_leader_stepdown_response" + +// JSApiMetaServerRemoveRequest will remove a peer from the meta group. +type JSApiMetaServerRemoveRequest struct { + // Server name of the peer to be removed. + Server string `json:"peer"` + // Peer ID of the peer to be removed. If specified this is used + // instead of the server name. + Peer string `json:"peer_id,omitempty"` +} + +// JSApiMetaServerRemoveResponse is the response to a peer removal request in the meta group. +type JSApiMetaServerRemoveResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiMetaServerRemoveResponseType = "io.nats.jetstream.api.v1.meta_server_remove_response" + +// JSApiMetaServerEvacuateRequest will evacuate a peer from the meta group. +type JSApiMetaServerEvacuateRequest struct { + // Server name of the peer to be evacuated. + Server string `json:"peer,omitempty"` + // Peer ID of the peer to be evacuated. If specified this is used + // instead of the server name. + Peer string `json:"peer_id,omitempty"` +} + +// JSApiMetaServerEvacuateResponse is the response to a peer evacuation request in the meta group. +type JSApiMetaServerEvacuateResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiMetaServerEvacuateResponseType = "io.nats.jetstream.api.v1.meta_server_evacuate_response" + +// JSApiMetaRescueRequest will unsafely lower the meta group's quorum requirement +// on the receiving server for disaster recovery. +type JSApiMetaRescueRequest struct { + // The new, temporarily lowered, quorum size the receiving servers should + // apply to the meta group. Must be at least 1 and no larger than the + // receiving server's current effective quorum. + QuorumNeeded int `json:"quorum_needed"` +} + +// JSApiMetaRescueResponse is the response to a meta rescue request. Since the +// request is a broadcast, each online server responds independently. +type JSApiMetaRescueResponse struct { + ApiResponse + // Server name of the responding server. + Server string `json:"server"` + // Server ID of the responding server. + ServerID string `json:"server_id"` + // The effective quorum before the rescue was applied. + PrevQuorum int `json:"prev_quorum,omitempty"` + // The effective quorum after the rescue was applied. + NewQuorum int `json:"new_quorum,omitempty"` +} + +const JSApiMetaRescueResponseType = "io.nats.jetstream.api.v1.meta_rescue_response" + +// JSApiMetaServerStreamMoveRequest will move a stream on a server to another +// response to this will come as JSApiStreamUpdateResponse/JSApiStreamUpdateResponseType +type JSApiMetaServerStreamMoveRequest struct { + // Server name of the peer to be evacuated. + Server string `json:"server,omitempty"` + // Cluster the server is in + Cluster string `json:"cluster,omitempty"` + // Domain the sever is in + Domain string `json:"domain,omitempty"` + // Ephemeral placement tags for the move + Tags []string `json:"tags,omitempty"` +} + +const JSApiAccountPurgeResponseType = "io.nats.jetstream.api.v1.account_purge_response" + +// JSApiAccountPurgeResponse is the response to a purge request in the meta group. +type JSApiAccountPurgeResponse struct { + ApiResponse + Initiated bool `json:"initiated,omitempty"` +} + +// JSApiMsgGetRequest get a message request. +type JSApiMsgGetRequest struct { + Seq uint64 `json:"seq,omitempty"` + LastFor string `json:"last_by_subj,omitempty"` + NextFor string `json:"next_by_subj,omitempty"` + + // Batch support. Used to request more than one msg at a time. + // Can be used with simple starting seq, but also NextFor with wildcards. + Batch int `json:"batch,omitempty"` + // This will make sure we limit how much data we blast out. If not set we will + // inherit the slow consumer default max setting of the server. Default is MAX_PENDING_SIZE. + MaxBytes int `json:"max_bytes,omitempty"` + // Return messages as of this start time. + StartTime *time.Time `json:"start_time,omitempty"` + + // Multiple response support. Will get the last msgs matching the subjects. These can include wildcards. + MultiLastFor []string `json:"multi_last,omitempty"` + // Only return messages up to this sequence. If not set, will be last sequence for the stream. + UpToSeq uint64 `json:"up_to_seq,omitempty"` + // Only return messages up to this time. + UpToTime *time.Time `json:"up_to_time,omitempty"` + // Only return the message payload, excluding headers if present. + NoHeaders bool `json:"no_hdr,omitempty"` +} + +type JSApiMsgGetResponse struct { + ApiResponse + Message *StoredMsg `json:"message,omitempty"` +} + +const JSApiMsgGetResponseType = "io.nats.jetstream.api.v1.stream_msg_get_response" + +// JSWaitQueueDefaultMax is the default max number of outstanding requests for pull consumers. +const JSWaitQueueDefaultMax = 512 + +type JSApiConsumerCreateResponse struct { + ApiResponse + *ConsumerInfo +} + +const JSApiConsumerCreateResponseType = "io.nats.jetstream.api.v1.consumer_create_response" + +type JSApiConsumerDeleteResponse struct { + ApiResponse + Success bool `json:"success,omitempty"` +} + +const JSApiConsumerDeleteResponseType = "io.nats.jetstream.api.v1.consumer_delete_response" + +type JSApiConsumerPauseRequest struct { + PauseUntil time.Time `json:"pause_until,omitempty"` +} + +const JSApiConsumerPauseResponseType = "io.nats.jetstream.api.v1.consumer_pause_response" + +type JSApiConsumerPauseResponse struct { + ApiResponse + Paused bool `json:"paused"` + PauseUntil time.Time `json:"pause_until"` + PauseRemaining time.Duration `json:"pause_remaining,omitempty"` +} + +type JSApiConsumerInfoResponse struct { + ApiResponse + *ConsumerInfo +} + +const JSApiConsumerInfoResponseType = "io.nats.jetstream.api.v1.consumer_info_response" + +type JSApiConsumersRequest struct { + ApiPagedRequest +} + +type JSApiConsumerNamesResponse struct { + ApiResponse + ApiPaged + Consumers []string `json:"consumers"` +} + +const JSApiConsumerNamesResponseType = "io.nats.jetstream.api.v1.consumer_names_response" + +type JSApiConsumerListResponse struct { + ApiResponse + ApiPaged + Consumers []*ConsumerInfo `json:"consumers"` + Missing []string `json:"missing,omitempty"` + Offline map[string]string `json:"offline,omitempty"` +} + +const JSApiConsumerListResponseType = "io.nats.jetstream.api.v1.consumer_list_response" + +// JSApiConsumerGetNextRequest is for getting next messages for pull based consumers. +type JSApiConsumerGetNextRequest struct { + Expires time.Duration `json:"expires,omitempty"` + Batch int `json:"batch,omitempty"` + MaxBytes int `json:"max_bytes,omitempty"` + NoWait bool `json:"no_wait,omitempty"` + Heartbeat time.Duration `json:"idle_heartbeat,omitempty"` + PriorityGroup +} + +// JSApiConsumerResetRequest is for resetting a consumer to a specific sequence. +type JSApiConsumerResetRequest struct { + Seq uint64 `json:"seq,omitempty"` +} + +// JSApiConsumerResetResponse is a superset of JSApiConsumerCreateResponse, but including an explicit ResetSeq. +type JSApiConsumerResetResponse struct { + ApiResponse + *ConsumerInfo + ResetSeq uint64 `json:"reset_seq"` +} + +const JSApiConsumerResetResponseType = "io.nats.jetstream.api.v1.consumer_reset_response" + +// Structure that holds state for a JetStream API request that is processed +// in a separate long-lived go routine. This is to avoid blocking connections. +type jsAPIRoutedReq struct { + jsub *subscription + sub *subscription + acc *Account + subject string + reply string + msg []byte + pa pubArg +} + +func (js *jetStream) apiDispatch(sub *subscription, c *client, acc *Account, subject, reply string, rmsg []byte) { + // Ignore system level directives meta stepdown, peer remove and meta rescue requests here. + if subject == JSApiLeaderStepDown || + subject == JSApiRemoveServer || + subject == JSApiEvacuateServer || + subject == JSApiMetaRescue || + strings.HasPrefix(subject, jsAPIAccountPre) { + return + } + // No lock needed, those are immutable. + s, rr := js.srv, js.apiSubs.Match(subject) + + hdr, msg := c.msgParts(rmsg) + if len(sliceHeader(ClientInfoHdr, hdr)) == 0 { + // Check if this is the system account. We will let these through for the account info only. + sacc := s.SystemAccount() + if sacc != acc { + return + } + if subject != JSApiAccountInfo { + // Only respond from the initial server entry to the NATS system. + if c.kind == CLIENT || c.kind == LEAF { + var resp = ApiResponse{ + Type: JSApiSystemResponseType, + Error: NewJSNotEnabledForAccountError(), + } + s.sendAPIErrResponse(nil, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + } + + // Short circuit for no interest. + if len(rr.psubs)+len(rr.qsubs) == 0 { + if (c.kind == CLIENT || c.kind == LEAF) && acc != s.SystemAccount() { + ci, acc, _, _, _ := s.getRequestInfo(c, rmsg) + var resp = ApiResponse{ + Type: JSApiSystemResponseType, + Error: NewJSBadRequestError(), + } + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + // We should only have psubs and only 1 per result. + if len(rr.psubs) != 1 { + s.Warnf("Malformed JetStream API Request: [%s] %q", subject, rmsg) + if c.kind == CLIENT || c.kind == LEAF { + ci, acc, _, _, _ := s.getRequestInfo(c, rmsg) + var resp = ApiResponse{ + Type: JSApiSystemResponseType, + Error: NewJSBadRequestError(), + } + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + jsub := rr.psubs[0] + + // We need to make sure not to block. We will send the request to a long-lived + // pool of go routines. + + // Increment inflight. Do this before queueing. + atomic.AddInt64(&js.apiInflight, 1) + + // Copy the state. Note the JSAPI only uses the hdr index to piece apart the + // header from the msg body. No other references are needed. + // Check pending and warn if getting backed up. + var queue *ipQueue[*jsAPIRoutedReq] + var limit int64 + if js.infoSubs.HasInterest(subject) { + queue = s.jsAPIRoutedInfoReqs + limit = atomic.LoadInt64(&js.infoQueueLimit) + } else { + queue = s.jsAPIRoutedReqs + limit = atomic.LoadInt64(&js.queueLimit) + } + pending, _ := queue.push(&jsAPIRoutedReq{jsub, sub, acc, subject, reply, copyBytes(rmsg), c.pa}) + if pending >= int(limit) { + s.rateLimitFormatWarnf("%s limit reached, dropping %d requests", queue.name, pending) + drained := int64(queue.drain()) + atomic.AddInt64(&js.apiInflight, -drained) + + s.publishAdvisory(nil, JSAdvisoryAPILimitReached, JSAPILimitReachedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSAPILimitReachedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Server: s.Name(), + Domain: js.config.Domain, + Dropped: drained, + }) + } +} + +func (s *Server) processJSAPIRoutedRequests() { + defer s.grWG.Done() + + s.mu.RLock() + queue, infoqueue := s.jsAPIRoutedReqs, s.jsAPIRoutedInfoReqs + client := &client{srv: s, kind: JETSTREAM} + s.mu.RUnlock() + + js := s.getJetStream() + + processFromQueue := func(ipq *ipQueue[*jsAPIRoutedReq]) { + // Only pop one item at a time here, otherwise if the system is recovering + // from queue buildup, then one worker will pull off all the tasks and the + // others will be starved of work. + if r, ok := ipq.popOne(); ok && r != nil { + client.pa = r.pa + start := time.Now() + r.jsub.icb(r.sub, client, r.acc, r.subject, r.reply, r.msg) + if dur := time.Since(start); dur >= readLoopReportThreshold { + s.Warnf("Internal subscription on %q took too long: %v", r.subject, dur) + } + atomic.AddInt64(&js.apiInflight, -1) + } + } + + for { + // First select case is prioritizing queue, we will only fall through + // to the second select case that considers infoqueue if queue is empty. + // This effectively means infos are deprioritized. + select { + case <-queue.ch: + processFromQueue(queue) + case <-s.quitCh: + return + default: + select { + case <-infoqueue.ch: + processFromQueue(infoqueue) + case <-queue.ch: + processFromQueue(queue) + case <-s.quitCh: + return + } + } + } +} + +func (s *Server) setJetStreamExportSubs() error { + js := s.getJetStream() + if js == nil { + return NewJSNotEnabledError() + } + + // Start the go routine that will process API requests received by the + // subscription below when they are coming from routes, etc.. + const maxProcs = 16 + mp := runtime.GOMAXPROCS(0) + // Cap at 16 max for now on larger core setups. + if mp > maxProcs { + mp = maxProcs + } + s.jsAPIRoutedReqs = newIPQueue[*jsAPIRoutedReq](s, "JetStream API queue") + s.jsAPIRoutedInfoReqs = newIPQueue[*jsAPIRoutedReq](s, "JetStream API info queue") + for i := 0; i < mp; i++ { + s.startGoRoutine(s.processJSAPIRoutedRequests) + } + + // This is the catch all now for all JetStream API calls. + if _, err := s.sysSubscribe(jsAllAPI, js.apiDispatch); err != nil { + return err + } + + if err := s.SystemAccount().AddServiceExport(jsAllAPI, nil); err != nil { + s.Warnf("Error setting up jetstream service exports: %v", err) + return err + } + + // API handles themselves. + // infopairs are deprioritized compared to pairs in processJSAPIRoutedRequests. + pairs := []struct { + subject string + handler msgHandler + }{ + {JSApiStreamCreate, s.jsStreamCreateRequest}, + {JSApiStreamUpdate, s.jsStreamUpdateRequest}, + {JSApiStreamDelete, s.jsStreamDeleteRequest}, + {JSApiStreamPurge, s.jsStreamPurgeRequest}, + {JSApiStreamSnapshot, s.jsStreamSnapshotRequest}, + {JSApiStreamRestore, s.jsStreamRestoreRequest}, + {JSApiStreamRemovePeer, s.jsStreamRemovePeerRequest}, + {JSApiStreamEvacuatePeer, s.jsStreamEvacuatePeerRequest}, + {JSApiStreamCancelMove, s.jsStreamCancelMoveRequest}, + {JSApiStreamLeaderStepDown, s.jsStreamLeaderStepDownRequest}, + {JSApiConsumerLeaderStepDown, s.jsConsumerLeaderStepDownRequest}, + {JSApiConsumerRemovePeer, s.jsConsumerRemovePeerRequest}, + {JSApiConsumerEvacuatePeer, s.jsConsumerEvacuatePeerRequest}, + {JSApiMsgDelete, s.jsMsgDeleteRequest}, + {JSApiMsgGet, s.jsMsgGetRequest}, + {JSApiConsumerCreateEx, s.jsConsumerCreateRequest}, + {JSApiConsumerCreate, s.jsConsumerCreateRequest}, + {JSApiDurableCreate, s.jsConsumerCreateRequest}, + {JSApiConsumerDelete, s.jsConsumerDeleteRequest}, + {JSApiConsumerPause, s.jsConsumerPauseRequest}, + {JSApiConsumerUnpin, s.jsConsumerUnpinRequest}, + } + infopairs := []struct { + subject string + handler msgHandler + }{ + {JSApiAccountInfo, s.jsAccountInfoRequest}, + {JSApiStreams, s.jsStreamNamesRequest}, + {JSApiStreamList, s.jsStreamListRequest}, + {JSApiStreamInfo, s.jsStreamInfoRequest}, + {JSApiConsumers, s.jsConsumerNamesRequest}, + {JSApiConsumerList, s.jsConsumerListRequest}, + {JSApiConsumerInfo, s.jsConsumerInfoRequest}, + } + + js.mu.Lock() + defer js.mu.Unlock() + + // As well as populating js.apiSubs for the dispatch function to use, we + // will also populate js.infoSubs, so that the dispatch function can + // decide quickly whether or not the request is an info request or not. + for _, p := range append(infopairs, pairs...) { + sub := &subscription{subject: []byte(p.subject), icb: p.handler} + if err := js.apiSubs.Insert(sub); err != nil { + return err + } + } + for _, p := range infopairs { + if err := js.infoSubs.Insert(p.subject, struct{}{}); err != nil { + return err + } + } + + return nil +} + +func (s *Server) sendAPIResponse(ci *ClientInfo, acc *Account, subject, reply, request, response string) { + acc.trackAPI() + if reply != _EMPTY_ { + s.sendInternalAccountMsg(nil, reply, response) + } + s.sendJetStreamAPIAuditAdvisory(ci, acc, subject, request, response) +} + +func (s *Server) sendAPIHdrResponse(ci *ClientInfo, acc *Account, subject, reply, request string, hdr []byte, response string) { + acc.trackAPI() + if reply != _EMPTY_ { + s.sendInternalAccountMsgWithReply(nil, reply, _EMPTY_, hdr, response, false) + } + s.sendJetStreamAPIAuditAdvisory(ci, acc, subject, request, response) +} + +func (s *Server) sendAPIErrResponse(ci *ClientInfo, acc *Account, subject, reply, request, response string) { + acc.trackAPIErr() + if reply != _EMPTY_ { + s.sendInternalAccountMsg(nil, reply, response) + } + s.sendJetStreamAPIAuditAdvisory(ci, acc, subject, request, response) +} + +const errRespDelay = 500 * time.Millisecond + +type delayedAPIResponse struct { + ci *ClientInfo + acc *Account + subject string + reply string + request string + hdr []byte + response string + rg *raftGroup + deadline time.Time + noJs bool + next *delayedAPIResponse +} + +// Add `r` in the list that is maintained ordered by the `delayedAPIResponse.deadline` time. +func addDelayedResponse(head, tail **delayedAPIResponse, r *delayedAPIResponse) { + // Check if list empty. + if *head == nil { + *head, *tail = r, r + return + } + // Check if it should be added at the end, which is if after or equal to the tail. + if r.deadline.After((*tail).deadline) || r.deadline.Equal((*tail).deadline) { + (*tail).next, *tail = r, r + return + } + // Find its spot in the list. + var prev *delayedAPIResponse + for c := *head; c != nil; c = c.next { + // We insert only if we are stricly before the current `c`. + if r.deadline.Before(c.deadline) { + r.next = c + if prev != nil { + prev.next = r + } else { + *head = r + } + return + } + prev = c + } +} + +func (s *Server) delayedAPIResponder() { + defer s.grWG.Done() + var ( + head, tail *delayedAPIResponse // Linked list. + r *delayedAPIResponse // Updated by calling next(). + rqch <-chan struct{} // Quit channel of the Raft group (if present). + tm = time.NewTimer(time.Hour) + ) + next := func() { + r, rqch = nil, nil + // Check that JetStream is still on. Do not exit the go routine + // since JS can be enabled/disabled. The go routine will exit + // only if server is shutdown. + js := s.getJetStream() + if js == nil { + // Reset head and tail here. Also drain the ipQueue. + head, tail = nil, nil + s.delayedAPIResponses.drain() + // Fall back into next "if" that resets timer. + } + // If there are no delayed messages then delay the timer for + // a while. + if head == nil { + tm.Reset(time.Hour) + return + } + // Get the first expected message and then reset the timer. + r = head + js.mu.RLock() + if r.rg != nil && r.rg.node != nil { + // If there's an attached Raft group to the delayed response + // then pull out the quit channel, so that we don't bother + // sending responses for entities which are now no longer + // running. + rqch = r.rg.node.QuitC() + } + js.mu.RUnlock() + tm.Reset(time.Until(r.deadline)) + } + pop := func() { + if head == nil { + return + } + head = head.next + if head == nil { + tail = nil + } + } + for { + select { + case <-s.delayedAPIResponses.ch: + v, ok := s.delayedAPIResponses.popOne() + if !ok { + continue + } + // Add it to the list, and if ends up being the head, set things up. + addDelayedResponse(&head, &tail, v) + if v == head { + next() + } + case <-s.quitCh: + return + case <-rqch: + // If we were the head, drop and setup things for next. + if r != nil && r == head { + pop() + } + next() + case <-tm.C: + if r != nil { + // If it's not a JS API error, send it as a raw response without additional API/audit tracking. + if r.noJs { + s.sendInternalAccountMsgWithReply(r.acc, r.subject, _EMPTY_, r.hdr, r.response, false) + } else { + s.sendAPIErrResponse(r.ci, r.acc, r.subject, r.reply, r.request, r.response) + } + pop() + } + next() + } + } +} + +func (s *Server) sendDelayedAPIErrResponse(ci *ClientInfo, acc *Account, subject, reply, request, response string, rg *raftGroup, duration time.Duration) { + s.delayedAPIResponses.push(&delayedAPIResponse{ + ci, acc, subject, reply, request, nil, response, rg, time.Now().Add(duration), false, nil, + }) +} + +func (s *Server) sendDelayedErrResponse(acc *Account, subject string, hdr []byte, response string, duration time.Duration) { + s.delayedAPIResponses.push(&delayedAPIResponse{ + nil, acc, subject, _EMPTY_, _EMPTY_, hdr, response, nil, time.Now().Add(duration), true, nil, + }) +} + +func (s *Server) getRequestInfo(c *client, raw []byte) (pci *ClientInfo, acc *Account, hdr, msg []byte, err error) { + hdr, msg = c.msgParts(raw) + var ci ClientInfo + + if len(hdr) > 0 { + if err := json.Unmarshal(sliceHeader(ClientInfoHdr, hdr), &ci); err != nil { + return nil, nil, nil, nil, err + } + } + + if ci.Service != _EMPTY_ { + acc, _ = s.LookupAccount(ci.Service) + } else if ci.Account != _EMPTY_ { + acc, _ = s.LookupAccount(ci.Account) + } else { + // Direct $SYS access. + acc = c.acc + if acc == nil { + acc = s.SystemAccount() + } + } + if acc == nil { + return nil, nil, nil, nil, ErrMissingAccount + } + return &ci, acc, hdr, msg, nil +} + +func (s *Server) unmarshalRequest(c *client, acc *Account, subject string, msg []byte, v any) error { + decoder := json.NewDecoder(bytes.NewReader(msg)) + decoder.DisallowUnknownFields() + + for { + if err := decoder.Decode(v); err != nil { + if err == io.EOF { + return nil + } + + var syntaxErr *json.SyntaxError + if errors.As(err, &syntaxErr) { + err = fmt.Errorf("%w at offset %d", err, syntaxErr.Offset) + } + + c.RateLimitWarnf("Invalid JetStream request '%s > %s': %s", acc, subject, err) + + if js := s.getJetStream(); js != nil && js.config.Strict { + return err + } + + return json.Unmarshal(msg, v) + } + } +} + +func (a *Account) trackAPI() { + a.mu.RLock() + jsa := a.js + a.mu.RUnlock() + if jsa != nil { + jsa.usageMu.Lock() + jsa.usageApi++ + jsa.apiTotal++ + jsa.sendClusterUsageUpdate() + atomic.AddInt64(&jsa.js.apiTotal, 1) + jsa.usageMu.Unlock() + } +} + +func (a *Account) trackAPIErr() { + a.mu.RLock() + jsa := a.js + a.mu.RUnlock() + if jsa != nil { + jsa.usageMu.Lock() + jsa.usageApi++ + jsa.apiTotal++ + jsa.usageErr++ + jsa.apiErrors++ + jsa.sendClusterUsageUpdate() + atomic.AddInt64(&jsa.js.apiTotal, 1) + atomic.AddInt64(&jsa.js.apiErrors, 1) + jsa.usageMu.Unlock() + } +} + +const badAPIRequestT = "Malformed JetStream API Request: %q" + +// Helper function to check on JetStream being enabled but also on status of leafnodes +// If the local account is not enabled but does have leafnode connectivity we will not +// want to error immediately and let the other side decide. +func (a *Account) checkJetStream() (enabled, shouldError bool) { + a.mu.RLock() + defer a.mu.RUnlock() + return a.js != nil, a.nleafs+a.nrleafs == 0 +} + +// Request for current usage and limits for this account. +func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiAccountInfoResponse{ApiResponse: ApiResponse{Type: JSApiAccountInfoResponseType}} + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if !doErr { + return + } + resp.Error = NewJSNotEnabledForAccountError() + } else { + stats := acc.JetStreamUsage() + resp.JetStreamAccountStats = &stats + } + b, err := json.Marshal(resp) + if err != nil { + return + } + + s.sendAPIResponse(ci, acc, subject, reply, string(msg), string(b)) +} + +// Helpers for token extraction. +func streamNameFromSubject(subject string) string { + return tokenAt(subject, 5) +} + +func consumerNameFromSubject(subject string) string { + return tokenAt(subject, 6) +} + +func (s *Server) jsonResponse(v any) string { + b, err := json.Marshal(v) + if err != nil { + s.Warnf("Problem marshaling JSON for JetStream API:", err) + return "" + } + return string(b) +} + +// Read lock must be held +func (jsa *jsAccount) tieredReservation(tier string, cfg *StreamConfig) int64 { + var reservation int64 + for _, mset := range jsa.streams { + mset.cfgMu.RLock() + name, storage, replicas, maxBytes := mset.cfg.Name, mset.cfg.Storage, mset.cfg.Replicas, mset.cfg.MaxBytes + mset.cfgMu.RUnlock() + // Don't count the stream toward the limit if it already exists. + if name == cfg.Name { + continue + } + if (tier == _EMPTY_ || isSameTier(replicas, cfg.Replicas)) && maxBytes > 0 && storage == cfg.Storage { + reservation = addSaturate(reservation, accountReservation(tier, replicas, maxBytes)) + } + } + return reservation +} + +// Request to create a stream. +func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + var cfg StreamConfigRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &cfg); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Initialize asset version metadata. + setStaticStreamMetadata(&cfg.StreamConfig) + + streamName := streamNameFromSubject(subject) + if streamName != cfg.Name { + resp.Error = NewJSStreamMismatchError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Check for path like separators in the name. + if strings.ContainsAny(streamName, `\/`) { + resp.Error = NewJSStreamNameContainsPathSeparatorsError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Can't create a stream with a sealed state. + if cfg.Sealed { + resp.Error = NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration for create can not be sealed")) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // If we are told to do mirror direct but are not mirroring, error. + if cfg.MirrorDirect && cfg.Mirror == nil { + resp.Error = NewJSStreamInvalidConfigError(fmt.Errorf("stream has no mirror but does have mirror direct")) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Hand off to cluster for processing. + if s.JetStreamIsClustered() { + s.jsClusteredStreamRequest(ci, acc, subject, reply, rmsg, &cfg) + return + } + + if err := acc.jsNonClusteredStreamLimitsCheck(&cfg.StreamConfig); err != nil { + resp.Error = err + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + mset, err := acc.addStreamPedantic(&cfg.StreamConfig, cfg.Pedantic) + if err != nil { + if IsNatsErr(err, JSStreamStoreFailedF) { + s.Warnf("Stream create failed for '%s > %s': %v", acc, streamName, err) + err = errStreamStoreFailed + } + resp.Error = NewJSStreamCreateError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + msetCfg := mset.config() + resp.StreamInfo = &StreamInfo{ + Created: mset.createdTime(), + State: mset.state(), + Config: *setDynamicStreamMetadata(&msetCfg), + TimeStamp: time.Now().UTC(), + Mirror: mset.mirrorInfo(), + Sources: mset.sourcesInfo(), + } + resp.DidCreate = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to update a stream. +func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + var ncfg StreamConfigRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &ncfg); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + cfg, apiErr := s.checkStreamCfg(&ncfg.StreamConfig, acc, ncfg.Pedantic) + if apiErr != nil { + resp.Error = apiErr + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + streamName := streamNameFromSubject(subject) + if streamName != cfg.Name { + resp.Error = NewJSStreamMismatchError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Handle clustered version here. + if s.JetStreamIsClustered() { + s.jsClusteredStreamUpdateRequest(ci, acc, subject, reply, copyBytes(rmsg), &cfg, ncfg.Pedantic) + return + } + + mset, err := acc.lookupStream(streamName) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if mset.offlineReason != _EMPTY_ { + resp.Error = NewJSStreamOfflineReasonError(errors.New(mset.offlineReason)) + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + + // Update asset version metadata. + setStaticStreamMetadata(&cfg) + + if err := mset.updatePedantic(&cfg, ncfg.Pedantic); err != nil { + resp.Error = NewJSStreamUpdateError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + msetCfg := mset.config() + resp.StreamInfo = &StreamInfo{ + Created: mset.createdTime(), + State: mset.state(), + Config: *setDynamicStreamMetadata(&msetCfg), + Domain: s.getOpts().JetStreamDomain, + Mirror: mset.mirrorInfo(), + Sources: mset.sourcesInfo(), + TimeStamp: time.Now().UTC(), + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request for the list of all stream names. +func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiStreamNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamNamesResponseType}} + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + var offset int + var filter string + + if isJSONObjectOrArray(msg) { + var req JSApiStreamNamesRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + offset = max(req.Offset, 0) + if req.Subject != _EMPTY_ { + filter = req.Subject + } + } + + // TODO(dlc) - Maybe hold these results for large results that we expect to be paged. + // TODO(dlc) - If this list is long maybe do this in a Go routine? + var numStreams int + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + // TODO(dlc) - Debug or Warn? + return + } + js.mu.RLock() + for stream, sa := range cc.streams[acc.Name] { + if IsNatsErr(sa.err, JSClusterNotAssignedErr) { + continue + } + if filter != _EMPTY_ { + // These could not have subjects auto-filled in since they are raw and unprocessed. + if len(sa.Config.Subjects) == 0 { + if SubjectsCollide(filter, sa.Config.Name) { + resp.Streams = append(resp.Streams, stream) + } + } else { + for _, subj := range sa.Config.Subjects { + if SubjectsCollide(filter, subj) { + resp.Streams = append(resp.Streams, stream) + break + } + } + } + } else { + resp.Streams = append(resp.Streams, stream) + } + } + js.mu.RUnlock() + if len(resp.Streams) > 1 { + slices.Sort(resp.Streams) + } + numStreams = len(resp.Streams) + if offset > numStreams { + offset = numStreams + } + if offset > 0 { + resp.Streams = resp.Streams[offset:] + } + if len(resp.Streams) > JSApiNamesLimit { + resp.Streams = resp.Streams[:JSApiNamesLimit] + } + } else { + // Snapshot names once to avoid repeated cfgMu RLocks during sort+append. + msets := acc.filteredStreams(filter) + names := make([]string, len(msets)) + for i, mset := range msets { + names[i] = mset.getCfgName() + } + if len(names) > 1 { + slices.Sort(names) + } + + numStreams = len(names) + if offset > numStreams { + offset = numStreams + } + + for _, name := range names[offset:] { + resp.Streams = append(resp.Streams, name) + if len(resp.Streams) >= JSApiNamesLimit { + break + } + } + } + resp.Total = numStreams + resp.Limit = JSApiNamesLimit + resp.Offset = offset + + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request for the list of all detailed stream info. +// TODO(dlc) - combine with above long term +func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiStreamListResponse{ + ApiResponse: ApiResponse{Type: JSApiStreamListResponseType}, + Streams: []*StreamInfo{}, + } + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + var offset int + var filter string + + if isJSONObjectOrArray(msg) { + var req JSApiStreamListRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + offset = max(req.Offset, 0) + if req.Subject != _EMPTY_ { + filter = req.Subject + } + } + + // Clustered mode will invoke a scatter and gather. + if s.JetStreamIsClustered() { + // Need to copy these off before sending.. don't move this inside startGoRoutine!!! + msg = copyBytes(msg) + s.startGoRoutine(func() { s.jsClusteredStreamListRequest(acc, ci, filter, offset, subject, reply, msg) }) + return + } + + // TODO(dlc) - Maybe hold these results for large results that we expect to be paged. + // TODO(dlc) - If this list is long maybe do this in a Go routine? + var msets []*stream + if filter == _EMPTY_ { + msets = acc.streams() + } else { + msets = acc.filteredStreams(filter) + } + + // Snapshot names once and sort the parallel slice to avoid repeated cfgMu RLocks. + type msetWithName struct { + mset *stream + name string + } + named := make([]msetWithName, len(msets)) + for i, mset := range msets { + named[i] = msetWithName{mset, mset.getCfgName()} + } + slices.SortFunc(named, func(a, b msetWithName) int { return cmp.Compare(a.name, b.name) }) + + scnt := len(named) + if offset > scnt { + offset = scnt + } + + var missingNames []string + for _, n := range named[offset:] { + mset, name := n.mset, n.name + if mset.offlineReason != _EMPTY_ { + if resp.Offline == nil { + resp.Offline = make(map[string]string, 1) + } + resp.Offline[name] = mset.offlineReason + missingNames = append(missingNames, name) + continue + } + + config := mset.config() + resp.Streams = append(resp.Streams, &StreamInfo{ + Created: mset.createdTime(), + State: mset.state(), + Config: config, + Domain: s.getOpts().JetStreamDomain, + Mirror: mset.mirrorInfo(), + Sources: mset.sourcesInfo(), + TimeStamp: time.Now().UTC(), + }) + if len(resp.Streams) >= JSApiListLimit { + break + } + } + resp.Total = scnt + resp.Limit = JSApiListLimit + resp.Offset = offset + resp.Missing = missingNames + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request for information about a stream. +func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, a *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + streamName := streamNameFromSubject(subject) + + var resp = JSApiStreamInfoResponse{ApiResponse: ApiResponse{Type: JSApiStreamInfoResponseType}} + + // If someone creates a duplicate stream that is identical we will get this request forwarded to us. + // Make sure the response type is for a create call. + if rt := getHeader(JSResponseType, hdr); len(rt) > 0 && string(rt) == jsCreateResponse { + resp.ApiResponse.Type = JSApiStreamCreateResponseType + } + + var clusterWideConsCount int + + js, cc := s.getJetStreamCluster() + if js == nil { + return + } + // If we are in clustered mode we need to be the stream leader to proceed. + if cc != nil { + // Check to make sure the stream is assigned. + js.mu.RLock() + isLeader, sa := cc.isLeader(), js.streamAssignment(acc.Name, streamName) + if sa == nil { + // Fallback for a stream that's being created. + sa = js.streamAssignmentOrInflight(acc.Name, streamName) + } + var offline bool + if sa != nil { + clusterWideConsCount = len(sa.consumers) + offline = s.allPeersOffline(sa.Group) + if sa.unsupported != nil && sa.Group != nil && cc.meta != nil && sa.Group.isMember(cc.meta.ID()) { + // If we're a member for this stream, and it's not supported, report it as offline. + resp.Error = NewJSStreamOfflineReasonError(errors.New(sa.unsupported.reason)) + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + js.mu.RUnlock() + return + } + } + js.mu.RUnlock() + + if isLeader && sa == nil { + // We can't find the stream, so mimic what would be the errors below. + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + // No stream present. + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } else if sa == nil { + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + // Delaying an error response gives the leader a chance to respond before us + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + } + return + } else if isLeader && offline { + resp.Error = NewJSStreamOfflineError() + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + + // Check to see if we are a member of the group and if the group has no leader. + isLeaderless := js.isGroupLeaderless(sa.Group) + + // We have the stream assigned and a leader, so only the stream leader should answer. + if !acc.JetStreamIsStreamLeader(streamName) && !isLeaderless { + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + // Delaying an error response gives the leader a chance to respond before us + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), sa.Group, errRespDelay) + return + } + + // We may be in process of electing a leader, but if this is a scale up from 1 we will still be the state leader + // while the new members work through the election and catchup process. + // Double check for that instead of exiting here and being silent. e.g. nats stream update test --replicas=3 + js.mu.RLock() + rg := sa.Group + var ourID string + if cc.meta != nil { + ourID = cc.meta.ID() + } + // We have seen cases where rg is nil at this point, + // so check explicitly and bail if that is the case. + bail := rg == nil || !rg.isMember(ourID) + if !bail { + // We know we are a member here, if this group is new and we are preferred allow us to answer. + // Also, we have seen cases where rg.node is nil at this point, + // so check explicitly and bail if that is the case. + bail = rg.Preferred != ourID || (rg.node != nil && time.Since(rg.node.Created()) > lostQuorumInterval) + } + js.mu.RUnlock() + if bail { + return + } + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + var details bool + var subjects string + var offset int + if isJSONObjectOrArray(msg) { + var req JSApiStreamInfoRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + details, subjects = req.DeletedDetails, req.SubjectsFilter + offset = max(req.Offset, 0) + } + + mset, err := acc.lookupStream(streamName) + // Error is not to be expected at this point, but could happen if same stream trying to be created. + if err != nil { + if cc != nil { + // This could be inflight, pause for a short bit and try again. + // This will not be inline, so ok. + time.Sleep(10 * time.Millisecond) + mset, err = acc.lookupStream(streamName) + } + // Check again. + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + if mset.offlineReason != _EMPTY_ { + resp.Error = NewJSStreamOfflineReasonError(errors.New(mset.offlineReason)) + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + + // Report the config as requested, the stream can still be running at its origin. + config := js.targetStreamConfig(mset, mset.config()) + resp.StreamInfo = &StreamInfo{ + Created: mset.createdTime(), + State: mset.stateWithDetail(details), + Config: *setDynamicStreamMetadata(&config), + Domain: s.getOpts().JetStreamDomain, + Cluster: js.clusterInfo(mset.raftGroup()), + Mirror: mset.mirrorInfo(), + Sources: mset.sourcesInfo(), + Alternates: js.streamAlternates(ci, config.Name), + TimeStamp: time.Now().UTC(), + } + if clusterWideConsCount > 0 { + resp.StreamInfo.State.Consumers = clusterWideConsCount + } + + // Check if they have asked for subject details. + if subjects != _EMPTY_ { + st := mset.store.SubjectsTotals(subjects) + if lst := len(st); lst > 0 { + // Common for both cases. + resp.Offset = offset + resp.Limit = JSMaxSubjectDetails + resp.Total = lst + + if offset == 0 && lst <= JSMaxSubjectDetails { + resp.StreamInfo.State.Subjects = st + } else { + // Here we have to filter list due to offset or maximum constraints. + subjs := make([]string, 0, len(st)) + for subj := range st { + subjs = append(subjs, subj) + } + // Sort it + slices.Sort(subjs) + + if offset > len(subjs) { + offset = len(subjs) + } + + end := offset + JSMaxSubjectDetails + if end > len(subjs) { + end = len(subjs) + } + actualSize := end - offset + var sd map[string]uint64 + + if actualSize > 0 { + sd = make(map[string]uint64, actualSize) + for _, ss := range subjs[offset:end] { + sd[ss] = st[ss] + } + } + resp.StreamInfo.State.Subjects = sd + } + } + } + // Check for out of band catchups. + if mset.hasCatchupPeers() { + mset.checkClusterInfo(resp.StreamInfo.Cluster) + } + + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to have a stream leader stepdown. +func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + // Have extra token for this one. + name := tokenAt(subject, 6) + + var resp = JSApiStreamLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiStreamLeaderStepDownResponseType}} + + // If we are not in clustered mode this is a failed request. + if !s.JetStreamIsClustered() { + resp.Error = NewJSClusterRequiredError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // If we are here we are clustered. See if we are the stream leader in order to proceed. + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.RLock() + isLeader, sa := cc.isLeader(), js.streamAssignment(acc.Name, name) + js.mu.RUnlock() + + if isLeader && sa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } else if sa == nil { + return + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + // Check to see if we are a member of the group and if the group has no leader. + if js.isGroupLeaderless(sa.Group) { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // We have the stream assigned and a leader, so only the stream leader should answer. + if !acc.JetStreamIsStreamLeader(name) { + return + } + + mset, err := acc.lookupStream(name) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if mset == nil { + resp.Success = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + node := mset.raftNode() + if node == nil { + resp.Success = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + var preferredLeader string + if isJSONObjectOrArray(msg) { + var req JSApiLeaderStepdownRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if preferredLeader, resp.Error = s.getStepDownPreferredPlacement(node, req.Placement); resp.Error != nil { + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + // Call actual stepdown. + err = node.StepDown(preferredLeader) + if err != nil { + resp.Error = NewJSRaftGeneralError(err, Unless(err)) + } else { + resp.Success = true + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to have a consumer leader stepdown. +func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiConsumerLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiConsumerLeaderStepDownResponseType}} + + // If we are not in clustered mode this is a failed request. + if !s.JetStreamIsClustered() { + resp.Error = NewJSClusterRequiredError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // If we are here we are clustered. See if we are the stream leader in order to proceed. + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Have extra token for this one. + stream := tokenAt(subject, 6) + consumer := tokenAt(subject, 7) + + js.mu.RLock() + isLeader, sa := cc.isLeader(), js.streamAssignment(acc.Name, stream) + js.mu.RUnlock() + + if isLeader && sa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } else if sa == nil { + return + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var ca *consumerAssignment + if sa.consumers != nil { + ca = sa.consumers[consumer] + } + if ca == nil { + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Check to see if we are a member of the group and if the group has no leader. + if js.isGroupLeaderless(ca.Group) { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if !acc.JetStreamIsConsumerLeader(stream, consumer) { + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + o := mset.lookupConsumer(consumer) + if o == nil { + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + n := o.raftNode() + if n == nil { + resp.Success = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + var preferredLeader string + if isJSONObjectOrArray(msg) { + var req JSApiLeaderStepdownRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if preferredLeader, resp.Error = s.getStepDownPreferredPlacement(n, req.Placement); resp.Error != nil { + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + // Call actual stepdown. + err = n.StepDown(preferredLeader) + if err != nil { + resp.Error = NewJSRaftGeneralError(err, Unless(err)) + } else { + resp.Success = true + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to cancel an in-progress reconfiguration of a clustered stream, rolling the +// desired state back to its origin. Not limited to moves, a scale or retention change +// in flight is rolled back just the same. +func (s *Server) jsStreamCancelMoveRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + name := tokenAt(subject, 5) + + var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + + // A move only exists in clustered mode. + if !s.JetStreamIsClustered() { + resp.Error = NewJSClusterRequiredError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.RLock() + isLeader := cc.isLeader() + js.mu.RUnlock() + + // The rollback is proposed to the meta layer, only the meta leader can do that. + if !isLeader { + return + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + osa := js.streamAssignmentOrInflight(acc.Name, name) + if osa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // A group that has no desired state or origin has no reconfigure in progress. + if osa.desiredOrigin() == nil { + resp.Error = NewJSStreamReconfigureNotInProgressError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + s.jsClusteredStreamCancelMoveLocked(osa, acc.Name, reply) +} + +// streamRemapPeerRequest is implemented by the stream remove and evacuate peer +// requests. They are distinct models carrying the same content. +type streamRemapPeerRequest interface { + // peer returns the server name or peer ID the request targets. + peer() string +} + +func (r *JSApiStreamRemovePeerRequest) peer() string { return r.Peer } +func (r *JSApiStreamEvacuatePeerRequest) peer() string { return r.Peer } + +// streamRemapPeerResponse is implemented by the stream remove and evacuate peer +// responses. They are distinct models carrying the same content. +type streamRemapPeerResponse interface { + setError(err *ApiError) + setSuccess() +} + +func (r *JSApiStreamRemovePeerResponse) setError(err *ApiError) { r.Error = err } +func (r *JSApiStreamRemovePeerResponse) setSuccess() { r.Success = true } + +func (r *JSApiStreamEvacuatePeerResponse) setError(err *ApiError) { r.Error = err } +func (r *JSApiStreamEvacuatePeerResponse) setSuccess() { r.Success = true } + +// Request to remove a peer from a clustered stream. +func (s *Server) jsStreamRemovePeerRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + req := &JSApiStreamRemovePeerRequest{} + resp := &JSApiStreamRemovePeerResponse{ApiResponse: ApiResponse{Type: JSApiStreamRemovePeerResponseType}} + s.jsStreamRemapPeerRequest(nil, c, nil, subject, reply, rmsg, req, resp, true) +} + +// Request to evacuate a peer from a clustered stream. +func (s *Server) jsStreamEvacuatePeerRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + req := &JSApiStreamEvacuatePeerRequest{} + resp := &JSApiStreamEvacuatePeerResponse{ApiResponse: ApiResponse{Type: JSApiStreamEvacuatePeerResponseType}} + s.jsStreamRemapPeerRequest(nil, c, nil, subject, reply, rmsg, req, resp, false) +} + +func (s *Server) jsStreamRemapPeerRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte, req streamRemapPeerRequest, resp streamRemapPeerResponse, remove bool) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + // Have extra token for this one. + name := tokenAt(subject, 6) + + // If we are not in clustered mode this is a failed request. + if !s.JetStreamIsClustered() { + resp.setError(NewJSClusterRequiredError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + // If we are here we are clustered. See if we are the stream leader in order to proceed. + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.setError(NewJSClusterNotAvailError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + js.mu.RLock() + isLeader := cc.isLeader() + js.mu.RUnlock() + + // Make sure we are meta leader. + if !isLeader { + return + } + + if errorOnRequiredApiLevel(hdr) { + resp.setError(NewJSRequiredApiLevelError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.setError(NewJSNotEnabledForAccountError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + } + return + } + if isEmptyRequest(msg) { + resp.setError(NewJSBadRequestError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + if err := s.unmarshalRequest(c, acc, subject, msg, req); err != nil { + resp.setError(NewJSInvalidJSONError(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + if req.peer() == _EMPTY_ { + resp.setError(NewJSBadRequestError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + sa := js.streamAssignmentOrInflight(acc.Name, name) + if sa == nil { + // No stream present. + resp.setError(NewJSStreamNotFoundError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + isGroupMember := func(peer string) bool { + return sa.Group.isMember(peer) || (sa.Group.Desired != nil && slices.Contains(sa.Group.Desired.Peers, peer)) + } + + // Check to see if we are a member of the group. + // Peer here is either a peer ID or a server name, convert to node name. + nodeName := getHash(req.peer()) + isMember := isGroupMember(nodeName) + if !isMember { + nodeName = req.peer() + isMember = isGroupMember(nodeName) + } + + // Make sure we are a member. + if !isMember { + resp.setError(NewJSClusterPeerNotMemberError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + // If we are here we have a valid peer member set for removal. + if !js.removePeerFromStreamLocked(sa, nodeName, peerRemoval{remove: remove, requireReplicas: true}) { + resp.setError(NewJSPeerRemapError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + resp.setSuccess() + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// consumerRemapPeerRequest is implemented by the consumer remove and evacuate peer +// requests. They are distinct models carrying the same content. +type consumerRemapPeerRequest interface { + // peer returns the server name or peer ID the request targets. + peer() string +} + +func (r *JSApiConsumerRemovePeerRequest) peer() string { return r.Peer } +func (r *JSApiConsumerEvacuatePeerRequest) peer() string { return r.Peer } + +// consumerRemapPeerResponse is implemented by the consumer remove and evacuate peer +// responses. They are distinct models carrying the same content. +type consumerRemapPeerResponse interface { + setError(err *ApiError) + setSuccess() +} + +func (r *JSApiConsumerRemovePeerResponse) setError(err *ApiError) { r.Error = err } +func (r *JSApiConsumerRemovePeerResponse) setSuccess() { r.Success = true } + +func (r *JSApiConsumerEvacuatePeerResponse) setError(err *ApiError) { r.Error = err } +func (r *JSApiConsumerEvacuatePeerResponse) setSuccess() { r.Success = true } + +// Request to remove a peer from a single clustered consumer. +func (s *Server) jsConsumerRemovePeerRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + req := &JSApiConsumerRemovePeerRequest{} + resp := &JSApiConsumerRemovePeerResponse{ApiResponse: ApiResponse{Type: JSApiConsumerRemovePeerResponseType}} + s.jsConsumerRemapPeerRequest(nil, c, nil, subject, reply, rmsg, req, resp, true) +} + +// Request to evacuate a peer from a single clustered consumer. +func (s *Server) jsConsumerEvacuatePeerRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + req := &JSApiConsumerEvacuatePeerRequest{} + resp := &JSApiConsumerEvacuatePeerResponse{ApiResponse: ApiResponse{Type: JSApiConsumerEvacuatePeerResponseType}} + s.jsConsumerRemapPeerRequest(nil, c, nil, subject, reply, rmsg, req, resp, false) +} + +func (s *Server) jsConsumerRemapPeerRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte, req consumerRemapPeerRequest, resp consumerRemapPeerResponse, remove bool) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + // If we are not in clustered mode this is a failed request. + if !s.JetStreamIsClustered() { + resp.setError(NewJSClusterRequiredError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.setError(NewJSClusterNotAvailError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + // Have extra tokens for this one. + stream := tokenAt(subject, 6) + consumer := tokenAt(subject, 7) + + js.mu.RLock() + isLeader := cc.isLeader() + js.mu.RUnlock() + + // Only the metaleader can change assignments. + if !isLeader { + return + } + + if errorOnRequiredApiLevel(hdr) { + resp.setError(NewJSRequiredApiLevelError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.setError(NewJSNotEnabledForAccountError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + } + return + } + if isEmptyRequest(msg) { + resp.setError(NewJSBadRequestError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + if err := s.unmarshalRequest(c, acc, subject, msg, req); err != nil { + resp.setError(NewJSInvalidJSONError(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + if req.peer() == _EMPTY_ { + resp.setError(NewJSBadRequestError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + sa := js.streamAssignmentOrInflight(acc.Name, stream) + if sa == nil { + resp.setError(NewJSStreamNotFoundError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + ca := js.consumerAssignmentOrInflight(acc.Name, stream, consumer) + if ca == nil || ca.Config == nil { + resp.setError(NewJSConsumerNotFoundError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + isGroupMember := func(peer string) bool { + return ca.Group.isMember(peer) || (ca.Group.Desired != nil && slices.Contains(ca.Group.Desired.Peers, peer)) + } + + // Check to see if we are a member of the group. + // Peer here is either a peer ID or a server name, convert to node name. + nodeName := getHash(req.peer()) + isMember := isGroupMember(nodeName) + if !isMember { + nodeName = req.peer() + isMember = isGroupMember(nodeName) + } + + // Make sure we are a member. + if !isMember { + resp.setError(NewJSClusterPeerNotMemberError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + // If we are here we have a valid peer member set for removal. + if !js.remapPeerFromConsumerLocked(sa, ca, nodeName, remove) { + resp.setError(NewJSPeerRemapError()) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + resp.setSuccess() + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to have the metaleader remove a peer from the system. +func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + if acc != s.SystemAccount() { + return + } + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.RLock() + isLeader := cc.isLeader() + meta := cc.meta + js.mu.RUnlock() + + // Extra checks here but only leader is listening. + if !isLeader { + return + } + + var resp = JSApiMetaServerRemoveResponse{ApiResponse: ApiResponse{Type: JSApiMetaServerRemoveResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if isEmptyRequest(msg) { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var req JSApiMetaServerRemoveRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + // Another peer-remove is already in progress, don't allow multiple concurrent changes. + if cc.peerRemoveReply != nil { + resp.Error = NewJSClusterServerMemberChangeInflightError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var found string + for _, p := range meta.Peers() { + // If Peer is specified, it takes precedence + if req.Peer != _EMPTY_ { + if p.ID == req.Peer { + found = req.Peer + break + } + continue + } + si, ok := s.nodeToInfo.Load(p.ID) + if ok && si.(nodeInfo).name == req.Server { + found = p.ID + break + } + } + + if found == _EMPTY_ { + resp.Error = NewJSClusterServerNotMemberError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if err := meta.ProposeRemovePeer(found); err != nil { + if err == errMembershipChange { + resp.Error = NewJSClusterServerMemberChangeInflightError() + } else { + resp.Error = NewJSRaftGeneralError(err) + } + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if cc.peerRemoveReply == nil { + cc.peerRemoveReply = make(map[string]peerRemoveInfo, 1) + } + // Only copy the request, the subject and reply are already copied. + cc.peerRemoveReply[found] = peerRemoveInfo{ci: ci, subject: subject, reply: reply, request: string(msg)} +} + +// Request to have the metaleader evacuate a peer from the system. +func (s *Server) jsLeaderServerEvacuateRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + if acc != s.SystemAccount() { + return + } + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.RLock() + isLeader := cc.isLeader() + meta := cc.meta + js.mu.RUnlock() + + // Extra checks here but only leader is listening. + if !isLeader { + return + } + + var resp = JSApiMetaServerEvacuateResponse{ApiResponse: ApiResponse{Type: JSApiMetaServerEvacuateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if isEmptyRequest(msg) { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var req JSApiMetaServerEvacuateRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + var found string + for _, p := range meta.Peers() { + // If Peer is specified, it takes precedence + if req.Peer != _EMPTY_ { + if p.ID == req.Peer { + found = req.Peer + break + } + continue + } + si, ok := s.nodeToInfo.Load(p.ID) + if ok && si.(nodeInfo).name == req.Server { + found = p.ID + break + } + } + + if found == _EMPTY_ { + resp.Error = NewJSClusterServerNotMemberError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + for _, sa := range js.streamAssignmentsOrInflightSeqAllAccounts() { + if sa.unsupported != nil { + continue + } + if sa.Group.isMember(found) || (sa.Group.Desired != nil && slices.Contains(sa.Group.Desired.Peers, found)) { + js.removePeerFromStreamLocked(sa, found, peerRemoval{exclude: true}) + } + } + + resp.Success = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// jsMetaRescueRequest handles $JS.API.META.RESCUE requests. This is a broadcast +// subject, every online server that receives the request evaluates and applies +// it locally, responding independently. +func (s *Server) jsMetaRescueRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + if acc != s.SystemAccount() { + return + } + + js := s.getJetStream() + if js == nil { + return + } + meta := js.getMetaGroup() + if meta == nil { + return + } + + var resp = JSApiMetaRescueResponse{ + ApiResponse: ApiResponse{Type: JSApiMetaRescueResponseType}, + Server: s.Name(), + ServerID: s.ID(), + } + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if isEmptyRequest(msg) { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var req JSApiMetaRescueRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + prev, cur, err := meta.RescueQuorum(req.QuorumNeeded) + if err != nil { + resp.Error = NewJSClusterRescueError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + resp.PrevQuorum, resp.NewQuorum = prev, cur + + s.publishAdvisory(nil, JSAdvisoryMetaRescue, &JSMetaRescueAdvisory{ + TypedEvent: TypedEvent{ + Type: JSMetaRescueAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Server: s.Name(), + ServerID: s.ID(), + PrevQuorum: prev, + NewQuorum: cur, + Cluster: s.cachedClusterName(), + Domain: s.getOpts().JetStreamDomain, + }) + + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) +} + +func (s *Server) peerSetToNames(ps []string) []string { + names := make([]string, len(ps)) + for i := 0; i < len(ps); i++ { + if si, ok := s.nodeToInfo.Load(ps[i]); !ok { + names[i] = ps[i] + } else { + names[i] = si.(nodeInfo).name + } + } + return names +} + +// looks up the peer id for a given server name. Cluster and domain name are optional filter criteria +func (s *Server) nameToPeer(js *jetStream, serverName, clusterName, domainName string) string { + js.mu.RLock() + defer js.mu.RUnlock() + if cc := js.cluster; cc != nil { + for _, p := range cc.meta.Peers() { + si, ok := s.nodeToInfo.Load(p.ID) + if ok && si.(nodeInfo).name == serverName { + if clusterName == _EMPTY_ || clusterName == si.(nodeInfo).cluster { + if domainName == _EMPTY_ || domainName == si.(nodeInfo).domain { + return p.ID + } + } + } + } + } + return _EMPTY_ +} + +// Request to have the metaleader move a stream on a peer to another +func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + // Extra checks here but only leader is listening. + js.mu.RLock() + isLeader := cc.isLeader() + js.mu.RUnlock() + + if !isLeader { + return + } + + accName := tokenAt(subject, 6) + streamName := tokenAt(subject, 7) + + if acc.GetName() != accName && acc != s.SystemAccount() { + return + } + + var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var req JSApiMetaServerStreamMoveRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + srcPeer := _EMPTY_ + if req.Server != _EMPTY_ { + srcPeer = s.nameToPeer(js, req.Server, req.Cluster, req.Domain) + } + + targetAcc, ok := s.accounts.Load(accName) + if !ok { + resp.Error = NewJSNoAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + osa := js.streamAssignmentOrInflight(accName, streamName) + if osa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + cfg := *osa.Config.clone() + currPeers, currCluster := copyStrings(osa.Group.Peers), osa.Group.Cluster + if d := osa.Group.Desired; d != nil { + currPeers, currCluster = copyStrings(d.Peers), d.Cluster + } + + // if server was picked, make sure src peer exists and move it to first position. + // removal will drop peers from the left + if req.Server != _EMPTY_ { + if srcPeer == _EMPTY_ { + resp.Error = NewJSClusterServerNotMemberError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + var peerFound bool + for i := 0; i < len(currPeers); i++ { + if currPeers[i] == srcPeer { + copy(currPeers[1:], currPeers[:i]) + currPeers[0] = srcPeer + peerFound = true + break + } + } + if !peerFound { + resp.Error = NewJSClusterPeerNotMemberError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + // make sure client is scoped to requested account + ciNew := *(ci) + ciNew.Account = accName + + // backup placement such that peers can be looked up with modified tag list + var origPlacement *Placement + if cfg.Placement != nil { + tmp := *cfg.Placement + origPlacement = &tmp + } + + if len(req.Tags) > 0 { + if cfg.Placement == nil { + cfg.Placement = &Placement{} + } + cfg.Placement.Tags = append(cfg.Placement.Tags, req.Tags...) + } + + // The cluster the new peer set is placed in, we stay in the current cluster unless + // we need to expand into another one below. + newCluster := currCluster + + peers, e := cc.selectPeerGroup(cfg.Replicas+1, currCluster, &cfg, currPeers, 1, nil) + if len(peers) <= cfg.Replicas { + // since expanding in the same cluster did not yield a result, try in different cluster + peers = nil + + clusters := map[string]struct{}{} + s.nodeToInfo.Range(func(_, ni any) bool { + if currCluster != ni.(nodeInfo).cluster { + clusters[ni.(nodeInfo).cluster] = struct{}{} + } + return true + }) + errs := &selectPeerError{} + errs.accumulate(e) + for cluster := range clusters { + newPeers, e := cc.selectPeerGroup(cfg.Replicas, cluster, &cfg, nil, 0, nil) + if len(newPeers) >= cfg.Replicas { + peers = append([]string{}, currPeers...) + peers = append(peers, newPeers[:cfg.Replicas]...) + // Keep only the new peers. + peers = peers[len(peers)-cfg.Replicas:] + newCluster = cluster + break + } + errs.accumulate(e) + } + if peers == nil { + resp.Error = NewJSClusterNoPeersError(errs) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } else { + // Keep only the new peers. + peers = peers[len(peers)-cfg.Replicas:] + } + + cfg.Placement = origPlacement + + s.Noticef("Requested move for stream '%s > %s' R=%d from %+v to %+v", + accName, streamName, cfg.Replicas, s.peerSetToNames(currPeers), s.peerSetToNames(peers)) + + s.jsClusteredStreamUpdateRequestLocked(&ciNew, targetAcc.(*Account), subject, reply, rmsg, &cfg, peers, newCluster, false) +} + +// Request to have the metaleader cancel an in-progress reconfiguration of a stream, +// rolling the desired state back to its origin. Not limited to moves, a scale or +// retention change in flight is rolled back just the same. +func (s *Server) jsLeaderServerStreamCancelMoveRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + // Extra checks here but only leader is listening. + js.mu.RLock() + isLeader := cc.isLeader() + js.mu.RUnlock() + + if !isLeader { + return + } + + var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + accName := tokenAt(subject, 6) + streamName := tokenAt(subject, 7) + + if acc.GetName() != accName && acc != s.SystemAccount() { + return + } + + _, ok := s.accounts.Load(accName) + if !ok { + resp.Error = NewJSNoAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + osa := js.streamAssignmentOrInflight(accName, streamName) + if osa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // A group that has no desired state or origin has no reconfigure in progress. + if osa.desiredOrigin() == nil { + resp.Error = NewJSStreamReconfigureNotInProgressError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + s.jsClusteredStreamCancelMoveLocked(osa, accName, reply) +} + +// Request to have an account purged +func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + if acc != s.SystemAccount() { + return + } + + js := s.getJetStream() + if js == nil { + return + } + + accName := tokenAt(subject, 5) + + var resp = JSApiAccountPurgeResponse{ApiResponse: ApiResponse{Type: JSApiAccountPurgeResponseType}} + + // Check for path like separators in the name. + if strings.ContainsAny(accName, `\/`) { + resp.Error = NewJSStreamGeneralError(errors.New("account name can not contain path separators")) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if !s.JetStreamIsClustered() { + var streams []*stream + var ac *Account + if ac, err = s.lookupAccount(accName); err == nil && ac != nil { + streams = ac.streams() + } + + s.Noticef("Purge request for account %s (streams: %d, hasAccount: %t)", + accName, len(streams), ac != nil) + + for _, mset := range streams { + err := mset.delete() + if err != nil { + resp.Error = NewJSStreamDeleteError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + if err := os.RemoveAll(filepath.Join(js.config.StoreDir, accName)); err != nil { + resp.Error = NewJSStreamGeneralError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + resp.Initiated = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + _, cc := s.getJetStreamCluster() + + js.mu.RLock() + isLeader := cc.isLeader() + meta := cc.meta + js.mu.RUnlock() + + if !isLeader { + return + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if js.isMetaRecovering() { + // While in recovery mode, the data structures are not fully initialized + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.Lock() + ns, nc := 0, 0 + for osa := range js.streamAssignmentsOrInflightSeq(accName) { + for oca := range js.consumerAssignmentsOrInflightSeq(accName, osa.Config.Name) { + ca := &consumerAssignment{Group: oca.Group, Stream: oca.Stream, Name: oca.Name, Config: oca.Config, Subject: subject, Client: oca.Client, Created: oca.Created} + if err = meta.Propose(cc.term, encodeDeleteConsumerAssignment(ca)); err != nil { + js.mu.Unlock() + resp.Error = NewJSStreamGeneralError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + cc.trackInflightConsumerProposal(accName, osa.Config.Name, ca, true) + nc++ + } + sa := &streamAssignment{Group: osa.Group, Config: osa.Config, Subject: subject, Client: osa.Client, Created: osa.Created} + if err = meta.Propose(cc.term, encodeDeleteStreamAssignment(sa)); err != nil { + js.mu.Unlock() + resp.Error = NewJSStreamGeneralError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + cc.trackInflightStreamProposal(accName, sa, true) + ns++ + } + js.mu.Unlock() + + hasAccount := ns > 0 + s.Noticef("Purge request for account %s (streams: %d, consumer: %d, hasAccount: %t)", accName, ns, nc, hasAccount) + + resp.Initiated = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) +} + +// Request to have the meta leader stepdown. +// These will only be received by the meta leader, so less checking needed. +func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + // This should only be coming from the System Account. + if acc != s.SystemAccount() { + s.RateLimitWarnf("JetStream API stepdown request from non-system account: %q user: %q", ci.serviceAccount(), ci.User) + return + } + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + // Extra checks here but only leader is listening. + js.mu.RLock() + isLeader := cc.isLeader() + meta := cc.meta + js.mu.RUnlock() + + if !isLeader { + return + } + + var preferredLeader string + var resp = JSApiLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiLeaderStepDownResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if isJSONObjectOrArray(msg) { + var req JSApiLeaderStepdownRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if preferredLeader, resp.Error = s.getStepDownPreferredPlacement(meta, req.Placement); resp.Error != nil { + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + // Call actual stepdown. + err = meta.StepDown(preferredLeader) + if err != nil { + resp.Error = NewJSRaftGeneralError(err, Unless(err)) + } else { + resp.Success = true + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Check if given []bytes is a JSON Object or Array. +// Technically, valid JSON can also be a plain string or number, but for our use case, +// we care only for JSON objects or arrays which starts with `[` or `{`. +// This function does not have to ensure valid JSON in its entirety. It is used merely +// to hint the codepath if it should attempt to parse the request as JSON or not. +func isJSONObjectOrArray(req []byte) bool { + // Skip leading JSON whitespace (space, tab, newline, carriage return) + i := 0 + for i < len(req) && (req[i] == ' ' || req[i] == '\t' || req[i] == '\n' || req[i] == '\r') { + i++ + } + // Check for empty input after trimming + if i >= len(req) { + return false + } + // Check if the first non-whitespace character is '{' or '[' + return req[i] == '{' || req[i] == '[' +} + +func isEmptyRequest(req []byte) bool { + if len(req) == 0 { + return true + } + if bytes.Equal(req, []byte("{}")) { + return true + } + // If we are here we didn't get our simple match, but still could be valid. + var v any + if err := json.Unmarshal(req, &v); err != nil { + return false + } + vm, ok := v.(map[string]any) + if !ok { + return false + } + return len(vm) == 0 +} + +// getStepDownPreferredPlacement attempts to work out what the best placement is +// for a stepdown request. The preferred server name always takes precedence, but +// if not specified, the placement will be used to filter by cluster. The caller +// should check for return API errors and return those to the requestor if needed. +func (s *Server) getStepDownPreferredPlacement(group RaftNode, placement *Placement) (string, *ApiError) { + if placement == nil { + return _EMPTY_, nil + } + var preferredLeader string + if placement.Preferred != _EMPTY_ { + for _, p := range group.Peers() { + si, ok := s.nodeToInfo.Load(p.ID) + if !ok || si == nil { + continue + } + if si.(nodeInfo).name == placement.Preferred { + preferredLeader = p.ID + break + } + } + if preferredLeader == group.ID() { + return _EMPTY_, NewJSClusterNoPeersError(fmt.Errorf("preferred server %q is already leader", placement.Preferred)) + } + if preferredLeader == _EMPTY_ { + return _EMPTY_, NewJSClusterNoPeersError(fmt.Errorf("preferred server %q not known", placement.Preferred)) + } + } else { + possiblePeers := make(map[*Peer]nodeInfo, len(group.Peers())) + ourID := group.ID() + for _, p := range group.Peers() { + if p == nil { + continue // ... shouldn't happen. + } + si, ok := s.nodeToInfo.Load(p.ID) + if !ok || si == nil { + continue + } + ni := si.(nodeInfo) + if ni.offline || p.ID == ourID { + continue + } + possiblePeers[p] = ni + } + // If cluster is specified, filter out anything not matching the cluster name. + if placement.Cluster != _EMPTY_ { + for p, si := range possiblePeers { + if si.cluster != placement.Cluster { + delete(possiblePeers, p) + } + } + } + // If tags are specified, filter out anything not matching all supplied tags. + if len(placement.Tags) > 0 { + for p, si := range possiblePeers { + matchesAll := true + for _, tag := range placement.Tags { + if matchesAll = matchesAll && si.tags.Contains(tag); !matchesAll { + break + } + } + if !matchesAll { + delete(possiblePeers, p) + } + } + } + // If there are no possible peers, return an error. + if len(possiblePeers) == 0 { + return _EMPTY_, NewJSClusterNoPeersError(fmt.Errorf("no replacement peer connected")) + } + // Take advantage of random map iteration order to select the preferred. + for p := range possiblePeers { + preferredLeader = p.ID + break + } + } + return preferredLeader, nil +} + +// Request to delete a stream. +func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiStreamDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamDeleteResponseType}} + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + if !isEmptyRequest(msg) { + resp.Error = NewJSNotEmptyRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + stream := streamNameFromSubject(subject) + + // Clustered. + if s.JetStreamIsClustered() { + s.jsClusteredStreamDeleteRequest(ci, acc, stream, subject, reply, msg) + return + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if err := mset.delete(); err != nil { + resp.Error = NewJSStreamDeleteError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + resp.Success = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to delete a message. +// This expects a stream sequence number as the msg body. +func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + stream := tokenAt(subject, 6) + + var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}} + + // If we are in clustered mode we need to be the stream leader to proceed. + if s.JetStreamIsClustered() { + // Check to make sure the stream is assigned. + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.RLock() + isLeader, sa := cc.isLeader(), js.streamAssignment(acc.Name, stream) + js.mu.RUnlock() + + if isLeader && sa == nil { + // We can't find the stream, so mimic what would be the errors below. + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + // No stream present. + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } else if sa == nil { + return + } + + // Check to see if we are a member of the group and if the group has no leader. + if js.isGroupLeaderless(sa.Group) { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // We have the stream assigned and a leader, so only the stream leader should answer. + if !acc.JetStreamIsStreamLeader(stream) { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + if isEmptyRequest(msg) { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + var req JSApiMsgDeleteRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + mset.cfgMu.RLock() + sealed, denyDelete := mset.cfg.Sealed, mset.cfg.DenyDelete + mset.cfgMu.RUnlock() + if sealed { + resp.Error = NewJSStreamSealedError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if denyDelete { + resp.Error = NewJSStreamMsgDeleteFailedError(errors.New("message delete not permitted")) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if s.JetStreamIsClustered() { + s.jsClusteredMsgDeleteRequest(ci, acc, mset, stream, subject, reply, &req, rmsg) + return + } + + var removed bool + if req.NoErase { + removed, err = mset.removeMsg(req.Seq) + } else { + removed, err = mset.eraseMsg(req.Seq) + } + if err != nil { + resp.Error = NewJSStreamMsgDeleteFailedError(err, Unless(err)) + } else if !removed { + resp.Error = NewJSSequenceNotFoundError(req.Seq) + } else { + resp.Success = true + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to get a raw stream message. +func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + stream := tokenAt(subject, 6) + + var resp = JSApiMsgGetResponse{ApiResponse: ApiResponse{Type: JSApiMsgGetResponseType}} + + // If we are in clustered mode we need to be the stream leader to proceed. + if s.JetStreamIsClustered() { + // Check to make sure the stream is assigned. + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.RLock() + isLeader, sa := cc.isLeader(), js.streamAssignment(acc.Name, stream) + js.mu.RUnlock() + + if isLeader && sa == nil { + // We can't find the stream, so mimic what would be the errors below. + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + // No stream present. + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } else if sa == nil { + return + } + + // Check to see if we are a member of the group and if the group has no leader. + if js.isGroupLeaderless(sa.Group) { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // We have the stream assigned and a leader, so only the stream leader should answer. + if !acc.JetStreamIsStreamLeader(stream) { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + if isEmptyRequest(msg) { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + var req JSApiMsgGetRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // This version does not support batch. + if req.Batch > 0 || req.MaxBytes > 0 { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Validate non-conflicting options. Seq, LastFor, and AsOfTime are mutually exclusive. + // NextFor can be paired with Seq or AsOfTime indicating a filter subject. + if (req.Seq > 0 && req.LastFor != _EMPTY_) || + (req.Seq == 0 && req.LastFor == _EMPTY_ && req.NextFor == _EMPTY_ && req.StartTime == nil) || + (req.Seq > 0 && req.StartTime != nil) || + (req.StartTime != nil && req.LastFor != _EMPTY_) || + (req.LastFor != _EMPTY_ && req.NextFor != _EMPTY_) { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if mset.offlineReason != _EMPTY_ { + // Just let the request time out. + return + } + + var svp StoreMsg + var sm *StoreMsg + + // Ensure this read request is isolated and doesn't interleave with writes. + mset.isolateMu.RLock() + defer mset.isolateMu.RUnlock() + + // If AsOfTime is set, perform this first to get the sequence. + var seq uint64 + if req.StartTime != nil { + seq = mset.store.GetSeqFromTime(*req.StartTime) + } else { + seq = req.Seq + } + + if seq > 0 && req.NextFor == _EMPTY_ { + sm, err = mset.store.LoadMsg(seq, &svp) + } else if req.NextFor != _EMPTY_ { + sm, _, err = mset.store.LoadNextMsg(req.NextFor, subjectHasWildcard(req.NextFor), seq, &svp) + } else { + sm, err = mset.store.LoadLastMsg(req.LastFor, &svp) + } + if err != nil { + resp.Error = NewJSNoMessageFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + resp.Message = &StoredMsg{ + Subject: sm.subj, + Sequence: sm.seq, + Data: sm.msg, + Time: time.Unix(0, sm.ts).UTC(), + } + if !req.NoHeaders { + resp.Message.Header = sm.hdr + } + + // Don't send response through API layer for this call. + s.sendInternalAccountMsg(nil, reply, s.jsonResponse(resp)) +} + +func (s *Server) jsConsumerUnpinRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + stream := streamNameFromSubject(subject) + consumer := consumerNameFromSubject(subject) + + var resp = JSApiConsumerUnpinResponse{ApiResponse: ApiResponse{Type: JSApiConsumerUnpinResponseType}} + + if s.JetStreamIsClustered() { + // Check to make sure the stream is assigned. + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + // First check if the stream and consumer is there. + js.mu.RLock() + sa := js.streamAssignment(acc.Name, stream) + if sa == nil { + js.mu.RUnlock() + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if sa.unsupported != nil { + js.mu.RUnlock() + // Just let the request time out. + return + } + + ca, ok := sa.consumers[consumer] + if !ok || ca == nil { + js.mu.RUnlock() + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if ca.unsupported != nil { + js.mu.RUnlock() + // Just let the request time out. + return + } + js.mu.RUnlock() + + // Then check if we are the leader. + mset, err := acc.lookupStream(stream) + if err != nil { + return + } + + o := mset.lookupConsumer(consumer) + if o == nil { + return + } + if !o.isLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var req JSApiConsumerUnpinRequest + if err := json.Unmarshal(msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if req.Group == _EMPTY_ { + resp.Error = NewJSInvalidJSONError(errors.New("consumer group not specified")) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if !validGroupName.MatchString(req.Group) { + resp.Error = NewJSConsumerInvalidGroupNameError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if mset.offlineReason != _EMPTY_ { + // Just let the request time out. + return + } + o := mset.lookupConsumer(consumer) + if o == nil { + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if o.offlineReason != _EMPTY_ { + // Just let the request time out. + return + } + + var foundPriority bool + for _, group := range o.config().PriorityGroups { + if group == req.Group { + foundPriority = true + break + } + } + if !foundPriority { + resp.Error = NewJSConsumerInvalidPriorityGroupError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + o.mu.Lock() + o.unassignPinId() + o.sendUnpinnedAdvisoryLocked(req.Group, "admin") + o.mu.Unlock() + o.signalNewMessages() + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to purge a stream. +func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + stream := streamNameFromSubject(subject) + + var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} + + // If we are in clustered mode we need to be the stream leader to proceed. + if s.JetStreamIsClustered() { + // Check to make sure the stream is assigned. + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.RLock() + isLeader, sa := cc.isLeader(), js.streamAssignment(acc.Name, stream) + js.mu.RUnlock() + + if isLeader && sa == nil { + // We can't find the stream, so mimic what would be the errors below. + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + // No stream present. + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } else if sa == nil { + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + // Check to see if we are a member of the group and if the group has no leader. + if js.isGroupLeaderless(sa.Group) { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // We have the stream assigned and a leader, so only the stream leader should answer. + if !acc.JetStreamIsStreamLeader(stream) { + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + var purgeRequest *JSApiStreamPurgeRequest + if isJSONObjectOrArray(msg) { + var req JSApiStreamPurgeRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if req.Sequence > 0 && req.Keep > 0 { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + purgeRequest = &req + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + mset.cfgMu.RLock() + sealed, denyPurge := mset.cfg.Sealed, mset.cfg.DenyPurge + mset.cfgMu.RUnlock() + if sealed { + resp.Error = NewJSStreamSealedError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if denyPurge { + resp.Error = NewJSStreamPurgeFailedError(errors.New("stream purge not permitted")) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if s.JetStreamIsClustered() { + s.jsClusteredStreamPurgeRequest(ci, acc, mset, stream, subject, reply, rmsg, purgeRequest) + return + } + + purged, err := mset.purge(purgeRequest) + if err != nil { + resp.Error = NewJSStreamGeneralError(err, Unless(err)) + } else { + resp.Purged = purged + resp.Success = true + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +func (acc *Account) jsNonClusteredStreamLimitsCheck(cfg *StreamConfig) *ApiError { + var replicas int + if cfg != nil { + replicas = cfg.Replicas + } + selectedLimits, tier, jsa, apiErr := acc.selectLimits(replicas) + if apiErr != nil { + return apiErr + } + jsa.js.mu.RLock() + defer jsa.js.mu.RUnlock() + jsa.mu.RLock() + defer jsa.mu.RUnlock() + if selectedLimits.MaxStreams > 0 && jsa.countStreams(tier, cfg) >= selectedLimits.MaxStreams { + return NewJSMaximumStreamsLimitError() + } + reserved := jsa.tieredReservation(tier, cfg) + if err := jsa.js.checkAllLimits(selectedLimits, tier, cfg, reserved, 0); err != nil { + return NewJSStreamLimitsError(err, Unless(err)) + } + return nil +} + +// Request to restore a stream. +func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamIsLeader() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if !acc.JetStreamEnabled() { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if isEmptyRequest(msg) { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var req JSApiStreamRestoreRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + stream := streamNameFromSubject(subject) + + if stream != req.Config.Name && req.Config.Name == _EMPTY_ { + req.Config.Name = stream + } + if stream != req.Config.Name { + resp.Error = NewJSStreamMismatchError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Check for path like separators in the name. + if strings.ContainsAny(stream, `\/`) { + resp.Error = NewJSStreamNameContainsPathSeparatorsError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if s.JetStreamIsClustered() { + s.jsClusteredStreamRestoreRequest(ci, acc, &req, subject, reply, rmsg) + return + } + + // check stream config at the start of the restore process, not at the end + cfg, apiErr := s.checkStreamCfg(&req.Config, acc, false) + if apiErr != nil { + resp.Error = apiErr + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if err := acc.jsNonClusteredStreamLimitsCheck(&cfg); err != nil { + resp.Error = err + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if _, err := acc.lookupStream(stream); err == nil { + resp.Error = NewJSStreamNameExistRestoreFailedError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + s.processStreamRestore(ci, acc, &cfg, subject, reply, string(msg)) +} + +func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamConfig, subject, reply, msg string) <-chan error { + var resp = JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}} + + streamName := cfg.Name + s.Noticef("Starting restore for stream '%s > %s'", acc.Name, streamName) + + start := time.Now().UTC() + domain := s.getOpts().JetStreamDomain + s.publishAdvisory(acc, JSAdvisoryStreamRestoreCreatePre+"."+streamName, &JSRestoreCreateAdvisory{ + TypedEvent: TypedEvent{ + Type: JSRestoreCreateAdvisoryType, + ID: nuid.Next(), + Time: start, + }, + Stream: streamName, + Client: ci.forAdvisory(), + Domain: domain, + }) + + // Create our internal subscription to accept the snapshot. + restoreSubj := fmt.Sprintf(jsRestoreDeliverT, streamName, nuid.Next()) + + type result struct { + err error + reply string + } + + // For signaling to upper layers. + var resultOnce sync.Once + var closeOnce sync.Once + resultCh := make(chan result, 1) + pr, pw := io.Pipe() + + setResult := func(err error, reply string) { + resultOnce.Do(func() { + resultCh <- result{err, reply} + }) + } + activeQ := newIPQueue[int](s, fmt.Sprintf("[ACC:%s] stream '%s' restore", acc.Name, streamName)) + restoreCh := make(chan struct { + mset *stream + err error + }, 1) + var restoreFinished atomic.Bool + closeWithError := func(err error) { + closeOnce.Do(func() { + if err != nil { + pw.CloseWithError(err) + } else { + pw.Close() + } + }) + } + + s.startGoRoutine(func() { + defer s.grWG.Done() + + var mset *stream + var err error + + // Determine the snapshot format. + var consumed bytes.Buffer + tee := io.TeeReader(pr, &consumed) + sr := s2.NewReader(tee) + var preamble [8]byte + if _, err = io.ReadFull(sr, preamble[:]); err == nil { + replay := io.MultiReader(&consumed, pr) + if bytes.Equal(preamble[:], []byte(archive.MagicBytes)) { + mset, err = acc.RestoreStreamV2(cfg, replay) + } else { + mset, err = acc.RestoreStream(cfg, replay) + } + } + restoreFinished.Store(true) + if err != nil { + pr.CloseWithError(err) + } else { + pr.Close() + } + restoreCh <- struct { + mset *stream + err error + }{ + mset: mset, + err: err, + } + }) + + processChunk := func(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + // We require reply subjects to communicate back failures, flow etc. If they do not have one log and cancel. + if reply == _EMPTY_ { + sub.client.processUnsub(sub.sid) + setResult(fmt.Errorf("restore for stream '%s > %s' requires reply subject for each chunk", acc.Name, streamName), reply) + return + } + // Account client messages have \r\n on end. This is an error. + if len(msg) < LEN_CR_LF { + sub.client.processUnsub(sub.sid) + setResult(fmt.Errorf("restore for stream '%s > %s' received short chunk", acc.Name, streamName), reply) + return + } + // Adjust. + msg = msg[:len(msg)-LEN_CR_LF] + + // This means we are complete with our transfer from the client. + if len(msg) == 0 { + s.Debugf("Finished streaming restore for stream '%s > %s'", acc.Name, streamName) + closeWithError(nil) + setResult(nil, reply) + return + } + + // Signal activity before and after the blocking write. + // The pre-write signal refreshes the stall watchdog when the + // chunk arrives; the post-write signal refreshes it again once + // RestoreStream has consumed the data. This keeps the idle + // window between chunks anchored to the end of the previous + // write instead of its start. + activeQ.push(0) + + if _, err := pw.Write(msg); err != nil { + if restoreFinished.Load() { + activeQ.push(len(msg)) + s.sendInternalAccountMsg(acc, reply, nil) + return + } + closeWithError(err) + sub.client.processUnsub(sub.sid) + var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} + if IsNatsErr(err, JSStorageResourcesExceededErr, JSMemoryResourcesExceededErr) { + s.resourcesExceededError(cfg.Storage) + } + resp.Error = NewJSStreamRestoreError(err, Unless(err)) + if s.sendInternalAccountMsg(acc, reply, s.jsonResponse(&resp)) == nil { + reply = _EMPTY_ + } + setResult(err, reply) + return + } + + activeQ.push(len(msg)) + + s.sendInternalAccountMsg(acc, reply, nil) + } + + sub, err := acc.subscribeInternal(restoreSubj, processChunk) + if err != nil { + closeWithError(err) + resp.Error = NewJSRestoreSubscribeFailedError(err, restoreSubj) + s.sendAPIErrResponse(ci, acc, subject, reply, msg, s.jsonResponse(&resp)) + return nil + } + + // Mark the subject so the end user knows where to send the snapshot chunks. + resp.DeliverSubject = restoreSubj + s.sendAPIResponse(ci, acc, subject, reply, msg, s.jsonResponse(resp)) + + // Returned to the caller to wait for completion. + doneCh := make(chan error, 1) + + // Monitor the progress from another Go routine. + s.startGoRoutine(func() { + defer s.grWG.Done() + defer func() { + closeWithError(ErrConnectionClosed) + sub.client.processUnsub(sub.sid) + activeQ.unregister() + }() + + const activityInterval = 5 * time.Second + notActive := time.NewTimer(activityInterval) + defer notActive.Stop() + + total := 0 + var inputDone bool + var replySubj string + var inputErr error + var restoreDone bool + var restoreResult struct { + mset *stream + err error + } + + finish := func(reply string, err error, mset *stream) { + end := time.Now().UTC() + + s.publishAdvisory(acc, JSAdvisoryStreamRestoreCompletePre+"."+streamName, &JSRestoreCompleteAdvisory{ + TypedEvent: TypedEvent{ + Type: JSRestoreCompleteAdvisoryType, + ID: nuid.Next(), + Time: end, + }, + Stream: streamName, + Start: start, + End: end, + Bytes: int64(total), + Client: ci.forAdvisory(), + Domain: domain, + }) + + var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} + if err != nil { + if IsNatsErr(err, JSStorageResourcesExceededErr, JSMemoryResourcesExceededErr) { + s.resourcesExceededError(cfg.Storage) + } + resp.Error = NewJSStreamRestoreError(err, Unless(err)) + s.Warnf("Restore failed for %s for stream '%s > %s' in %v", + friendlyBytes(int64(total)), acc.Name, streamName, end.Sub(start)) + } else if mset == nil { + err = fmt.Errorf("restore for stream '%s > %s' did not create a stream", acc.Name, streamName) + resp.Error = NewJSStreamRestoreError(err) + s.Warnf("Restore failed for %s for stream '%s > %s' in %v", + friendlyBytes(int64(total)), acc.Name, streamName, end.Sub(start)) + } else { + msetCfg := mset.config() + resp.StreamInfo = &StreamInfo{ + Created: mset.createdTime(), + State: mset.state(), + Config: *setDynamicStreamMetadata(&msetCfg), + TimeStamp: time.Now().UTC(), + } + s.Noticef("Completed restore of %s for stream '%s > %s' in %v", + friendlyBytes(int64(total)), acc.Name, streamName, end.Sub(start).Round(time.Millisecond)) + } + if reply != _EMPTY_ { + s.sendInternalAccountMsg(acc, reply, s.jsonResponse(&resp)) + } + doneCh <- err + } + + for { + select { + case result := <-resultCh: + replySubj = result.reply + inputDone = true + inputErr = result.err + notActive.Stop() + if result.err != nil { + closeWithError(result.err) + s.Warnf(result.err.Error()) + } + if restoreDone { + err := inputErr + if err == nil { + err = restoreResult.err + } + finish(replySubj, err, restoreResult.mset) + return + } + case rr := <-restoreCh: + restoreDone = true + restoreResult = rr + if inputDone { + err := inputErr + if err == nil { + err = rr.err + } + finish(replySubj, err, rr.mset) + return + } + case <-activeQ.ch: + if n, ok := activeQ.popOne(); ok { + total += n + if !inputDone { + notActive.Reset(activityInterval) + } + } + case <-notActive.C: + err := fmt.Errorf("restore for stream '%s > %s' is stalled", acc.Name, streamName) + closeWithError(err) + doneCh <- err + return + } + } + }) + + return doneCh +} + +// Process a snapshot request. +func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + smsg := string(msg) + stream := streamNameFromSubject(subject) + + // If we are in clustered mode we need to be the stream leader to proceed. + if s.JetStreamIsClustered() && !acc.JetStreamIsStreamLeader(stream) { + return + } + + var resp = JSApiStreamSnapshotResponse{ApiResponse: ApiResponse{Type: JSApiStreamSnapshotResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if !acc.JetStreamEnabled() { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) + return + } + if isEmptyRequest(msg) { + resp.Error = NewJSBadRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) + return + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) + } + return + } + + var req JSApiStreamSnapshotRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) + return + } + if !IsValidPublishSubject(req.DeliverSubject) { + resp.Error = NewJSSnapshotDeliverSubjectInvalidError() + s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) + return + } + + // We will do the snapshot in a go routine as well since check msgs may + // stall this go routine. + go func() { + if req.CheckMsgs { + s.Noticef("Starting health check and snapshot for stream '%s > %s'", mset.jsa.account.Name, mset.name()) + } else { + s.Noticef("Starting snapshot for stream '%s > %s'", mset.jsa.account.Name, mset.name()) + } + + start := time.Now().UTC() + + sr, err := mset.snapshot(0, req.CheckMsgs, !req.NoConsumers) + if err != nil { + s.Warnf("Snapshot of stream '%s > %s' failed: %v", mset.jsa.account.Name, mset.name(), err) + resp.Error = NewJSStreamSnapshotError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) + return + } + + config := mset.config() + resp.State = &sr.State + resp.Config = &config + + s.sendAPIResponse(ci, acc, subject, reply, smsg, s.jsonResponse(resp)) + + s.publishAdvisory(acc, JSAdvisoryStreamSnapshotCreatePre+"."+mset.name(), &JSSnapshotCreateAdvisory{ + TypedEvent: TypedEvent{ + Type: JSSnapshotCreatedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: mset.name(), + State: sr.State, + Client: ci.forAdvisory(), + Domain: s.getOpts().JetStreamDomain, + }) + + // Now do the real streaming. + err = s.streamSnapshot(acc, mset, sr, &req) + end := time.Now().UTC() + + var errStr string + if err != nil { + errStr = err.Error() + } + s.publishAdvisory(acc, JSAdvisoryStreamSnapshotCompletePre+"."+mset.name(), &JSSnapshotCompleteAdvisory{ + TypedEvent: TypedEvent{ + Type: JSSnapshotCompleteAdvisoryType, + ID: nuid.Next(), + Time: end, + }, + Stream: mset.name(), + Start: start, + End: end, + Client: ci.forAdvisory(), + Domain: s.getOpts().JetStreamDomain, + Error: errStr, + }) + + if err != nil { + s.Warnf("Snapshot for stream '%s > %s' failed after %v: %s", + mset.jsa.account.Name, + mset.name(), + end.Sub(start), + err, + ) + } else { + s.Noticef("Completed snapshot of %s for stream '%s > %s' in %v", + friendlyBytes(int64(sr.State.Bytes)), + mset.jsa.account.Name, + mset.name(), + end.Sub(start), + ) + } + }() +} + +// Default chunk size for now. +const defaultSnapshotChunkSize = 128 * 1024 // 128KiB +const defaultSnapshotWindowSize = 8 * 1024 * 1024 // 8MiB +const defaultSnapshotAckTimeout = 5 * time.Second + +var snapshotAckTimeout = defaultSnapshotAckTimeout + +// streamSnapshot will stream out our snapshot to the reply subject. +func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult, req *JSApiStreamSnapshotRequest) error { + chunkSize, wndSize := req.ChunkSize, req.WindowSize + if chunkSize == 0 { + chunkSize = defaultSnapshotChunkSize + } + if wndSize == 0 { + wndSize = defaultSnapshotWindowSize + } + chunkSize = min(max(1024, chunkSize), 1024*1024) // Clamp within 1KiB to 1MiB + wndSize = min(max(1024, wndSize), 32*1024*1024) // Clamp within 1KiB to 32MiB + wndSize = max(wndSize, chunkSize) // Guarantee at least one chunk + maxInflight := wndSize / chunkSize // Between 1 and 32,768 + + // Setup for the chunk stream. + reply := req.DeliverSubject + r := sr.Reader + defer r.Close() + + // In case we run into an error, this allows subscription callbacks + // to not sit and block endlessly. + done := make(chan struct{}) + defer close(done) + + // Check interest for the snapshot deliver subject. + inch := make(chan bool, 1) + if err := acc.sl.RegisterNotification(req.DeliverSubject, inch); err != nil { + return fmt.Errorf("could not register snapshot delivery interest for %q: %w", req.DeliverSubject, err) + } + defer acc.sl.ClearNotification(req.DeliverSubject, inch) + hasInterest := <-inch + if !hasInterest { + // Allow 2 seconds or so for interest to show up. + select { + case <-inch: + case <-time.After(2 * time.Second): + } + } + + // One slot per chunk. Each chunk read takes a slot, each ack will + // replace it. Smooths out in-flight number of chunks. + slots := make(chan struct{}, maxInflight) + for range maxInflight { + slots <- struct{}{} + } + + // We will place sequence number and size of chunk sent in the reply. + ackSubj := fmt.Sprintf(jsSnapshotAckT, mset.name(), nuid.Next()) + ackSub, _ := mset.subscribeInternal(ackSubj+".>", func(_ *subscription, _ *client, _ *Account, subject, _ string, _ []byte) { + select { + case slots <- struct{}{}: + case <-done: + } + }) + defer mset.unsubscribe(ackSub) + + var hdr []byte + chunk := make([]byte, chunkSize) + errCh := sr.errCh + var snapshotErr error + ackTimer := time.NewTimer(snapshotAckTimeout) + defer stopAndClearTimer(&ackTimer) + // index only incremented when a chunk is actually being sent. + for index := 1; ; { + select { + case <-slots: + // A slot has become available. + case <-inch: + // The receiver appears to have gone away. + snapshotErr = errors.New("no interest") + hdr = []byte("NATS/1.0 408 No Interest\r\n\r\n") + goto done + case err, ok := <-errCh: + if !ok { + // Channel closed normally, e.g. on completion. + errCh = nil + continue + } + // The snapshotting goroutine has failed for some reason. + snapshotErr = err + hdr = fmt.Appendf(nil, "NATS/1.0 500 %s\r\n\r\n", err) + goto done + case <-ackTimer.C: + // It's taking a very long time for the receiver to send us acks, + // they have probably stalled or there is high loss on the link. + snapshotErr = errors.New("no flow response") + hdr = []byte("NATS/1.0 408 No Flow Response\r\n\r\n") + goto done + } + n, err := io.ReadFull(r, chunk) + chunk := chunk[:n] + if err != nil { + if n > 0 { + mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, chunk, nil, 0)) + } + select { + case err, ok := <-errCh: + if ok { + snapshotErr = err + hdr = fmt.Appendf(nil, "NATS/1.0 500 %s\r\n\r\n", err) + } + default: + } + break + } + ackReply := fmt.Sprintf("%s.%d.%d", ackSubj, len(chunk), index) + if hdr == nil { + hdr = []byte("NATS/1.0 204\r\n\r\n") + } + mset.outq.send(newJSPubMsg(reply, _EMPTY_, ackReply, nil, chunk, nil, 0)) + ackTimer.Reset(snapshotAckTimeout) + index++ + } + +done: + mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + return snapshotErr +} + +// For determining consumer request type. +type ccReqType uint8 + +const ( + ccNew = iota + ccLegacyEphemeral + ccLegacyDurable +) + +// Request to create a consumer where stream and optional consumer name are part of the subject, and optional +// filtered subjects can be at the tail end. +// Assumes stream and consumer names are single tokens. +func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} + + var req CreateConsumerRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + var js *jetStream + isClustered := s.JetStreamIsClustered() + + // Determine if we should proceed here when we are in clustered mode. + direct := req.Config.Direct + if isClustered { + if direct { + // If it's just a direct consumer, check for stream leader. + if !req.Config.Sourcing { + // Check to see if we have this stream and are the stream leader. + if !acc.JetStreamIsStreamLeader(streamNameFromSubject(subject)) { + return + } + } else { + // Otherwise, we either need this to be answered by the stream or meta leader. + var cc *jetStreamCluster + js, cc = s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + js.mu.RLock() + sa := js.streamAssignmentOrInflight(acc.Name, streamNameFromSubject(subject)) + if sa == nil { + js.mu.RUnlock() + return + } + // If the stream is WQ or Interest, we need the meta leader to answer. + if sa.Config.Retention != LimitsPolicy { + direct = false + } + js.mu.RUnlock() + if direct { + // Check to see if we have this stream and are the stream leader. + if !acc.JetStreamIsStreamLeader(streamNameFromSubject(subject)) { + return + } + } else { + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + } + } else { + var cc *jetStreamCluster + js, cc = s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + var streamName, consumerName, filteredSubject string + var rt ccReqType + + if n := numTokens(subject); n < 5 { + s.Warnf(badAPIRequestT, msg) + return + } else if n == 5 { + // Legacy ephemeral. + rt = ccLegacyEphemeral + streamName = streamNameFromSubject(subject) + consumerName = req.Config.Name + } else { + // New style and durable legacy. + if tokenAt(subject, 4) == "DURABLE" { + rt = ccLegacyDurable + if n != 7 { + resp.Error = NewJSConsumerDurableNameNotInSubjectError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + streamName = tokenAt(subject, 6) + consumerName = tokenAt(subject, 7) + } else { + streamName = streamNameFromSubject(subject) + consumerName = consumerNameFromSubject(subject) + // New has optional filtered subject as part of main subject.. + if n > 6 { + tokens := strings.Split(subject, tsep) + filteredSubject = strings.Join(tokens[6:], tsep) + } + } + } + + if streamName != req.Stream { + resp.Error = NewJSStreamMismatchError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if consumerName != _EMPTY_ { + // Check for path like separators in the name. + if strings.ContainsAny(consumerName, `\/`) { + resp.Error = NewJSConsumerNameContainsPathSeparatorsError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + // Should we expect a durable name + if rt == ccLegacyDurable { + if numTokens(subject) < 7 { + resp.Error = NewJSConsumerDurableNameNotInSubjectError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Now check on requirements for durable request. + if req.Config.Durable == _EMPTY_ { + resp.Error = NewJSConsumerDurableNameNotSetError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if consumerName != req.Config.Durable { + resp.Error = NewJSConsumerDurableNameNotMatchSubjectError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Durable, so we need to honor the name. + req.Config.Name = consumerName + } + // If new style and durable set make sure they match. + if rt == ccNew { + if req.Config.Durable != _EMPTY_ { + if consumerName != req.Config.Durable { + resp.Error = NewJSConsumerDurableNameNotMatchSubjectError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + // New style ephemeral so we need to honor the name. + req.Config.Name = consumerName + } + // Check for legacy ephemeral mis-configuration. + if rt == ccLegacyEphemeral && req.Config.Durable != _EMPTY_ { + resp.Error = NewJSConsumerEphemeralWithDurableNameError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // in case of multiple filters provided, error if new API is used. + if filteredSubject != _EMPTY_ && len(req.Config.FilterSubjects) != 0 { + resp.Error = NewJSConsumerMultipleFiltersNotAllowedError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // Check for a filter subject. + if filteredSubject != _EMPTY_ && req.Config.FilterSubject != filteredSubject { + resp.Error = NewJSConsumerCreateFilterSubjectMismatchError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if isClustered && !direct { + s.jsClusteredConsumerRequest(ci, acc, subject, reply, hdr, msg, &req) + return + } + + // If we are here we are single server mode. + if req.Config.Replicas > 1 { + resp.Error = NewJSStreamReplicasNotSupportedError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + stream, err := acc.lookupStream(req.Stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if stream.offlineReason != _EMPTY_ { + resp.Error = NewJSStreamOfflineReasonError(errors.New(stream.offlineReason)) + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + + // If the consumer is a direct sourcing consumer, we need to "upgrade" + // it to be durable without AckNone if not a Limits-based stream. + if req.Config.Direct && req.Config.Sourcing && req.Config.Name != _EMPTY_ { + if !isClustered && stream.isInterestRetention() { + req.Config.Direct = false + req.Config.Durable = req.Config.Name + req.Config.AckPolicy = AckFlowControl + req.Config.AckWait = 0 + req.Config.MaxDeliver = 0 + req.Config.InactiveThreshold = 0 + } else { + // Otherwise, need to append a randomized suffix since the source uses a stable name. + req.Config.Name = fmt.Sprintf("%s-%s", req.Config.Name, createConsumerName()) + consumerName = req.Config.Name + } + } + + // If the user provided an expected stream identity, reject the request on a mismatch. + var streamIdentity string + if req.Config.Direct || req.Config.Sourcing { + streamIdentity = stream.identity() + if reqIdentity := sliceHeader(JSStreamIdentity, hdr); len(reqIdentity) > 0 && bytesToString(reqIdentity) != streamIdentity { + resp.Error = NewJSConsumerStreamIdentityMismatchError(streamIdentity) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + if o := stream.lookupConsumer(consumerName); o != nil { + if o.offlineReason != _EMPTY_ { + resp.Error = NewJSConsumerOfflineReasonError(errors.New(o.offlineReason)) + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + // If the consumer already exists then don't allow updating the PauseUntil, just set + // it back to whatever the current configured value is. + o.mu.RLock() + req.Config.PauseUntil = o.cfg.PauseUntil + // If a durable sourcing consumer is used, we need to reset the deliver policy. + if req.Config.Sourcing && req.Config.Durable != _EMPTY_ { + req.Config.DeliverPolicy = o.cfg.DeliverPolicy + req.Config.OptStartSeq = o.cfg.OptStartSeq + req.Config.OptStartTime = o.cfg.OptStartTime + } + o.mu.RUnlock() + } + + // Initialize/update asset version metadata. + setStaticConsumerMetadata(&req.Config) + + o, err := stream.addConsumerWithAction(&req.Config, req.Action, req.Pedantic) + + if err != nil { + if IsNatsErr(err, JSConsumerStoreFailedErrF) { + cname := req.Config.Durable // Will be empty if ephemeral. + s.Warnf("Consumer create failed for '%s > %s > %s': %v", acc, req.Stream, cname, err) + err = errConsumerStoreFailed + } + resp.Error = NewJSConsumerCreateError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if resp.ConsumerInfo = setDynamicConsumerInfoMetadata(o.initialInfo()); resp.ConsumerInfo == nil { + // The consumer was closed before we could respond. + resp.Error = NewJSConsumerCreateError(errConsumerClosed) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if streamIdentity != _EMPTY_ { + rhdr := genHeader(nil, JSStreamIdentity, streamIdentity) + s.sendAPIHdrResponse(ci, acc, subject, reply, string(msg), rhdr, s.jsonResponse(resp)) + } else { + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + } + + o.mu.RLock() + if o.cfg.PauseUntil != nil && !o.cfg.PauseUntil.IsZero() && time.Now().Before(*o.cfg.PauseUntil) { + o.sendPauseAdvisoryLocked(&o.cfg) + } + o.mu.RUnlock() +} + +// Request for the list of all consumer names. +func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiConsumerNamesResponse{ + ApiResponse: ApiResponse{Type: JSApiConsumerNamesResponseType}, + Consumers: []string{}, + } + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + var offset int + if isJSONObjectOrArray(msg) { + var req JSApiConsumersRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + offset = max(req.Offset, 0) + } + + streamName := streamNameFromSubject(subject) + var numConsumers int + + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + // TODO(dlc) - Debug or Warn? + return + } + js.mu.RLock() + sas := cc.streams[acc.Name] + if sas == nil { + js.mu.RUnlock() + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + sa := sas[streamName] + if sa == nil || sa.err != nil { + js.mu.RUnlock() + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + for consumer := range sa.consumers { + resp.Consumers = append(resp.Consumers, consumer) + } + if len(resp.Consumers) > 1 { + slices.Sort(resp.Consumers) + } + numConsumers = len(resp.Consumers) + if offset > numConsumers { + offset = numConsumers + } + resp.Consumers = resp.Consumers[offset:] + if len(resp.Consumers) > JSApiNamesLimit { + resp.Consumers = resp.Consumers[:JSApiNamesLimit] + } + js.mu.RUnlock() + + } else { + mset, err := acc.lookupStream(streamName) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + obs := mset.getPublicConsumers() + slices.SortFunc(obs, func(i, j *consumer) int { return cmp.Compare(i.name, j.name) }) + + numConsumers = len(obs) + if offset > numConsumers { + offset = numConsumers + } + + for _, o := range obs[offset:] { + resp.Consumers = append(resp.Consumers, o.String()) + if len(resp.Consumers) >= JSApiNamesLimit { + break + } + } + } + resp.Total = numConsumers + resp.Limit = JSApiNamesLimit + resp.Offset = offset + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request for the list of all detailed consumer information. +func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiConsumerListResponse{ + ApiResponse: ApiResponse{Type: JSApiConsumerListResponseType}, + Consumers: []*ConsumerInfo{}, + } + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + var offset int + if isJSONObjectOrArray(msg) { + var req JSApiConsumersRequest + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + offset = max(req.Offset, 0) + } + + streamName := streamNameFromSubject(subject) + + // Clustered mode will invoke a scatter and gather. + if s.JetStreamIsClustered() { + // Need to copy these off before sending.. don't move this inside startGoRoutine!!! + msg = copyBytes(msg) + s.startGoRoutine(func() { + s.jsClusteredConsumerListRequest(acc, ci, offset, streamName, subject, reply, msg) + }) + return + } + + mset, err := acc.lookupStream(streamName) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + obs := mset.getPublicConsumers() + slices.SortFunc(obs, func(i, j *consumer) int { return cmp.Compare(i.name, j.name) }) + + ocnt := len(obs) + if offset > ocnt { + offset = ocnt + } + + var missingNames []string + for _, o := range obs[offset:] { + if o.offlineReason != _EMPTY_ { + if resp.Offline == nil { + resp.Offline = make(map[string]string, 1) + } + resp.Offline[o.name] = o.offlineReason + missingNames = append(missingNames, o.name) + continue + } + if cinfo := o.info(); cinfo != nil { + resp.Consumers = append(resp.Consumers, cinfo) + } + if len(resp.Consumers) >= JSApiListLimit { + break + } + } + resp.Total = ocnt + resp.Limit = JSApiListLimit + resp.Offset = offset + resp.Missing = missingNames + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request for information about an consumer. +func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + streamName := streamNameFromSubject(subject) + consumerName := consumerNameFromSubject(subject) + + var resp = JSApiConsumerInfoResponse{ApiResponse: ApiResponse{Type: JSApiConsumerInfoResponseType}} + + if !isEmptyRequest(msg) { + resp.Error = NewJSNotEmptyRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // If we are in clustered mode we need to be the consumer leader to proceed. + if s.JetStreamIsClustered() { + // Check to make sure the consumer is assigned. + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.RLock() + meta := cc.meta + js.mu.RUnlock() + + if meta == nil { + return + } + + // Since these could wait on the Raft group lock, don't do so under the JS lock. + ourID := meta.ID() + groupLeaderless := meta.Leaderless() + groupCreated := meta.Created() + + js.mu.RLock() + isLeader, sa, ca := cc.isLeader(), js.streamAssignment(acc.Name, streamName), js.consumerAssignment(acc.Name, streamName, consumerName) + if sa == nil { + // Fallback for a stream that's being created. + sa = js.streamAssignmentOrInflight(acc.Name, streamName) + } + if sa != nil && ca == nil { + // Fallback for a consumer that's being created. + ca = js.consumerAssignmentOrInflight(acc.Name, streamName, consumerName) + } + var rg *raftGroup + var offline, isMember bool + if ca != nil { + if rg = ca.Group; rg != nil { + offline = s.allPeersOffline(rg) + isMember = rg.isMember(ourID) + } + if ca.unsupported != nil && isMember { + // If we're a member for this consumer, and it's not supported, report it as offline. + resp.Error = NewJSConsumerOfflineReasonError(errors.New(ca.unsupported.reason)) + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + js.mu.RUnlock() + return + } + } + // Capture consumer leader here. + isConsumerLeader := cc.isConsumerLeader(acc.Name, streamName, consumerName) + // Also capture if we think there is no meta leader. + var isLeaderLess bool + if !isLeader { + isLeaderLess = groupLeaderless && time.Since(groupCreated) > lostQuorumInterval + } + js.mu.RUnlock() + + if isLeader && ca == nil { + // We can't find the consumer, so mimic what would be the errors below. + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + if sa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // If we are here the consumer is not present. + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } else if ca == nil { + if isLeaderLess { + resp.Error = NewJSClusterNotAvailError() + // Delaying an error response gives the leader a chance to respond before us + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + } + return + } else if isLeader && offline { + resp.Error = NewJSConsumerOfflineError() + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + + // Check to see if we are a member of the group and if the group has no leader. + if isMember && js.isGroupLeaderless(ca.Group) { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // We have the consumer assigned and a leader, so only the consumer leader should answer. + if !isConsumerLeader { + if isLeaderLess { + resp.Error = NewJSClusterNotAvailError() + // Delaying an error response gives the leader a chance to respond before us + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), ca.Group, errRespDelay) + return + } + + var node RaftNode + var leaderNotPartOfGroup bool + + // We have a consumer assignment. + if isMember { + js.mu.RLock() + if rg != nil && rg.node != nil { + node = rg.node + if gl := node.GroupLeader(); gl != _EMPTY_ && !rg.isMember(gl) { + leaderNotPartOfGroup = true + } + } + js.mu.RUnlock() + } + + // Check if we should ignore all together. + if node == nil { + // We have been assigned but have not created a node yet. If we are a member return + // our config and defaults for state and no cluster info. + if isMember { + // Since we access consumerAssignment, need js lock. + js.mu.RLock() + resp.ConsumerInfo = &ConsumerInfo{ + Stream: ca.Stream, + Name: ca.Name, + Created: ca.Created, + Config: setDynamicConsumerMetadata(ca.Config), + TimeStamp: time.Now().UTC(), + } + b := s.jsonResponse(resp) + js.mu.RUnlock() + s.sendAPIResponse(ci, acc, subject, reply, string(msg), b) + } + return + } + // If we are a member and we have a group leader or we had a previous leader consider bailing out. + bail := !node.Leaderless() || node.HadPreviousLeader() || rg == nil + if !bail { + js.mu.RLock() + bail = rg.Preferred != ourID || (rg.node != nil && time.Since(rg.node.Created()) > lostQuorumInterval) + js.mu.RUnlock() + } + if bail { + if leaderNotPartOfGroup { + resp.Error = NewJSConsumerOfflineError() + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + } + return + } + // If we are here we are a member and this is just a new consumer that does not have a (preferred) leader yet. + // Will fall through and return what we have. All consumers can respond but this should be very rare + // but makes more sense to clients when they try to create, get a consumer exists, and then do consumer info. + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if !acc.JetStreamEnabled() { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + mset, err := acc.lookupStream(streamName) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + obs := mset.lookupConsumer(consumerName) + if obs == nil { + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if obs.offlineReason != _EMPTY_ { + resp.Error = NewJSConsumerOfflineReasonError(errors.New(obs.offlineReason)) + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + + if resp.ConsumerInfo = setDynamicConsumerInfoMetadata(obs.info()); resp.ConsumerInfo == nil { + // This consumer returned nil which means it's closed. Respond with not found. + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to delete an Consumer. +func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var resp = JSApiConsumerDeleteResponse{ApiResponse: ApiResponse{Type: JSApiConsumerDeleteResponseType}} + + // Determine if we should proceed here when we are in clustered mode. + if s.JetStreamIsClustered() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + if !isEmptyRequest(msg) { + resp.Error = NewJSNotEmptyRequestError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + stream := streamNameFromSubject(subject) + consumer := consumerNameFromSubject(subject) + + if s.JetStreamIsClustered() { + s.jsClusteredConsumerDeleteRequest(ci, acc, stream, consumer, subject, reply, rmsg) + return + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + obs := mset.lookupConsumer(consumer) + if obs == nil { + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if err := obs.delete(); err != nil { + resp.Error = NewJSStreamGeneralError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + resp.Success = true + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// Request to pause or unpause a Consumer. +func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + if c == nil || !s.JetStreamEnabled() { + return + } + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) + if err != nil { + s.Warnf(badAPIRequestT, msg) + return + } + + var req JSApiConsumerPauseRequest + var resp = JSApiConsumerPauseResponse{ApiResponse: ApiResponse{Type: JSApiConsumerPauseResponseType}} + + if isJSONObjectOrArray(msg) { + if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { + resp.Error = NewJSInvalidJSONError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + // Determine if we should proceed here when we are in clustered mode. + isClustered := s.JetStreamIsClustered() + js, cc := s.getJetStreamCluster() + if isClustered { + if js == nil || cc == nil { + return + } + if js.isLeaderless() { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Make sure we are meta leader. + if !s.JetStreamIsLeader() { + return + } + } + + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if hasJS, doErr := acc.checkJetStream(); !hasJS { + if doErr { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + } + return + } + + stream := streamNameFromSubject(subject) + consumer := consumerNameFromSubject(subject) + + if isClustered { + js.mu.Lock() + sa := js.streamAssignmentOrInflight(acc.Name, stream) + if sa == nil { + js.mu.Unlock() + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if sa.unsupported != nil { + js.mu.Unlock() + // Just let the request time out. + return + } + ca := js.consumerAssignmentOrInflight(acc.Name, stream, consumer) + if ca == nil { + js.mu.Unlock() + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if ca.unsupported != nil { + js.mu.Unlock() + // Just let the request time out. + return + } + + nca := ca.clone() + // Don't respond to the original create request again. + nca.Reply = _EMPTY_ + // We need a copy to prevent concurrent reads/writes. + ncfg := *ca.Config + ncfg.Metadata = maps.Clone(ncfg.Metadata) + nca.Config = &ncfg + meta := cc.meta + pauseUTC := req.PauseUntil.UTC() + if !pauseUTC.IsZero() { + nca.Config.PauseUntil = &pauseUTC + } else { + nca.Config.PauseUntil = nil + } + + // Update asset version metadata due to updating pause/resume. + // Only PauseUntil is updated above, so reuse config for both. + setStaticConsumerMetadata(nca.Config) + + eca := encodeAddConsumerAssignment(nca) + if err = meta.Propose(cc.term, eca); err != nil { + js.mu.Unlock() + return + } + cc.trackInflightConsumerProposal(acc.Name, stream, nca, false) + js.mu.Unlock() + + resp.PauseUntil = pauseUTC + if resp.Paused = time.Now().Before(pauseUTC); resp.Paused { + resp.PauseRemaining = time.Until(pauseUTC) + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) + return + } + + mset, err := acc.lookupStream(stream) + if err != nil { + resp.Error = NewJSStreamNotFoundError(Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if mset.offlineReason != _EMPTY_ { + // Just let the request time out. + return + } + + obs := mset.lookupConsumer(consumer) + if obs == nil { + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if obs.offlineReason != _EMPTY_ { + // Just let the request time out. + return + } + + // We're only holding the read lock and release below, + // we need a copy to prevent concurrent reads/writes. + obs.mu.RLock() + ncfg := obs.cfg + ncfg.Metadata = maps.Clone(ncfg.Metadata) + obs.mu.RUnlock() + + pauseUTC := req.PauseUntil.UTC() + if !pauseUTC.IsZero() { + ncfg.PauseUntil = &pauseUTC + } else { + ncfg.PauseUntil = nil + } + + // Update asset version metadata due to updating pause/resume. + setStaticConsumerMetadata(&ncfg) + + if err := obs.updateConfig(&ncfg); err != nil { + // The only type of error that should be returned here is from o.store, + // so use a store failed error type. + resp.Error = NewJSConsumerStoreFailedError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + resp.PauseUntil = pauseUTC + if resp.Paused = time.Now().Before(pauseUTC); resp.Paused { + resp.PauseRemaining = time.Until(pauseUTC) + } + s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp)) +} + +// sendJetStreamAPIAuditAdvisory will send the audit event for a given event. +func (s *Server) sendJetStreamAPIAuditAdvisory(ci *ClientInfo, acc *Account, subject, request, response string) { + s.publishAdvisory(acc, JSAuditAdvisory, JSAPIAudit{ + TypedEvent: TypedEvent{ + Type: JSAPIAuditType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Server: s.Name(), + Client: ci.forAdvisory(), + Subject: subject, + Request: request, + Response: response, + Domain: s.getOpts().JetStreamDomain, + }) +} diff --git a/server/jetstream_cluster.go b/server/jetstream_cluster.go new file mode 100644 index 0000000..9efb1d5 --- /dev/null +++ b/server/jetstream_cluster.go @@ -0,0 +1,14071 @@ +// Copyright 2020-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 ( + "bytes" + "cmp" + crand "crypto/rand" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "iter" + "maps" + "math" + "math/rand/v2" + "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/nuid" +) + +// jetStreamCluster holds information about the meta group and stream assignments. +type jetStreamCluster struct { + // The metacontroller raftNode. + meta RaftNode + // For stream and consumer assignments. All servers will have this be the same. + // ACCOUNT -> STREAM -> Stream Assignment -> Consumers + streams map[string]map[string]*streamAssignment + // These are inflight proposals and used to apply limits when there are + // concurrent requests that would otherwise be accepted. + // We also record the assignment for the stream/consumer. This is needed since if we have + // concurrent requests for same account and stream/consumer we need to let it process to get + // a response but they need to be same group, peers etc. and sync subjects. + inflightStreams map[string]map[string]*inflightStreamInfo + inflightConsumers map[string]map[string]map[string]*inflightConsumerInfo + // Tracks raft groups currently being started by createRaftGroup, so that + // concurrent callers for the same group can wait without holding js.mu + // across the disk I/O performed during startup. + creatingRaftGroups map[string]chan struct{} + // Holds a map of a peer ID to the reply subject, to only respond after gaining + // quorum on the peer-remove action. + peerRemoveReply map[string]peerRemoveInfo + // Raft term, used to determine if we are still the leader for the current term. + term uint64 + // Signals meta-leader should check the stream assignments. + streamsCheck bool + // Server. + s *Server + // Internal client. + c *client + // Processing assignment results. + streamResults *subscription + consumerResults *subscription + // Processing desired assignment reconciliation. + streamReconcile *subscription + consumerReconcile *subscription + // Internal requests to create or delete a consumer assignment, used by servers + // that have a local consumer the meta layer doesn't know about (yet). + consumerCreate *subscription + consumerDelete *subscription + // System level request to have the leader stepdown. + stepdown *subscription + // System level requests to remove a peer. + peerRemove *subscription + // System level requests to evacuate a peer. + peerEvacuate *subscription + // System level request to move a stream + peerStreamMove *subscription + // System level request to cancel a stream move + peerStreamCancelMove *subscription + // System level broadcast request to unsafely rescue the meta group. + // Unlike the subscriptions above, this is active on every server, + // not only on the meta leader. + metaRescue *subscription + // To pop out the monitorCluster before the raft layer. + qch chan struct{} + // To notify others that monitorCluster has actually stopped. + stopped chan struct{} + // Track last meta snapshot time and duration for monitoring. + lastMetaSnapTime int64 // Unix nanoseconds + lastMetaSnapDuration int64 // Duration in nanoseconds + // If a write error was encountered while applying meta entries, and if so what error. + werr error +} + +// Used to track inflight stream create/update/delete requests that have been proposed but not yet applied. +type inflightStreamInfo struct { + ops uint64 // Inflight operations, i.e. inflight stream creates/updates/deletes. + deleted bool // Whether the stream has been deleted. + *streamAssignment +} + +// Used to track inflight consumer create/update/delete requests that have been proposed but not yet applied. +type inflightConsumerInfo struct { + ops uint64 // Inflight operations, i.e. inflight consumer creates/updates/deletes. + deleted bool // Whether the consumer has been deleted. + *consumerAssignment +} + +// Used to track inflight peer-remove info to respond 'success' after quorum. +type peerRemoveInfo struct { + ci *ClientInfo + subject string + reply string + request string +} + +// Used to guide placement of streams and meta controllers in clustered JetStream. +type Placement struct { + Cluster string `json:"cluster,omitempty"` + Tags []string `json:"tags,omitempty"` + Preferred string `json:"preferred,omitempty"` +} + +func (p *Placement) clone() *Placement { + if p == nil { + return nil + } + cp := *p + cp.Tags = copyStrings(p.Tags) + return &cp +} + +// Define types of the entry. +type entryOp uint8 + +// ONLY ADD TO THE END, DO NOT INSERT IN BETWEEN WILL BREAK SERVER INTEROP. +const ( + // Meta ops. + assignStreamOp entryOp = iota + assignConsumerOp + removeStreamOp + removeConsumerOp + // Stream ops. + streamMsgOp + purgeStreamOp + deleteMsgOp + // Consumer ops. + updateDeliveredOp + updateAcksOp + // Compressed consumer assignments. + assignCompressedConsumerOp + // Filtered Consumer skip. + updateSkipOp + // Update Stream. + updateStreamOp + // For updating information on pending pull requests. + addPendingRequest + removePendingRequest + // For sending compressed streams, either through RAFT or catchup. + compressedStreamMsgOp + // For sending deleted gaps on catchups for replicas. + deleteRangeOp + // Batch stream ops. + batchMsgOp + batchCommitMsgOp + // Consumer rest to specific starting sequence. + resetSeqOp +) + +// raftGroups are controlled by the metagroup controller. +// The raftGroups will house streams and consumers. +type raftGroup struct { + Name string `json:"name"` + Peers []string `json:"peers"` + Storage StorageType `json:"store"` + Cluster string `json:"cluster,omitempty"` + Preferred string `json:"preferred,omitempty"` + ScaleUp bool `json:"scale_up,omitempty"` + // Excluded peers are evacuated and can't be added to this group as part of the meta + // leader's reconciliation. Cleared when this group is at the configured replicas. + Excluded []string `json:"excluded,omitempty"` + // Desired holds the target placement while this group is being moved or + // scaled; it is nil when the group is stable. + Desired *desiredRaftGroup `json:"desired,omitempty"` + // Internal + node RaftNode + // migration reports what the group leader is currently doing to move this group + // toward its desired state. Local to the group leader and not replicated: only + // the leader drives the migration, and only the leader answers INFO requests. + migration *DesiredClusterInfoStatus +} + +// desiredGroupPlacement specifies the desired peer set. +// A stream or consumer raftGroup can be scaled or moved safely based on this desired state. +type desiredRaftGroup struct { + Created time.Time `json:"created"` + ID string `json:"id"` + // Term is the raft term of the group leader driving this desired state. It acts as a + // fencing token; reconcile requests from older leadership terms are rejected. + Term uint64 `json:"term,omitempty"` + Peers []string `json:"peers"` + Cluster string `json:"cluster,omitempty"` + Preferred string `json:"preferred,omitempty"` + + // ScaleDown marks this desired state as a scale down. The Peers field is the + // total set that can be scaled down from. + ScaleDown bool `json:"scale_down,omitempty"` + + // Move marks this desired state as retargeting placement. + Move bool `json:"move,omitempty"` + + // Removed are peers an operator peer-removed from this group. A group that + // can't reach quorum may only evict what's recorded here. + Removed []string `json:"removed,omitempty"` + + Origin *desiredRaftGroupOrigin `json:"origin,omitempty"` +} + +// desiredRaftGroupOrigin specifies the original properties of the asset before any desired state changes were made. +// Multiple desired state changes MUST NOT update these values, only the initial values must be set, allowing to +// revert to before any changes were made. +type desiredRaftGroupOrigin struct { + Peers []string `json:"peers"` + Cluster string `json:"cluster,omitempty"` + Replicas int `json:"replicas"` + Placement *Placement `json:"placement,omitempty"` + // When changing between retention policies, this is the origin retention. + // While recorded the stream acts under Limits retention until converged. + Retention *RetentionPolicy `json:"retention,omitempty"` +} + +// legacyMoveOrigin returns the origin of a move that was started before the upgrade to +// desired state, or nil if there is no such move. Recognized by the group having more +// peers than configured replicas. +func (sa *streamAssignment) legacyMoveOrigin() *desiredRaftGroupOrigin { + // Only a group that has not been reconciled into desired state yet can hold a legacy move. + if sa == nil || sa.Config == nil || sa.Group == nil || sa.Group.Desired != nil { + return nil + } + replicas := sa.Config.Replicas + if replicas < 1 || len(sa.Group.Peers) <= replicas { + return nil + } + return &desiredRaftGroupOrigin{ + Peers: copyStrings(sa.Group.Peers[:replicas]), + // Group.Cluster was only updated once a legacy move completed, so it still + // holds the origin cluster. + Cluster: sa.Group.Cluster, + Replicas: replicas, + // NOTE: a legacy move did not record the placement it started with. We can't + // restore what's already lost, so we just preserve the final placement as before. + Placement: sa.Config.Placement, + } +} + +// desiredOrigin returns the origin to roll back to, either recorded on the desired +// state or reconstructed from a legacy move. Nil if there's nothing to roll back to. +func (sa *streamAssignment) desiredOrigin() *desiredRaftGroupOrigin { + if sa == nil || sa.Group == nil { + return nil + } + if sa.Group.Desired != nil { + return sa.Group.Desired.Origin + } + return sa.legacyMoveOrigin() +} + +// isR1ScaleUpSource reports whether the given peer is the source for an +// in-progress R1 scale-up. +func (sa *streamAssignment) isR1ScaleUpSource(peer string) bool { + if sa == nil || sa.Group == nil || sa.Group.Desired == nil { + return false + } + d := sa.Group.Desired + return d.Origin != nil && d.Origin.Replicas == 1 && len(d.Peers) > 1 && + len(d.Origin.Peers) == 1 && d.Origin.Peers[0] == peer +} + +// moveInFlight returns whether a move is still converging, including one that was +// started before the upgrade to desired state. +func (sa *streamAssignment) moveInFlight() bool { + if sa == nil || sa.Group == nil { + return false + } + return (sa.Group.Desired != nil && sa.Group.Desired.Move) || sa.legacyMoveOrigin() != nil +} + +// withDesired returns a copy of rg with target expressed as the desired state. +// target MUST already be a copy or fresh group, as it's directly referenced. +func (rg *raftGroup) withDesired(target *raftGroup) *raftGroup { + // When scaling up from a single replica, set it as preferred, as it has the data. + if len(rg.Peers) == 1 { + target.Preferred = rg.Peers[0] + } else if target.Preferred != _EMPTY_ && !slices.Contains(target.Peers, target.Preferred) { + // Don't carry a stale preferred into the desired group. + target.Preferred = _EMPTY_ + } + ng := rg.copyGroup() + ng.Name = target.Name + ng.Desired = &desiredRaftGroup{ + Created: time.Now().UTC(), + ID: nuid.Next(), + Term: 0, + Peers: target.Peers, + Cluster: target.Cluster, + Preferred: target.Preferred, + } + if rg.Desired != nil { + // Must preserve the original created timestamp, term, + // and whether an in-flight move is being retargeted. + ng.Desired.Created = rg.Desired.Created + ng.Desired.Term = rg.Desired.Term + ng.Desired.Move = rg.Desired.Move + // Must preserve the prior origin (if any). + if rg.Desired.Origin != nil { + origin := *rg.Desired.Origin + ng.Desired.Origin = &origin + } + // Must also preserve which peers were peer-removed. + for _, peer := range rg.Desired.Removed { + if !slices.Contains(ng.Desired.Peers, peer) { + ng.Desired.Removed = append(ng.Desired.Removed, peer) + } + } + } + return ng +} + +// atDesiredOrigin returns a copy of the config with the desired origin applied (if any). +func (cfg *StreamConfig) atDesiredOrigin(rg *raftGroup) *StreamConfig { + if rg.Desired == nil || rg.Desired.Origin == nil { + return cfg + } + newCfg := cfg.clone() + // The origin always records the placement it started from, nil included, so an + // unconstrained origin must be restored just the same. + newCfg.Placement = rg.Desired.Origin.Placement.clone() + if rg.Desired.Origin.Retention != nil { + // Any retention change means the stream acts under Limits until converged. + // Either we're moving from Limits to Interest, and we only apply Interest at the end. + // Or, we're moving from Interest to Limits, and must release the Interest restrictions prior to converging. + newCfg.Retention = LimitsPolicy + } + return newCfg +} + +// atDesiredTarget returns a copy of the running config with the fields that atDesiredOrigin can +// hold back restored from target, which is the config as the user requested it. While the desired +// state is pending the stream runs at its origin, but clients MUST be reported the config they +// requested. Restoring unconditionally is safe, target always holds the requested values, whether +// the desired state is still pending or was already reached. +func (cfg StreamConfig) atDesiredTarget(target *StreamConfig) StreamConfig { + if target == nil { + return cfg + } + // Must copy, the target can be owned by the meta layer and outlive its lock here. + cfg.Placement = target.Placement.clone() + cfg.Retention = target.Retention + return cfg +} + +// targetStreamConfig returns the stream's cfg as the user requested it, looking up the requested config +// from the meta layer. Can differ from the running config while the desired state is still pending, +// or while an update was committed but not applied onto the stream yet. +func (js *jetStream) targetStreamConfig(mset *stream, cfg StreamConfig) StreamConfig { + // Only a clustered stream can differ from what was requested. + if js == nil || js.cluster == nil || mset == nil { + return cfg + } + accName := mset.accName() + js.mu.RLock() + defer js.mu.RUnlock() + sa := js.streamAssignment(accName, cfg.Name) + if sa == nil { + return cfg + } + return cfg.atDesiredTarget(sa.Config) +} + +// populateOrigin populates the desired origin for the raft group, capturing the state from +// before any desired state changes were made. Skipped if there's no desired state to record +// it onto, or if the origin was already recorded, since it MUST NOT be overwritten. +func (rg *raftGroup) populateOrigin(osa *streamAssignment) { + if rg.Desired == nil || rg.Desired.Origin != nil { + return + } + currPeers, currCluster := copyStrings(osa.Group.Peers), osa.Group.Cluster + // If we had desired state without origin, need to capture what was + // rolled back to as the new origin. + if d := osa.Group.Desired; d != nil { + currPeers, currCluster = copyStrings(d.Peers), d.Cluster + } else if legacy := osa.legacyMoveOrigin(); legacy != nil { + // A legacy move is only encoded as an over-replicated peer set. Capture the peer + // set it started from, or a rollback would restore the enlarged set instead. + currPeers, currCluster = legacy.Peers, legacy.Cluster + } + rg.Desired.Origin = &desiredRaftGroupOrigin{ + Peers: currPeers, + Cluster: currCluster, + Replicas: osa.Config.Replicas, + Placement: osa.Config.Placement.clone(), + Retention: nil, // Don't capture retention, only a retention change does. + } +} + +// withRetentionChange returns rg with the desired state registering a change from osa's retention +// into newRetention. Returned as-is if the retention is unchanged, or for a singleton without +// desired state, since then it can be applied immediately. +// +// The config always holds the retention to move to, and the origin what retention can be rolled +// back to. While recorded the stream functions under Limits retention until converged. Moving into +// Interest or WorkQueue needs consumers to have parity with the stream first. Similarly, the reverse +// requires releasing those restrictions back to Limits prior to converging. +func (rg *raftGroup) withRetentionChange(osa *streamAssignment, newRetention RetentionPolicy) *raftGroup { + if newRetention == osa.Config.Retention { + return rg + } + // A retention change should go through desired state, unless it is a singleton without it. + if rg.Desired == nil { + if len(rg.Peers) == 1 { + return rg + } + // Must always register desired state. + rg = osa.Group.withDesired(rg) + // A legacy move absorbed into desired state must keep counting as a move in flight. + if osa.legacyMoveOrigin() != nil { + rg.Desired.Move = true + } + } + // Desired state MUST always have an origin recorded, or it can't be rolled back or canceled. + rg.populateOrigin(osa) + // Only record the retention if we hadn't already recorded it, the origin must remain + // the retention from before any desired state changes were made. Must check osa, since + // populateOrigin above could have just recorded a fresh origin onto rg without a retention. + if d := osa.Group.Desired; d != nil && d.Origin != nil && d.Origin.Retention != nil { + return rg + } + retention := osa.Config.Retention + rg.Desired.Origin.Retention = &retention + return rg +} + +// streamAssignment is what the meta controller uses to assign streams to peers. +type streamAssignment struct { + Client *ClientInfo `json:"client,omitempty"` + Created time.Time `json:"created"` + ConfigJSON json.RawMessage `json:"stream"` + Config *StreamConfig `json:"-"` + Group *raftGroup `json:"group"` + Sync string `json:"sync"` + Subject string `json:"subject,omitempty"` + Reply string `json:"reply,omitempty"` + Restore *StreamState `json:"restore_state,omitempty"` + // Internal + consumers map[string]*consumerAssignment + responded atomic.Bool // copied via clone() to satisfy go vet's noCopy check + recovering bool + reassigning bool // i.e. due to placement issues, lack of resources, etc. + resetting bool // i.e. there was an error, and we're stopping and starting the stream + err error + unsupported *unsupportedStreamAssignment +} + +func (sa *streamAssignment) hasResponded() bool { + return sa.responded.Load() +} + +// markResponded sets the responded flag and returns the prior value. +func (sa *streamAssignment) markResponded() bool { + return sa.responded.Swap(true) +} + +func (sa *streamAssignment) clearResponded() { + sa.responded.Store(false) +} + +// clone returns a copy of sa. Field-explicit (rather than `*sa`) and +// pointer-returning so the embedded atomic.Bool isn't value-copied; +// responded is transferred via Load/Store. Concurrent callers may write +// responded via markResponded/clearResponded without holding js.mu. +func (sa *streamAssignment) clone() *streamAssignment { + csa := &streamAssignment{ + Client: sa.Client, + Created: sa.Created, + ConfigJSON: sa.ConfigJSON, + Config: sa.Config, + Group: sa.Group, + Sync: sa.Sync, + Subject: sa.Subject, + Reply: sa.Reply, + Restore: sa.Restore, + consumers: sa.consumers, + recovering: sa.recovering, + reassigning: sa.reassigning, + resetting: sa.resetting, + err: sa.err, + unsupported: sa.unsupported, + } + csa.responded.Store(sa.responded.Load()) + return csa +} + +type unsupportedStreamAssignment struct { + reason string + info StreamInfo + sysc *client + infoSub *subscription +} + +func newUnsupportedStreamAssignment(s *Server, sa *streamAssignment, err error) *unsupportedStreamAssignment { + reason := "stopped" + if err != nil { + if errstr := err.Error(); strings.HasPrefix(errstr, "json:") { + reason = fmt.Sprintf("unsupported - config error: %s", strings.TrimPrefix(err.Error(), "json: ")) + } else { + reason = fmt.Sprintf("stopped - %s", errstr) + } + } else if sa.Config != nil && !supportsRequiredApiLevel(sa.Config.Metadata) { + if req := getRequiredApiLevel(sa.Config.Metadata); req != _EMPTY_ { + reason = fmt.Sprintf("unsupported - required API level: %s, current API level: %d", req, JSApiLevel) + } + } + return &unsupportedStreamAssignment{ + reason: reason, + info: StreamInfo{ + Created: sa.Created, + Config: *setDynamicStreamMetadata(sa.Config), + Domain: s.getOpts().JetStreamDomain, + TimeStamp: time.Now().UTC(), + }, + } +} + +func (usa *unsupportedStreamAssignment) setupInfoSub(s *Server, sa *streamAssignment) { + if usa.infoSub != nil { + return + } + + // Bind to the system account. + ic := s.createInternalJetStreamClient() + ic.registerWithAccount(s.SystemAccount()) + usa.sysc = ic + + // Note below the way we subscribe here is so that we can send requests to ourselves. + isubj := fmt.Sprintf(clusterStreamInfoT, sa.Client.serviceAccount(), sa.Config.Name) + usa.infoSub, _ = s.systemSubscribe(isubj, _EMPTY_, false, ic, usa.handleClusterStreamInfoRequest) +} + +func (usa *unsupportedStreamAssignment) handleClusterStreamInfoRequest(_ *subscription, c *client, _ *Account, _, reply string, _ []byte) { + s, acc := c.srv, c.acc + info := streamInfoClusterResponse{OfflineReason: usa.reason, StreamInfo: usa.info} + s.sendDelayedErrResponse(acc, reply, nil, s.jsonResponse(&info), errRespDelay) +} + +func (usa *unsupportedStreamAssignment) closeInfoSub(s *Server) { + if usa.infoSub != nil { + s.sysUnsubscribe(usa.infoSub) + usa.infoSub = nil + } + if usa.sysc != nil { + usa.sysc.closeConnection(ClientClosed) + usa.sysc = nil + } +} + +// consumerAssignment is what the meta controller uses to assign consumers to streams. +type consumerAssignment struct { + Client *ClientInfo `json:"client,omitempty"` + Created time.Time `json:"created"` + Name string `json:"name"` + Stream string `json:"stream"` + ConfigJSON json.RawMessage `json:"consumer"` + Config *ConsumerConfig `json:"-"` + Group *raftGroup `json:"group"` + Subject string `json:"subject,omitempty"` + Reply string `json:"reply,omitempty"` + State *ConsumerState `json:"state,omitempty"` + // Internal + responded atomic.Bool // copied via clone() to satisfy go vet's noCopy check + recovering bool + err error + unsupported *unsupportedConsumerAssignment +} + +func (ca *consumerAssignment) hasResponded() bool { + return ca.responded.Load() +} + +// markResponded sets the responded flag and returns the prior value. +func (ca *consumerAssignment) markResponded() bool { + return ca.responded.Swap(true) +} + +func (ca *consumerAssignment) clearResponded() { + ca.responded.Store(false) +} + +// sameIdentity reports whether nca refers to the same logical consumer as ca. +// Only stable identity fields (Name, Stream, Created time) are compared; +// request-routing fields like Client/Reply and transient flags are +// intentionally excluded since processClusterCreateConsumer may set the +// per-object o.ca to a clone with the original requester's Client/Reply +// preserved while the meta-layer holds the newer values. +func (ca *consumerAssignment) sameIdentity(nca *consumerAssignment) bool { + return ca != nil && nca != nil && + nca.Name == ca.Name && + nca.Stream == ca.Stream && + nca.Created.Equal(ca.Created) +} + +// clone returns a copy of ca. Field-explicit (rather than `*ca`) and +// pointer-returning so the embedded atomic.Bool isn't value-copied; +// responded is transferred via Load/Store. Concurrent callers may write +// responded via markResponded/clearResponded without holding js.mu. +func (ca *consumerAssignment) clone() *consumerAssignment { + cca := &consumerAssignment{ + Client: ca.Client, + Created: ca.Created, + Name: ca.Name, + Stream: ca.Stream, + ConfigJSON: ca.ConfigJSON, + Config: ca.Config, + Group: ca.Group, + Subject: ca.Subject, + Reply: ca.Reply, + State: ca.State, + recovering: ca.recovering, + err: ca.err, + unsupported: ca.unsupported, + } + cca.responded.Store(ca.responded.Load()) + return cca +} + +type unsupportedConsumerAssignment struct { + reason string + info ConsumerInfo + sysc *client + infoSub *subscription +} + +func newUnsupportedConsumerAssignment(ca *consumerAssignment, err error) *unsupportedConsumerAssignment { + reason := "stopped" + if err != nil { + if errstr := err.Error(); strings.HasPrefix(errstr, "json:") { + reason = fmt.Sprintf("unsupported - config error: %s", strings.TrimPrefix(err.Error(), "json: ")) + } else { + reason = fmt.Sprintf("stopped - %s", errstr) + } + } else if ca.Config != nil && !supportsRequiredApiLevel(ca.Config.Metadata) { + if req := getRequiredApiLevel(ca.Config.Metadata); req != _EMPTY_ { + reason = fmt.Sprintf("unsupported - required API level: %s, current API level: %d", getRequiredApiLevel(ca.Config.Metadata), JSApiLevel) + } + } + return &unsupportedConsumerAssignment{ + reason: reason, + info: ConsumerInfo{ + Stream: ca.Stream, + Name: ca.Name, + Created: ca.Created, + Config: setDynamicConsumerMetadata(ca.Config), + TimeStamp: time.Now().UTC(), + }, + } +} + +func (uca *unsupportedConsumerAssignment) setupInfoSub(s *Server, ca *consumerAssignment) { + if uca.infoSub != nil { + return + } + + // Bind to the system account. + ic := s.createInternalJetStreamClient() + ic.registerWithAccount(s.SystemAccount()) + uca.sysc = ic + + // Note below the way we subscribe here is so that we can send requests to ourselves. + isubj := fmt.Sprintf(clusterConsumerInfoT, ca.Client.serviceAccount(), ca.Stream, ca.Name) + uca.infoSub, _ = s.systemSubscribe(isubj, _EMPTY_, false, ic, uca.handleClusterConsumerInfoRequest) +} + +func (uca *unsupportedConsumerAssignment) handleClusterConsumerInfoRequest(_ *subscription, c *client, _ *Account, _, reply string, _ []byte) { + s, acc := c.srv, c.acc + info := consumerInfoClusterResponse{OfflineReason: uca.reason, ConsumerInfo: uca.info} + s.sendDelayedErrResponse(acc, reply, nil, s.jsonResponse(&info), errRespDelay) +} + +func (uca *unsupportedConsumerAssignment) closeInfoSub(s *Server) { + if uca.infoSub != nil { + s.sysUnsubscribe(uca.infoSub) + uca.infoSub = nil + } + if uca.sysc != nil { + uca.sysc.closeConnection(ClientClosed) + uca.sysc = nil + } +} + +type writeableConsumerAssignment struct { + Client *ClientInfo `json:"client,omitempty"` + Created time.Time `json:"created"` + Name string `json:"name"` + Stream string `json:"stream"` + ConfigJSON json.RawMessage `json:"consumer"` + Group *raftGroup `json:"group"` +} + +// streamPurge is what the stream leader will replicate when purging a stream. +type streamPurge struct { + Client *ClientInfo `json:"client,omitempty"` + Stream string `json:"stream"` + LastSeq uint64 `json:"last_seq"` + Subject string `json:"subject"` + Reply string `json:"reply"` + Request *JSApiStreamPurgeRequest `json:"request,omitempty"` +} + +// streamMsgDelete is what the stream leader will replicate when deleting a message. +type streamMsgDelete struct { + Client *ClientInfo `json:"client,omitempty"` + Stream string `json:"stream"` + Seq uint64 `json:"seq"` + NoErase bool `json:"no_erase,omitempty"` + Subject string `json:"subject"` + Reply string `json:"reply"` +} + +const ( + defaultStoreDirName = "_js_" + defaultMetaGroupName = "_meta_" + defaultMetaFSBlkSize = 1024 * 1024 + jsExcludePlacement = "!jetstream" +) + +// Returns information useful in mixed mode. +func (s *Server) trackedJetStreamServers() (js, total int) { + s.mu.RLock() + defer s.mu.RUnlock() + if !s.isRunning() || !s.eventsEnabled() { + return -1, -1 + } + s.nodeToInfo.Range(func(k, v any) bool { + si := v.(nodeInfo) + if si.js { + js++ + } + total++ + return true + }) + return js, total +} + +func (s *Server) getJetStreamCluster() (*jetStream, *jetStreamCluster) { + if s.isShuttingDown() { + return nil, nil + } + + js := s.getJetStream() + if js == nil { + return nil, nil + } + + // Only set once, do not need a lock. + return js, js.cluster +} + +func (s *Server) JetStreamIsClustered() bool { + return s.jsClustered.Load() +} + +func (s *Server) JetStreamIsLeader() bool { + return s.isMetaLeader.Load() +} + +func (s *Server) JetStreamIsCurrent() bool { + js := s.getJetStream() + if js == nil { + return false + } + // Grab what we need and release js lock. + js.mu.RLock() + var meta RaftNode + cc := js.cluster + if cc != nil { + meta = cc.meta + } + js.mu.RUnlock() + + if cc == nil { + // Non-clustered mode + return true + } + return meta.Current() +} + +func (s *Server) JetStreamSnapshotMeta() error { + js := s.getJetStream() + if js == nil { + return NewJSNotEnabledError() + } + js.mu.RLock() + cc := js.cluster + isLeader := cc.isLeader() + meta := cc.meta + js.mu.RUnlock() + + if !isLeader { + return errNotLeader + } + + snap, _, _, err := js.metaSnapshot() + if err != nil { + return err + } + + return meta.InstallSnapshot(snap, false) +} + +func (s *Server) JetStreamStepdownStream(account, stream string) error { + js, cc := s.getJetStreamCluster() + if js == nil { + return NewJSNotEnabledError() + } + if cc == nil { + return NewJSClusterNotActiveError() + } + // Grab account + acc, err := s.LookupAccount(account) + if err != nil { + return err + } + // Grab stream + mset, err := acc.lookupStream(stream) + if err != nil { + return err + } + + if node := mset.raftNode(); node != nil { + node.StepDown() + } + + return nil +} + +func (s *Server) JetStreamStepdownConsumer(account, stream, consumer string) error { + js, cc := s.getJetStreamCluster() + if js == nil { + return NewJSNotEnabledError() + } + if cc == nil { + return NewJSClusterNotActiveError() + } + // Grab account + acc, err := s.LookupAccount(account) + if err != nil { + return err + } + // Grab stream + mset, err := acc.lookupStream(stream) + if err != nil { + return err + } + + o := mset.lookupConsumer(consumer) + if o == nil { + return NewJSConsumerNotFoundError() + } + + if node := o.raftNode(); node != nil { + node.StepDown() + } + + return nil +} + +func (s *Server) JetStreamSnapshotStream(account, stream string) error { + js, cc := s.getJetStreamCluster() + if js == nil { + return NewJSNotEnabledForAccountError() + } + if cc == nil { + return NewJSClusterNotActiveError() + } + // Grab account + acc, err := s.LookupAccount(account) + if err != nil { + return err + } + // Grab stream + mset, err := acc.lookupStream(stream) + if err != nil { + return err + } + + // Hold lock when installing snapshot. + mset.mu.Lock() + defer mset.mu.Unlock() + if mset.node == nil { + return nil + } + if err := mset.flushAllPending(); err != nil { + return err + } + return mset.node.InstallSnapshot(mset.stateSnapshotLocked(), false) +} + +func (s *Server) JetStreamClusterPeers() []string { + js := s.getJetStream() + if js == nil { + return nil + } + js.mu.RLock() + defer js.mu.RUnlock() + + cc := js.cluster + if !cc.isLeader() || cc.meta == nil { + return nil + } + peers := cc.meta.Peers() + var nodes []string + for _, p := range peers { + si, ok := s.nodeToInfo.Load(p.ID) + if !ok || si == nil { + continue + } + ni := si.(nodeInfo) + // Ignore if offline, no JS, or no current stats have been received. + if ni.offline || !ni.js || ni.stats == nil { + continue + } + nodes = append(nodes, si.(nodeInfo).name) + } + return nodes +} + +// Read lock should be held. +func (cc *jetStreamCluster) isLeader() bool { + if cc == nil { + // Non-clustered mode + return true + } + return cc.meta != nil && cc.meta.Leader() +} + +// isStreamCurrent will determine if the stream is up to date. +// For R1 it will make sure the stream is present on this server. +// Read lock should be held. +func (cc *jetStreamCluster) isStreamCurrent(account, stream string) bool { + if cc == nil { + // Non-clustered mode + return true + } + as := cc.streams[account] + if as == nil { + return false + } + sa := as[stream] + if sa == nil { + return false + } + rg := sa.Group + if rg == nil { + return false + } + + if rg.node == nil || rg.node.Current() { + // Check if we are processing a snapshot and are catching up. + acc, err := cc.s.LookupAccount(account) + if err != nil { + return false + } + mset, err := acc.lookupStream(stream) + if err != nil { + return false + } + if mset.isCatchingUp() { + return false + } + // Success. + return true + } + + return false +} + +// isStreamHealthy will determine if the stream is up to date or very close. +// For R1 it will make sure the stream is present on this server. +func (js *jetStream) isStreamHealthy(acc *Account, sa *streamAssignment) error { + js.mu.RLock() + if sa != nil && sa.unsupported != nil { + js.mu.RUnlock() + return nil + } + s, cc := js.srv, js.cluster + if cc == nil { + // Non-clustered mode + js.mu.RUnlock() + return nil + } + if sa == nil || sa.Group == nil { + js.mu.RUnlock() + return errors.New("stream assignment or group missing") + } + // Surface any persisted assignment-level error (e.g. failed create on this + // peer due to account limits) so the health check reflects the broken state + // instead of falling through to runtime-only checks. + if sa.err != nil { + err := sa.err + js.mu.RUnlock() + return fmt.Errorf("stream assignment error: %w", err) + } + streamName := sa.Config.Name + node := sa.Group.node + js.mu.RUnlock() + + // First lookup stream and make sure its there. + mset, err := acc.lookupStream(streamName) + if err != nil { + return errors.New("stream not found") + } + + msetNode := mset.raftNode() + mset.cfgMu.RLock() + replicas := mset.cfg.Replicas + mset.cfgMu.RUnlock() + var nrgWerr error + if node != nil { + nrgWerr = node.GetWriteErr() + } + streamWerr := mset.getWriteErr() + switch { + case replicas <= 1: + return nil // No further checks for R=1 streams + + case node == nil: + return errors.New("group node missing") + + case msetNode == nil: + // Can happen when the stream's node is not yet initialized. + return errors.New("stream node missing") + + case node != msetNode: + s.Warnf("Detected stream cluster node skew '%s > %s'", acc.GetName(), streamName) + return errors.New("cluster node skew detected") + + case nrgWerr != nil: + return fmt.Errorf("node write error: %v", nrgWerr) + + case streamWerr != nil: + return fmt.Errorf("stream write error: %v", streamWerr) + + case !mset.isMonitorRunning(): + return errors.New("monitor goroutine not running") + + case mset.isCatchingUp(): + return errors.New("stream catching up") + + case !node.Healthy(): + return errors.New("group node unhealthy") + + default: + return nil + } +} + +// isConsumerHealthy will determine if the consumer is up to date. +// For R1 it will make sure the consumer is present on this server. +func (js *jetStream) isConsumerHealthy(mset *stream, sa *streamAssignment, consumer string, ca *consumerAssignment) error { + js.mu.RLock() + if ca != nil && ca.unsupported != nil { + js.mu.RUnlock() + return nil + } + if mset == nil { + js.mu.RUnlock() + return errors.New("stream missing") + } + s, cc := js.srv, js.cluster + if cc == nil { + // Non-clustered mode + js.mu.RUnlock() + return nil + } + if ca == nil || ca.Group == nil { + js.mu.RUnlock() + return errors.New("consumer assignment or group missing") + } + // Surface any persisted assignment-level error (e.g. failed create on this + // peer) so the health check reflects the broken state instead of falling + // through to runtime-only checks. + if ca.err != nil { + err := ca.err + js.mu.RUnlock() + return fmt.Errorf("consumer assignment error: %w", err) + } + created := ca.Created + node := ca.Group.node + // The stream may be running at its origin while its config already carries the + // target replica count. A consumer that has not been mapped onto the new peers + // yet inherits that target, so fall back on the origin count below. + var originReplicas int + if o := sa.desiredOrigin(); o != nil { + originReplicas = o.Replicas + } + js.mu.RUnlock() + + // Check if not running at all. + o := mset.lookupConsumer(consumer) + if o == nil { + if time.Since(created) < 5*time.Second { + // No further checks, consumer is not available yet but should be soon. + // We'll start erroring once we're sure this consumer is actually broken. + return nil + } + return errors.New("consumer not found") + } + + oNode := o.raftNode() + rc, _ := o.replica() + var nrgWerr error + if node != nil { + nrgWerr = node.GetWriteErr() + } + consumerWerr := o.getWriteErr() + switch { + case rc <= 1: + return nil // No further checks for R=1 consumers + + case node == nil: + // Not mapped onto the new peers yet, so still running at the stream's origin + // count. Only evaluated here, a consumer that does have a node must be checked + // as usual for the rest of the migration. + if originReplicas > 0 && originReplicas < rc { + return nil + } + return errors.New("group node missing") + + case oNode == nil: + // Can happen when the consumer's node is not yet initialized. + return errors.New("consumer node missing") + + case node != oNode: + mset.mu.RLock() + accName, streamName := mset.acc.GetName(), mset.cfg.Name + mset.mu.RUnlock() + s.Warnf("Detected consumer cluster node skew '%s > %s > %s'", accName, streamName, consumer) + return errors.New("cluster node skew detected") + + case nrgWerr != nil: + return fmt.Errorf("node write error: %v", nrgWerr) + + case consumerWerr != nil: + return fmt.Errorf("consumer write error: %v", consumerWerr) + + case !o.isMonitorRunning(): + return errors.New("monitor goroutine not running") + + case !node.Healthy(): + return errors.New("group node unhealthy") + + default: + return nil + } +} + +// subjectsOverlap checks all existing stream assignments for the account cross-cluster for subject overlap +// Use only for clustered JetStream +// Read lock should be held. +func (js *jetStream) subjectsOverlap(acc string, subjects []string, osa *streamAssignment) bool { + for sa := range js.streamAssignmentsOrInflightSeq(acc) { + // can't overlap yourself, assume osa pre-checked for deep equal if passed + if osa != nil && sa.Config.Name == osa.Config.Name { + continue + } + for _, subj := range sa.Config.Subjects { + for _, tsubj := range subjects { + if SubjectsCollide(tsubj, subj) { + return true + } + } + } + } + return false +} + +func (a *Account) getJetStreamFromAccount() (*Server, *jetStream, *jsAccount) { + a.mu.RLock() + jsa := a.js + a.mu.RUnlock() + if jsa == nil { + return nil, nil, nil + } + jsa.mu.RLock() + js := jsa.js + jsa.mu.RUnlock() + if js == nil { + return nil, nil, nil + } + // Lock not needed, set on creation. + s := js.srv + return s, js, jsa +} + +func (s *Server) JetStreamIsStreamLeader(account, stream string) bool { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return false + } + js.mu.RLock() + defer js.mu.RUnlock() + return cc.isStreamLeader(account, stream) +} + +func (a *Account) JetStreamIsStreamLeader(stream string) bool { + s, js, jsa := a.getJetStreamFromAccount() + if s == nil || js == nil || jsa == nil { + return false + } + js.mu.RLock() + defer js.mu.RUnlock() + return js.cluster.isStreamLeader(a.Name, stream) +} + +func (s *Server) JetStreamIsStreamCurrent(account, stream string) bool { + js, cc := s.getJetStreamCluster() + if js == nil { + return false + } + js.mu.RLock() + defer js.mu.RUnlock() + return cc.isStreamCurrent(account, stream) +} + +func (a *Account) JetStreamIsConsumerLeader(stream, consumer string) bool { + s, js, jsa := a.getJetStreamFromAccount() + if s == nil || js == nil || jsa == nil { + return false + } + js.mu.RLock() + defer js.mu.RUnlock() + return js.cluster.isConsumerLeader(a.Name, stream, consumer) +} + +func (s *Server) JetStreamIsConsumerLeader(account, stream, consumer string) bool { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return false + } + js.mu.RLock() + defer js.mu.RUnlock() + return cc.isConsumerLeader(account, stream, consumer) +} + +func (s *Server) enableJetStreamClustering() error { + if !s.isRunning() { + return nil + } + js := s.getJetStream() + if js == nil { + return NewJSNotEnabledForAccountError() + } + // Already set. + if js.cluster != nil { + return nil + } + + s.Noticef("Starting JetStream cluster") + // We need to determine if we have a stable cluster name and expected number of servers. + s.Debugf("JetStream cluster checking for stable cluster name and peers") + + hasLeafNodeSystemShare := s.canExtendOtherDomain() + if s.isClusterNameDynamic() && !hasLeafNodeSystemShare { + return errors.New("JetStream cluster requires cluster name") + } + if s.configuredRoutes() == 0 && !hasLeafNodeSystemShare { + return errors.New("JetStream cluster requires configured routes or solicited leafnode for the system account") + } + + return js.setupMetaGroup() +} + +// isClustered returns if we are clustered. +// Lock should not be held. +func (js *jetStream) isClustered() bool { + // This is only ever set, no need for lock here. + return js.cluster != nil +} + +// isClusteredNoLock returns if we are clustered, but unlike isClustered() does +// not use the jetstream's lock, instead, uses an atomic operation. +// There are situations where some code wants to know if we are clustered but +// can't use js.isClustered() without causing a lock inversion. +func (js *jetStream) isClusteredNoLock() bool { + return atomic.LoadInt32(&js.clustered) == 1 +} + +func (js *jetStream) setupMetaGroup() error { + s := js.srv + s.Noticef("Creating JetStream metadata controller") + + // Setup our WAL for the metagroup. + sysAcc := s.SystemAccount() + if sysAcc == nil { + return ErrNoSysAccount + } + storeDir := filepath.Join(js.config.StoreDir, sysAcc.Name, defaultStoreDirName, defaultMetaGroupName) + + js.srv.optsMu.RLock() + syncAlways := js.srv.opts.SyncAlways + syncInterval := js.srv.opts.SyncInterval + js.srv.optsMu.RUnlock() + fs, err := newFileStoreWithCreated( + FileStoreConfig{StoreDir: storeDir, BlockSize: defaultMetaFSBlkSize, AsyncFlush: false, SyncAlways: syncAlways, SyncInterval: syncInterval, srv: s}, + StreamConfig{Name: defaultMetaGroupName, Storage: FileStorage}, + time.Now().UTC(), + s.jsKeyGen(s.getOpts().JetStreamKey, defaultMetaGroupName), + s.jsKeyGen(s.getOpts().JetStreamOldKey, defaultMetaGroupName), + ) + if err != nil { + s.Errorf("Error creating filestore: %v", err) + return err + } + + cfg := &RaftConfig{Name: defaultMetaGroupName, Store: storeDir, Log: fs, Recovering: true} + + // If we are soliciting leafnode connections and we are sharing a system account and do not disable it with a hint, + // we want to move to observer mode so that we extend the solicited cluster or supercluster but do not form our own. + cfg.Observer = s.canExtendOtherDomain() && s.getOpts().JetStreamExtHint != jsNoExtend + + var bootstrap bool + if ps, err := readPeerState(s.diskIOSemaphore(), storeDir); err != nil { + s.Noticef("JetStream cluster bootstrapping") + bootstrap = true + peers := s.ActivePeers() + s.Debugf("JetStream cluster initial peers: %+v", peers) + if err := s.bootstrapRaftNode(cfg, peers, false); err != nil { + return err + } + if cfg.Observer { + s.Noticef("Turning JetStream metadata controller Observer Mode on") + s.Noticef("In cases where the JetStream domain is not intended to be extended through a SYS account leaf node connection") + s.Noticef("and waiting for leader election until first contact is not acceptable,") + s.Noticef(`manually disable Observer Mode by setting the JetStream Option "extension_hint: %s"`, jsNoExtend) + } + } else { + s.Noticef("JetStream cluster recovering state") + // correlate the value of observer with observations from a previous run. + if cfg.Observer { + switch ps.domainExt { + case extExtended: + s.Noticef("Keeping JetStream metadata controller Observer Mode on - due to previous contact") + case extNotExtended: + s.Noticef("Turning JetStream metadata controller Observer Mode off - due to previous contact") + cfg.Observer = false + case extUndetermined: + s.Noticef("Turning JetStream metadata controller Observer Mode on - no previous contact") + s.Noticef("In cases where the JetStream domain is not intended to be extended through a SYS account leaf node connection") + s.Noticef("and waiting for leader election until first contact is not acceptable,") + s.Noticef(`manually disable Observer Mode by setting the JetStream Option "extension_hint: %s"`, jsNoExtend) + } + } else { + // To track possible configuration changes, responsible for an altered value of cfg.Observer, + // set extension state to undetermined. + ps.domainExt = extUndetermined + if err := writePeerState(s.diskIOSemaphore(), storeDir, ps); err != nil { + return err + } + } + } + + // Start up our meta node. + n, err := s.startRaftNode(sysAcc.GetName(), cfg, pprofLabels{ + "type": "metaleader", + "account": sysAcc.Name, + }) + if err != nil { + s.Warnf("Could not start metadata controller: %v", err) + return err + } + + // If we are bootstrapped with no state, start campaign early. + if bootstrap { + n.Campaign() + } + + c := s.createInternalJetStreamClient() + + js.mu.Lock() + defer js.mu.Unlock() + js.cluster = &jetStreamCluster{ + meta: n, + streams: make(map[string]map[string]*streamAssignment), + s: s, + c: c, + qch: make(chan struct{}), + stopped: make(chan struct{}), + } + atomic.StoreInt32(&js.clustered, 1) + c.registerWithAccount(sysAcc) + + // Every server listens for meta rescue requests, they must be handled + // even (especially) when the meta group has no leader. + js.cluster.metaRescue, _ = s.systemSubscribe(JSApiMetaRescue, _EMPTY_, false, c, s.jsMetaRescueRequest) + + // Set to true before we start. + js.metaRecovering = true + js.srv.startGoRoutine( + js.monitorCluster, + pprofLabels{ + "type": "metaleader", + "account": sysAcc.Name, + }, + ) + return nil +} + +func (js *jetStream) getMetaGroup() RaftNode { + js.mu.RLock() + defer js.mu.RUnlock() + if js.cluster == nil { + return nil + } + return js.cluster.meta +} + +func (js *jetStream) server() *Server { + // Lock not needed, only set once on creation. + return js.srv +} + +// Will respond if we do not think we have a metacontroller leader. +func (js *jetStream) isLeaderless() bool { + js.mu.RLock() + cc := js.cluster + if cc == nil || cc.meta == nil { + js.mu.RUnlock() + return false + } + meta := cc.meta + js.mu.RUnlock() + + // If we don't have a leader. + // Make sure we have been running for enough time. + if meta.Leaderless() && time.Since(meta.Created()) > lostQuorumInterval { + return true + } + return false +} + +// Will respond iff we are a member and we know we have no leader. +func (js *jetStream) isGroupLeaderless(rg *raftGroup) bool { + if rg == nil || js == nil { + return false + } + js.mu.RLock() + cc := js.cluster + started := js.started + + // If we are not a member we can not say.. + if cc.meta == nil { + js.mu.RUnlock() + return false + } + if !rg.isMember(cc.meta.ID()) { + js.mu.RUnlock() + return false + } + // Single peer groups always have a leader if we are here. + if rg.node == nil { + js.mu.RUnlock() + return false + } + node := rg.node + js.mu.RUnlock() + // If we don't have a leader. + if node.Leaderless() { + // Threshold for jetstream startup. + const startupThreshold = 10 * time.Second + + if node.HadPreviousLeader() { + // Make sure we have been running long enough to intelligently determine this. + if time.Since(started) > startupThreshold { + return true + } + } + // Make sure we have been running for enough time. + if time.Since(node.Created()) > lostQuorumInterval { + return true + } + } + + return false +} + +func (s *Server) JetStreamIsStreamAssigned(account, stream string) bool { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return false + } + acc, _ := s.LookupAccount(account) + if acc == nil { + return false + } + js.mu.RLock() + assigned := cc.isStreamAssigned(acc, stream) + js.mu.RUnlock() + return assigned +} + +// streamAssigned informs us if this server has this stream assigned. +func (jsa *jsAccount) streamAssigned(stream string) bool { + jsa.mu.RLock() + js, acc := jsa.js, jsa.account + jsa.mu.RUnlock() + + if js == nil { + return false + } + js.mu.RLock() + assigned := js.cluster.isStreamAssigned(acc, stream) + js.mu.RUnlock() + return assigned +} + +// Read lock should be held. +func (cc *jetStreamCluster) isStreamAssigned(a *Account, stream string) bool { + // Non-clustered mode always return true. + if cc == nil { + return true + } + if cc.meta == nil { + return false + } + as := cc.streams[a.Name] + if as == nil { + return false + } + sa := as[stream] + if sa == nil { + return false + } + return sa.Group.isMember(cc.meta.ID()) +} + +// Read lock should be held. +func (cc *jetStreamCluster) isStreamLeader(account, stream string) bool { + // Non-clustered mode always return true. + if cc == nil { + return true + } + if cc.meta == nil { + return false + } + + var sa *streamAssignment + if as := cc.streams[account]; as != nil { + sa = as[stream] + } + if sa == nil { + return false + } + rg := sa.Group + if rg == nil { + return false + } + // Check if we are the leader of this raftGroup assigned to the stream. + ourID := cc.meta.ID() + for _, peer := range rg.Peers { + if peer == ourID { + if len(rg.Peers) == 1 || (rg.node != nil && rg.node.Leader()) { + return true + } + } + } + return false +} + +// Read lock should be held. +func (cc *jetStreamCluster) isConsumerLeader(account, stream, consumer string) bool { + // Non-clustered mode always return true. + if cc == nil { + return true + } + if cc.meta == nil { + return false + } + + var sa *streamAssignment + if as := cc.streams[account]; as != nil { + sa = as[stream] + } + if sa == nil { + return false + } + // Check if we are the leader of this raftGroup assigned to this consumer. + ca := sa.consumers[consumer] + if ca == nil { + return false + } + rg := ca.Group + ourID := cc.meta.ID() + for _, peer := range rg.Peers { + if peer == ourID { + if len(rg.Peers) == 1 || (rg.node != nil && rg.node.Leader()) { + return true + } + } + } + return false +} + +// Track the stream for the account `accName` in the inflight proposals map. +// This is done after proposing a stream change. +// (Write) Lock held on entry. +func (cc *jetStreamCluster) trackInflightStreamProposal(accName string, sa *streamAssignment, deleted bool) { + if cc.inflightStreams == nil { + cc.inflightStreams = make(map[string]map[string]*inflightStreamInfo) + } + streams, ok := cc.inflightStreams[accName] + if !ok { + streams = make(map[string]*inflightStreamInfo) + cc.inflightStreams[accName] = streams + } + if inflight, ok := streams[sa.Config.Name]; ok { + inflight.ops++ + inflight.deleted = deleted + inflight.streamAssignment = sa + } else { + streams[sa.Config.Name] = &inflightStreamInfo{1, deleted, sa} + } +} + +// Remove the stream `streamName` for the account `accName` from the inflight proposals map. +// This is done after successful create/update/delete. +// (Write) Lock held on entry. +func (cc *jetStreamCluster) removeInflightStreamProposal(accName, streamName string) { + if streams, ok := cc.inflightStreams[accName]; !ok { + return // No accounts. + } else if inflight, ok := streams[streamName]; !ok { + return // No changes for this stream. + } else if inflight.ops > 1 { + // Decrement one pending operation. + inflight.ops-- + } else { + // No pending operations left, clean up. + delete(streams, streamName) + if len(streams) == 0 { + delete(cc.inflightStreams, accName) + } + } +} + +// Track the consumer for the `streamName` and account `accName` in the inflight proposals map. +// This is done after proposing a consumer change. +// (Write) Lock held on entry. +func (cc *jetStreamCluster) trackInflightConsumerProposal(accName, streamName string, ca *consumerAssignment, deleted bool) { + if cc.inflightConsumers == nil { + cc.inflightConsumers = make(map[string]map[string]map[string]*inflightConsumerInfo) + } + streams, ok := cc.inflightConsumers[accName] + if !ok { + streams = make(map[string]map[string]*inflightConsumerInfo) + cc.inflightConsumers[accName] = streams + } + consumers, ok := streams[streamName] + if !ok { + consumers = make(map[string]*inflightConsumerInfo) + streams[streamName] = consumers + } + if inflight, ok := consumers[ca.Name]; ok { + inflight.ops++ + inflight.deleted = deleted + inflight.consumerAssignment = ca + } else { + consumers[ca.Name] = &inflightConsumerInfo{1, deleted, ca} + } +} + +// Remove the consumer `consumerName` for the `streamName` and account `accName` from the inflight proposals map. +// This is done after successful create/update/delete. +// (Write) Lock held on entry. +func (cc *jetStreamCluster) removeInflightConsumerProposal(accName, streamName, consumerName string) { + if streams, ok := cc.inflightConsumers[accName]; !ok { + return // No accounts. + } else if consumers, ok := streams[streamName]; !ok { + return // No streams. + } else if inflight, ok := consumers[consumerName]; !ok { + return // No changes for this consumer. + } else if inflight.ops > 1 { + // Decrement one pending operation. + inflight.ops-- + } else { + // No pending operations left, clean up. + delete(consumers, consumerName) + if len(consumers) == 0 { + delete(streams, streamName) + } + if len(streams) == 0 { + delete(cc.inflightConsumers, accName) + } + } +} + +// Return the cluster quit chan. +func (js *jetStream) clusterQuitC() chan struct{} { + js.mu.RLock() + defer js.mu.RUnlock() + if js.cluster != nil { + return js.cluster.qch + } + return nil +} + +// Return the cluster stopped chan. +func (js *jetStream) clusterStoppedC() chan struct{} { + js.mu.RLock() + defer js.mu.RUnlock() + if js.cluster != nil { + return js.cluster.stopped + } + return nil +} + +// Mark that the meta layer is recovering. +func (js *jetStream) setMetaRecovering() { + js.mu.Lock() + defer js.mu.Unlock() + if js.cluster != nil { + // metaRecovering + js.metaRecovering = true + } +} + +// Mark that the meta layer is no longer recovering. +func (js *jetStream) clearMetaRecovering() { + js.mu.Lock() + defer js.mu.Unlock() + js.metaRecovering = false +} + +// Return whether the meta layer is recovering. +func (js *jetStream) isMetaRecovering() bool { + js.mu.RLock() + defer js.mu.RUnlock() + return js.metaRecovering +} + +// setMetaWriteErr stores the write error on the meta group. +func (js *jetStream) setMetaWriteErr(err error) { + js.mu.Lock() + defer js.mu.Unlock() + js.setMetaWriteErrLocked(err) +} + +func (js *jetStream) setMetaWriteErrLocked(err error) { + cc := js.cluster + if cc == nil || cc.werr != nil { + return + } + // Ignore non-write errors. + if err == ErrStoreClosed { + return + } + js.srv.Errorf("JetStream cluster meta group critical write error: %v", err) + cc.werr = err + assert.Unreachable("Meta group encountered write error", map[string]any{ + "err": err, + }) + + // Step down and put into observer mode so another server can pick up the meta leadership. + if node := cc.meta; node != nil { + node.StepDown() + node.SetObserver(true) + } +} + +// getMetaWriteErr returns the write error stored on the meta group (if any). +func (js *jetStream) getMetaWriteErr() error { + js.mu.RLock() + defer js.mu.RUnlock() + if js.cluster == nil { + return nil + } + return js.cluster.werr +} + +// During recovery track any stream and consumer delete and update operations. +type recoveryUpdates struct { + removeStreams map[string]*streamAssignment + removeConsumers map[string]map[string]*consumerAssignment + addStreams map[string]*streamAssignment + updateStreams map[string]*streamAssignment + updateConsumers map[string]map[string]*consumerAssignment +} + +func (ru *recoveryUpdates) removeStream(sa *streamAssignment) { + key := sa.recoveryKey() + ru.removeStreams[key] = sa + delete(ru.addStreams, key) + delete(ru.updateStreams, key) + delete(ru.updateConsumers, key) + delete(ru.removeConsumers, key) +} + +func (ru *recoveryUpdates) addStream(sa *streamAssignment) { + key := sa.recoveryKey() + ru.addStreams[key] = sa + // A fresh add supersedes any earlier staged update for this stream. + delete(ru.updateStreams, key) +} + +func (ru *recoveryUpdates) updateStream(sa *streamAssignment) { + key := sa.recoveryKey() + // A stream still staged for creation must just be replaced. Otherwise, + // a R1 create followed by R3 scale up would temporarily run as R1 still. + if _, staged := ru.addStreams[key]; staged { + ru.addStreams[key] = sa + delete(ru.updateStreams, key) + return + } + ru.updateStreams[key] = sa +} + +func (ru *recoveryUpdates) removeConsumer(ca *consumerAssignment) { + key := ca.recoveryKey() + skey := ca.streamRecoveryKey() + if _, ok := ru.removeConsumers[skey]; !ok { + ru.removeConsumers[skey] = map[string]*consumerAssignment{} + } + // Keep the first staged removal; it's for the consumer we hold. Later + // removals are for a same-name recreate, and processConsumerRemoval + // ignores a removal whose raft group doesn't match ours. + if _, ok := ru.removeConsumers[skey][key]; !ok { + ru.removeConsumers[skey][key] = ca + } + if consumers, ok := ru.updateConsumers[skey]; ok { + delete(consumers, key) + } +} + +func (ru *recoveryUpdates) addOrUpdateConsumer(ca *consumerAssignment) { + key := ca.recoveryKey() + skey := ca.streamRecoveryKey() + if _, ok := ru.updateConsumers[skey]; !ok { + ru.updateConsumers[skey] = map[string]*consumerAssignment{} + } + ru.updateConsumers[skey][key] = ca +} + +// Called after recovery of the cluster on startup to check for any orphans. +// Streams and consumers are recovered from disk, and the meta layer's mappings +// should clean them up, but under crash scenarios there could be orphans. +func (js *jetStream) checkForOrphans() { + // Can not hold jetstream lock while trying to delete streams or consumers. + js.mu.Lock() + s, cc := js.srv, js.cluster + s.Debugf("JetStream cluster checking for orphans") + + // We only want to cleanup any orphans if we know we are current with the meta-leader. + meta := cc.meta + if meta == nil || meta.Leaderless() { + js.mu.Unlock() + s.Debugf("JetStream cluster skipping check for orphans, no meta-leader") + return + } + if !meta.Healthy() { + js.mu.Unlock() + s.Debugf("JetStream cluster skipping check for orphans, not current with the meta-leader") + return + } + streams, consumers := js.getOrphans() + js.mu.Unlock() + + for _, mset := range streams { + mset.mu.RLock() + accName, stream := mset.acc.Name, mset.cfg.Name + mset.mu.RUnlock() + s.Warnf("Detected orphaned stream '%s > %s', will cleanup", accName, stream) + if err := mset.delete(); err != nil { + s.Warnf("Deleting stream encountered an error: %v", err) + } + } + for _, o := range consumers { + o.mu.RLock() + accName, mset, consumer := o.acc.Name, o.mset, o.name + o.mu.RUnlock() + stream := "N/A" + if mset != nil { + mset.mu.RLock() + stream = mset.cfg.Name + mset.mu.RUnlock() + } + if o.isDurable() { + s.Warnf("Detected orphaned durable consumer '%s > %s > %s', will cleanup", accName, stream, consumer) + } else { + s.Debugf("Detected orphaned consumer '%s > %s > %s', will cleanup", accName, stream, consumer) + } + + if err := o.delete(); err != nil { + s.Warnf("Deleting consumer encountered an error: %v", err) + } + } +} + +// Returns orphaned streams and consumers that were recovered from disk, but don't +// exist as clustered stream/consumer assignments. +// Lock should be held. +func (js *jetStream) getOrphans() (streams []*stream, consumers []*consumer) { + cc := js.cluster + for accName, jsa := range js.accounts { + asa := cc.streams[accName] + jsa.mu.RLock() + for stream, mset := range jsa.streams { + if sa := asa[stream]; sa == nil { + streams = append(streams, mset) + } else { + // This one is good, check consumers now. + for _, o := range mset.getPublicConsumers() { + if sa.consumers[o.String()] == nil { + consumers = append(consumers, o) + } + } + } + } + jsa.mu.RUnlock() + } + return streams, consumers +} + +func (js *jetStream) monitorCluster() { + s, n := js.server(), js.getMetaGroup() + qch, stopped, rqch, lch, aq := js.clusterQuitC(), js.clusterStoppedC(), n.QuitC(), n.LeadChangeC(), n.ApplyQ() + + defer s.grWG.Done() + defer close(stopped) + + s.Debugf("Starting metadata monitor") + defer s.Debugf("Exiting metadata monitor") + + // Make sure to stop the raft group on exit to prevent accidental memory bloat. + defer n.Stop() + defer s.isMetaLeader.Store(false) + + const compactInterval = time.Minute + const compactMinInterval = 15 * time.Second + t := time.NewTicker(compactInterval) + defer t.Stop() + + // Used to check cold boot cluster when possibly in mixed mode. + const leaderCheckInterval = time.Second + lt := time.NewTicker(leaderCheckInterval) + defer lt.Stop() + + // Check the general health once an hour. + const healthCheckInterval = 1 * time.Hour + ht := time.NewTicker(healthCheckInterval) + defer ht.Stop() + + // Utility to check health. + checkHealth := func() { + if hs := s.healthz(nil); hs.Error != _EMPTY_ { + s.Warnf("%v", hs.Error) + } + } + + var ( + isLeader bool + lastSnapTime time.Time + compactSizeMin = uint64(8 * 1024 * 1024) // 8MB + minSnapDelta = 30 * time.Second + ) + + // Highwayhash key for generating hashes. + key := make([]byte, 32) + crand.Read(key) + + // Set to true to start. + js.setMetaRecovering() + recovering := true + + // Snapshotting function. + var ( + snapMu sync.Mutex + snapshotting bool + // The first snapshot we do after recovery should clean up most of the log. + fallbackSnapshot = true + // Suppress non-forced snapshots after an error. Timer-based snapshots will become forced. + // If failures continue, we force a snapshot even if we were catching up others + // (otherwise we might indefinitely stall and grow log size). + failedSnapshots atomic.Uint32 + ) + doSnapshot := func(force bool) { + // Suppress during recovery. + // If snapshots have failed, and we're not forced to, we'll wait for the timer since it'll now be forced. + if recovering || (!force && failedSnapshots.Load() > 0) { + return + } + // Suppress if an async snapshot is already in progress. + snapMu.Lock() + if snapshotting { + snapMu.Unlock() + return + } + // Look up what the threshold is for compaction. Re-reading from config here as it is reloadable. + js.srv.optsMu.RLock() + ethresh := js.srv.opts.JetStreamMetaCompact + szthresh := js.srv.opts.JetStreamMetaCompactSize + // Allows reverting to sync/blocking snapshots instead of the default async snapshots. + syncSnapshot := js.srv.opts.JetStreamMetaCompactSync + js.srv.optsMu.RUnlock() + // Work out our criteria for snapshotting. + byEntries, bySize := ethresh > 0, szthresh > 0 + byNeither := !byEntries && !bySize + // For the meta layer we want to snapshot when over the above threshold (which could be 0 by default). + ne, nsz := n.Size() + createSnapshot := force || byNeither || (byEntries && ne > ethresh) || (bySize && nsz > szthresh) || n.NeedSnapshot() + if !createSnapshot { + snapMu.Unlock() + return + } + + // Start a checkpoint, we can either install the snapshot sync or async from here. + // If we had a significant number of failed snapshots, start relaxing Raft-layer checks + // to force it through. We might have been catching up a peer for a long period, and this + // protects our log size from growing indefinitely. + forceSnapshot := failedSnapshots.Load() > 4 + c, err := n.CreateSnapshotCheckpoint(forceSnapshot) + if err != nil { + if err != errNoSnapAvailable && err != errNodeClosed { + s.Warnf("Error snapshotting JetStream cluster state: %v", err) + // If this is the first failure, reduce the interval of the snapshot timer. + // This ensures we're not waiting too long for snapshotting to eventually become forced. + if failedSnapshots.Load() == 0 { + t.Reset(compactMinInterval) + } + failedSnapshots.Add(1) + } + snapMu.Unlock() + return + } + + // If we're meant to install an async snapshot, check if the underlying log has grown way too large. + // In that case, we fall back to a synchronous/blocking snapshot after all. This is a protective + // measure to prevent the log from growing faster than we can asynchronously compact. + if !fallbackSnapshot && !syncSnapshot { + if szthresh == 0 { + szthresh = compactSizeMin + } + if thresh := 10 * szthresh; nsz >= thresh { + s.rateLimitFormatWarnf("JetStream cluster metalayer log size has exceeded async threshold (%s), will fall back to blocking snapshot", friendlyBytes(thresh)) + fallbackSnapshot = true + } + } + + // Check if we should fall back to a blocking snapshot, either due to failures or extreme log size. + if fallbackSnapshot || syncSnapshot { + if !syncSnapshot { + s.rateLimitFormatWarnf("Metalayer blocking snapshot starting") + } + start := time.Now() + snap, nsa, nca, err := js.metaSnapshot() + if err != nil { + s.Warnf("Error generating JetStream cluster snapshot: %v", err) + c.Abort() + } else if csz, err := c.InstallSnapshot(snap); err == nil { + lastSnapTime = time.Now() + // If there was a failed snapshot before, we reduced the timer's interval. + // Reset it back to the original interval now. + if failedSnapshots.Load() > 0 { + t.Reset(compactInterval) + } + failedSnapshots.Store(0) + // Fallback snapshot was successful, the next one can be async again. + fallbackSnapshot = false + took := time.Since(start) + if !syncSnapshot || took > 2*time.Second { + s.rateLimitFormatWarnf("Metalayer blocking snapshot took %.3fs (streams: %d, consumers: %d, compacted: %s)", + took.Seconds(), nsa, nca, friendlyBytes(csz)) + } + } else { + c.Abort() + if err != errNoSnapAvailable && err != errNodeClosed && err != errSnapAborted { + s.Warnf("Error snapshotting JetStream cluster state: %v", err) + // If this is the first failure, reduce the interval of the snapshot timer. + // This ensures we're not waiting too long for snapshotting to eventually become forced. + if failedSnapshots.Load() == 0 { + t.Reset(compactMinInterval) + } + failedSnapshots.Add(1) + } + } + snapMu.Unlock() + return + } + + // Perform the snapshot asynchronously. + start := time.Now() + snapshotting = true + snapMu.Unlock() + s.startGoRoutine(func() { + defer s.grWG.Done() + + abort := func(err error) { + snapMu.Lock() + c.Abort() + if err != errNoSnapAvailable && err != errNodeClosed && err != errSnapAborted { + s.Warnf("Error snapshotting JetStream cluster state: %v, will fall back to blocking snapshot", err) + fallbackSnapshot = true + // If this is the first failure, reduce the interval of the snapshot timer. + // This ensures we're not waiting too long for snapshotting to eventually become forced. + if failedSnapshots.Load() == 0 { + t.Reset(compactMinInterval) + } + failedSnapshots.Add(1) + } + snapshotting = false + snapMu.Unlock() + } + + // The strategy of asynchronous snapshotting: + // - Not holding any locks. Only minimal for the Raft node itself, importantly not the JS lock. + // - Load the last snapshot. + // - Replay stream/consumer changes through the assignment state machine. + // - Encode and install as the new snapshot. + if data, err := c.LoadLastSnapshot(); err != nil && err != errNoSnapAvailable { + abort(err) + } else if streams, err := js.decodeMetaSnapshot(data); err != nil { + abort(err) + } else if err = js.collectStreamAndConsumerChanges(c, streams); err != nil { + abort(err) + } else if snap, nsa, nca, err := js.encodeMetaSnapshot(streams); err != nil { + abort(err) + } else if csz, err := c.InstallSnapshot(snap); err != nil { + abort(err) + } else { + // Successful snapshot. + // If there was a failed snapshot before, we reduced the timer's interval. + // Reset it back to the original interval now. + if failedSnapshots.Load() > 0 { + t.Reset(compactInterval) + } + failedSnapshots.Store(0) + if took := time.Since(start); took > 2*time.Second { + s.rateLimitFormatWarnf("Metalayer async snapshot took %.3fs (streams: %d, consumers: %d, compacted: %s)", + took.Seconds(), nsa, nca, friendlyBytes(csz)) + } + snapMu.Lock() + lastSnapTime = time.Now() + snapshotting = false + snapMu.Unlock() + } + }) + } + + var ru *recoveryUpdates + + // Make sure to cancel any pending checkForOrphans calls if the + // monitor goroutine exits. + var oc *time.Timer + defer stopAndClearTimer(&oc) + + for { + select { + case <-s.quitCh: + // Server shutting down, but we might receive this before qch, so try to snapshot. + snapMu.Lock() + fallbackSnapshot = true + snapMu.Unlock() + doSnapshot(false) + return + case <-rqch: + // Raft node is closed, no use in trying to snapshot. + return + case <-qch: + // Clean signal from shutdown routine so do best effort attempt to snapshot meta layer. + snapMu.Lock() + fallbackSnapshot = true + snapMu.Unlock() + doSnapshot(false) + return + case <-aq.ch: + ces := aq.pop() + for _, ce := range ces { + if recovering && ru == nil { + ru = &recoveryUpdates{ + removeStreams: make(map[string]*streamAssignment), + removeConsumers: make(map[string]map[string]*consumerAssignment), + addStreams: make(map[string]*streamAssignment), + updateStreams: make(map[string]*streamAssignment), + updateConsumers: make(map[string]map[string]*consumerAssignment), + } + } + if ce == nil { + if ru != nil { + // Process any removes that are still valid after recovery. + for _, cas := range ru.removeConsumers { + for _, ca := range cas { + js.processConsumerRemoval(ca) + } + } + for _, sa := range ru.removeStreams { + js.processStreamRemoval(sa) + } + // Process stream additions. + for _, sa := range ru.addStreams { + js.processStreamAssignment(sa) + } + // Process pending updates. + for _, sa := range ru.updateStreams { + js.processUpdateStreamAssignment(sa) + } + // Now consumers. + for _, cas := range ru.updateConsumers { + for _, ca := range cas { + js.processConsumerAssignment(ca) + } + } + } + // Signals we have replayed all of our metadata. + wasMetaRecovering := js.isMetaRecovering() + js.clearMetaRecovering() + recovering = false + // Clear. + ru = nil + s.Debugf("Recovered JetStream cluster metadata") + // Snapshot now so we start with freshly compacted log. + doSnapshot(true) + if wasMetaRecovering { + // Reset, it could be we didn't need to install a snapshot. This ensures we don't degrade + // to a blocking snapshot if we install our first snapshot during normal operations. + snapMu.Lock() + fallbackSnapshot = false + snapMu.Unlock() + oc = time.AfterFunc(30*time.Second, js.checkForOrphans) + // Do a health check here as well. + go checkHealth() + } + continue + } + if isRecovering, didSnap, err := js.applyMetaEntries(ce.Entries, ru); err == nil { + var nb uint64 + // Some entries can fail without an error when shutting down, don't move applied forward. + if !js.isShuttingDown() { + _, nb = n.Applied(ce.Index) + } + if js.hasPeerEntries(ce.Entries) || (didSnap && !isLeader) { + doSnapshot(true) + } else if nb > compactSizeMin { + snapMu.Lock() + expired := time.Since(lastSnapTime) > minSnapDelta + snapMu.Unlock() + if expired { + doSnapshot(false) + } + } + recovering = isRecovering + } else { + s.Errorf("Error applying JetStream cluster entries: %v", err) + // Encountered an unexpected error, can't continue. + js.setMetaWriteErr(err) + ce.ReturnToPool() + aq.recycle(&ces) + return + } + ce.ReturnToPool() + } + aq.recycle(&ces) + + case lc := <-lch: + isLeader = lc.isLeader + // Process the change. + js.processLeaderChange(isLeader, lc.term) + if isLeader { + s.sendInternalMsgLocked(serverStatsPingReqSubj, _EMPTY_, nil, nil) + // Install a snapshot as we become leader. + js.checkClusterSize() + doSnapshot(false) + } + + case <-t.C: + // Start forcing snapshots if they failed previously. + forceIfFailed := failedSnapshots.Load() > 0 + doSnapshot(forceIfFailed) + // Periodically check the cluster size. + if n.Leader() { + js.checkClusterSize() + } + case <-ht.C: + // Do this in a separate go routine. + go checkHealth() + + case <-lt.C: + s.Debugf("Checking JetStream cluster state") + // If we have a current leader or had one in the past we can cancel this here since the metaleader + // will be in charge of all peer state changes. + // For cold boot only. + if !n.Leaderless() || n.HadPreviousLeader() { + lt.Stop() + continue + } + // If we are here we do not have a leader and we did not have a previous one, so cold start. + // Check to see if we can adjust our cluster size down iff we are in mixed mode and we have + // seen a total that is what our original estimate was. + cs := n.ClusterSize() + if js, total := s.trackedJetStreamServers(); js < total && total >= cs && js != cs { + s.Noticef("Adjusting JetStream expected peer set size to %d from original %d", js, cs) + n.AdjustBootClusterSize(js) + } + } + } +} + +// This is called on first leader transition to double check the peers and cluster set size. +func (js *jetStream) checkClusterSize() { + s, n := js.server(), js.getMetaGroup() + if n == nil { + return + } + // We will check that we have a correct cluster set size by checking for any non-js servers + // which can happen in mixed mode. + ps := n.(*raft).currentPeerState() + if len(ps.knownPeers) >= ps.clusterSize { + return + } + + // Grab our active peers. + peers := s.ActivePeers() + + // If we have not registered all of our peers yet we can't do + // any adjustments based on a mixed mode. We will periodically check back. + if len(peers) < ps.clusterSize { + return + } + + s.Debugf("Checking JetStream cluster size") + + // If we are here our known set as the leader is not the same as the cluster size. + // Check to see if we have a mixed mode setup. + var totalJS int + for _, p := range peers { + if si, ok := s.nodeToInfo.Load(p); ok && si != nil { + if si.(nodeInfo).js { + totalJS++ + } + } + } + // If we have less then our cluster size adjust that here. Can not do individual peer removals since + // they will not be in the tracked peers. + if totalJS < ps.clusterSize { + s.Debugf("Adjusting JetStream cluster size from %d to %d", ps.clusterSize, totalJS) + if err := n.AdjustClusterSize(totalJS); err != nil { + s.Warnf("Error adjusting JetStream cluster size: %v", err) + } + } +} + +// Represents our stable meta state that we can write out. +type writeableStreamAssignment struct { + Client *ClientInfo `json:"client,omitempty"` + Created time.Time `json:"created"` + ConfigJSON json.RawMessage `json:"stream"` + Group *raftGroup `json:"group"` + Sync string `json:"sync"` + Consumers []*writeableConsumerAssignment +} + +// Returns the stream config as registered in the meta layer, from an inflight +// proposal that has not been applied yet, or from an applied assignment otherwise. +func (js *jetStream) clusterStreamConfig(accName, streamName string) (StreamConfig, bool) { + js.mu.RLock() + defer js.mu.RUnlock() + if sa := js.streamAssignmentOrInflight(accName, streamName); sa != nil { + return *sa.Config, true + } + return StreamConfig{}, false +} + +func (js *jetStream) metaSnapshot() ([]byte, int, int, error) { + js.mu.RLock() + defer js.mu.RUnlock() + cc := js.cluster + return js.encodeMetaSnapshot(cc.streams) +} + +func (js *jetStream) applyMetaSnapshot(buf []byte, ru *recoveryUpdates, isRecovering bool) error { + streams, err := js.decodeMetaSnapshot(buf) + if err != nil { + return err + } + + js.mu.Lock() + cc := js.cluster + + var saAdd, saDel, saChk []*streamAssignment + // Walk through the old list to generate the delete list. + for account, asa := range cc.streams { + nasa := streams[account] + for sn, sa := range asa { + if nsa := nasa[sn]; nsa == nil { + // Stream was removed. + saDel = append(saDel, sa) + } else if !nsa.Created.Equal(sa.Created) && (nsa.Group == nil || sa.Group == nil || nsa.Group.Name != sa.Group.Name) { + // Stream was recreated. + saDel = append(saDel, sa) + saAdd = append(saAdd, nsa) + } else { + saChk = append(saChk, nsa) + } + } + } + // Walk through the new list to generate the add list. + for account, nasa := range streams { + asa := cc.streams[account] + for sn, sa := range nasa { + if asa[sn] == nil { + saAdd = append(saAdd, sa) + } + } + } + + // Now walk the ones to check and process consumers. + var caAdd, caDel []*consumerAssignment + for _, sa := range saChk { + // Make sure to add in all the new ones from sa. + for _, ca := range sa.consumers { + caAdd = append(caAdd, ca) + } + if osa := js.streamAssignment(sa.Client.serviceAccount(), sa.Config.Name); osa != nil { + for _, ca := range osa.consumers { + // Consumer was either removed or recreated. + if nca := sa.consumers[ca.Name]; nca == nil { + caDel = append(caDel, ca) + } else if !nca.Created.Equal(ca.Created) && (nca.Group == nil || ca.Group == nil || nca.Group.Name != ca.Group.Name) { + caDel = append(caDel, ca) + } + } + } + } + js.mu.Unlock() + + // Reconcile the staged recoveryUpdates against this snapshot. Any staged operations that the + // snapshot does not contain should be dropped, otherwise deleted assets could be revived. + if isRecovering && ru != nil { + supersededStream := func(sa *streamAssignment) bool { + acc := sa.Client.serviceAccount() + return streams[acc] == nil || streams[acc][sa.Config.Name] == nil + } + supersededConsumer := func(ca *consumerAssignment) bool { + nsa := streams[ca.Client.serviceAccount()][ca.Stream] + return nsa == nil || nsa.consumers[ca.Name] == nil + } + for key, sa := range ru.addStreams { + if supersededStream(sa) { + delete(ru.addStreams, key) + delete(ru.updateStreams, key) + delete(ru.updateConsumers, key) + delete(ru.removeConsumers, key) + } + } + for key, sa := range ru.updateStreams { + if supersededStream(sa) { + delete(ru.updateStreams, key) + delete(ru.updateConsumers, key) + delete(ru.removeConsumers, key) + } + } + for skey, consumers := range ru.updateConsumers { + for ckey, ca := range consumers { + if supersededConsumer(ca) { + delete(consumers, ckey) + } + } + if len(consumers) == 0 { + delete(ru.updateConsumers, skey) + } + } + } + + // Do removals first. + for _, sa := range saDel { + js.setStreamAssignmentRecovering(sa) + if isRecovering { + ru.removeStream(sa) + } else { + js.processStreamRemoval(sa) + } + } + // Now do add for the streams. Also add in all consumers. + for _, sa := range saAdd { + consumers := sa.consumers + js.setStreamAssignmentRecovering(sa) + if isRecovering { + // Since we're recovering and storing up changes, we'll need to clear out these consumers. + // Some might be removed, and we'll recover those later, must not be able to remember them. + sa.consumers = nil + ru.addStream(sa) + } else { + js.processStreamAssignment(sa) + } + + // We can simply process the consumers. + for _, ca := range consumers { + js.setConsumerAssignmentRecovering(ca) + if isRecovering { + ru.addOrUpdateConsumer(ca) + } else { + js.processConsumerAssignment(ca) + } + } + } + + // Perform updates on those in saChk. These were existing so make + // sure to process any changes. + for _, sa := range saChk { + js.setStreamAssignmentRecovering(sa) + if isRecovering { + ru.updateStream(sa) + } else { + js.processUpdateStreamAssignment(sa) + } + } + + // Now do the deltas for existing stream's consumers. + for _, ca := range caDel { + js.setConsumerAssignmentRecovering(ca) + if isRecovering { + ru.removeConsumer(ca) + } else { + js.processConsumerRemoval(ca) + } + } + for _, ca := range caAdd { + js.setConsumerAssignmentRecovering(ca) + if isRecovering { + ru.addOrUpdateConsumer(ca) + } else { + js.processConsumerAssignment(ca) + } + } + + // If we're not recovering, we need to check if we have any left-over streams or consumers that aren't + // tracked in our assignments. This could happen if we've restarted and recovered streams or consumers + // from disk, but then got sent a snapshot to catch up from the meta-leader. + if !isRecovering { + // This logic is similar to that of checkForOrphans. But, this cleanup is focused on aligning with + // a snapshot from a meta leader as a result of catchup. We silently delete them here, instead of + // logging and sending out advisories. + js.mu.RLock() + deleteStreams, deleteConsumers := js.getOrphans() + js.mu.RUnlock() + for _, mset := range deleteStreams { + mset.stop(true, false) + } + for _, o := range deleteConsumers { + o.deleteWithoutAdvisory() + } + } + return nil +} + +// Decode the meta snapshot from buf into the relevant stream and consumer assignments. +func (js *jetStream) decodeMetaSnapshot(buf []byte) (map[string]map[string]*streamAssignment, error) { + var wsas []writeableStreamAssignment + if len(buf) > 0 { + jse, err := s2.Decode(nil, buf) + if err != nil { + return nil, err + } + if err = json.Unmarshal(jse, &wsas); err != nil { + return nil, err + } + } + + // Build our new version here outside of js. + streams := make(map[string]map[string]*streamAssignment) + for _, wsa := range wsas { + as := streams[wsa.Client.serviceAccount()] + if as == nil { + as = make(map[string]*streamAssignment) + streams[wsa.Client.serviceAccount()] = as + } + sa := &streamAssignment{Client: wsa.Client, Created: wsa.Created, ConfigJSON: wsa.ConfigJSON, Group: wsa.Group, Sync: wsa.Sync} + if err := decodeStreamAssignmentConfig(js.srv, sa); err != nil { + return nil, err + } + if len(wsa.Consumers) > 0 { + sa.consumers = make(map[string]*consumerAssignment) + for _, wca := range wsa.Consumers { + if wca.Stream == _EMPTY_ { + wca.Stream = sa.Config.Name // Rehydrate from the stream name. + } + ca := &consumerAssignment{Client: wca.Client, Created: wca.Created, Name: wca.Name, Stream: wca.Stream, ConfigJSON: wca.ConfigJSON, Group: wca.Group} + if err := decodeConsumerAssignmentConfig(ca); err != nil { + return nil, err + } + sa.consumers[ca.Name] = ca + } + } + as[sa.Config.Name] = sa + } + return streams, nil +} + +// Encode the meta assignments into an encoded and compressed buffer. +// Returns the snapshot itself, and the amount of streams and consumers. +func (js *jetStream) encodeMetaSnapshot(streams map[string]map[string]*streamAssignment) ([]byte, int, int, error) { + start := time.Now() + nsa := 0 + nca := 0 + for _, asa := range streams { + nsa += len(asa) + } + out := make([]writeableStreamAssignment, 0, nsa) + for _, asa := range streams { + for _, sa := range asa { + wsa := writeableStreamAssignment{ + Client: sa.Client.forAssignmentSnap(), + Created: sa.Created, + ConfigJSON: sa.ConfigJSON, + Group: sa.Group, + Sync: sa.Sync, + Consumers: make([]*writeableConsumerAssignment, 0, len(sa.consumers)), + } + for _, ca := range sa.consumers { + wca := writeableConsumerAssignment{ + Client: ca.Client.forAssignmentSnap(), + Created: ca.Created, + Name: ca.Name, + Stream: ca.Stream, + ConfigJSON: ca.ConfigJSON, + Group: ca.Group, + } + wsa.Consumers = append(wsa.Consumers, &wca) + nca++ + } + out = append(out, wsa) + } + } + + if len(out) == 0 { + return nil, nsa, nca, nil + } + + // Track how long it took to marshal the JSON + mstart := time.Now() + b, err := json.Marshal(out) + mend := time.Since(mstart) + + // Must not be possible for a JSON marshaling error to result + // in an empty snapshot. + if err != nil { + return nil, nsa, nca, err + } + + // Track how long it took to compress the JSON. + cstart := time.Now() + snap := s2.Encode(nil, b) + cend := time.Since(cstart) + took := time.Since(start) + + if took > 2*time.Second { + js.srv.rateLimitFormatWarnf("Metalayer snapshot generation took %.3fs (streams: %d, consumers: %d, marshal: %.3fs, s2: %.3fs, uncompressed: %s, compressed: %s)", + took.Seconds(), nsa, nca, mend.Seconds(), cend.Seconds(), friendlyBytes(len(b)), friendlyBytes(len(snap))) + } + + // Track in jsz monitoring as well. + if cc := js.cluster; cc != nil { + atomic.StoreInt64(&cc.lastMetaSnapTime, start.UnixNano()) + atomic.StoreInt64(&cc.lastMetaSnapDuration, int64(took)) + } + + return snap, nsa, nca, nil +} + +// Given a checkpoint, collects all relevant append entries that can be snapshotted. +func (js *jetStream) collectStreamAndConsumerChanges(c RaftNodeCheckpoint, streams map[string]map[string]*streamAssignment) error { + ru := &recoveryUpdates{ + removeStreams: make(map[string]*streamAssignment), + removeConsumers: make(map[string]map[string]*consumerAssignment), + addStreams: make(map[string]*streamAssignment), + updateStreams: make(map[string]*streamAssignment), + updateConsumers: make(map[string]map[string]*consumerAssignment), + } + for ae, err := range c.AppendEntriesSeq() { + if err != nil { + return err + } + for _, e := range ae.entries { + if e.Type == EntryNormal { + buf := e.Data + if len(buf) == 0 { + return errBadEntryOp + } + op := entryOp(buf[0]) + switch op { + case assignStreamOp, updateStreamOp, removeStreamOp: + sa, err := decodeStreamAssignment(js.srv, buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode stream assignment: %q", buf[1:]) + return err + } + if op == removeStreamOp { + ru.removeStream(sa) + } else { + ru.addStream(sa) + } + case assignConsumerOp: + ca, err := decodeConsumerAssignment(buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode consumer assignment: %q", buf[1:]) + return err + } + ru.addOrUpdateConsumer(ca) + case assignCompressedConsumerOp: + ca, err := decodeConsumerAssignmentCompressed(buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode compressed consumer assignment: %q", buf[1:]) + return err + } + ru.addOrUpdateConsumer(ca) + case removeConsumerOp: + ca, err := decodeConsumerAssignment(buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode consumer assignment: %q", buf[1:]) + return err + } + ru.removeConsumer(ca) + default: + return fmt.Errorf("unknown meta entry op type: %v", entryOp(buf[0])) + } + } + } + } + + // With our recovery structure, apply the stream/consumer diff. + for _, cas := range ru.removeConsumers { + for _, ca := range cas { + if asa, ok := streams[ca.Client.serviceAccount()]; ok { + if sa, ok := asa[ca.Stream]; ok { + delete(sa.consumers, ca.Name) + } + } + } + } + for _, sa := range ru.removeStreams { + if asa, ok := streams[sa.Client.serviceAccount()]; ok { + delete(asa, sa.Config.Name) + } + } + for _, sa := range ru.addStreams { + as := streams[sa.Client.serviceAccount()] + if as == nil { + as = make(map[string]*streamAssignment) + streams[sa.Client.serviceAccount()] = as + } + // Preserve consumers from the previous assignment. + if osa := as[sa.Config.Name]; osa != nil { + sa.consumers = osa.consumers + } + as[sa.Config.Name] = sa + } + for _, cas := range ru.updateConsumers { + for _, ca := range cas { + if asa, ok := streams[ca.Client.serviceAccount()]; ok { + if sa, ok := asa[ca.Stream]; ok { + if sa.consumers == nil { + sa.consumers = make(map[string]*consumerAssignment) + } + sa.consumers[ca.Name] = ca + } + } + } + } + return nil +} + +// Called on recovery to make sure we do not process like original. +func (js *jetStream) setStreamAssignmentRecovering(sa *streamAssignment) { + js.mu.Lock() + defer js.mu.Unlock() + sa.markResponded() + sa.recovering = true + sa.Restore = nil + if sa.Group != nil { + sa.Group.Preferred = _EMPTY_ + // Must be preserved while a migration is inflight, so peers that create + // their raft node late keep the empty-log protection. + if sa.Group.Desired == nil { + sa.Group.ScaleUp = false + } + } +} + +// Called on recovery to make sure we do not process like original. +func (js *jetStream) setConsumerAssignmentRecovering(ca *consumerAssignment) { + js.mu.Lock() + defer js.mu.Unlock() + ca.markResponded() + ca.recovering = true + if ca.Group != nil { + ca.Group.Preferred = _EMPTY_ + // Must be preserved while a migration is inflight, so peers that create + // their raft node late keep the empty-log protection. + if ca.Group.Desired == nil { + ca.Group.ScaleUp = false + } + } +} + +// Just copies over and changes out the group so it can be encoded. +// Lock should be held. +func (sa *streamAssignment) copyGroup() *streamAssignment { + csa := sa.clone() + csa.Group = sa.Group.copyGroup() + return csa +} + +// Just copies over and changes out the group so it can be encoded. +// Lock should be held. +func (ca *consumerAssignment) copyGroup() *consumerAssignment { + cca := ca.clone() + cca.Group = ca.Group.copyGroup() + return cca +} + +// addRemoved records peers that were peer-removed from this group, so a group +// that can't reach quorum knows which of its peers an operator took out. +// Lock should be held. +func (d *desiredRaftGroup) addRemoved(peers []string) { + if d == nil { + return + } + for _, peer := range peers { + if slices.Contains(d.Peers, peer) || slices.Contains(d.Removed, peer) { + continue + } + d.Removed = append(d.Removed, peer) + } +} + +// copyGroup returns a copy of rg whose Peers slice and nested Desired placement +// are independent of the original, so it can be mutated and encoded. +// Lock should be held. +func (rg *raftGroup) copyGroup() *raftGroup { + if rg == nil { + return nil + } + cg := *rg + cg.Peers = copyStrings(rg.Peers) + cg.Excluded = copyStrings(rg.Excluded) + if rg.Desired != nil { + cd := *rg.Desired + cd.Peers = copyStrings(rg.Desired.Peers) + cd.Removed = copyStrings(rg.Desired.Removed) + if rg.Desired.Origin != nil { + cr := *rg.Desired.Origin + cr.Placement = rg.Desired.Origin.Placement.clone() + cd.Origin = &cr + } + cg.Desired = &cd + } + return &cg +} + +// Lock should be held. +func (sa *streamAssignment) missingPeers() bool { + targetPeers := sa.targetPeers() + return len(targetPeers) < sa.Config.Replicas +} + +// The peer set this stream, and its consumers, must end up on. +// Lock should be held. +func (sa *streamAssignment) targetPeers() []string { + targetPeers := sa.Group.targetPeers() + if sa.Group.Desired == nil && len(targetPeers) > sa.Config.Replicas { + // If the stream is already moving, without desired state, the last N peers are the target. + targetPeers = targetPeers[len(targetPeers)-sa.Config.Replicas:] + } + return targetPeers +} + +// The peer set this group must end up on. Peers are only known if not scaling +// down, the group leader selects which peers to scale down to. +// Lock should be held. +func (rg *raftGroup) targetPeers() []string { + if rg.Desired != nil && !rg.Desired.ScaleDown { + return rg.Desired.Peers + } + return rg.Peers +} + +// Returns the group's peers that are no longer part of the meta peer set. +// Lock should be held. +func (rg *raftGroup) stalePeers(metaPeers map[string]struct{}) []string { + var stale []string + for _, p := range rg.Peers { + if _, ok := metaPeers[p]; !ok { + stale = append(stale, p) + } + } + if rg.Desired != nil { + for _, p := range rg.Desired.Peers { + if _, ok := metaPeers[p]; !ok && !slices.Contains(stale, p) { + stale = append(stale, p) + } + } + } + return stale +} + +// peerSelectable reports whether we've received STATSZ for this node. +func (s *Server) peerSelectable(peer string) bool { + si, ok := s.nodeToInfo.Load(peer) + if !ok || si == nil { + return false + } + return si.(nodeInfo).selectable() +} + +// Called when a peer is added to the meta group. Reconciling only makes sense +// once we can actually place on it, so hold it until its first STATSZ arrives. +func (js *jetStream) processAddPeer(peer string) { + // Only the meta leader reconciles, check the atomic so we don't need the JS lock. + if !js.srv.isMetaLeader.Load() { + return + } + js.prMu.Lock() + if js.prNewPeers == nil { + js.prNewPeers = make(map[string]struct{}) + } + js.prNewPeers[peer] = struct{}{} + js.prMu.Unlock() + + // Must track before checking, or STATSZ landing in between would find nothing + // tracked while we'd go on waiting for a transition that already happened. + if js.srv.peerSelectable(peer) { + js.processPeerStatsz(peer) + } +} + +// Called when we gain or lose meta leadership, to reset which peers we're still +// waiting on. +// Lock should be held. +func (js *jetStream) seedNewPeers(isLeader bool) { + js.prMu.Lock() + defer js.prMu.Unlock() + + // Anything we were waiting on is from a term we no longer own. + js.prNewPeers = nil + if !isLeader { + return + } + + cc := js.cluster + if cc == nil || cc.meta == nil { + return + } + s := js.srv + for _, p := range cc.meta.Peers() { + if s.peerSelectable(p.ID) { + continue + } + if js.prNewPeers == nil { + js.prNewPeers = make(map[string]struct{}) + } + js.prNewPeers[p.ID] = struct{}{} + } +} + +// Called on a STATSZ that makes a node selectable for placement. Only meta peers +// that were just added are of interest, everything else either can't be placed +// on or was already reconciled when it joined. +func (js *jetStream) processPeerStatsz(peer string) { + // Only the meta leader reconciles, check the atomic so we don't need the JS lock. + if !js.srv.isMetaLeader.Load() { + return + } + js.prMu.Lock() + _, isNew := js.prNewPeers[peer] + delete(js.prNewPeers, peer) + js.prMu.Unlock() + + if isNew { + js.signalPeerReconcile(peer) + } +} + +// Signals that assignments should be reconciled for this peer. +// Only the leader will process checking. +func (js *jetStream) signalPeerReconcile(peer string) { + // Only the meta leader reconciles, check the atomic so we don't need the JS lock. + if !js.srv.isMetaLeader.Load() { + return + } + js.prMu.Lock() + defer js.prMu.Unlock() + + if js.prPeers == nil { + js.prPeers = make(map[string]struct{}) + } + js.prPeers[peer] = struct{}{} + + // A sweep is already in flight, it will pick this peer up. + if js.prRunning { + return + } + // Don't hold up the caller, and don't propose under its locks. + js.prRunning = true + if !js.srv.startGoRoutine(js.runPeerReconcile) { + js.prRunning, js.prPeers = false, nil + } +} + +// Reconciles assignments for all peers signaled through signalPeerReconcile, +// until none are left pending. Runs on its own goroutine, one at a time. +func (js *jetStream) runPeerReconcile() { + defer js.srv.grWG.Done() + + for { + js.prMu.Lock() + peers := js.prPeers + js.prPeers = nil + // Stop once nothing is left pending. Clearing must happen under the same + // lock as the check, or a peer signaled right now would be dropped. + if len(peers) == 0 { + js.prRunning = false + js.prMu.Unlock() + return + } + js.prMu.Unlock() + + js.mu.Lock() + cc := js.cluster + // Only reconcile if we're in a position to, drop these peers otherwise. We + // loop back around either way, so we always stop through the check above. + if cc != nil && cc.meta != nil && !js.metaRecovering && cc.isLeader() { + js.reconcilePeerAssignments(peers) + } + js.mu.Unlock() + } +} + +func (js *jetStream) processRemovePeer(peer string) { + // We may be already disabled. + if js == nil || js.disabled.Load() { + return + } + + // It's leaving the meta group, so stop waiting for its first STATSZ. + js.prMu.Lock() + delete(js.prNewPeers, peer) + js.prMu.Unlock() + + // Account usage from remote servers is tracked by node ID. Once a server is + // removed from the meta group it will never update that entry again, so drop + // its last reported contribution from every account total. + js.mu.RLock() + accounts := make([]*jsAccount, 0, len(js.accounts)) + for _, jsa := range js.accounts { + accounts = append(accounts, jsa) + } + js.mu.RUnlock() + for _, jsa := range accounts { + jsa.removeRemoteUsage(peer) + } + + js.mu.Lock() + s, cc := js.srv, js.cluster + if cc == nil || cc.meta == nil { + js.mu.Unlock() + return + } + isLeader := cc.isLeader() + // All nodes will check if this is them. + isUs := cc.meta.ID() == peer + js.mu.Unlock() + + if isUs { + s.Errorf("JetStream being DISABLED, our server was removed from the cluster") + adv := &JSServerRemovedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSServerRemovedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Server: s.Name(), + ServerID: s.ID(), + Cluster: s.cachedClusterName(), + Domain: s.getOpts().JetStreamDomain, + } + s.publishAdvisory(nil, JSAdvisoryServerRemoved, adv) + + go s.DisableJetStream() + } + + // Now check if we are meta-leader. We will attempt re-assignment. + if !isLeader { + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + for _, sa := range js.streamAssignmentsOrInflightSeqAllAccounts() { + if sa.unsupported != nil { + continue + } + if sa.Group.isMember(peer) || (sa.Group.Desired != nil && slices.Contains(sa.Group.Desired.Peers, peer)) { + js.removePeerFromStreamLocked(sa, peer, peerRemoval{remove: true}) + } + } +} + +// checkEvictedPeers drops the peers an operator peer-removed from a group that +// can't reach quorum without them. Removing them through the log would require +// quorum, so a stuck group never recovers on its own. Once they're gone it can +// elect a leader again and reconciliation takes over. +// Returns whether the caller should check back. +func (js *jetStream) checkEvictedPeers(n RaftNode, rg *raftGroup, desc string) bool { + js.mu.RLock() + cc := js.cluster + if n == nil || cc == nil || cc.meta == nil || js.metaRecovering || len(rg.Peers) == 0 { + js.mu.RUnlock() + return true + } + // Can only evict safely if the assignment recorded removed peers. + if rg.Desired == nil || len(rg.Desired.Removed) == 0 { + js.mu.RUnlock() + return false + } + // Wait until we're leaderless, but we're in contact with a meta leader. + if !n.Leaderless() || cc.meta.Leaderless() { + js.mu.RUnlock() + return true + } + + groupPeers, removedPeers := copyStrings(rg.Peers), copyStrings(rg.Desired.Removed) + js.mu.RUnlock() + + // Use voting membership, so a peer we speculatively dropped for an uncommitted + // removal is still accounted for. That also covers a scale-down or move that + // appended a removal of ourselves but never got to commit it: the assignment + // can still list us, and EvictPeers reverts the pending removal. + current, ourPeerId := n.VotingPeerNames(), n.ID() + + // Skip if we're not member anymore, we're not allowed to make changes to this group. + if !slices.Contains(current, ourPeerId) || !slices.Contains(groupPeers, ourPeerId) { + return false + } + + // Only drop peers an operator removed, and that the assignment hasn't taken + // back on since. Everyone else could still be taking part. + var evict []string + for _, peer := range current { + // Skip ourselves. + if peer == ourPeerId { + continue + } + if slices.Contains(removedPeers, peer) && !slices.Contains(groupPeers, peer) { + evict = append(evict, peer) + } + } + if len(evict) == 0 { + return false + } + evicted, err := n.EvictPeers(evict) + if err == errQuorumPossible { + js.srv.Debugf("JetStream cluster can not evict peers for %s, the peers left could still reach quorum without us", desc) + } else if err == nil && len(evicted) > 0 { + js.srv.Noticef("JetStream cluster evicted peers %+v for %s", evicted, desc) + } + return true +} + +// peerRemoval controls how a peer is taken out of a stream's group. +type peerRemoval struct { + // remove drops the peer right away, otherwise the group migrates off of it first. + remove bool + // requireReplicas rejects the removal when it would leave the group below its replica count. + requireReplicas bool + // exclude keeps the peer out of this group's placement until the group is healed. + // Set when evacuating a server, which stays in the meta peer set and would + // otherwise be a candidate to hand the stream straight back to. + exclude bool +} + +// Removes or evacuates the given peer from the stream's group, and its consumers. +// Reports whether the removal was applied. +// Lock should be held. +func (js *jetStream) removePeerFromStreamLocked(sa *streamAssignment, peer string, opts peerRemoval) bool { + cc := js.cluster + if cc == nil || cc.meta == nil { + return false + } + csa, replaced := cc.remapStreamAssignment(sa, peer, opts.remove) + if csa == nil { + return false + } + accName := sa.Client.serviceAccount() + // Reject if the group would miss replicas and all are required. + if opts.requireReplicas && csa.missingPeers() { + cc.s.Warnf("JetStream cluster rejected peer removal, no replacement available for stream '%s > %s'", accName, sa.Config.Name) + return false + } + if !replaced { + cc.s.Warnf("JetStream cluster could not replace peer for stream '%s > %s'", accName, sa.Config.Name) + } + // If the peer couldn't be replaced but we need to exclude, track it so it doesn't get readded. + if opts.exclude && csa.missingPeers() && !slices.Contains(csa.Group.Excluded, peer) { + csa.Group.Excluded = append(csa.Group.Excluded, peer) + } + + // If we're evacuating a peer, consumer remapping can happen lazily. + if !opts.remove { + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(csa)); err != nil { + return false + } + cc.trackInflightStreamProposal(accName, csa, false) + return true + } + + // Send our proposal for this csa. Also use same group definition for all the consumers as well. + consumers, deleted, _ := js.remapConsumerAssignments(accName, csa) + + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(csa)); err != nil { + return false + } + cc.trackInflightStreamProposal(accName, csa, false) + for _, cca := range consumers { + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(cca)); err != nil { + return false + } + cc.trackInflightConsumerProposal(accName, csa.Config.Name, cca, false) + } + // These are ephemerals that had all of their peers removed. + for _, ca := range deleted { + if err := cc.meta.Propose(cc.term, encodeDeleteConsumerAssignment(ca)); err != nil { + return false + } + cc.trackInflightConsumerProposal(accName, csa.Config.Name, ca, true) + } + return true +} + +// Removes or evacuates the given peer from the consumer's group. +// Reports whether the removal was applied. +// Lock should be held. +func (js *jetStream) remapPeerFromConsumerLocked(sa *streamAssignment, ca *consumerAssignment, peer string, remove bool) bool { + cc := js.cluster + if cc == nil || cc.meta == nil || sa == nil || ca == nil { + return false + } + cca := cc.remapConsumerAssignment(sa, ca, peer, remove) + if cca == nil { + return false + } + accName := ca.Client.serviceAccount() + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(cca)); err != nil { + return false + } + cc.trackInflightConsumerProposal(accName, sa.Config.Name, cca, false) + return true +} + +// Removes or evacuates the given peer from a single consumer, replacing it with one of the +// stream's other peers. The stream's own group is left alone, so the replacement can only +// come from where the stream already is. +// Returns nil if nothing changes, or if the consumer would be left short. +// Lock should be held. +func (cc *jetStreamCluster) remapConsumerAssignment(sa *streamAssignment, ca *consumerAssignment, peer string, remove bool) *consumerAssignment { + // A single peer stream has nowhere to hand this consumer to. + streamPeers := sa.targetPeers() + if len(streamPeers) < 2 { + return nil + } + + // The peer must exist in the actual or desired peer set, otherwise there's nothing to do. + if !ca.Group.isMember(peer) && (ca.Group.Desired == nil || !slices.Contains(ca.Group.Desired.Peers, peer)) { + return nil + } + + // A group that's already converging is driven by its desired peers, so base on those. + // The actual peers would hand back the ones the migration is dropping. + basePeers := ca.Group.Peers + if d := ca.Group.Desired; d != nil { + basePeers = d.Peers + } + + // A scale down that's already pending stays pending, its leader picks which peers remain. + scaleDown := ca.Group.Desired != nil && ca.Group.Desired.ScaleDown + + // The count the consumer must come out at, interest/workqueue require peer parity. + replicas := ca.Config.replicas(sa.Config) + if sa.Config.Retention != LimitsPolicy { + replicas = sa.Config.Replicas + } + + retain := make([]string, 0, len(basePeers)) + for _, p := range basePeers { + if p != peer { + retain = append(retain, p) + } + } + + // The replacement must be a stream peer this consumer is not on yet. Prefer one that's + // up, but take one that's down over none at all. + var online, offline []string + for _, p := range streamPeers { + if p == peer || slices.Contains(retain, p) { + continue + } + if si, ok := cc.s.nodeToInfo.Load(p); ok && si != nil && si.(nodeInfo).offline { + offline = append(offline, p) + continue + } + online = append(online, p) + } + candidates := online + if len(candidates) == 0 { + candidates = offline + } + + newPeers := retain + if len(retain) < len(basePeers) && len(candidates) > 0 { + rand.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] }) + newPeers = append(retain, candidates[0]) + } + // Reject if the consumer would be left short. A pending scale down is no exception, its + // desired peers are what the leader still has to pick a full replica set out of. + if len(newPeers) < replicas { + return nil + } + + cca := ca.copyGroup() + cca.Group.Peers = newPeers + // Always use desired state, so peers and where they converge to can never diverge. + // Reconciling that state renames a single node group once it lands. + cca.Group = ca.Group.withDesired(cca.Group) + cca.Group.Desired.ScaleDown = scaleDown + + if remove { + removeFrom := func(from []string) []string { + return slices.DeleteFunc(from, func(p string) bool { return p == peer }) + } + cca.Group.Peers = removeFrom(cca.Group.Peers) + cca.Group.Desired.Peers = removeFrom(cca.Group.Desired.Peers) + cca.Group.Desired.addRemoved([]string{peer}) + } + // Don't allow moving to an empty set. + if len(cca.Group.Desired.Peers) == 0 { + return nil + } + // Don't carry a stale preferred into the remaining group. + isMember := func(p string) bool { + return slices.Contains(cca.Group.Peers, p) || slices.Contains(cca.Group.Desired.Peers, p) + } + if cca.Group.Preferred != _EMPTY_ && !isMember(cca.Group.Preferred) { + cca.Group.Preferred = _EMPTY_ + } + if cca.Group.Desired.Preferred != _EMPTY_ && !isMember(cca.Group.Desired.Preferred) { + cca.Group.Desired.Preferred = _EMPTY_ + } + // Nothing actually changes, don't propose a no-op migration. + if slices.Equal(cca.Group.Peers, ca.Group.Peers) && slices.Equal(cca.Group.Desired.Peers, basePeers) { + return nil + } + // If no peers remain, immediately jump to the desired set. + if len(cca.Group.Peers) == 0 { + cca.Group.Peers = copyStrings(cca.Group.Desired.Peers) + } + return cca +} + +// Check if we have peer related entries. +func (js *jetStream) hasPeerEntries(entries []*Entry) bool { + for _, e := range entries { + if e.Type == EntryRemovePeer || e.Type == EntryAddPeer { + return true + } + } + return false +} + +const ksep = ":" + +func (sa *streamAssignment) recoveryKey() string { + if sa == nil { + return _EMPTY_ + } + return sa.Client.serviceAccount() + ksep + sa.Config.Name +} + +func (ca *consumerAssignment) streamRecoveryKey() string { + if ca == nil { + return _EMPTY_ + } + return ca.Client.serviceAccount() + ksep + ca.Stream +} + +func (ca *consumerAssignment) recoveryKey() string { + if ca == nil { + return _EMPTY_ + } + return ca.Client.serviceAccount() + ksep + ca.Stream + ksep + ca.Name +} + +func (js *jetStream) applyMetaEntries(entries []*Entry, ru *recoveryUpdates) (bool, bool, error) { + var didSnap bool + isRecovering := ru != nil + + for _, e := range entries { + // If we received a lower-level catchup entry, mark that we're recovering. + // We can optimize by staging all meta operations until we're caught up. + // At that point we can apply the diff in one go. + if e.Type == EntryCatchup { + isRecovering = true + // A catchup entry only contains this, so we can exit now and have the + // recoveryUpdates struct be populated for the next invocation of applyMetaEntries. + return isRecovering, didSnap, nil + } + + if e.Type == EntrySnapshot { + if err := js.applyMetaSnapshot(e.Data, ru, isRecovering); err != nil { + return isRecovering, didSnap, err + } + didSnap = true + } else if e.Type == EntryRemovePeer { + if !js.isMetaRecovering() { + peer := string(e.Data) + js.processRemovePeer(peer) + + // The meta leader can now respond to the peer-removal, + // since a quorum of nodes has this in their log. + s := js.srv + if s.JetStreamIsLeader() { + var ( + info peerRemoveInfo + ok bool + ) + js.mu.Lock() + if cc := js.cluster; cc != nil && cc.peerRemoveReply != nil { + if info, ok = cc.peerRemoveReply[peer]; ok { + delete(cc.peerRemoveReply, peer) + } + if len(cc.peerRemoveReply) == 0 { + cc.peerRemoveReply = nil + } + } + js.mu.Unlock() + + if info.reply != _EMPTY_ { + sysAcc := s.SystemAccount() + var resp = JSApiMetaServerRemoveResponse{ApiResponse: ApiResponse{Type: JSApiMetaServerRemoveResponseType}} + resp.Success = true + s.sendAPIResponse(info.ci, sysAcc, info.subject, info.reply, info.request, s.jsonResponse(&resp)) + } + } + } + } else if e.Type == EntryAddPeer { + if !js.isMetaRecovering() { + js.processAddPeer(string(e.Data)) + } + } else { + buf := e.Data + if len(buf) == 0 { + return isRecovering, didSnap, errBadEntryOp + } + switch entryOp(buf[0]) { + case assignStreamOp: + sa, err := decodeStreamAssignment(js.srv, buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode stream assignment: %q", buf[1:]) + return isRecovering, didSnap, err + } + if isRecovering { + js.setStreamAssignmentRecovering(sa) + ru.addStream(sa) + } else { + js.processStreamAssignment(sa) + } + case removeStreamOp: + sa, err := decodeStreamAssignment(js.srv, buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode stream assignment: %q", buf[1:]) + return isRecovering, didSnap, err + } + if isRecovering { + js.setStreamAssignmentRecovering(sa) + ru.removeStream(sa) + } else { + js.processStreamRemoval(sa) + } + case assignConsumerOp: + ca, err := decodeConsumerAssignment(buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode consumer assignment: %q", buf[1:]) + return isRecovering, didSnap, err + } + if isRecovering { + js.setConsumerAssignmentRecovering(ca) + ru.addOrUpdateConsumer(ca) + } else { + js.processConsumerAssignment(ca) + } + case assignCompressedConsumerOp: + ca, err := decodeConsumerAssignmentCompressed(buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode compressed consumer assignment: %q", buf[1:]) + return isRecovering, didSnap, err + } + if isRecovering { + js.setConsumerAssignmentRecovering(ca) + ru.addOrUpdateConsumer(ca) + } else { + js.processConsumerAssignment(ca) + } + case removeConsumerOp: + ca, err := decodeConsumerAssignment(buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode consumer assignment: %q", buf[1:]) + return isRecovering, didSnap, err + } + if isRecovering { + js.setConsumerAssignmentRecovering(ca) + ru.removeConsumer(ca) + } else { + js.processConsumerRemoval(ca) + } + case updateStreamOp: + sa, err := decodeStreamAssignment(js.srv, buf[1:]) + if err != nil { + js.srv.Errorf("JetStream cluster failed to decode stream assignment: %q", buf[1:]) + return isRecovering, didSnap, err + } + if isRecovering { + js.setStreamAssignmentRecovering(sa) + ru.updateStream(sa) + } else { + js.processUpdateStreamAssignment(sa) + } + default: + return isRecovering, didSnap, fmt.Errorf("unknown meta entry op type: %v", entryOp(buf[0])) + } + } + } + return isRecovering, didSnap, nil +} + +func (rg *raftGroup) isMember(id string) bool { + if rg == nil { + return false + } + for _, peer := range rg.Peers { + if peer == id { + return true + } + } + return false +} + +func (rg *raftGroup) setPreferred(s *Server) { + if rg == nil || len(rg.Peers) == 0 { + return + } + if len(rg.Peers) == 1 { + rg.Preferred = rg.Peers[0] + } else { + var online []string + for _, p := range rg.Peers { + si, ok := s.nodeToInfo.Load(p) + if !ok || si == nil { + continue + } + ni := si.(nodeInfo) + if ni.offline { + continue + } + online = append(online, p) + } + + if len(online) == 0 { + // No online servers, just randomly select a peer for the preferred. + pi := rand.Int32N(int32(len(rg.Peers))) + rg.Preferred = rg.Peers[pi] + } else if len(online) == 1 { + // Only one online server. + rg.Preferred = online[0] + } else { + // Randomly select an online peer. + pi := rand.Int32N(int32(len(online))) + rg.Preferred = online[pi] + } + } +} + +// createRaftGroup is called to spin up this raft group if needed. +func (js *jetStream) createRaftGroup(accName string, rg *raftGroup, recovering bool, storage StorageType, labels pprofLabels) (RaftNode, error) { + // js.mu protects the lookup/registration of raft groups so that two parallel + // calls for the same identifier can't end up creating duplicate instances. + // It is released around blocking work (waiting for a previous instance to + // shut down, and the disk I/O inside startRaftNode); concurrent callers for + // the same rg.Name are gated through cc.creatingRaftGroups instead. + js.mu.Lock() + defer js.mu.Unlock() + + s, cc := js.srv, js.cluster + if cc == nil || cc.meta == nil { + return nil, NewJSClusterNotActiveError() + } + + // While a desired state is pending the group keeps a node even at a single peer, so + // scaling down doesn't lose the Raft term a newer server resumes the migration from. + // If this is a single peer raft group or we are not a member return. + if (rg.Desired == nil && len(rg.Peers) <= 1) || !rg.isMember(cc.meta.ID()) { + // Nothing to do here. + return nil, nil + } + +retry: + // If another goroutine is mid-creation for this raft group, wait for it + // to finish (without holding js.mu) and then retry the lookup. + if ch, ok := cc.creatingRaftGroups[rg.Name]; ok { + js.mu.Unlock() + <-ch + js.mu.Lock() + // js.cluster could have been swapped out (shutdown). + if js.cluster == nil || js.cluster.meta == nil { + return nil, NewJSClusterNotActiveError() + } + cc = js.cluster + goto retry + } + // Check if we already have this assigned. + if node := s.lookupRaftNode(rg.Name); node != nil { + if node.State() == Closed { + // We're waiting for this node to finish shutting down before we replace it. + js.mu.Unlock() + node.WaitForStop() + js.mu.Lock() + goto retry + } + s.Debugf("JetStream cluster already has raft group %q assigned", rg.Name) + rg.node = node + return node, nil + } + + s.Debugf("JetStream cluster creating raft group:%+v", rg) + + sysAcc := s.SystemAccount() + if sysAcc == nil { + s.Debugf("JetStream cluster detected shutdown processing raft group: %+v", rg) + return nil, errors.New("shutting down") + } + + // Check here to see if we have a max HA Assets limit set. + if maxHaAssets := s.getOpts().JetStreamLimits.MaxHAAssets; maxHaAssets > 0 { + if s.numRaftNodes()+len(cc.creatingRaftGroups) > maxHaAssets { + s.Warnf("Maximum HA Assets limit reached: %d", maxHaAssets) + // Since the meta leader assigned this, send a statsz update to them to get them up to date. + go s.sendStatszUpdate() + return nil, errors.New("system limit reached") + } + } + + // Register an in-flight sentinel so concurrent callers for the same group + // will wait for us. Then drop js.mu around all the blocking work below + // (file store creation, peer state read, snapshot replay, fsyncs) so we + // don't serialize every stream/consumer assignment behind one disk fsync. + if cc.creatingRaftGroups == nil { + cc.creatingRaftGroups = make(map[string]chan struct{}) + } + doneCh := make(chan struct{}) + cc.creatingRaftGroups[rg.Name] = doneCh + + // Snapshot rg fields; we drop js.mu below and rg is shared. + rgName, rgScaleUp := rg.Name, rg.ScaleUp + rgPeers := copyStrings(rg.Peers) + storeDir := filepath.Join(js.config.StoreDir, sysAcc.Name, defaultStoreDirName, rg.Name) + js.mu.Unlock() + + n, err := func() (RaftNode, error) { + var store StreamStore + if storage == FileStorage { + opts := s.getOpts() + fs, err := newFileStoreWithCreated( + FileStoreConfig{StoreDir: storeDir, BlockSize: defaultMediumBlockSize, AsyncFlush: false, SyncAlways: opts.SyncAlways, SyncInterval: opts.SyncInterval, srv: s}, + StreamConfig{Name: rgName, Storage: FileStorage, Metadata: labels}, + time.Now().UTC(), + s.jsKeyGen(opts.JetStreamKey, rgName), + s.jsKeyGen(opts.JetStreamOldKey, rgName), + ) + if err != nil { + s.Errorf("Error creating filestore WAL: %v", err) + return nil, err + } + store = fs + } else { + ms, err := newMemStore(&StreamConfig{Name: rgName, Storage: MemoryStorage}) + if err != nil { + s.Errorf("Error creating memstore WAL: %v", err) + return nil, err + } + store = ms + } + + cfg := &RaftConfig{Name: rgName, Store: storeDir, Log: store, Track: true, Managed: true, Recovering: recovering, ScaleUp: rgScaleUp} + + if _, err := readPeerState(s.diskIOSemaphore(), storeDir); err != nil { + s.bootstrapRaftNode(cfg, rgPeers, true) + } + + n, err := s.startRaftNode(accName, cfg, labels) + if err != nil || n == nil { + s.Debugf("Error creating raft group: %v", err) + return nil, err + } + return n, nil + }() + + js.mu.Lock() + delete(cc.creatingRaftGroups, rg.Name) + close(doneCh) + if err != nil || n == nil { + return nil, err + } + if js.cluster == nil || js.cluster.meta == nil { + // Cluster was torn down while we were creating; let n be reaped at shutdown. + return nil, NewJSClusterNotActiveError() + } + // Need JS lock to be held for the assignment to avoid data-race reports + rg.node = n + preferred := rg.Preferred + if rg.Desired != nil { + preferred = rg.Desired.Preferred + } + // See if we are preferred and should start campaign immediately. + if n.ID() == preferred && n.Term() == 0 { + n.CampaignImmediately() + } + return n, nil +} + +func (mset *stream) raftGroup() *raftGroup { + if mset == nil { + return nil + } + mset.mu.RLock() + defer mset.mu.RUnlock() + if mset.sa == nil { + return nil + } + return mset.sa.Group +} + +func (mset *stream) raftNode() RaftNode { + if mset == nil { + return nil + } + mset.mu.RLock() + defer mset.mu.RUnlock() + return mset.node +} + +func (mset *stream) removeNode() { + mset.mu.Lock() + defer mset.mu.Unlock() + if n := mset.node; n != nil { + n.Delete() + mset.node = nil + } +} + +// This will wait for a period of time until all consumers are registered and have +// their consumer assignments assigned. +// Should only be called from monitorStream. +func (mset *stream) waitOnConsumerAssignments() { + mset.mu.RLock() + s, js, acc, sa, name, replicas := mset.srv, mset.js, mset.acc, mset.sa, mset.cfg.Name, mset.cfg.Replicas + mset.mu.RUnlock() + + if s == nil || js == nil || acc == nil || sa == nil { + return + } + + js.mu.RLock() + numExpectedConsumers := len(sa.consumers) + js.mu.RUnlock() + + // Max to wait. + const maxWaitTime = 10 * time.Second + const sleepTime = 500 * time.Millisecond + + // Wait up to 10s + timeout := time.Now().Add(maxWaitTime) + for time.Now().Before(timeout) { + var numReady int + for _, o := range mset.getConsumers() { + // Make sure we are registered with our consumer assignment. + if ca := o.consumerAssignment(); ca != nil { + if replicas > 1 && !o.isMonitorRunning() { + break + } + numReady++ + } else { + break + } + } + // Check if we are good. + if numReady >= numExpectedConsumers { + break + } + + s.Debugf("Waiting for consumers for interest based stream '%s > %s'", acc.Name, name) + select { + case <-s.quitCh: + return + case <-mset.monitorQuitC(): + return + case <-time.After(sleepTime): + } + } + + if actual := mset.numConsumers(); actual < numExpectedConsumers { + s.Warnf("All consumers not online for '%s > %s': expected %d but only have %d", acc.Name, name, numExpectedConsumers, actual) + } +} + +func prepareStreamRecovery(mset *stream, n RaftNode) error { + if n == nil || mset == nil || !mset.shouldReplayFromWAL() { + return nil + } + + index, _, _ := n.Progress() + if index == 0 { + return nil + } + + snapIndex, snapData, err := n.LoadLastSnapshot() + if err != nil { + if err != errNoSnapAvailable { + return err + } + // A newly created R3 stream can legitimately have no + // snapshot before its first periodic snapshot. + // In that case the WAL contains a complete history + // we can replay from. + // During an R1 scaleup, the new WAL does not contain + // the existing R1 history. Preserve the store if + // the source has restarted before the initial snapshot + // was installed + if mset.streamAssignment().isR1ScaleUpSource(n.ID()) { + return nil + } + } + + // In case of clean shutdown, no need to replay from WAL + if snapIndex == index { + return nil + } + + var snap *StreamReplicatedState + if err == nil { + snap, err = decodeStreamSnapshot(snapData) + if err != nil { + return err + } + } + return mset.prepareForWALReplay(snap) +} + +// Monitor our stream node for this stream. +func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnapshot bool) { + s, cc := js.server(), js.cluster + defer s.grWG.Done() + if mset != nil { + defer mset.monitorWg.Done() + } + js.mu.RLock() + n := sa.Group.node + meta := cc.meta + js.mu.RUnlock() + + if n == nil || meta == nil { + s.Warnf("No RAFT group for '%s > %s'", sa.Client.serviceAccount(), sa.Config.Name) + return + } + + // Make sure only one is running. + if mset != nil { + if mset.checkInMonitor() { + return + } + defer mset.clearMonitorRunning() + } + + // Make sure to stop the raft group on exit to prevent accidental memory bloat. + // This should be below the checkInMonitor call though to avoid stopping it out + // from underneath the one that is running since it will be the same raft node. + defer func() { + // We might be closing during shutdown, don't pre-emptively stop here since we'll still want to install snapshots. + if mset != nil && !mset.closed.Load() { + n.Stop() + } + }() + + qch, mqch, lch, aq, uch := n.QuitC(), mset.monitorQuitC(), n.LeadChangeC(), n.ApplyQ(), mset.updateC() + + s.Debugf("Starting stream monitor for '%s > %s' [%s]", sa.Client.serviceAccount(), sa.Config.Name, n.Group()) + defer s.Debugf("Exiting stream monitor for '%s > %s' [%s]", sa.Client.serviceAccount(), sa.Config.Name, n.Group()) + + // Make sure we do not leave the apply channel to fill up and block the raft layer. + defer func() { + if n.State() == Closed { + return + } + n.StepDown() + // Drain the commit queue... + aq.drain() + }() + + const ( + compactInterval = 2 * time.Minute + compactMinInterval = 15 * time.Second + compactSizeMin = 8 * 1024 * 1024 + compactNumMin = 65536 + ) + + // Spread these out for large numbers on server restart. + rci := time.Duration(rand.Int64N(int64(time.Minute))) + t := time.NewTicker(compactInterval + rci) + defer t.Stop() + + js.mu.RLock() + isLeader := cc.isStreamLeader(sa.Client.serviceAccount(), sa.Config.Name) + var leaderTerm uint64 + isRestore := sa.Restore != nil + js.mu.RUnlock() + + acc, err := s.LookupAccount(sa.Client.serviceAccount()) + if err != nil { + s.Warnf("Could not retrieve account for stream '%s > %s'", sa.Client.serviceAccount(), sa.Config.Name) + return + } + accName := acc.GetName() + + // Don't allow the upper layer to install snapshots until we have + // fully recovered from disk. + isRecovering := true + + // Prepare the store for recovery. A newly created Raft node for + // an existing stream will skip this step, and instead bootstrap + // the WAL with a snapshot. + if !sendSnapshot { + if err := prepareStreamRecovery(mset, n); err != nil { + s.Warnf("Error preparing WAL replay recovery for stream '%s > %s': %v", accName, sa.Config.Name, err) + return + } + } + + var ( + snapMu sync.Mutex + snapshotting bool + fallbackSnapshot bool + failedSnapshots int + ) + + // Make sure all pending data is flushed before allowing snapshots. Returns false if + // it couldn't be flushed, in which case we have no safe way to continue. + flushPending := func() bool { + err := mset.flushAllPending() + if err == nil { + return true + } + s.Errorf("Failed to flush pending data for '%s > %s' [%s]: %v", accName, mset.name(), n.Group(), err) + assert.Unreachable("Stream snapshot flush failed", map[string]any{ + "account": accName, + "stream": mset.name(), + "group": n.Group(), + "err": err, + }) + mset.setWriteErr(err) + n.Stop() + return false + } + + doSnapshot := func(force bool) { + // Suppress during recovery. + if mset == nil || isRecovering || isRestore { + return + } + snapMu.Lock() + defer snapMu.Unlock() + + // Don't suppress a snapshot during shutdown. + if js.isShuttingDown() { + if !flushPending() { + return + } + if err := n.InstallSnapshot(mset.stateSnapshot(), true); err != nil && + err != errNoSnapAvailable && err != errNodeClosed { + s.RateLimitWarnf("Failed to install snapshot for '%s > %s' [%s]: %v", + mset.acc.Name, mset.name(), n.Group(), err) + } + return + } + + // If snapshots have failed, and we're not forced to, we'll wait for the timer since it'll now be forced. + if !force && failedSnapshots > 0 { + return + } + // Suppress if an async snapshot is already in progress. + if snapshotting { + return + } + + // If we had a significant number of failed snapshots, start relaxing Raft-layer checks + // to force it through. We might have been catching up a peer for a long period, and this + // protects our log size from growing indefinitely. + forceSnapshot := failedSnapshots > 4 + c, err := n.CreateSnapshotCheckpoint(forceSnapshot) + if err != nil { + if err != errNoSnapAvailable && err != errNodeClosed { + s.RateLimitWarnf("Failed to install snapshot for '%s > %s' [%s]: %v", + mset.acc.Name, mset.name(), n.Group(), err) + // If this is the first failure, reduce the interval of the snapshot timer. + // This ensures we're not waiting too long for snapshotting to eventually become forced. + if failedSnapshots == 0 { + t.Reset(compactMinInterval) + } + failedSnapshots++ + } + return + } + if !flushPending() { + c.Abort() + return + } + + snap := mset.stateSnapshot() + + handleInstallResult := func(err error) { + snapshotting = false + if err == nil { + // If there was a failed snapshot before, we reduced the timer's interval. + // Reset it back to the original interval now. + if failedSnapshots > 0 { + t.Reset(compactInterval + rci) + } + failedSnapshots = 0 + fallbackSnapshot = false + } else { + c.Abort() + + if err == errNoSnapAvailable || err == errNodeClosed || err == errCatchupsRunning || err == errSnapAborted { + return + } + + s.RateLimitWarnf("Failed to install snapshot for '%s > %s' [%s]: %v, will fall back to blocking snapshot", + mset.acc.Name, mset.name(), n.Group(), err) + fallbackSnapshot = true + // If this is the first failure, reduce the interval of the snapshot timer. + // This ensures we're not waiting too long for snapshotting to eventually become forced. + if failedSnapshots == 0 { + t.Reset(compactMinInterval) + } + failedSnapshots++ + } + } + + snapshotting = true + if fallbackSnapshot { + _, err = c.InstallSnapshot(snap) + handleInstallResult(err) + } else { + started := s.startGoRoutine(func() { + defer s.grWG.Done() + + _, err := c.InstallSnapshot(snap) + + snapMu.Lock() + defer snapMu.Unlock() + handleInstallResult(err) + }) + if !started { + snapshotting = false + c.Abort() + } + } + } + + // We will establish a restoreDoneCh no matter what. Will never be triggered unless + // we replace with the restore chan. + restoreDoneCh := make(<-chan error) + + // For migration tracking. + var mmt *time.Timer + var mmtc <-chan time.Time + var positions peerPositions + var posQ *ipQueue[*clusterStreamInfoResponse] + var posCh <-chan struct{} + defer func() { posQ.unregister() }() + listenForPositions := func() { + if positions.sub != nil { + return + } + if posQ == nil { + posQ = newIPQueue[*clusterStreamInfoResponse](s, fmt.Sprintf("[ACC:%s] stream '%s' positions", accName, mset.name())) + posCh = posQ.ch + } + reply := syncReplySubject() + sub, err := s.sysSubscribe(reply, func(_ *subscription, _ *client, _ *Account, _, _ string, msg []byte) { + var resp clusterStreamInfoResponse + if err := json.Unmarshal(msg, &resp); err == nil { + posQ.push(&resp) + } + }) + if err == nil { + positions.reply, positions.sub = reply, sub + } + } + stopListeningForPositions := func() { + if positions.sub != nil { + s.sysUnsubscribe(positions.sub) + } + posQ.drain() + positions = peerPositions{} + } + + resetMigrationMonitoring := func(delay time.Duration) { + if mmt != nil { + if !mmt.Stop() { + // Drain if the timer wasn't stopped. + select { + case <-mmt.C: + default: + } + } + mmt.Reset(delay) + } + } + startMigrationMonitoring := func() { + if mmt == nil { + mmt = time.NewTimer(migrateFastCheckInterval) + mmtc = mmt.C + } else { + resetMigrationMonitoring(migrateFastCheckInterval) + } + } + stopMigrationMonitoring := func() { + if mmt != nil { + mmt.Stop() + mmt, mmtc = nil, nil + } + // Don't leave a stale status behind for a migration that's done or abandoned. + js.setMigrationStatus(mset.raftGroup(), nil) + stopListeningForPositions() + } + defer stopMigrationMonitoring() + + // Start monitoring if we're already meant to be migrating. If we're out of quorum + // with peer-removed peers, it will trigger a shrink of the group. + if mset.isMigrating() { + startMigrationMonitoring() + } + + // This is to optionally track when we are ready as a non-leader for direct access participation. + // Either direct or if we are a direct mirror, or both. + var dat *time.Ticker + var datc <-chan time.Time + + startDirectAccessMonitoring := func() { + if dat == nil { + dat = time.NewTicker(2 * time.Second) + datc = dat.C + } + } + + stopDirectMonitoring := func() { + if dat != nil { + dat.Stop() + dat, datc = nil, nil + } + } + defer stopDirectMonitoring() + + // For checking interest state if applicable. + var cist *time.Ticker + var cistc <-chan time.Time + + checkInterestInterval := checkInterestStateT + time.Duration(rand.IntN(checkInterestStateJ))*time.Second + + if mset != nil && mset.isInterestRetention() { + // Wait on our consumers to be assigned and running before proceeding. + // This can become important when a server has lots of assets + // since we process streams first then consumers as an asset class. + mset.waitOnConsumerAssignments() + // Setup our periodic check here. We will check once we have restored right away. + cist = time.NewTicker(checkInterestInterval) + cistc = cist.C + } + + // State to check for atomic batch completeness before applying it. + batch := &batchApply{} + defer func() { + for _, bce := range batch.entries { + bce.ReturnToPool() + } + batch.clearBatchState() + }() + + for { + select { + case <-s.quitCh: + // Server shutting down, but we might receive this before qch, so try to snapshot. + doSnapshot(false) + return + case <-mqch: + // Clean signal from shutdown routine so do best effort attempt to snapshot. + // Don't snapshot if not shutting down, monitor goroutine could be going away + // on a scale down or a remove for example. + if js.isShuttingDown() { + doSnapshot(false) + } + return + case <-qch: + // Raft node is closed, no use in trying to snapshot. + return + case <-aq.ch: + var ne, nb uint64 + // If we bump clfs we will want to write out snapshot if within our time window. + pclfs := mset.getCLFS() + + ces := aq.pop() + for _, ce := range ces { + // No special processing needed for when we are caught up on restart. + if ce == nil { + if !isRecovering { + continue + } + isRecovering = false + // If we are interest based make sure to check consumers if interest retention policy. + // This is to make sure we process any outstanding acks from all consumers. + if mset != nil && mset.isInterestRetention() { + fire := time.Duration(rand.IntN(5)+5) * time.Second + time.AfterFunc(fire, mset.checkInterestState) + } + // If we became leader during this time and we need to send a snapshot to our + // followers, i.e. as a result of a scale-up from R1, do it now. + if sendSnapshot && isLeader && mset != nil && n != nil { + n.SendSnapshot(mset.stateSnapshot()) + sendSnapshot = false + } + continue + } else if len(ce.Entries) == 0 { + // If we have a partial batch, it needs to be rejected to ensure CLFS is correct. + if mset != nil && batch != nil && batch.id != _EMPTY_ { + batch.rejectBatchState(mset) + } + + // Entry could be empty on a restore when mset is nil. + ne, nb = n.Applied(ce.Index) + ce.ReturnToPool() + continue + } + + // While migrating, react quickly to peer add/remove entries. + if mmt != nil { + for _, e := range ce.Entries { + if e.Type == EntryAddPeer || e.Type == EntryRemovePeer { + startMigrationMonitoring() + break + } + } + } + + // Apply our entries. + if maxApplied, err := js.applyStreamEntries(mset, n, ce, isRecovering, batch); err == nil { + // Update our applied. + if maxApplied > 0 { + // Indicate we've processed (but not applied) everything up to this point. + ne, nb = n.Processed(ce.Index, min(maxApplied, ce.Index)) + // Don't return entry to the pool, this is handled by the in-progress batch. + } else { + ne, nb = n.Applied(ce.Index) + ce.ReturnToPool() + } + } else { + // Make sure to clean up. + ce.ReturnToPool() + // Our stream was closed out from underneath of us, simply return here. + if err == errStreamClosed || err == errCatchupStreamStopped || err == ErrServerNotRunning { + aq.recycle(&ces) + return + } + s.Errorf("Error applying stream entries to '%s > %s': %v", accName, sa.Config.Name, err) + if isClusterResetErr(err) { + if mset.isMirror() && mset.IsLeader() { + mset.retryMirrorConsumer() + continue + } + // If the error signals we timed out of a snapshot, we should try to replay the snapshot + // instead of fully resetting the state. Resetting the clustered state may result in + // race conditions and should only be used as a last effort attempt. + if errors.Is(err, errCatchupAbortedNoLeader) || err == errCatchupTooManyRetries || err == errAlreadyLeader { + if n.DrainAndReplaySnapshot() { + break + } else if n.IsDeleted() { + // The only reason we can't replay is our node being deleted, which + // means the meta layer is deliberately tearing this monitor down. + // Don't fall through to a reset, that would resurrect this group. + s.Warnf("Will not reset stream '%s > %s', raft group %q was removed", + accName, sa.Config.Name, n.Group()) + aq.recycle(&ces) + return + } + } + // We will attempt to reset our cluster state. + if mset.resetClusteredState(n, err) { + aq.recycle(&ces) + return + } + } else if isOutOfSpaceErr(err) { + // If applicable this will tear all of this down, but don't assume so and return. + s.handleOutOfSpace(mset) + } else { + // Encountered an unexpected error, can't continue. + mset.setWriteErr(err) + aq.recycle(&ces) + return + } + } + } + aq.recycle(&ces) + + // Check about snapshotting + // If we have at least min entries to compact, go ahead and try to snapshot/compact. + if ne >= compactNumMin || nb > compactSizeMin || mset.getCLFS() > pclfs { + doSnapshot(false) + } + + case lc := <-lch: + // Not a change in leadership, just a nudge about a newly observed peer. + if lc.nudge { + if mset.isMigrating() { + startMigrationMonitoring() + } + continue + } + isLeader, leaderTerm = lc.isLeader, lc.term + stopListeningForPositions() + // Process our leader change. + js.processStreamLeaderChange(mset, isLeader, lc.term) + + if isLeader { + if mset != nil && n != nil && sendSnapshot && !isRecovering { + // If we *are* recovering at the time then this will get done when the apply queue + // handles the nil guard to show the catchup ended. + n.SendSnapshot(mset.stateSnapshot()) + sendSnapshot = false + } + if isRestore { + acc, _ := s.LookupAccount(sa.Client.serviceAccount()) + restoreDoneCh = s.processStreamRestore(sa.Client, acc, sa.Config, _EMPTY_, sa.Reply, _EMPTY_) + continue + } else if n != nil && n.NeedSnapshot() { + doSnapshot(false) + } + // Always cancel if this was running. + stopDirectMonitoring() + } else if !n.Leaderless() { + js.setStreamAssignmentRecovering(sa) + } + + // We may receive a leader change after the stream assignment which would cancel us + // monitoring for this closely. So re-assess our state here as well. + // Or the old leader is no longer part of the set and transferred leadership + // for this leader to resume with removal + migrating := mset.isMigrating() + + // Check for migrations here. We set the state on the stream assignment update below. + if migrating { + startMigrationMonitoring() + } else { + stopMigrationMonitoring() + } + + // Here we are checking if we are not the leader but we have been asked to allow + // direct access. We now allow non-leaders to participate in the queue group. + if !isLeader && mset != nil { + mset.mu.RLock() + ad, md := mset.cfg.AllowDirect, mset.cfg.MirrorDirect + mset.mu.RUnlock() + if ad || md { + startDirectAccessMonitoring() + } + } + + case <-cistc: + cist.Reset(checkInterestInterval) + // We may be adjusting some things with consumers so do this in its own go routine. + go mset.checkInterestState() + + case <-datc: + if mset == nil || isRecovering { + continue + } + // If we are leader we can stop, we know this is setup now. + if isLeader { + stopDirectMonitoring() + continue + } + + mset.mu.Lock() + ad, md, current := mset.cfg.AllowDirect, mset.cfg.MirrorDirect, mset.isCurrent() + if !current { + const syncThreshold = 90.0 + // We are not current, but current means exactly caught up. Under heavy publish + // loads we may never reach this, so check if we are within 90% caught up. + _, c, a := mset.node.Progress() + if c == 0 { + mset.mu.Unlock() + continue + } + if p := float64(a) / float64(c) * 100.0; p < syncThreshold { + mset.mu.Unlock() + continue + } else { + s.Debugf("Stream '%s > %s' enabling direct gets at %.0f%% synchronized", + sa.Client.serviceAccount(), sa.Config.Name, p) + } + } + // We are current, cancel monitoring and create the direct subs as needed. + if ad { + mset.subscribeToDirect() + } + if md { + mset.subscribeToMirrorDirect() + } + mset.mu.Unlock() + // Stop direct monitoring. + stopDirectMonitoring() + + case <-t.C: + // Start forcing snapshots if they failed previously. + snapMu.Lock() + forceIfFailed := failedSnapshots > 0 + snapMu.Unlock() + doSnapshot(forceIfFailed) + + case <-uch: + // keep stream assignment current + sa = mset.streamAssignment() + + // We get this when we have a new stream assignment caused by an update. + // We want to know if we are migrating. + migrating := mset.isMigrating() + if migrating { + startMigrationMonitoring() + } else { + stopMigrationMonitoring() + } + case <-posCh: + var update bool + resps := posQ.pop() + for _, resp := range resps { + update = positions.record(resp) || update + } + posQ.recycle(&resps) + if update && isLeader { + resetMigrationMonitoring(migrateFastCheckInterval) + } + + case <-mmtc: + if !isLeader { + // We're not the leader, but check if we're out of quorum and + // need to shrink based on peer-removes. + if !js.checkEvictedPeers(n, sa.Group, fmt.Sprintf("stream '%s > %s'", accName, sa.Config.Name)) { + stopMigrationMonitoring() + continue + } + resetMigrationMonitoring(migrateFallbackCheckInterval) + continue + } + // Reset to the slower fallback speed. + resetMigrationMonitoring(migrateFallbackCheckInterval) + listenForPositions() + status := js.runStreamMigration(mset, sa, n, leaderTerm, &positions) + // Only retry meta leader requests slowly, their reply wakes us on the fast path. + // Being blocked on a consumer is the same, the last consumer to converge signals + // the stream monitor, so there's nothing to poll for in between. + if status != nil && (status.Type == MigrationStatusMeta || status.Type == MigrationStatusBlocked) { + resetMigrationMonitoring(migrateMetaRetryInterval) + } + // Resolve after determining the status, so that we don't set it on a stale group. + js.setMigrationStatus(mset.raftGroup(), status) + + case err := <-restoreDoneCh: + // We have completed a restore from snapshot on this server. The stream assignment has + // already been assigned but the replicas will need to catch up out of band. Consumers + // will need to be assigned by forwarding the proposal and stamping the initial state. + s.Debugf("Stream restore for '%s > %s' completed", sa.Client.serviceAccount(), sa.Config.Name) + if err != nil { + s.Debugf("Stream restore failed: %v", err) + } + isRestore = false + sa.Restore = nil + // If we were successful lookup up our stream now. + if err == nil { + if mset, err = acc.lookupStream(sa.Config.Name); mset != nil { + mset.startMonitorWg() + defer mset.monitorWg.Done() + mset.checkInMonitor() + mset.setStreamAssignment(sa) + // Make sure to update our updateC which would have been nil. + uch = mset.updateC() + // Also update our mqch + mqch = mset.monitorQuitC() + // Setup a periodic check here if we are interest based as well. + if mset.isInterestRetention() { + cist = time.NewTicker(checkInterestInterval) + cistc = cist.C + } + } + } + // We may have lost leadership while the restore was running. + if err == nil && !isLeader { + err = errNotLeader + } + if err != nil { + if mset != nil { + mset.delete() + } + js.mu.Lock() + s.Warnf("Stream restore failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err) + sa.err = err + if n != nil { + n.Delete() + } + result := &streamAssignmentResult{ + Account: sa.Client.serviceAccount(), + Stream: sa.Config.Name, + Restore: &JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}}, + } + result.Restore.Error = NewJSStreamAssignmentError(err, Unless(err)) + js.mu.Unlock() + // Send response to the metadata leader. They will forward to the user as needed. + s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result) + return + } + // Trigger the stream followers to catchup. + var term uint64 + if n = mset.raftNode(); n != nil { + n.SendSnapshot(mset.stateSnapshot()) + term = n.Term() + } + js.processStreamLeaderChange(mset, isLeader, term) + + // Check to see if we have restored consumers here. + // These are not currently assigned so we will need to do so here. + for _, o := range mset.getPublicConsumers() { + // Place our initial state along with the assignment, so it gets + // distributed to the other members of the consumer's group. + js.requestConsumerAssignment(sa, o, true) + } + } + } +} + +type desiredNeed int + +const ( + desiredReady desiredNeed = iota + desiredMissing + desiredStaleTerm +) + +// desiredSnapshot copies the group's desired state, so it stays usable after the +// JetStream lock is released. needDesired encodes whether desired state is missing +// or was recorded under another leader term, and the meta leader must record it first. +// Lock should be held. +func (rg *raftGroup) desiredSnapshot(leaderTerm uint64) (id string, scaleDown bool, peers []string, needDesired desiredNeed) { + desired := rg.Desired + if desired != nil { + // MUST copy the peers, the assignment can be updated once we release. + id, scaleDown, peers = desired.ID, desired.ScaleDown, copyStrings(desired.Peers) + } + if desired == nil || desired.ID == _EMPTY_ { + needDesired = desiredMissing + } else if desired.Term != leaderTerm { + needDesired = desiredStaleTerm + } + return id, scaleDown, peers, needDesired +} + +// peerIDs returns the IDs of the given peers. +func peerIDs(peers []*Peer) []string { + ids := make([]string, 0, len(peers)) + for _, p := range peers { + ids = append(ids, p.ID) + } + return ids +} + +// removeEvictedPeers removes a raft member that is not a current member or no +// longer part of the meta group, it has been evicted from the cluster, e.g. by a +// server peer-remove. It can't take part in a migration, so remove it from the +// group right away rather than waiting for the rest of the migration to complete. +// Returns (trimmed) hosted peers, and a status if we've acted, in which case the +// migration must wait for the next cycle. +func (s *Server) removeEvictedPeers(n, meta RaftNode, actual []*Peer, actualPeers, current, desiredPeers, hosted []string) ([]string, *DesiredClusterInfoStatus) { + metaPeers := meta.PeerNames() + isEvicted := func(peer string) bool { + return !slices.Contains(current, peer) || !slices.Contains(metaPeers, peer) + } + hosted = slices.DeleteFunc(hosted, isEvicted) + var evicted []string + for _, peer := range actualPeers { + if isEvicted(peer) { + evicted = append(evicted, peer) + } + } + if len(evicted) == 0 { + return hosted, nil + } + // Remove evicted peers one at a time, the leader last so leadership stays + // stable throughout. Step down and perform a leader transfer if we'd remove + // ourselves, preferring a successor that is already in the desired peer set. + ourPeerId := n.ID() + remove := s.selectPeerToRemove(ourPeerId, actual, evicted, nil) + if remove == ourPeerId { + err := n.StepDown(s.selectStepDownPreferred(ourPeerId, actual, desiredPeers)) + return hosted, mstat(MigrationStatusMembership, "stepping down, evicted from group").withErr(err) + } + err := n.ProposeRemovePeer(remove) + name := s.serverNameForNode(remove) + if name == _EMPTY_ { + name = fmt.Sprintf("with id %s", remove) + } + return hosted, mstat(MigrationStatusMembership, "removing evicted peer %s", name).withErr(err) +} + +// extendPeerSet extends the actual peer set through the log, but only with peers +// that are part of the desired set, or that still host a consumer. +// Returns a status if we've acted, and the migration must wait for the next cycle. +func (s *Server) extendPeerSet(n RaftNode, actual []*Peer, actualPeers, current, desiredPeers, hosted []string) *DesiredClusterInfoStatus { + var candidates []string + for _, peer := range current { + if slices.Contains(actualPeers, peer) { + continue + } + if slices.Contains(desiredPeers, peer) || slices.Contains(hosted, peer) { + candidates = append(candidates, peer) + } + } + if len(candidates) == 0 { + return nil + } + add := s.selectPeerToAdd(n, n.ID(), actual, candidates) + if add == _EMPTY_ { + // We haven't heard from any candidates, send a heartbeat now to get them to respond + // if they were waiting. We'll be signaled right away after a new peer is observed. + n.SendHeartbeat() + return mstat(MigrationStatusQuorum, "waiting for quorum to add peer") + } + err := n.ProposeAddPeer(add) + name := s.serverNameForNode(add) + if name == _EMPTY_ { + name = fmt.Sprintf("with id %s", add) + } + return mstat(MigrationStatusMembership, "adding peer %s", name).withErr(err) +} + +// Migrate a stream from peer set A to peer set B. +func (js *jetStream) runStreamMigration(mset *stream, sa *streamAssignment, n RaftNode, leaderTerm uint64, positions *peerPositions) *DesiredClusterInfoStatus { + // Sanity-check: we're still the leader. + if leaderTerm == 0 || !n.Leader() { + return nil + } + ourPeerId, s := n.ID(), js.srv + + // Store whether any peers are currently being caught up, in which case they're + // not current yet and we need to wait before removing peers. + catchups := mset.catchupPeers() + + // Snapshot the assignment state we need up front, so the Raft reads below don't + // contend for Raft locks while holding the JetStream lock. + js.mu.RLock() + cc := js.cluster + // We are shutting down. + if cc == nil || cc.meta == nil { + js.mu.RUnlock() + return mstat(MigrationStatusUnavailable, "shutting down") + } + if sa == nil || sa.Group == nil { + js.mu.RUnlock() + return mstat(MigrationStatusUnavailable, "no stream assignment") + } + meta := cc.meta + accName, streamName, replicas := sa.Client.serviceAccount(), sa.Config.Name, sa.Config.Replicas + current := copyStrings(sa.Group.Peers) + // Peers we can't drop yet; they still host a consumer. + hostedPeers := sa.consumerHostedPeers() + desiredID, desiredScaleDown, desiredPeers, needDesired := sa.Group.desiredSnapshot(leaderTerm) + js.mu.RUnlock() + + update := desiredAssignmentUpdate{Term: leaderTerm, ID: desiredID} + sendMetaUpdate := func() { + reconcile := &streamAssignmentReconcile{Account: accName, Stream: streamName, desiredAssignmentUpdate: update} + s.sendInternalMsgLocked(streamAssignmentReconcileSubj, _EMPTY_, nil, reconcile) + } + if needDesired != desiredReady { + sendMetaUpdate() + // Will only happen on legacy moves or peer sets having drifted outside desired state. + if needDesired == desiredMissing { + return mstat(MigrationStatusMeta, "requesting desired state from meta leader") + } + return mstat(MigrationStatusMeta, "recording leadership term with meta leader") + } + // A snapshot is required. Automatically installs a snapshot for a R1 scaleup. + if n.NeedSnapshot() { + if err := mset.flushAllPending(); err != nil { + if errors.Is(err, ErrStoreClosed) { + return mstat(MigrationStatusUnavailable, "shutting down") + } + return mstat(MigrationStatusSnapshot, "waiting to flush pending state for snapshot").withErr(err) + } + if err := n.InstallSnapshot(mset.stateSnapshot(), true); err != nil { + return mstat(MigrationStatusSnapshot, "waiting to install snapshot").withErr(err) + } + // The snapshot is installed, continue right away so new peers can be added + // and catch up from it without waiting for another check. + } + // If a membership change is in progress, we just wait for it to clear. + if n.MembershipChangeInProgress() { + return mstat(MigrationStatusMembership, "waiting for membership change to commit") + } + + actual := n.Peers() + actualPeers := peerIDs(actual) + + // Remove any peers that have been evicted from the cluster. + hostedPeers, status := s.removeEvictedPeers(n, meta, actual, actualPeers, current, desiredPeers, hostedPeers) + if status != nil { + return status + } + // Extend the actual peer set through the log. + if status := s.extendPeerSet(n, actual, actualPeers, current, desiredPeers, hostedPeers); status != nil { + return status + } + // If scaling down, we need to select where to. + if desiredScaleDown { + update.ScaleDownPeers = s.selectScaleDownPeers(ourPeerId, actual, desiredPeers, replicas) + sendMetaUpdate() + return mstat(MigrationStatusMeta, "selecting peers to scale down to") + } + + // Add peers in our desired peer set. + foundAll := true + for _, peer := range desiredPeers { + if !slices.Contains(current, peer) { + foundAll = false + break + } + } + if !foundAll { + combined := current + for _, peer := range desiredPeers { + if !slices.Contains(current, peer) { + combined = append(combined, peer) + } + } + update.MetaPeers = combined + sendMetaUpdate() + return mstat(MigrationStatusMeta, "expanding assignment with desired peers") + } + + slices.Sort(current) + slices.Sort(actualPeers) + peersMatch := slices.Equal(current, actualPeers) + + // Remove peers not in our desired peer set. + var remaining []string + for _, peer := range actualPeers { + if !slices.Contains(desiredPeers, peer) { + remaining = append(remaining, peer) + } + } + + // If the peer sets are an exact match, we can remove a peer. + if len(remaining) > 0 && peersMatch { + // Removing a peer is destructive, so re-validate our snapshot against the + // assignment before acting on it. While we hold the lock, look up the live + // assignment and also check on any consumers and make sure they have moved + // properly before scaling down ourselves. + js.mu.RLock() + sa = js.streamAssignment(accName, streamName) + if sa == nil || sa.Group == nil || sa.Group.Desired == nil || sa.Group.Desired.ID != desiredID { + // The desired state moved on under us, reassess on the next cycle. + js.mu.RUnlock() + return mstat(MigrationStatusMeta, "desired state changed, reassessing") + } + hostedPeers = sa.consumerHostedPeers() + var blockedBy string + for name, c := range sa.consumers { + if c.unsupported != nil { + continue + } + for _, peer := range c.Group.Peers { + // If we have peers still in the old set block. + if !slices.Contains(desiredPeers, peer) { + blockedBy = name + break + } + } + if blockedBy != _EMPTY_ { + break + } + } + js.mu.RUnlock() + if blockedBy != _EMPTY_ { + s.Debugf("Scale down of '%s > %s' blocked by consumer '%s'", accName, streamName, blockedBy) + // We need to wait but still nudge the meta leader since it might need to remap consumers. + sendMetaUpdate() + return mstat(MigrationStatusBlocked, "waiting for consumer '%s' to migrate", blockedBy) + } + + // Ask the peers about their stream state. Answers arrive asynchronously. + positions.request(mset, accName, streamName) + + // Remove old peers one at a time, the leader selected last. + remove := s.selectPeerToRemove(ourPeerId, actual, remaining, hostedPeers) + if remove == _EMPTY_ { + return mstat(MigrationStatusBlocked, "waiting to select a peer to remove") + } + + // Tally up what we can rely on before removing a peer. + // - currentDesired: desired peers that are current and caught up. We can only + // transfer leadership to a peer that's current, so it's what we can step down to. + // - caughtUpDesired: desired peers that are caught up, they might not all be + // current. We use this where we only need them to have the data. + // - copiesAfterRemoval: whether we have sufficient copies of the data after + // having removed the peer, so it's tallied without it. + var currentDesired []string + var caughtUpDesired, copiesAfterRemoval int + for _, p := range actual { + // A peer only counts once it told us its store holds the stream, being current + // on the log is not enough. + if p.ID != ourPeerId && (slices.Contains(catchups, p.ID) || !positions.isCaughtUp(p.ID)) { + continue + } + if slices.Contains(desiredPeers, p.ID) { + caughtUpDesired++ + if p.Current { + currentDesired = append(currentDesired, p.ID) + } + } + if p.ID != remove { + copiesAfterRemoval++ + } + } + quorum := len(desiredPeers)/2 + 1 + // We only need to weigh the peers we're keeping, we already have Raft quorum, or + // we couldn't have grown the group to get here. Peers outside the desired set are + // on their way out, and we don't want them to block us. + if caughtUpDesired < quorum { + return mstat(MigrationStatusCatchup, "waiting for desired peers to catch up") + } + // Backstop for peers that are neither catching up nor caught up, they're down or + // haven't started. + if copiesAfterRemoval < quorum { + return mstat(MigrationStatusCatchup, "waiting for more peers to catch up") + } + // The group left after the removal must still be able to commit. + if !s.canRemovePeer(ourPeerId, remove, actual) { + return mstat(MigrationStatusQuorum, "waiting for quorum to remove peer") + } + // If we have current desired peers, and we're not a desired peer ourselves, step down. + if len(currentDesired) > 0 && !slices.Contains(desiredPeers, ourPeerId) { + preferred := s.selectStepDownPreferred(ourPeerId, actual, currentDesired) + if preferred == _EMPTY_ { + return mstat(MigrationStatusMembership, "waiting for a desired peer to step down to") + } + err := n.StepDown(preferred) + name := s.serverNameForNode(preferred) + if name == _EMPTY_ { + name = fmt.Sprintf("peer with id %s", preferred) + } + return mstat(MigrationStatusMembership, "stepping down to %s", name).withErr(err) + } + + // Step down and perform a leader transfer if we'd remove ourselves. We are + // selected last, so leadership changes at most once, and every remaining + // member is already in the desired peer set so any successor works. + if remove != ourPeerId { + err := n.ProposeRemovePeer(remove) + name := s.serverNameForNode(remove) + if name == _EMPTY_ { + name = fmt.Sprintf("with id %s", remove) + } + return mstat(MigrationStatusMembership, "removing peer %s", name).withErr(err) + } + err := n.StepDown() + return mstat(MigrationStatusMembership, "stepping down before removing ourselves").withErr(err) + } + + // Before publishing a stable R1 assignment, make the store independently durable + // because applying the assignment will remove the Raft node and its WAL. + if peersMatch && len(current) == 1 { + if err := mset.flushForScaleDown(); err != nil { + if errors.Is(err, ErrStoreClosed) { + return mstat(MigrationStatusUnavailable, "shutting down") + } + return mstat(MigrationStatusSnapshot, "waiting to flush stream before finalizing R1 assignment").withErr(err) + } + } + + // We're a step closer to being done. + update.MetaPeers = actualPeers + update.PeersMatch = peersMatch + sendMetaUpdate() + return mstat(MigrationStatusMeta, "waiting for peer set to settle") +} + +// setMigrationStatus records what the group leader is currently doing to converge +// this group toward its desired state, so it can be reported through clusterInfo. +func (js *jetStream) setMigrationStatus(rg *raftGroup, status *DesiredClusterInfoStatus) { + if js == nil || rg == nil { + return + } + js.mu.Lock() + defer js.mu.Unlock() + rg.migration = status +} + +// Determine if we are migrating +func (mset *stream) isMigrating() bool { + if mset == nil { + return false + } + + mset.mu.RLock() + js, sa := mset.js, mset.sa + mset.mu.RUnlock() + + js.mu.RLock() + defer js.mu.RUnlock() + + // During migration we will always be R>1, even when we start R1. + // So if we do not have a group or node, we know we are not migrating. + if sa == nil || sa.Group == nil || sa.Group.node == nil { + return false + } + if sa.Group.Desired != nil { + return true + } + // Without desired state, more peers than replicas is a legacy move left to finish. + // Fewer is under-replicated, healed by the meta leader; migrating would never converge. + if len(sa.Group.Peers) > sa.Config.Replicas { + return true + } + // Final sanity check is that the actual peer set equals the one in the assignment. + peers := sa.Group.node.PeerNames() + if len(peers) > sa.Config.Replicas { + return true + } + for _, peer := range sa.Group.Peers { + if !slices.Contains(peers, peer) { + return true + } + } + return false +} + +// resetClusteredState is called when a clustered stream had an error (e.g sequence mismatch, bad snapshot) and needs to be reset. +func (mset *stream) resetClusteredState(n RaftNode, err error) bool { + mset.mu.RLock() + s, js, jsa, sa, acc, node, name := mset.srv, mset.js, mset.jsa, mset.sa, mset.acc, mset.node, mset.nameLocked(false) + stype, tierName, replicas := mset.cfg.Storage, mset.tier, mset.cfg.Replicas + mset.mu.RUnlock() + + // The stream might already be deleted and not assigned to us anymore. + // In any case, don't revive the stream if it's already closed. + // A nil node means it was removed out from underneath us, which only happens + // when the meta layer is deliberately tearing this monitor down. + if mset.closed.Load() || node == nil || node.IsDeleted() { + s.Warnf("Will not reset stream '%s > %s', stream is closed", acc, mset.name()) + // Explicitly returning true here, we want the outside to break out of the monitoring loop as well. + return true + } + + // The node we were monitoring is no longer this stream's node. The meta layer has + // remapped us into a different group and is tearing this monitor down, so resetting + // here would resurrect a group that has already been replaced. + if n != nil && n != node { + s.Warnf("Will not reset stream '%s > %s', raft group %q was replaced by %q", + acc, mset.name(), n.Group(), node.Group()) + // Explicitly returning true here, we want the outside to break out of the monitoring loop as well. + return true + } + + assert.Unreachable("Reset clustered state", map[string]any{ + "stream": name, + "account": acc.Name, + "err": err, + }) + + // Stepdown regardless if we are the leader here. + node.StepDown() + + // If we detect we are shutting down just return. + if js != nil && js.isShuttingDown() { + s.Debugf("Will not reset stream '%s > %s', JetStream shutting down", acc, mset.name()) + return false + } + + // Server + if js.limitsExceeded(stype) { + s.Warnf("Will not reset stream '%s > %s', server resources exceeded", acc, mset.name()) + return false + } + + // Account + if exceeded, _ := jsa.limitsExceeded(stype, tierName, replicas); exceeded { + s.Warnf("Stream '%s > %s' errored, account resources exceeded", acc, mset.name()) + return false + } + + if errors.Is(err, errCatchupAbortedNoLeader) || err == errCatchupTooManyRetries { + // Don't delete all state, could've just been temporarily unable to reach the leader. + node.Stop() + } else { + // We delete our raft state. Will recreate. + node.Delete() + } + + // Preserve our current state and messages unless we have a first sequence mismatch. + shouldDelete := err == errFirstSequenceMismatch + + // Need to do the rest in a separate Go routine. + go func() { + mset.stopMonitoring() + mset.resetAndWaitOnConsumers() + // Stop our stream. + mset.stop(shouldDelete, false) + + if sa != nil { + js.mu.Lock() + if js.shuttingDown { + js.mu.Unlock() + return + } + + s.Warnf("Resetting stream cluster state for '%s > %s'", sa.Client.serviceAccount(), sa.Config.Name) + // Mark stream assignment as resetting, so we don't double-account reserved resources. + // But only if we're not also releasing the resources as part of the delete. + sa.resetting = !shouldDelete + // Now wipe groups from assignments. + sa.Group.node = nil + var consumers []*consumerAssignment + if cc := js.cluster; cc != nil && cc.meta != nil { + ourID := cc.meta.ID() + for _, ca := range sa.consumers { + if ca.unsupported != nil { + continue + } + if rg := ca.Group; rg != nil && rg.isMember(ourID) { + rg.node = nil // Erase group raft/node state. + consumers = append(consumers, ca) + } + } + } + js.mu.Unlock() + + // This will reset the stream and consumers. + // Reset stream. + js.processClusterCreateStream(acc, sa) + // Reset consumers. + for _, ca := range consumers { + js.processClusterCreateConsumer(nil, ca, nil, false) + } + } + }() + + return true +} + +func isControlHdr(hdr []byte) bool { + return bytes.HasPrefix(hdr, []byte("NATS/1.0 100 ")) +} + +// Apply our stream entries. +// Return maximum allowed applied value, if currently inside a batch, zero otherwise. +func (js *jetStream) applyStreamEntries(mset *stream, n RaftNode, ce *CommittedEntry, isRecovering bool, batch *batchApply) (uint64, error) { + for i, e := range ce.Entries { + // Ignore if lower-level catchup is started. + // We don't need to optimize during this, all entries are handled as normal. + if e.Type == EntryCatchup { + continue + } + + // Check if a batch is abandoned. + if e.Type != EntryNormal && batch != nil && batch.id != _EMPTY_ { + batch.rejectBatchState(mset) + } + + if e.Type == EntryNormal { + if len(e.Data) == 0 { + return 0, errBadEntryOp + } + buf, op := e.Data, entryOp(e.Data[0]) + if op == batchMsgOp { + batchId, batchSeq, _, _, err := decodeBatchMsg(buf[1:]) + if err != nil { + return 0, err + } + + // Need to grab the stream lock first. + if isRecovering { + mset.mu.Lock() + } + + // Previous batch (if any) was abandoned. + if batch.id != _EMPTY_ && batchId != batch.id { + batch.rejectBatchState(mset) + } + if batchSeq == 1 { + // If this is the first message in the batch, need to mark the start index. + // We'll continue to check batch-completeness and try to find the commit. + // At that point we'll commit the whole batch. + batch.rejectBatchState(mset) + batch.entryStart = i + batch.maxApplied = ce.Index - 1 + } + batch.id = batchId + + // While recovering, we could come up in the middle of a compacted batch that has already been applied. + // This is possible if two batches are part of the same append entry, and the first batch was fully + // applied but the second wasn't. + // If we still see the first message of the batch, we don't skip any messages of the batch here. + if isRecovering { + if batchSeq > 1 && batch.count == 0 { + if skip, err := mset.skipBatchIfRecovering(batch, buf); err != nil || skip { + mset.mu.Unlock() + if err != nil { + return 0, err + } + continue + } + } + mset.mu.Unlock() + } + + batch.count++ + // If the sequence is not monotonically increasing/we identify gaps, the batch can't be accepted. + if batchSeq != batch.count { + batch.rejectBatchState(mset) + continue + } + continue + } else if op == batchCommitMsgOp { + batchId, batchSeq, _, _, err := decodeBatchMsg(buf[1:]) + if err != nil { + return 0, err + } + mset.mu.Lock() + // Previous batch (if any) was abandoned. + if batch.id != _EMPTY_ && batchId != batch.id { + batch.rejectBatchState(mset) + } + if batchSeq == 1 { + // If this is the first message in the batch, need to mark the start index. + // This is a batch of size one that immediately commits. + batch.rejectBatchState(mset) + batch.entryStart = i + batch.maxApplied = ce.Index - 1 + } + batch.id = batchId + + // While recovering, we could come up in the middle of a compacted batch that has already been applied. + // This is possible if two batches are part of the same append entry, and the first batch was fully + // applied but the second wasn't. + // If we still see the first message of the batch, we don't skip any messages of the batch here. + if isRecovering && batchSeq > 1 && batch.count == 0 { + if skip, err := mset.skipBatchIfRecovering(batch, buf); err != nil || skip { + mset.mu.Unlock() + if err != nil { + return 0, err + } + continue + } + } + + batch.count++ + // Detected a gap, reject the batch. + if batchSeq != batch.count { + batch.rejectBatchState(mset) + mset.mu.Unlock() + continue + } + + // Ensure the whole batch is fully isolated, and reads + // can only happen after the full batch is committed. + mset.mu.Unlock() + mset.isolateMu.Lock() + + // Process any entries that are part of this batch but prior to the current one. + var entries []*Entry + for j, bce := range batch.entries { + if j == 0 { + // The first needs only the entries when the batch is started. + entries = bce.Entries[batch.entryStart:] + } else { + // Otherwise, all entries are used. + entries = bce.Entries + } + clearAndUnlock := func() { + // Make sure to return remaining entries to the pool on an error. + for _, nce := range batch.entries[j:] { + nce.ReturnToPool() + } + // Important to clear, otherwise we could return the entries to the pool multiple times. + batch.clearBatchState() + mset.isolateMu.Unlock() + } + for _, entry := range entries { + // Non-normal entries (e.g. EntryCatchup) can be buffered but must be ignored. + if entry.Type != EntryNormal { + continue + } + _, _, op, buf, err = decodeBatchMsg(entry.Data[1:]) + if err != nil { + clearAndUnlock() + return 0, err + } + if err = js.applyStreamMsgOp(mset, op, buf, isRecovering, false); err != nil { + clearAndUnlock() + return 0, err + } + } + // Return the entry to the pool now. + bce.ReturnToPool() + } + if len(batch.entries) == 0 { + // Get within the same entry, but within the range of this batch. + entries = ce.Entries[batch.entryStart : i+1] + } else { + // Get all entries up to and including the current one. + entries = ce.Entries[:i+1] + } + clearAndUnlock := func() { + // Important to clear, otherwise we could return the entries to the pool multiple times. + batch.clearBatchState() + mset.isolateMu.Unlock() + } + // Process remaining entries in the current entry. + for _, entry := range entries { + // Non-normal entries (e.g. EntryCatchup) can be buffered but must be ignored. + if entry.Type != EntryNormal { + continue + } + _, _, op, buf, err = decodeBatchMsg(entry.Data[1:]) + if err != nil { + clearAndUnlock() + return 0, err + } + if err = js.applyStreamMsgOp(mset, op, buf, isRecovering, false); err != nil { + clearAndUnlock() + return 0, err + } + } + // Clear state, batch was successful. + batch.clearBatchState() + mset.isolateMu.Unlock() + continue + } else if batch != nil && batch.id != _EMPTY_ { + // If a batch is abandoned without a commit, reject it. + batch.rejectBatchState(mset) + } + + switch op { + case streamMsgOp, compressedStreamMsgOp: + mbuf := buf[1:] + if err := js.applyStreamMsgOp(mset, op, mbuf, isRecovering, true); err != nil { + return 0, err + } + + case deleteRangeOp: + dr, err := decodeDeleteRange(buf[1:]) + if err != nil { + if node := mset.raftNode(); node != nil { + s := js.srv + s.Errorf("JetStream cluster could not decode delete range for '%s > %s' [%s]", + mset.account(), mset.name(), node.Group()) + } + return 0, err + } + if dr.Num == 0 { + continue + } + // Delete ranges mutate message state, so hold the isolation lock like any other write. + mset.isolateMu.Lock() + mset.mu.Lock() + first, num := dr.First, dr.Num + lseq := first + num - 1 + if mset.lseq >= lseq { + mset.mu.Unlock() + mset.isolateMu.Unlock() + continue + } + // Trim any prefix already applied so we only skip the uncovered tail. + if mset.lseq >= first { + first = mset.lseq + 1 + num = lseq - mset.lseq + } + if err = mset.store.SkipMsgs(first, num); err != nil { + mset.mu.Unlock() + mset.isolateMu.Unlock() + js.srv.RateLimitWarnf("JetStream cluster failed to apply delete range [%d..%d] for '%s > %s': %v", + first, lseq, mset.account().Name, mset.cfg.Name, err) + return 0, err + } + mset.clearAllPreAcksInRange(first, lseq) + mset.lseq = lseq + mset.mu.Unlock() + mset.isolateMu.Unlock() + + case deleteMsgOp: + md, err := decodeMsgDelete(buf[1:]) + if err != nil { + if node := mset.raftNode(); node != nil { + s := js.srv + s.Errorf("JetStream cluster could not decode delete msg for '%s > %s' [%s]", + mset.account(), mset.name(), node.Group()) + } + return 0, err + } + s := js.server() + + var removed bool + if md.NoErase { + removed, err = mset.removeMsg(md.Seq) + } else { + removed, err = mset.eraseMsg(md.Seq) + } + // Only respond for deletes that originated from a client request. + isClientReq := md.Client != nil + + var isLeader bool + if node := mset.raftNode(); node != nil && node.Leader() { + isLeader = true + } + + if err == ErrStoreEOF { + if isClientReq && isLeader && !isRecovering { + var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}} + resp.Error = NewJSStreamMsgDeleteFailedError(err, Unless(err)) + s.sendAPIErrResponse(md.Client, mset.account(), md.Subject, md.Reply, _EMPTY_, s.jsonResponse(resp)) + } + continue + } + + if err != nil && !isRecovering { + s.Debugf("JetStream cluster failed to delete stream msg %d from '%s > %s': %v", + md.Seq, md.Client.serviceAccount(), md.Stream, err) + } + + if isClientReq && isLeader && !isRecovering { + var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}} + if err != nil { + resp.Error = NewJSStreamMsgDeleteFailedError(err, Unless(err)) + s.sendAPIErrResponse(md.Client, mset.account(), md.Subject, md.Reply, _EMPTY_, s.jsonResponse(resp)) + } else if !removed { + resp.Error = NewJSSequenceNotFoundError(md.Seq) + s.sendAPIErrResponse(md.Client, mset.account(), md.Subject, md.Reply, _EMPTY_, s.jsonResponse(resp)) + } else { + resp.Success = true + s.sendAPIResponse(md.Client, mset.account(), md.Subject, md.Reply, _EMPTY_, s.jsonResponse(resp)) + } + } + case purgeStreamOp: + sp, err := decodeStreamPurge(buf[1:]) + if err != nil { + if node := mset.raftNode(); node != nil { + s := js.srv + s.Errorf("JetStream cluster could not decode purge msg for '%s > %s' [%s]", + mset.account(), mset.name(), node.Group()) + } + return 0, err + } + // If no explicit request, fill in with leader stamped last sequence to protect ourselves on replay during server start. + if sp.Request == nil || sp.Request.Sequence == 0 { + purgeSeq := sp.LastSeq + 1 + if sp.Request == nil { + sp.Request = &JSApiStreamPurgeRequest{Sequence: purgeSeq} + } else if sp.Request.Keep == 0 { + sp.Request.Sequence = purgeSeq + } else if isRecovering { + continue + } + } + + s := js.server() + purged, err := mset.purge(sp.Request) + if err != nil { + s.Warnf("JetStream cluster failed to purge stream %q for account %q: %v", sp.Stream, sp.Client.serviceAccount(), err) + } + + js.mu.RLock() + isLeader := js.cluster.isStreamLeader(sp.Client.serviceAccount(), sp.Stream) + js.mu.RUnlock() + + if isLeader && !isRecovering { + var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} + if err != nil { + resp.Error = NewJSStreamGeneralError(err, Unless(err)) + s.sendAPIErrResponse(sp.Client, mset.account(), sp.Subject, sp.Reply, _EMPTY_, s.jsonResponse(resp)) + } else { + resp.Purged = purged + resp.Success = true + s.sendAPIResponse(sp.Client, mset.account(), sp.Subject, sp.Reply, _EMPTY_, s.jsonResponse(resp)) + } + } + default: + return 0, fmt.Errorf("unknown stream entry op type: %v", op) + } + } else if e.Type == EntrySnapshot { + onBadState := func(err error) { + // If we are the leader or recovering, meaning we own the snapshot, + // we should stepdown and clear our raft state since our snapshot is bad. + if isRecovering || mset.IsLeader() { + mset.mu.RLock() + s, accName, streamName := mset.srv, mset.acc.GetName(), mset.cfg.Name + mset.mu.RUnlock() + s.Warnf("Detected bad stream state, resetting '%s > %s'", accName, streamName) + mset.resetClusteredState(n, err) + } + } + + ss, err := decodeStreamSnapshot(e.Data) + if err != nil { + onBadState(err) + return 0, err + } + + if err := mset.processSnapshot(ss, ce.Index); err != nil { + return 0, err + } + } else if e.Type == EntryRemovePeer { + js.mu.RLock() + var ourID string + if js.cluster != nil && js.cluster.meta != nil { + ourID = js.cluster.meta.ID() + } + js.mu.RUnlock() + // Cancel any catchup if one was still running for this peer. + if mset != nil { + mset.mu.Lock() + mset.clearCatchupPeerLocked(string(e.Data)) + mset.mu.Unlock() + } + // We only need to do processing if this is us. + if peer := string(e.Data); peer == ourID && mset != nil { + // Double check here with the registered stream assignment. + shouldRemove := true + if sa := mset.streamAssignment(); sa != nil && sa.Group != nil { + js.mu.RLock() + shouldRemove = !sa.Group.isMember(ourID) + js.mu.RUnlock() + } + if shouldRemove { + mset.stop(true, false) + } + } + } + } + + // If we're still actively processing a batch, must store the entry in-memory + // to come back to it later once we find the commit. + if batch != nil && batch.id != _EMPTY_ { + if batch.entries == nil { + batch.entries = []*CommittedEntry{ce} + } else { + batch.entries = append(batch.entries, ce) + } + maxApplied := batch.maxApplied + return maxApplied, nil + } + return 0, nil +} + +// skipBatchIfRecovering returns whether the batched message can be skipped because the batch was already fully applied. +// Stream lock should be held. +func (mset *stream) skipBatchIfRecovering(batch *batchApply, buf []byte) (bool, error) { + _, _, op, mbuf, err := decodeBatchMsg(buf[1:]) + if err != nil { + return false, err + } + + if op == compressedStreamMsgOp { + if mbuf, err = s2.Decode(nil, mbuf); err != nil { + return false, err + } + } + + _, _, _, _, lseq, _, _, err := decodeStreamMsg(mbuf) + if err != nil { + return false, err + } + + // Grab last sequence and CLFS. + last, clfs := mset.lastSeqAndCLFS() + + // We can skip if we know this is less than what we already have. + if lseq-clfs < last { + mset.srv.Debugf("Apply stream entries for '%s > %s' skipping message with sequence %d with last of %d", + mset.accountLocked(false), mset.nameLocked(false), lseq+1-clfs, last) + // Check for any preAcks in case we are interest based. + mset.clearAllPreAcks(lseq + 1 - clfs) + batch.clearBatchState() + return true, nil + } + return false, nil +} + +// needIsolation should be false only if the caller already holds isolateMu +// across a whole atomic batch; mset.mu must NOT be held by the caller. +func (js *jetStream) applyStreamMsgOp(mset *stream, op entryOp, mbuf []byte, isRecovering bool, needIsolation bool) error { + s := js.srv + + if op == compressedStreamMsgOp { + var err error + mbuf, err = s2.Decode(nil, mbuf) + if err != nil { + return err + } + } + + subject, reply, hdr, msg, lseq, ts, sourced, err := decodeStreamMsg(mbuf) + if err != nil { + mset.mu.RLock() + acc, name, node := mset.accountLocked(false), mset.nameLocked(false), mset.node + mset.mu.RUnlock() + if node != nil { + s.Errorf("JetStream cluster could not decode stream msg for '%s > %s' [%s]", + acc, name, node.Group()) + } + return err + } + + // Check for flowcontrol here. + if len(msg) == 0 && len(hdr) > 0 && reply != _EMPTY_ && isControlHdr(hdr) { + if !isRecovering { + mset.mu.RLock() + mset.sendFlowControlReply(reply, hdr) + mset.mu.RUnlock() + } + return nil + } + + mset.mu.RLock() + // Grab last sequence and CLFS. + last, clfs := mset.lastSeqAndCLFS() + mset.mu.RUnlock() + + // We can skip if we know this is less than what we already have. + if lseq-clfs < last { + mset.mu.Lock() + s.Debugf("Apply stream entries for '%s > %s' skipping message with sequence %d with last of %d", + mset.accountLocked(false), mset.nameLocked(false), lseq+1-clfs, last) + // Check for any preAcks in case we are interest based. + mset.clearAllPreAcks(lseq + 1 - clfs) + mset.mu.Unlock() + return nil + } + + // Skip by hand here since first msg special case. + // Reason is sequence is unsigned and for lseq being 0 + // the lseq under stream would have to be -1. + if lseq == 0 && last != 0 { + return nil + } + + // Messages to be skipped have no subject or timestamp or msg or hdr. + if subject == _EMPTY_ && ts == 0 && len(msg) == 0 && len(hdr) == 0 { + // Skip and update our lseq. + last, err := mset.store.SkipMsg(0) + if err != nil { + return err + } + mset.mu.Lock() + mset.lseq = last + mset.clearAllPreAcks(last) + mset.mu.Unlock() + return nil + } + + var mt *msgTrace + // If not recovering, see if we find a message trace object for this + // sequence. Only the leader that has proposed this entry will have + // stored the trace info. + if !isRecovering { + mt = mset.getAndDeleteMsgTrace(lseq) + } + // Process the actual message here. + err = mset.processJetStreamMsg(subject, reply, hdr, msg, lseq, ts, mt, sourced, needIsolation) + + // Take into account subject transforms, if any. + // The untransformed subject is replicated, but the transformed subject is used for consistency checks below. + csubject := subject + if mset.inflightTransform != nil { + mset.clMu.Lock() + if subj, found := mset.inflightTransform[lseq]; found { + csubject = subj + delete(mset.inflightTransform, lseq) + } + mset.clMu.Unlock() + } + + // If we have inflight make sure to clear after processing. + // TODO(dlc) - technically check on inflight != nil could cause datarace. + // But do not want to acquire lock since tracking this will be rare. + if mset.inflight != nil { + mset.clMu.Lock() + if i, found := mset.inflight[csubject]; found { + // Decrement from pending operations. Once it reaches zero, it can be deleted. + if i.ops > 0 { + var sz uint64 + if mset.store.Type() == FileStorage { + sz = fileStoreMsgSizeRaw(len(csubject), len(hdr), len(msg)) + } else { + sz = memStoreMsgSizeRaw(len(csubject), len(hdr), len(msg)) + } + if i.bytes >= sz { + i.bytes -= sz + } else { + i.bytes = 0 + } + i.ops-- + } + if i.ops == 0 { + delete(mset.inflight, csubject) + } + } + mset.clMu.Unlock() + } + + // Update running total for counter. + if mset.clusteredCounterTotal != nil { + mset.clMu.Lock() + if counter, found := mset.clusteredCounterTotal[csubject]; found { + // Decrement from pending operations. Once it reaches zero, it can be deleted. + if counter.ops > 0 { + counter.ops-- + } + if counter.ops == 0 { + delete(mset.clusteredCounterTotal, csubject) + } + } + mset.clMu.Unlock() + } + + // Clear expected per subject state after processing. + if mset.expectedPerSubjectSequence != nil { + mset.clMu.Lock() + if subj, found := mset.expectedPerSubjectSequence[lseq]; found { + delete(mset.expectedPerSubjectSequence, lseq) + delete(mset.expectedPerSubjectInProcess, subj) + } + mset.clMu.Unlock() + } + + if err != nil { + if err == errLastSeqMismatch { + + var state StreamState + mset.store.FastState(&state) + + // If we have no msgs and the other side is delivering us a sequence past where we + // should be reset. This is possible if the other side has a stale snapshot and no longer + // has those messages. So compact and retry to reset. + if state.Msgs == 0 { + _, err = mset.store.Compact(lseq + 1) + if err == nil { + // Retry + err = mset.processJetStreamMsg(subject, reply, hdr, msg, lseq, ts, mt, sourced, needIsolation) + } + } + // FIXME(dlc) - We could just run a catchup with a request defining the span between what we expected + // and what we got. + } + + // Only return in place if we are going to reset our stream or we are out of space, or we are closed. + if isClusterResetErr(err) || isOutOfSpaceErr(err) || err == errStreamClosed { + return err + } + s.Debugf("Apply stream entries for '%s > %s' got error processing message: %v", + mset.accountLocked(true), mset.nameLocked(true), err) + + // There are some errors that we can't recover from. + if err != ErrMaxMsgs && err != ErrMaxBytes && err != ErrMaxMsgsPerSubject && err != ErrMsgTooLarge && err != ErrStoreClosed { + return err + } + } + return nil +} + +// Returns the PeerInfo for all replicas of a raft node. This is different than node.Peers() +// and is used for external facing advisories. +func (s *Server) replicas(node RaftNode) []*PeerInfo { + var replicas []*PeerInfo + for _, rp := range node.Peers() { + if sir, ok := s.nodeToInfo.Load(rp.ID); ok && sir != nil { + si := sir.(nodeInfo) + pi := &PeerInfo{Peer: rp.ID, Name: si.name, Current: rp.Current, Offline: si.offline, Lag: rp.Lag} + if !rp.Last.IsZero() { + pi.Active = time.Since(rp.Last) + } + replicas = append(replicas, pi) + } + } + return replicas +} + +// Process a leader change for the clustered stream. +func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool, term uint64) { + if mset == nil { + return + } + sa := mset.streamAssignment() + if sa == nil { + return + } + + // Acquire clMu before ddMu so any inflight proposals finish first, and we can + // clean up if they added new dedupe IDs. + mset.clMu.Lock() + + // Clear inflight dedupe IDs, where seq=0. + mset.ddMu.Lock() + var removed int + for i := len(mset.ddarr) - 1; i >= mset.ddindex; i-- { + dde := mset.ddarr[i] + if dde.seq != 0 { + break + } + removed++ + delete(mset.ddmap, dde.id) + } + if removed > 0 { + if len(mset.ddmap) > 0 { + mset.ddarr = mset.ddarr[:len(mset.ddarr)-removed] + } else { + mset.ddmap = nil + mset.ddarr = nil + mset.ddindex = 0 + } + } + mset.ddMu.Unlock() + + // Clear inflight if we have it. + mset.inflight = nil + mset.inflightTransform = nil + // Clear running counter totals. + mset.clusteredCounterTotal = nil + // Clear expected per subject state. + mset.expectedPerSubjectSequence = nil + mset.expectedPerSubjectInProcess = nil + + // Clear clseq on every leader transition. recalculateClusteredSeq + // repopulates it on the next proposal. + if mset.clseq > 0 { + mset.clseq = 0 + } + mset.clMu.Unlock() + + js.mu.RLock() + s, account, err := js.srv, sa.Client.serviceAccount(), sa.err + client, subject, reply := sa.Client, sa.Subject, sa.Reply + hasResponded := sa.markResponded() + // A migration status is only valid for the leader and term that wrote it. + clearMigration := sa.Group.migration != nil + js.mu.RUnlock() + + if clearMigration { + js.mu.Lock() + sa.Group.migration = nil + js.mu.Unlock() + } + + streamName := mset.name() + + if isLeader { + s.Noticef("JetStream cluster new stream leader for '%s > %s'", account, streamName) + s.sendStreamLeaderElectAdvisory(mset) + } else { + // We are stepping down. + // Make sure if we are doing so because we have lost quorum that we send the appropriate advisories. + if node := mset.raftNode(); node != nil && !node.Quorum() && time.Since(node.Created()) > 5*time.Second { + s.sendStreamLostQuorumAdvisory(mset) + } + } + + // Tell stream to switch leader status. + mset.setLeader(isLeader, term) + + if !isLeader || hasResponded { + return + } + + acc, _ := s.LookupAccount(account) + if acc == nil { + return + } + + // Send our response. + var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} + if err != nil { + resp.Error = NewJSStreamCreateError(err, Unless(err)) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } else { + // Report the config as requested, the stream can still be running at its origin. + msetCfg := js.targetStreamConfig(mset, mset.config()) + resp.StreamInfo = &StreamInfo{ + Created: mset.createdTime(), + State: mset.state(), + Config: *setDynamicStreamMetadata(&msetCfg), + Cluster: js.clusterInfo(mset.raftGroup()), + Sources: mset.sourcesInfo(), + Mirror: mset.mirrorInfo(), + TimeStamp: time.Now().UTC(), + } + resp.DidCreate = true + s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + if node := mset.raftNode(); node != nil { + mset.sendCreateAdvisory() + } + } +} + +// Fixed value ok for now. +const lostQuorumAdvInterval = 10 * time.Second + +// Migration monitoring intervals for streams and consumers. We check quickly +// after observing a relevant change (an assignment update or a peer +// add/remove), and otherwise poll on a slower fallback interval so blocked +// migrations don't spam the meta leader with reconcile requests. +const ( + migrateFastCheckInterval = 50 * time.Millisecond + migrateFallbackCheckInterval = 500 * time.Millisecond + migrateMetaRetryInterval = 5 * time.Second + + // migratePosAskInterval is how often a migrating leader re-asks peers for their stream state. + migratePosAskInterval = 2 * time.Second +) + +// Determines if we should send lost quorum advisory. We throttle these after first one. +func (mset *stream) shouldSendLostQuorum() bool { + mset.mu.Lock() + defer mset.mu.Unlock() + if time.Since(mset.lqsent) >= lostQuorumAdvInterval { + mset.lqsent = time.Now() + return true + } + return false +} + +func (s *Server) sendStreamLostQuorumAdvisory(mset *stream) { + if mset == nil { + return + } + node, stream, acc := mset.raftNode(), mset.name(), mset.account() + if node == nil { + return + } + if !mset.shouldSendLostQuorum() { + return + } + + s.Warnf("JetStream cluster stream '%s > %s' has NO quorum, stalled", acc.GetName(), stream) + + subj := JSAdvisoryStreamQuorumLostPre + "." + stream + adv := &JSStreamQuorumLostAdvisory{ + TypedEvent: TypedEvent{ + Type: JSStreamQuorumLostAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: stream, + Replicas: s.replicas(node), + Domain: s.getOpts().JetStreamDomain, + } + + // 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) +} + +func (s *Server) sendStreamLeaderElectAdvisory(mset *stream) { + if mset == nil { + return + } + node, stream, acc := mset.raftNode(), mset.name(), mset.account() + if node == nil { + return + } + subj := JSAdvisoryStreamLeaderElectedPre + "." + stream + adv := &JSStreamLeaderElectedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSStreamLeaderElectedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: stream, + Leader: s.serverNameForNode(node.GroupLeader()), + Replicas: s.replicas(node), + Domain: s.getOpts().JetStreamDomain, + } + + // 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) +} + +// Will look up a stream assignment. +// Lock should be held. +func (js *jetStream) streamAssignment(account, stream string) (sa *streamAssignment) { + cc := js.cluster + if cc == nil { + return nil + } + + if as := cc.streams[account]; as != nil { + sa = as[stream] + } + return sa +} + +// Will look up a stream assignment, either an applied or inflight assignment. +// Lock should be held. +func (js *jetStream) streamAssignmentOrInflight(account, stream string) *streamAssignment { + cc := js.cluster + if cc == nil { + return nil + } + if streams, ok := cc.inflightStreams[account]; ok { + if inflight, ok := streams[stream]; ok { + if !inflight.deleted { + return inflight.streamAssignment + } else { + return nil + } + } + } + + if as := cc.streams[account]; as != nil { + return as[stream] + } + return nil +} + +// Will gather all stream assignments for a specific account, both applied and inflight assignments. +// Lock should be held. +func (js *jetStream) streamAssignmentsOrInflightSeq(account string) iter.Seq[*streamAssignment] { + return func(yield func(*streamAssignment) bool) { + cc := js.cluster + if cc == nil { + return + } + inflight := cc.inflightStreams[account] + for _, i := range inflight { + if !i.deleted && !yield(i.streamAssignment) { + return + } + } + for _, sa := range cc.streams[account] { + // Skip if we already iterated over it as inflight. + if _, ok := inflight[sa.Config.Name]; ok { + continue + } + if !yield(sa) { + return + } + } + } +} + +// Will gather all stream assignments for all accounts, both applied and inflight assignments. +// Lock should be held. +func (js *jetStream) streamAssignmentsOrInflightSeqAllAccounts() iter.Seq2[string, *streamAssignment] { + return func(yield func(string, *streamAssignment) bool) { + cc := js.cluster + if cc == nil { + return + } + for accName, inflight := range cc.inflightStreams { + for _, i := range inflight { + if !i.deleted && !yield(accName, i.streamAssignment) { + return + } + } + } + for accName, asa := range cc.streams { + for _, sa := range asa { + // Skip if we already iterated over it as inflight. + if inflight, ok := cc.inflightStreams[accName]; ok { + if _, ok := inflight[sa.Config.Name]; ok { + continue + } + } + if !yield(accName, sa) { + return + } + } + } + } +} + +// processStreamAssignment is called when followers have replicated an assignment. +func (js *jetStream) processStreamAssignment(sa *streamAssignment) { + js.mu.Lock() + s, cc := js.srv, js.cluster + accName, stream := sa.Client.serviceAccount(), sa.Config.Name + noMeta := cc == nil || cc.meta == nil + var ourID string + if !noMeta { + ourID = cc.meta.ID() + } + var isMember bool + if sa.Group != nil && ourID != _EMPTY_ { + isMember = sa.Group.isMember(ourID) + } + + if s == nil || noMeta { + js.mu.Unlock() + return + } + + // Remove this stream from the inflight proposals + cc.removeInflightStreamProposal(accName, sa.Config.Name) + + accStreams := cc.streams[accName] + if accStreams == nil { + accStreams = make(map[string]*streamAssignment) + } else if osa := accStreams[stream]; osa != nil { + if osa != sa { + // Copy over private existing state from former SA. + if sa.Group != nil { + sa.Group.node = osa.Group.node + sa.Group.migration = osa.Group.migration + } + sa.consumers = osa.consumers + if osa.hasResponded() { + sa.markResponded() + } + sa.err = osa.err + } + // Unsubscribe if it was previously unsupported. + if osa.unsupported != nil { + osa.unsupported.closeInfoSub(js.srv) + // If we've seen unsupported once, it remains for the lifetime of this server process. + if sa.unsupported == nil { + sa.unsupported = osa.unsupported + } + } + } + + // Update our state. + accStreams[stream] = sa + cc.streams[accName] = accStreams + hasResponded := sa.hasResponded() + + // If unsupported, we can't register any further. + if sa.unsupported != nil { + sa.unsupported.setupInfoSub(s, sa) + s.Warnf("Detected unsupported stream '%s > %s': %s", accName, stream, sa.unsupported.reason) + js.mu.Unlock() + + // Need to stop the stream, we can't keep running with an old config. + acc, err := s.lookupOrFetchAccount(accName, isMember) + if err != nil { + return + } + mset, err := acc.lookupStream(stream) + if err != nil || mset.closed.Load() { + return + } + s.Warnf("Stopping unsupported stream '%s > %s'", accName, stream) + mset.stop(false, false) + return + } + js.mu.Unlock() + + acc, err := s.lookupOrFetchAccount(accName, isMember) + if err != nil { + ll := fmt.Sprintf("Account [%s] lookup for stream create failed: %v", accName, err) + if isMember { + if !hasResponded { + // If we can not lookup the account and we are a member, send this result back to the metacontroller leader. + result := &streamAssignmentResult{ + Account: accName, + Stream: stream, + Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}, + } + result.Response.Error = NewJSNoAccountError() + s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result) + } + s.Warnf(ll) + } else { + s.Debugf(ll) + } + return + } + + // Check if this is for us.. + if isMember { + js.processClusterCreateStream(acc, sa) + } else if mset, _ := acc.lookupStream(sa.Config.Name); mset != nil { + // We have one here even though we are not a member. This can happen on re-assignment. + s.removeStream(mset, sa) + } + + // If this stream assignment does not have a sync subject (bug) set that the meta-leader should check when elected. + if sa.Sync == _EMPTY_ { + js.mu.Lock() + cc.streamsCheck = true + js.mu.Unlock() + } +} + +// processUpdateStreamAssignment is called when followers have replicated an updated assignment. +func (js *jetStream) processUpdateStreamAssignment(sa *streamAssignment) { + js.mu.RLock() + s, cc := js.srv, js.cluster + js.mu.RUnlock() + if s == nil || cc == nil { + // TODO(dlc) - debug at least + return + } + + accName := sa.Client.serviceAccount() + stream := sa.Config.Name + + js.mu.Lock() + if cc.meta == nil { + js.mu.Unlock() + return + } + + // Remove this stream from the inflight proposals + cc.removeInflightStreamProposal(accName, sa.Config.Name) + + ourID := cc.meta.ID() + + var isMember bool + if sa.Group != nil { + isMember = sa.Group.isMember(ourID) + } + + accStreams := cc.streams[accName] + if accStreams == nil { + js.mu.Unlock() + return + } + osa := accStreams[stream] + if osa == nil { + js.mu.Unlock() + return + } + + // Copy over private existing state from former SA. + if sa.Group != nil { + sa.Group.node = osa.Group.node + sa.Group.migration = osa.Group.migration + } + sa.consumers = osa.consumers + sa.err = osa.err + + // If we detect we are scaling down to 1, non-clustered, and we had a previous node, clear it here. + // Note the group can converge onto one peer without the config scaling down based on peer-removes. + if len(sa.Group.Peers) == 1 && sa.Group.node != nil && sa.Group.Desired == nil { + sa.Group.node = nil + } + + // Update our state. + accStreams[stream] = sa + cc.streams[accName] = accStreams + + // Make sure we respond if we are a member. + if isMember { + sa.clearResponded() + } else { + // Make sure to clean up any old node in case this stream moves back here. + if sa.Group != nil { + sa.Group.node = nil + } + } + + // Unsubscribe if it was previously unsupported. + if osa.unsupported != nil { + osa.unsupported.closeInfoSub(js.srv) + // If we've seen unsupported once, it remains for the lifetime of this server process. + if sa.unsupported == nil { + sa.unsupported = osa.unsupported + } + } + + // If unsupported, we can't register any further. + if sa.unsupported != nil { + sa.unsupported.setupInfoSub(s, sa) + s.Warnf("Detected unsupported stream '%s > %s': %s", accName, stream, sa.unsupported.reason) + js.mu.Unlock() + + // Need to stop the stream, we can't keep running with an old config. + acc, err := s.lookupOrFetchAccount(accName, isMember) + if err != nil { + return + } + mset, err := acc.lookupStream(stream) + if err != nil || mset.closed.Load() { + return + } + s.Warnf("Stopping unsupported stream '%s > %s'", accName, stream) + mset.stop(false, false) + return + } + js.mu.Unlock() + + acc, err := s.lookupOrFetchAccount(accName, isMember) + if err != nil { + ll := fmt.Sprintf("Update Stream Account %s, error on lookup: %v", accName, err) + if isMember { + s.Warnf(ll) + } else { + s.Debugf(ll) + } + return + } + + // Check if this is for us.. + if isMember { + js.processClusterUpdateStream(acc, osa, sa) + } else if mset, _ := acc.lookupStream(sa.Config.Name); mset != nil { + // We have one here even though we are not a member. This can happen on re-assignment. + s.removeStream(mset, sa) + } +} + +// Common function to remove ourselves from this server. +// This can happen on re-assignment, move, etc +func (s *Server) removeStream(mset *stream, nsa *streamAssignment) { + if mset == nil { + return + } + // Make sure to use the new stream assignment, not our own. + s.Debugf("JetStream removing stream '%s > %s' from this server", nsa.Client.serviceAccount(), nsa.Config.Name) + if node := mset.raftNode(); node != nil { + node.StepDown(nsa.Group.Preferred) + // shutdown monitor by shutting down raft. + node.Delete() + } + + var isShuttingDown bool + // Make sure this node is no longer attached to our stream assignment. + if js, _ := s.getJetStreamCluster(); js != nil { + js.mu.Lock() + nsa.Group.node = nil + nsa.err = nil + isShuttingDown = js.shuttingDown + js.mu.Unlock() + } + + if !isShuttingDown { + // wait for monitor to be shutdown. + mset.stopMonitoring() + } + mset.stop(true, false) +} + +// processClusterUpdateStream is called when we have a stream assignment that +// has been updated for an existing assignment and we are a member. +func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAssignment) { + if sa == nil { + return + } + + js.mu.RLock() + s, rg, desired := js.srv, sa.Group, sa.Group.Desired + client, subject, reply := sa.Client, sa.Subject, sa.Reply + alreadyRunning, numReplicas := osa.Group.node != nil, len(rg.Peers) + // A remap below clears alreadyRunning, but we must still know whether we were + // running clustered before, or we'd skip the cleanup we owe on a downgrade. + wasRunning := alreadyRunning + wasClustered := len(osa.Group.Peers) > 1 || osa.Group.Desired != nil + needsNode := rg.node == nil + storage, cfg := sa.Config.Storage, sa.Config + newCfg := cfg.atDesiredOrigin(rg) + recovering := sa.recovering + hasResponded := sa.markResponded() + hadErr := sa.err != nil + js.mu.RUnlock() + + mset, err := acc.lookupStream(cfg.Name) + if err == nil && mset != nil { + // Make sure we have not had a new group assigned to us. + if osa.Group.Name != sa.Group.Name { + s.Warnf("JetStream cluster detected stream remapping for '%s > %s' from %q to %q", + acc, cfg.Name, osa.Group.Name, sa.Group.Name) + mset.removeNode() + mset.stopMonitoring() + alreadyRunning, needsNode = false, true + // Make sure to clear from original. + js.mu.Lock() + osa.Group.node = nil + js.mu.Unlock() + } + + if !alreadyRunning && (numReplicas > 1 || desired != nil) { + if needsNode { + // Must run before startClusterSubs reads mset.sa.Sync. + mset.setStreamAssignment(sa) + + // Since we are scaling up we want to make sure our sync subject + // is registered before we start our raft node. + mset.mu.Lock() + mset.startClusterSubs() + mset.mu.Unlock() + + js.createRaftGroup(acc.GetName(), rg, recovering, storage, pprofLabels{ + "type": "stream", + "account": mset.accName(), + "stream": mset.name(), + }) + + // Re-link the assignment so mset.node picks up the newly created + // node before the monitor starts. + mset.setStreamAssignment(sa) + } + mset.startMonitorWg() + // Only snapshot for a legacy scale up. + sendSnapshot := needsNode && desired == nil + // Start monitoring.. + started := s.startGoRoutine( + func() { js.monitorStream(mset, sa, sendSnapshot) }, + pprofLabels{ + "type": "stream", + "account": mset.accName(), + "stream": mset.name(), + }, + ) + if !started { + mset.monitorWg.Done() + } + } else if numReplicas == 1 && desired == nil && wasRunning { + // We downgraded to R1. Make sure we cleanup the raft node and the stream monitor. + mset.removeNode() + mset.stopMonitoring() + // In case we need to shutdown the cluster specific subs, etc. + mset.mu.Lock() + // Stop responding to sync requests. + mset.stopClusterSubs() + // Clear catchup state + mset.clearAllCatchupPeers() + mset.mu.Unlock() + // Remove from meta layer. + js.mu.Lock() + rg.node = nil + js.mu.Unlock() + } + // Set the new stream assignment. + mset.setStreamAssignment(sa) + + // Call update. + err = mset.updateWithAdvisory(newCfg, !recovering, false) + } + + // If not found we must be expanding into this node since if we are here we know we are a member. + if err == ErrJetStreamStreamNotFound { + js.processStreamAssignment(sa) + return + } + + if err != nil { + js.mu.Lock() + s.Warnf("Stream update failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err) + sa.err = err + result := &streamAssignmentResult{ + Account: sa.Client.serviceAccount(), + Stream: sa.Config.Name, + Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}, + Update: true, + } + result.Response.Error = NewJSStreamGeneralError(err, Unless(err)) + js.mu.Unlock() + + // Send response to the metadata leader. They will forward to the user as needed. + s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result) + return + } else if hadErr { + js.mu.Lock() + sa.err = nil + js.mu.Unlock() + } + + isLeader := mset.IsLeader() + + // If the stream is scaled down, there is a chance we weren't already the leader. + if isLeader && numReplicas == 1 && desired == nil && wasClustered { + js.processStreamLeaderChange(mset, true, 0) + } + + // Check for missing syncSubject bug. + if isLeader && osa != nil && osa.Sync == _EMPTY_ { + if node := mset.raftNode(); node != nil { + node.StepDown() + } + return + } + + // If we were a single node being promoted assume leadership role for purpose of responding. + if !hasResponded && !isLeader && !alreadyRunning { + isLeader = true + } + + // Check if we should bail. + if !isLeader || hasResponded || recovering { + return + } + + // Send our response. + var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + // Report the config as requested, the stream can still be running at its origin. + // Reading cfg without js.mu is safe, an assignment's config is never changed in place. + msetCfg := mset.config().atDesiredTarget(cfg) + resp.StreamInfo = &StreamInfo{ + Created: mset.createdTime(), + State: mset.state(), + Config: *setDynamicStreamMetadata(&msetCfg), + Cluster: js.clusterInfo(mset.raftGroup()), + Mirror: mset.mirrorInfo(), + Sources: mset.sourcesInfo(), + TimeStamp: time.Now().UTC(), + } + + s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) +} + +// processClusterCreateStream is called when we have a stream assignment that +// has been committed and this server is a member of the peer group. +func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignment) { + if sa == nil { + return + } + + js.mu.RLock() + s, rg, created := js.srv, sa.Group, sa.Created + alreadyRunning := rg.node != nil + newCfg := sa.Config.atDesiredOrigin(rg) + storage := sa.Config.Storage + restore := sa.Restore + recovering := sa.recovering + hadErr := sa.err != nil + js.mu.RUnlock() + + // Process the raft group and make sure it's running if needed. + _, err := js.createRaftGroup(acc.GetName(), rg, recovering, storage, pprofLabels{ + "type": "stream", + "account": acc.Name, + "stream": sa.Config.Name, + }) + + // If we are restoring, create the stream if we are R>1 and not the preferred who handles the + // receipt of the snapshot itself. + shouldCreate := true + if restore != nil { + if len(rg.Peers) == 1 || rg.node != nil && rg.node.ID() == rg.Preferred { + shouldCreate = false + } else { + js.mu.Lock() + sa.Restore = nil + js.mu.Unlock() + } + } + + // Our stream. + var mset *stream + + // Process here if not restoring or not the leader. + if shouldCreate && err == nil { + // Go ahead and create or update the stream. + mset, err = acc.lookupStream(sa.Config.Name) + if err == nil && mset != nil { + osa := mset.streamAssignment() + // If we already have a stream assignment and they are the same exact config, short circuit here. + if osa != nil { + if reflect.DeepEqual(osa.Config, sa.Config) { + if sa.Group.Name == osa.Group.Name && reflect.DeepEqual(sa.Group.Peers, osa.Group.Peers) && + reflect.DeepEqual(sa.Group.Desired, osa.Group.Desired) { + // Since this already exists we know it succeeded, just respond to this caller. + js.mu.RLock() + client, subject, reply, recovering := sa.Client, sa.Subject, sa.Reply, sa.recovering + js.mu.RUnlock() + + if !recovering { + var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} + // Report the config as requested, the stream can still be running at its origin. + msetCfg := js.targetStreamConfig(mset, mset.config()) + resp.StreamInfo = &StreamInfo{ + Created: mset.createdTime(), + State: mset.state(), + Config: *setDynamicStreamMetadata(&msetCfg), + Cluster: js.clusterInfo(mset.raftGroup()), + Sources: mset.sourcesInfo(), + Mirror: mset.mirrorInfo(), + TimeStamp: time.Now().UTC(), + } + s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } + return + } else if sa.Group.Name != osa.Group.Name { + // We had a bug where we could have multiple assignments for the same + // stream but with different group assignments, including multiple raft + // groups. So check for that here. We can only bet on the last one being + // consistent in the long run, so let it continue if we see this condition. + // Only a differing group name means a duplicate group; a differing peer + // set or desired state is a normal peer change and must fall through. + s.Warnf("JetStream cluster detected duplicate assignment for stream %q for account %q", sa.Config.Name, acc.Name) + if osa.Group.node != nil && osa.Group.node != sa.Group.node { + osa.Group.node.Delete() + osa.Group.node = nil + } + } + } + } + mset.setStreamAssignment(sa) + // Check if our config has really been updated. + cfg := mset.config() + if !reflect.DeepEqual(&cfg, newCfg) { + if err = mset.updateWithAdvisory(newCfg, false, false); err != nil { + if js.isShuttingDown() { + s.Debugf("Could not update stream, JetStream shutting down") + return + } + s.Warnf("JetStream cluster error updating stream %q for account %q: %v", sa.Config.Name, acc.Name, err) + js.mu.Lock() + sa.err = err + // Detach the node from the assignment; we stop (not delete) it below, + // and it unregisters itself once its goroutines exit. + if sa.Group != nil { + sa.Group.node = nil + } + js.mu.Unlock() + // Stop the monitor and raft node without deleting any state. + mset.stopMonitoring() + mset.stop(false, false) + return + } + } + } else if err == NewJSStreamNotFoundError() { + // Add in the stream here. + mset, err = acc.addStreamWithAssignment(newCfg, nil, sa, false, true) + } + if mset != nil { + mset.setCreatedTime(created) + } + } + + // This is an error condition. + if err != nil { + // If we're shutting down we could get a variety of errors, for example: + // 'JetStream not enabled for account' when looking up the stream. + // Normally we can continue and delete state, but need to be careful when shutting down. + if js.isShuttingDown() { + s.Debugf("Could not create stream, JetStream shutting down") + return + } + + s.Warnf("Stream create failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err) + if IsNatsErr(err, JSStreamStoreFailedF) { + err = errStreamStoreFailed + } + js.mu.Lock() + + sa.err = err + hasResponded := sa.hasResponded() + + // If out of space do nothing for now. + if isOutOfSpaceErr(err) { + hasResponded = true + } + + if rg.node != nil { + rg.node.Delete() + } + + var result *streamAssignmentResult + if !hasResponded { + result = &streamAssignmentResult{ + Account: sa.Client.serviceAccount(), + Stream: sa.Config.Name, + Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}, + } + result.Response.Error = NewJSStreamCreateError(err, Unless(err)) + } + js.mu.Unlock() + + // Send response to the metadata leader. They will forward to the user as needed. + if result != nil { + s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result) + } + return + } else if hadErr { + js.mu.Lock() + sa.err = nil + js.mu.Unlock() + } + + // Re-capture node. + js.mu.RLock() + node := rg.node + js.mu.RUnlock() + + // Start our monitoring routine. + if node != nil { + if !alreadyRunning { + if mset != nil { + mset.startMonitorWg() + } + started := s.startGoRoutine( + func() { js.monitorStream(mset, sa, false) }, + pprofLabels{ + "type": "stream", + "account": mset.accName(), + "stream": mset.name(), + }, + ) + if !started && mset != nil { + mset.monitorWg.Done() + } + } + } else { + // Single replica stream, process manually here. + // If we are restoring, process that first. + if sa.Restore != nil { + // We are restoring a stream here. + restoreDoneCh := s.processStreamRestore(sa.Client, acc, sa.Config, _EMPTY_, sa.Reply, _EMPTY_) + s.startGoRoutine(func() { + defer s.grWG.Done() + select { + case err := <-restoreDoneCh: + if err == nil { + mset, err = acc.lookupStream(sa.Config.Name) + if mset != nil { + mset.setStreamAssignment(sa) + mset.setCreatedTime(created) + } + } + if err != nil { + if mset != nil { + mset.delete() + } + js.mu.Lock() + s.Warnf("Stream restore failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err) + sa.err = err + result := &streamAssignmentResult{ + Account: sa.Client.serviceAccount(), + Stream: sa.Config.Name, + Restore: &JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}}, + } + result.Restore.Error = NewJSStreamRestoreError(err, Unless(err)) + js.mu.Unlock() + // Send response to the metadata leader. They will forward to the user as needed. + b, _ := json.Marshal(result) // Avoids auto-processing and doing fancy json with newlines. + s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, b) + return + } + js.processStreamLeaderChange(mset, true, 0) + + // Check to see if we have restored consumers here. + // These are not currently assigned so we will need to do so here. + // The stream is R1, so the consumers can only be assigned to us + // and don't need their state distributed with the assignment. + for _, o := range mset.getPublicConsumers() { + js.requestConsumerAssignment(sa, o, false) + } + case <-s.quitCh: + return + } + }) + } else { + js.processStreamLeaderChange(mset, true, 0) + } + } +} + +// processStreamRemoval is called when followers have replicated an assignment. +func (js *jetStream) processStreamRemoval(sa *streamAssignment) { + js.mu.Lock() + s, cc := js.srv, js.cluster + if s == nil || cc == nil || cc.meta == nil { + // TODO(dlc) - debug at least + js.mu.Unlock() + return + } + accName, stream, created := sa.Client.serviceAccount(), sa.Config.Name, sa.Created + var isMember bool + if sa.Group != nil { + isMember = sa.Group.isMember(cc.meta.ID()) + } + wasLeader := cc.isStreamLeader(accName, stream) + + // Check if we already have this assigned. + accStreams := cc.streams[accName] + needDelete := accStreams != nil && accStreams[stream] != nil + if needDelete { + if osa := accStreams[stream]; osa != nil && osa.unsupported != nil { + osa.unsupported.closeInfoSub(js.srv) + // Remember we used to be unsupported, just so we can send a successful delete response. + if sa.unsupported == nil { + sa.unsupported = osa.unsupported + } + } + delete(accStreams, stream) + if len(accStreams) == 0 { + delete(cc.streams, accName) + } + } + cc.removeInflightStreamProposal(accName, sa.Config.Name) + js.mu.Unlock() + + // During initial/startup recovery we'll not have registered the stream assignment, + // but might have recovered the stream from disk. We'll need to make sure that we only + // delete the stream if it wasn't created after this delete. + if !needDelete && !created.IsZero() { + if acc, err := s.lookupOrFetchAccount(accName, isMember); err == nil { + if mset, err := acc.lookupStream(stream); err == nil { + needDelete = !mset.createdTime().After(created) + } + } + } + + if needDelete { + js.processClusterDeleteStream(sa, isMember, wasLeader) + } +} + +func (js *jetStream) processClusterDeleteStream(sa *streamAssignment, isMember, wasLeader bool) { + if sa == nil { + return + } + js.mu.RLock() + s := js.srv + node := sa.Group.node + hadLeader := node == nil || !node.Leaderless() + offline := s.allPeersOffline(sa.Group) || sa.unsupported != nil + var isMetaLeader bool + if cc := js.cluster; cc != nil { + isMetaLeader = cc.isLeader() + } + recovering := sa.recovering + js.mu.RUnlock() + + stopped := false + var resp = JSApiStreamDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamDeleteResponseType}} + var err error + var acc *Account + + // Go ahead and delete the stream if we have it and the account here. + if acc, _ = s.LookupAccount(sa.Client.serviceAccount()); acc != nil { + if mset, _ := acc.lookupStream(sa.Config.Name); mset != nil { + // shut down monitor by shutting down raft + if n := mset.raftNode(); n != nil { + n.Delete() + } + // wait for monitor to be shut down + mset.stopMonitoring() + err = mset.stop(true, wasLeader) + stopped = true + } else if isMember { + s.Warnf("JetStream failed to lookup running stream while removing stream '%s > %s' from this server", + sa.Client.serviceAccount(), sa.Config.Name) + } + } else if isMember { + s.Warnf("JetStream failed to lookup account while removing stream '%s > %s' from this server", sa.Client.serviceAccount(), sa.Config.Name) + } + + // Always delete the node if present. + if node != nil { + node.Delete() + } + + // This is a stop gap cleanup in case + // 1) the account or mset does not exist and/or + // 2) node was nil (and couldn't be deleted) + if !stopped || node == nil { + if sacc := s.SystemAccount(); sacc != nil { + saccName := sacc.GetName() + os.RemoveAll(filepath.Join(js.config.StoreDir, saccName, defaultStoreDirName, sa.Group.Name)) + // cleanup dependent consumer groups + if !stopped { + for _, ca := range sa.consumers { + // Make sure we cleanup any possible running nodes for the consumers. + if isMember && ca.Group != nil && ca.Group.node != nil { + ca.Group.node.Delete() + } + os.RemoveAll(filepath.Join(js.config.StoreDir, saccName, defaultStoreDirName, ca.Group.Name)) + } + } + } + } + accDir := filepath.Join(js.config.StoreDir, sa.Client.serviceAccount()) + streamDir := filepath.Join(accDir, streamsDir) + os.RemoveAll(filepath.Join(streamDir, sa.Config.Name)) + + // no op if not empty + os.Remove(streamDir) + os.Remove(accDir) + + // Normally we want only the leader to respond here, but if we had no leader then all members will respond to make + // sure we get feedback to the user. + if !isMember || (hadLeader && !wasLeader) { + // If all the peers are offline and we are the meta leader we will also respond, so suppress returning here. + if !(offline && isMetaLeader) { + return + } + } + + // Do not respond if the account does not exist any longer + if acc == nil || recovering { + return + } + + if err != nil { + resp.Error = NewJSStreamGeneralError(err, Unless(err)) + s.sendAPIErrResponse(sa.Client, acc, sa.Subject, sa.Reply, _EMPTY_, s.jsonResponse(resp)) + } else { + resp.Success = true + s.sendAPIResponse(sa.Client, acc, sa.Subject, sa.Reply, _EMPTY_, s.jsonResponse(resp)) + } +} + +// consumersConverged reports whether the stream is still migrating while none of its +// consumers has desired state anymore. The stream can't finalize until all of its +// consumers have moved, and the meta leader proposes nothing for the stream when only +// consumers change, so the last consumer to converge must wake the stream monitor. +// Lock should be held. +func (sa *streamAssignment) consumersConverged() bool { + if sa.Group == nil || sa.Group.Desired == nil { + return false + } + for _, ca := range sa.consumers { + if ca.Group != nil && ca.Group.Desired != nil { + return false + } + } + return true +} + +// processConsumerAssignment is called when followers have replicated an assignment for a consumer. +func (js *jetStream) processConsumerAssignment(ca *consumerAssignment) { + js.mu.RLock() + s, cc := js.srv, js.cluster + accName, stream, consumerName := ca.Client.serviceAccount(), ca.Stream, ca.Name + noMeta := cc == nil || cc.meta == nil + shuttingDown := js.shuttingDown + var ourID string + if !noMeta { + ourID = cc.meta.ID() + } + var isMember bool + if ca.Group != nil && ourID != _EMPTY_ { + isMember = ca.Group.isMember(ourID) + } + js.mu.RUnlock() + + if s == nil || noMeta || shuttingDown { + return + } + + js.mu.Lock() + sa := js.streamAssignment(accName, stream) + if sa == nil { + js.mu.Unlock() + s.Debugf("Consumer create failed, could not locate stream '%s > %s'", accName, stream) + return + } + + // Track if this existed already. + var wasExisting bool + + // Check if we have an existing consumer assignment. + if sa.consumers == nil { + sa.consumers = make(map[string]*consumerAssignment) + } + oca := sa.consumers[ca.Name] + if oca != nil { + wasExisting = true + // Copy over private existing state from former CA. + if ca.Group != nil { + ca.Group.node = oca.Group.node + ca.Group.migration = oca.Group.migration + } + if oca.hasResponded() { + ca.markResponded() + } + ca.err = oca.err + + // Unsubscribe if it was previously unsupported. + if oca.unsupported != nil { + oca.unsupported.closeInfoSub(s) + // If we've seen unsupported once, it remains for the lifetime of this server process. + if ca.unsupported == nil { + ca.unsupported = oca.unsupported + } + } + } + + // Capture the optional state. We will pass it along if we are a member to apply. + // This is only applicable when restoring a stream with consumers. + state := ca.State + ca.State = nil + + // Place into our internal map under the stream assignment. + // Ok to replace an existing one, we check on process call below. + sa.consumers[ca.Name] = ca + cc.removeInflightConsumerProposal(accName, stream, consumerName) + + // If this consumer just finished converging and it was the last one the stream's + // migration was waiting on, the stream monitor needs to know right away. + nudgeStream := oca != nil && oca.Group != nil && oca.Group.Desired != nil && sa.consumersConverged() + + // If unsupported, we can't register any further. + if ca.unsupported != nil { + ca.unsupported.setupInfoSub(s, ca) + s.Warnf("Detected unsupported consumer '%s > %s > %s': %s", accName, stream, ca.Name, ca.unsupported.reason) + + // Mark stream as unsupported as well + if sa.unsupported == nil { + sa.unsupported = newUnsupportedStreamAssignment(s, sa, fmt.Errorf("unsupported consumer %q", ca.Name)) + } + sa.unsupported.setupInfoSub(s, sa) + js.mu.Unlock() + + // Be conservative by protecting the whole stream, even if just one consumer is unsupported. + // This ensures it's safe, even with Interest-based retention where it would otherwise + // continue accepting but dropping messages. + acc, err := s.lookupOrFetchAccount(accName, isMember) + if err != nil { + return + } + mset, err := acc.lookupStream(stream) + if err != nil || mset.closed.Load() { + return + } + s.Warnf("Stopping unsupported stream '%s > %s'", accName, stream) + mset.stop(false, false) + return + } + js.mu.Unlock() + + acc, err := s.lookupOrFetchAccount(accName, isMember) + if err != nil { + ll := fmt.Sprintf("Account [%s] lookup for consumer create failed: %v", accName, err) + if isMember { + if !js.isMetaRecovering() { + // If we can not lookup the account and we are a member, send this result back to the metacontroller leader. + result := &consumerAssignmentResult{ + Account: accName, + Stream: stream, + Consumer: consumerName, + Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}, + } + result.Response.Error = NewJSNoAccountError() + s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result) + } + s.Warnf(ll) + } else { + s.Debugf(ll) + } + return + } + + // Check if this is for us.. + if isMember { + js.processClusterCreateConsumer(oca, ca, state, wasExisting) + } else if mset, _ := acc.lookupStream(sa.Config.Name); mset != nil { + if o := mset.lookupConsumer(ca.Name); o != nil { + // We have one here even though we are not a member. This can happen on re-assignment. + s.removeConsumer(o, ca) + } + } + + if nudgeStream { + if mset, _ := acc.lookupStream(stream); mset != nil { + mset.signalUpdate() + } + } +} + +// Common function to remove ourselves from this server. +// This can happen on re-assignment, move, etc +func (s *Server) removeConsumer(o *consumer, nca *consumerAssignment) { + if o == nil { + return + } + // Make sure to use the new stream assignment, not our own. + s.Debugf("JetStream removing consumer '%s > %s > %s' from this server", nca.Client.serviceAccount(), nca.Stream, nca.Name) + if node := o.raftNode(); node != nil { + node.StepDown(nca.Group.Preferred) + // shutdown monitor by shutting down raft. + node.Delete() + } + + var isShuttingDown bool + // Make sure this node is no longer attached to our consumer assignment. + if js, _ := s.getJetStreamCluster(); js != nil { + js.mu.Lock() + nca.Group.node = nil + nca.err = nil + isShuttingDown = js.shuttingDown + js.mu.Unlock() + } + + if !isShuttingDown { + // wait for monitor to be shutdown. + o.stopMonitoring() + } + o.deleteWithoutAdvisory() +} + +func (js *jetStream) processConsumerRemoval(ca *consumerAssignment) { + js.mu.Lock() + s, cc := js.srv, js.cluster + if s == nil || cc == nil || cc.meta == nil { + // TODO(dlc) - debug at least + js.mu.Unlock() + return + } + + accName, stream, name, created := ca.Client.serviceAccount(), ca.Stream, ca.Name, ca.Created + wasLeader := cc.isConsumerLeader(accName, stream, name) + + // Delete from our state. + var needDelete, nudgeStream bool + if accStreams := cc.streams[accName]; accStreams != nil { + if sa := accStreams[ca.Stream]; sa != nil && sa.consumers != nil && sa.consumers[ca.Name] != nil { + oca := sa.consumers[ca.Name] + // Make sure this removal is for what we have, otherwise ignore. + if ca.Group != nil && oca.Group != nil && (ca.Group.Name == oca.Group.Name || (!ca.Created.IsZero() && ca.Created.Equal(oca.Created))) { + needDelete = true + delete(sa.consumers, ca.Name) + // A consumer dropped during a stream migration could be the last one + // the stream was waiting on, the stream monitor needs to know right away. + nudgeStream = sa.consumersConverged() + // Remember we used to be unsupported, just so we can send a successful delete response. + if ca.unsupported == nil { + ca.unsupported = oca.unsupported + } + } + } + } + cc.removeInflightConsumerProposal(accName, stream, name) + js.mu.Unlock() + + // During initial/startup recovery we'll not have registered the consumer assignment, + // but might have recovered the consumer from disk. We'll need to make sure that we only + // delete the consumer if it wasn't created after this delete. + if !needDelete && !created.IsZero() { + if acc, err := s.LookupAccount(accName); err == nil { + if mset, err := acc.lookupStream(stream); err == nil { + if o := mset.lookupConsumer(name); o != nil { + needDelete = !o.createdTime().After(created) + } + } + } + } + + if needDelete { + js.processClusterDeleteConsumer(ca, wasLeader) + } + + if nudgeStream { + if acc, err := s.LookupAccount(accName); err == nil { + if mset, _ := acc.lookupStream(stream); mset != nil { + mset.signalUpdate() + } + } + } +} + +type consumerAssignmentResult struct { + Account string `json:"account"` + Stream string `json:"stream"` + Consumer string `json:"consumer"` + Response *JSApiConsumerCreateResponse `json:"response,omitempty"` +} + +// processClusterCreateConsumer is when we are a member of the group and need to create the consumer. +func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, state *ConsumerState, wasExisting bool) { + if ca == nil { + return + } + js.mu.RLock() + s := js.srv + rg := ca.Group + alreadyRunning := rg != nil && rg.node != nil + accName, stream, consumer := ca.Client.serviceAccount(), ca.Stream, ca.Name + recovering := ca.recovering + js.mu.RUnlock() + + acc, err := s.LookupAccount(accName) + if err != nil { + s.Warnf("JetStream cluster failed to lookup account %q: %v", accName, err) + return + } + + // Go ahead and create or update the consumer. + mset, err := acc.lookupStream(stream) + if err != nil { + if !js.isMetaRecovering() { + js.mu.Lock() + s.Warnf("Consumer create failed, could not locate stream '%s > %s > %s'", ca.Client.serviceAccount(), ca.Stream, ca.Name) + ca.err = NewJSStreamNotFoundError() + result := &consumerAssignmentResult{ + Account: ca.Client.serviceAccount(), + Stream: ca.Stream, + Consumer: ca.Name, + Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}, + } + result.Response.Error = NewJSStreamNotFoundError() + s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result) + js.mu.Unlock() + } + return + } + + // Check if we already have this consumer running. + o := mset.lookupConsumer(consumer) + + if o != nil && oca != nil && oca.Group.Name != ca.Group.Name { + s.Warnf("JetStream cluster detected consumer remapping for '%s > %s' from %q to %q", + acc, ca.Name, oca.Group.Name, ca.Group.Name) + o.clearNode() + o.stopMonitoring() + alreadyRunning = false + // Make sure to clear from original. + js.mu.Lock() + oca.Group.node = nil + js.mu.Unlock() + } + + // Process the raft group and make sure it's running if needed. + storage := mset.config().Storage + if ca.Config.MemoryStorage { + storage = MemoryStorage + } + // No-op if R1. + js.createRaftGroup(accName, rg, recovering, storage, pprofLabels{ + "type": "consumer", + "account": mset.accName(), + "stream": ca.Stream, + "consumer": ca.Name, + }) + + // Check if we already have this consumer running. + var didCreate, isConfigUpdate, needsLocalResponse bool + var oreply string + if o == nil { + // Add in the consumer if needed. + if o, err = mset.addConsumerWithAssignment(ca.Config, ca.Name, ca, js.isMetaRecovering(), ActionCreateOrUpdate, false); err == nil { + didCreate = true + } + } else { + // This consumer exists. + // Only update if config is really different. + cfg := o.config() + if isConfigUpdate = !reflect.DeepEqual(&cfg, ca.Config); isConfigUpdate { + // Call into update, ignore consumer exists error here since this means an old deliver subject is bound + // which can happen on restart etc. + // JS lock needed as this can mutate the consumer assignments and race with updateInactivityThreshold. + js.mu.Lock() + err := o.updateConfig(ca.Config) + js.mu.Unlock() + if err != nil && err != NewJSConsumerNameExistError() { + // This is essentially an update that has failed. Respond back to metaleader if we are not recovering. + js.mu.RLock() + if !js.metaRecovering { + result := &consumerAssignmentResult{ + Account: accName, + Stream: stream, + Consumer: consumer, + Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}, + } + result.Response.Error = NewJSConsumerNameExistError() + s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result) + } + s.Warnf("Consumer create failed during update for '%s > %s > %s': %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, err) + js.mu.RUnlock() + return + } + } + + var sendState bool + js.mu.RLock() + n := rg.node + // Check if we already had a consumer assignment and its still pending. + cca, oca := ca, o.consumerAssignment() + if oca != nil { + // An internal re-proposal (scale, move, peer remove, reconcile) carries + // the reply of the request it copied, only a client update brings a new one. + oreply = oca.Reply + if !oca.hasResponded() { + // We can't override info for replying here otherwise leader once elected can not respond. + // So copy over original client and the reply from the old ca. + cac := ca.clone() + cac.Client = oca.Client + cac.Reply = oca.Reply + cca = cac + needsLocalResponse = true + } + // If we look like we are scaling up (legacy), let's send our current state to the group. + sendState = len(ca.Group.Peers) > len(oca.Group.Peers) && ca.Group.Desired == nil && o.IsLeader() && n != nil + // Signal that this is an update + if ca.Reply != _EMPTY_ && ca.Reply != oreply { + isConfigUpdate = true + } + } + js.mu.RUnlock() + + if sendState { + if snap, err := o.store.EncodedState(); err == nil { + n.SendSnapshot(snap) + } + } + + // Set CA for our consumer. + o.setConsumerAssignment(cca) + s.Debugf("JetStream cluster, consumer '%s > %s > %s' was already running", ca.Client.serviceAccount(), ca.Stream, ca.Name) + } + + // If we have an initial state set apply that now. + if state != nil && o != nil { + o.mu.Lock() + err = o.setStoreState(state) + o.mu.Unlock() + } + + if err != nil { + // If we're shutting down we could get a variety of errors. + // Normally we can continue and delete state, but need to be careful when shutting down. + if js.isShuttingDown() { + s.Debugf("Could not create consumer, JetStream shutting down") + return + } + + s.Warnf("Consumer create failed for '%s > %s > %s': %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, err) + if IsNatsErr(err, JSConsumerStoreFailedErrF) { + err = errConsumerStoreFailed + } + + js.mu.Lock() + ca.err = err + hasResponded := ca.hasResponded() + + // If out of space do nothing for now. + if isOutOfSpaceErr(err) { + hasResponded = true + } + + if rg.node != nil { + rg.node.Delete() + // Clear the node here. + rg.node = nil + } + + // If we did seem to create a consumer make sure to stop it. + if o != nil { + o.stop() + } + + var result *consumerAssignmentResult + if !hasResponded && !js.metaRecovering { + result = &consumerAssignmentResult{ + Account: ca.Client.serviceAccount(), + Stream: ca.Stream, + Consumer: ca.Name, + Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}, + } + result.Response.Error = NewJSConsumerCreateError(err, Unless(err)) + } else if err == errNoInterest { + // This is a stranded ephemeral, let's clean this one up. + subject := fmt.Sprintf(JSApiConsumerDeleteT, ca.Stream, ca.Name) + mset.outq.send(newJSPubMsg(subject, _EMPTY_, _EMPTY_, nil, nil, nil, 0)) + } + js.mu.Unlock() + + if result != nil { + // Send response to the metadata leader. They will forward to the user as needed. + b, _ := json.Marshal(result) // Avoids auto-processing and doing fancy json with newlines. + s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, b) + } + } else { + js.mu.RLock() + hadErr := ca.err != nil + node := rg.node + js.mu.RUnlock() + if hadErr { + js.mu.Lock() + ca.err = nil + js.mu.Unlock() + } + + if didCreate { + o.setCreatedTime(ca.Created) + } else { + // Check for scale down to 1.. + if node != nil && len(rg.Peers) == 1 && rg.Desired == nil { + o.clearNode() + o.stopMonitoring() + // Need to clear from rg too. + js.mu.Lock() + rg.node = nil + client, subject, reply := ca.Client, ca.Subject, ca.Reply + js.mu.Unlock() + // Perform the leader change in a goroutine, otherwise we could block meta operations. + if o.shouldStartMonitor() { + started := s.startGoRoutine( + func() { + defer s.grWG.Done() + defer o.clearMonitorRunning() + err = o.setLeader(true, 0) + var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} + if err != nil { + resp.Error = NewJSConsumerCreateError(err, Unless(err)) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } else if resp.ConsumerInfo = setDynamicConsumerInfoMetadata(o.info()); resp.ConsumerInfo == nil { + // The consumer was closed before we could respond. + resp.Error = NewJSConsumerCreateError(errConsumerClosed) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } else if resp.Config.Direct || resp.Config.Sourcing { + rhdr := genHeader(nil, JSStreamIdentity, mset.identity()) + s.sendAPIHdrResponse(client, acc, subject, reply, _EMPTY_, rhdr, s.jsonResponse(&resp)) + } else { + s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } + }, + pprofLabels{ + "type": "consumer", + "account": mset.accName(), + "stream": mset.name(), + "consumer": ca.Name, + }, + ) + if !started { + o.clearMonitorRunning() + } + } + return + } + } + + if node == nil { + // Wait for the previous routine to stop running. + o.stopMonitoring() + // Single replica consumer, process manually here. + // Force response in case we think this is an update. + if !js.isMetaRecovering() && isConfigUpdate { + ca.clearResponded() + } + cca := o.consumerAssignment() + // Perform the leader change in a goroutine, otherwise we could block meta operations. + if o.shouldStartMonitor() { + started := s.startGoRoutine( + func() { + defer s.grWG.Done() + defer o.clearMonitorRunning() + js.processConsumerLeaderChangeWithAssignment(o, cca, true, 0) + }, + pprofLabels{ + "type": "consumer", + "account": mset.accName(), + "stream": mset.name(), + "consumer": ca.Name, + }, + ) + if !started { + o.clearMonitorRunning() + } + } + } else { + // Clustered consumer. + // Start our monitoring routine if needed. + if !alreadyRunning { + // Wait for the previous routine to stop running. + o.stopMonitoring() + if o.shouldStartMonitor() { + started := s.startGoRoutine( + func() { js.monitorConsumer(o, ca) }, + pprofLabels{ + "type": "consumer", + "account": mset.accName(), + "stream": mset.name(), + "consumer": ca.Name, + }, + ) + if !started { + o.clearMonitorRunning() + } + } + } + // For existing consumer, only send response if not recovering. + if wasExisting && !js.isMetaRecovering() { + if o.IsLeader() || (!didCreate && needsLocalResponse) { + // Process if existing as an update. Double check that this is not recovered. + js.mu.RLock() + client, subject, reply, recovering, sourcing := ca.Client, ca.Subject, ca.Reply, ca.recovering, ca.Config.Sourcing + js.mu.RUnlock() + // Only a client update needs a response, an internal re-proposal has + // nothing to answer and must not reset a sourcing consumer. + if !recovering && reply != _EMPTY_ && reply != oreply { + // If it's a sourcing consumer, we need to respond after the consumer has been reset instead. + if sourcing { + var resp = JSApiConsumerResetResponse{ApiResponse: ApiResponse{Type: JSApiConsumerResetResponseType}} + resetSeq, canRespond, err := o.resetStartingSeq(0, reply, true, true) + if err != nil { + resp.Error = NewJSConsumerInvalidResetError(err) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } else if canRespond { + if resp.ConsumerInfo = setDynamicConsumerInfoMetadata(o.info()); resp.ConsumerInfo == nil { + // The consumer was closed before we could respond. + resp.Error = NewJSConsumerInvalidResetError(errConsumerClosed) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } else { + resp.ResetSeq = resetSeq + rhdr := genHeader(nil, JSStreamIdentity, mset.identity()) + s.sendAPIHdrResponse(client, acc, subject, reply, _EMPTY_, rhdr, s.jsonResponse(&resp)) + } + } + } else { + var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} + if resp.ConsumerInfo = setDynamicConsumerInfoMetadata(o.info()); resp.ConsumerInfo == nil { + // The consumer was closed before we could respond. + resp.Error = NewJSConsumerCreateError(errConsumerClosed) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } else if resp.Config.Direct || resp.Config.Sourcing { + rhdr := genHeader(nil, JSStreamIdentity, mset.identity()) + s.sendAPIHdrResponse(client, acc, subject, reply, _EMPTY_, rhdr, s.jsonResponse(&resp)) + } else { + s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } + } + } + } + } + } + } +} + +func (js *jetStream) processClusterDeleteConsumer(ca *consumerAssignment, wasLeader bool) { + if ca == nil { + return + } + js.mu.RLock() + s := js.srv + node := ca.Group.node + offline := s.allPeersOffline(ca.Group) || ca.unsupported != nil + var isMetaLeader bool + if cc := js.cluster; cc != nil { + isMetaLeader = cc.isLeader() + } + recovering := ca.recovering + js.mu.RUnlock() + + stopped := false + var resp = JSApiConsumerDeleteResponse{ApiResponse: ApiResponse{Type: JSApiConsumerDeleteResponseType}} + var err error + var acc *Account + + // Go ahead and delete the consumer if we have it and the account. + if acc, _ = s.LookupAccount(ca.Client.serviceAccount()); acc != nil { + if mset, _ := acc.lookupStream(ca.Stream); mset != nil { + if o := mset.lookupConsumer(ca.Name); o != nil { + err = o.stopWithFlags(true, false, true, wasLeader) + stopped = true + } + } + } + + // Always delete the node if present. + if node != nil { + node.Delete() + } + + // This is a stop gap cleanup in case + // 1) the account, mset, or consumer does not exist and/or + // 2) node was nil (and couldn't be deleted) + if !stopped || node == nil { + if sacc := s.SystemAccount(); sacc != nil { + os.RemoveAll(filepath.Join(js.config.StoreDir, sacc.GetName(), defaultStoreDirName, ca.Group.Name)) + } + } + + accDir := filepath.Join(js.config.StoreDir, ca.Client.serviceAccount()) + consumersDir := filepath.Join(accDir, streamsDir, ca.Stream, consumerDir) + os.RemoveAll(filepath.Join(consumersDir, ca.Name)) + + if !wasLeader || ca.Reply == _EMPTY_ { + if !(offline && isMetaLeader) { + return + } + } + + // Do not respond if the account does not exist any longer or this is during recovery. + if acc == nil || recovering { + return + } + + if err != nil { + resp.Error = NewJSConsumerNotFoundError(Unless(err)) + s.sendAPIErrResponse(ca.Client, acc, ca.Subject, ca.Reply, _EMPTY_, s.jsonResponse(resp)) + } else { + resp.Success = true + s.sendAPIResponse(ca.Client, acc, ca.Subject, ca.Reply, _EMPTY_, s.jsonResponse(resp)) + } +} + +// Returns the consumer assignment, or nil if not present. +// Lock should be held. +func (js *jetStream) consumerAssignment(account, stream, consumer string) *consumerAssignment { + if sa := js.streamAssignment(account, stream); sa != nil { + return sa.consumers[consumer] + } + return nil +} + +// Will look up a consumer assignment, either an applied or inflight assignment. +// Lock should be held. +func (js *jetStream) consumerAssignmentOrInflight(account, stream, consumer string) *consumerAssignment { + cc := js.cluster + if cc == nil { + return nil + } + if streams, ok := cc.inflightConsumers[account]; ok { + if consumers, ok := streams[stream]; ok { + if inflight, ok := consumers[consumer]; ok { + if !inflight.deleted { + return inflight.consumerAssignment + } else { + return nil + } + } + } + } + if sa := js.streamAssignment(account, stream); sa != nil { + return sa.consumers[consumer] + } + return nil +} + +// Will gather all consumer assignments for the specified account and stream, both applied and inflight assignments. +// Lock should be held. +func (js *jetStream) consumerAssignmentsOrInflightSeq(account, stream string) iter.Seq[*consumerAssignment] { + return func(yield func(*consumerAssignment) bool) { + cc := js.cluster + if cc == nil { + return + } + + var inflight map[string]*inflightConsumerInfo + if streams, ok := cc.inflightConsumers[account]; ok { + inflight = streams[stream] + } + for _, i := range inflight { + if !i.deleted && !yield(i.consumerAssignment) { + return + } + } + sa := js.streamAssignment(account, stream) + if sa == nil { + return + } + for _, ca := range sa.consumers { + // Skip if we already iterated over it as inflight. + if _, ok := inflight[ca.Name]; ok { + continue + } + if !yield(ca) { + return + } + } + } +} + +// consumerAssigned informs us if this server has this consumer assigned. +func (jsa *jsAccount) consumerAssigned(stream, consumer string) bool { + jsa.mu.RLock() + js, acc := jsa.js, jsa.account + jsa.mu.RUnlock() + + if js == nil { + return false + } + js.mu.RLock() + defer js.mu.RUnlock() + return js.cluster.isConsumerAssigned(acc, stream, consumer) +} + +// Read lock should be held. +func (cc *jetStreamCluster) isConsumerAssigned(a *Account, stream, consumer string) bool { + // Non-clustered mode always return true. + if cc == nil { + return true + } + if cc.meta == nil { + return false + } + var sa *streamAssignment + accStreams := cc.streams[a.Name] + if accStreams != nil { + sa = accStreams[stream] + } + if sa == nil { + // TODO(dlc) - This should not happen. + return false + } + ca := sa.consumers[consumer] + if ca == nil { + return false + } + return ca.Group.isMember(cc.meta.ID()) +} + +// Returns our stream and underlying raft node. +func (o *consumer) streamAndNode() (*stream, RaftNode) { + if o == nil { + return nil, nil + } + o.mu.RLock() + defer o.mu.RUnlock() + return o.mset, o.node +} + +// setWriteErr stores the write error in the consumer. +func (o *consumer) setWriteErr(err error) { + o.mu.Lock() + defer o.mu.Unlock() + o.setWriteErrLocked(err) +} + +func (o *consumer) setWriteErrLocked(err error) { + if o.werr != nil { + return + } + // Ignore non-write errors. + if err == ErrStoreClosed { + return + } + o.srv.Errorf("JetStream consumer '%s > %s > %s' critical write error: %v", o.acc.Name, o.stream, o.name, err) + o.werr = err + assert.Unreachable("Consumer encountered write error", map[string]any{ + "account": o.acc.Name, + "stream": o.stream, + "consumer": o.name, + "err": err, + }) + + // If consumer is replicated, put it in observer mode to make sure another server can pick it up. + if node := o.node; node != nil { + node.StepDown() + node.SetObserver(true) + } +} + +// getWriteErr returns the write error stored in the consumer (if any). +func (o *consumer) getWriteErr() error { + o.mu.RLock() + defer o.mu.RUnlock() + return o.werr +} + +// Return the replica count for this consumer. If the consumer has been +// stopped, this will return an error. +func (o *consumer) replica() (int, error) { + o.mu.RLock() + oCfg := o.cfg + mset := o.mset + o.mu.RUnlock() + if mset == nil { + return 0, errBadConsumer + } + sCfg := mset.config() + return oCfg.replicas(&sCfg), nil +} + +func (o *consumer) raftGroup() *raftGroup { + if o == nil { + return nil + } + o.mu.RLock() + defer o.mu.RUnlock() + if o.ca == nil { + return nil + } + return o.ca.Group +} + +func (o *consumer) raftNode() RaftNode { + if o == nil { + return nil + } + o.mu.RLock() + defer o.mu.RUnlock() + return o.node +} + +func (js *jetStream) monitorConsumer(o *consumer, ca *consumerAssignment) { + s, n, meta := js.server(), o.raftNode(), js.getMetaGroup() + defer s.grWG.Done() + + defer o.clearMonitorRunning() + + if n == nil || meta == nil { + s.Warnf("No RAFT group for '%s > %s > %s'", o.acc.Name, ca.Stream, ca.Name) + return + } + + // Make sure to stop the raft group on exit to prevent accidental memory bloat. + // This should be below the checkInMonitor call though to avoid stopping it out + // from underneath the one that is running since it will be the same raft node. + defer n.Stop() + + qch, mqch, lch, aq, uch := n.QuitC(), o.monitorQuitC(), n.LeadChangeC(), n.ApplyQ(), o.updateC() + + s.Debugf("Starting consumer monitor for '%s > %s > %s' [%s]", o.acc.Name, ca.Stream, ca.Name, n.Group()) + defer s.Debugf("Exiting consumer monitor for '%s > %s > %s' [%s]", o.acc.Name, ca.Stream, ca.Name, n.Group()) + + const ( + compactInterval = 2 * time.Minute + compactMinInterval = 15 * time.Second + compactSizeMin = 64 * 1024 // What is stored here is always small for consumers. + compactNumMin = 1024 + minSnapDelta = 10 * time.Second + ) + + // Spread these out for large numbers on server restart. + rci := time.Duration(rand.Int64N(int64(time.Minute))) + t := time.NewTicker(compactInterval + rci) + defer t.Stop() + + // Highwayhash key for generating hashes. + key := make([]byte, 32) + crand.Read(key) + + var lastSnapTime time.Time + + // Don't allow the upper layer to install snapshots until we have + // fully recovered from disk. + recovering := true + + var failedSnapshots int + doSnapshot := func(force bool) { + // Bail if trying too fast and not in a forced situation. + // If snapshots have failed, and we're not forced to, we'll wait for the timer since it'll now be forced. + if recovering || (!force && (time.Since(lastSnapTime) < minSnapDelta || failedSnapshots > 0)) { + return + } + + if snap, err := o.store.EncodedState(); err == nil { + // If we had a significant number of failed snapshots, start relaxing Raft-layer checks + // to force it through. We might have been catching up a peer for a long period, and this + // protects our log size from growing indefinitely. + forceSnapshot := failedSnapshots > 4 + if err := n.InstallSnapshot(snap, forceSnapshot); err == nil { + lastSnapTime = time.Now() + // If there was a failed snapshot before, we reduced the timer's interval. + // Reset it back to the original interval now. + if failedSnapshots > 0 { + t.Reset(compactInterval + rci) + } + failedSnapshots = 0 + } else if err != errNoSnapAvailable && err != errNodeClosed && err != errCatchupsRunning { + s.RateLimitWarnf("Failed to install snapshot for '%s > %s > %s' [%s]: %v", o.acc.Name, ca.Stream, ca.Name, n.Group(), err) + // If this is the first failure, reduce the interval of the snapshot timer. + // This ensures we're not waiting too long for snapshotting to eventually become forced. + if failedSnapshots == 0 { + t.Reset(compactMinInterval) + } + failedSnapshots++ + } + } else { + s.RateLimitWarnf("Failed to install snapshot for '%s > %s > %s' [%s]: %v", o.acc.Name, ca.Stream, ca.Name, n.Group(), err) + } + } + + // For migration tracking. + var mmt *time.Timer + var mmtc <-chan time.Time + + resetMigrationMonitoring := func(delay time.Duration) { + if mmt != nil { + if !mmt.Stop() { + // Drain if the timer wasn't stopped. + select { + case <-mmt.C: + default: + } + } + mmt.Reset(delay) + } + } + startMigrationMonitoring := func() { + if mmt == nil { + mmt = time.NewTimer(migrateFastCheckInterval) + mmtc = mmt.C + } else { + resetMigrationMonitoring(migrateFastCheckInterval) + } + } + stopMigrationMonitoring := func() { + if mmt != nil { + mmt.Stop() + mmt, mmtc = nil, nil + } + // Don't leave a stale status behind for a migration that's done or abandoned. + js.setMigrationStatus(o.raftGroup(), nil) + } + defer stopMigrationMonitoring() + + // Start monitoring if we're already meant to be migrating. If we're out of quorum + // with peer-removed peers, it will trigger a shrink of the group. + if o.isMigrating() { + startMigrationMonitoring() + } + + // Track if we are leader. + var isLeader bool + var leaderTerm uint64 + + for { + select { + case <-s.quitCh: + // Server shutting down, but we might receive this before qch, so try to snapshot. + doSnapshot(false) + return + case <-mqch: + // Clean signal from shutdown routine so do best effort attempt to snapshot. + // Don't snapshot if not shutting down, monitor goroutine could be going away + // on a scale down or a remove for example. + if s.isShuttingDown() { + doSnapshot(false) + } + return + case <-qch: + // Raft node is closed, no use in trying to snapshot. + return + case <-aq.ch: + ces := aq.pop() + for _, ce := range ces { + // No special processing needed for when we are caught up on restart. + if ce == nil { + if !recovering { + continue + } + recovering = false + if n.NeedSnapshot() { + doSnapshot(true) + } + continue + } + // While migrating, react quickly to peer add/remove entries. + if mmt != nil { + for _, e := range ce.Entries { + if e.Type == EntryAddPeer || e.Type == EntryRemovePeer { + startMigrationMonitoring() + break + } + } + } + if err := js.applyConsumerEntries(o, ce, isLeader); err == nil { + var ne, nb uint64 + // We can't guarantee writes are flushed while we're shutting down. Just rely on replay during recovery. + if !js.isShuttingDown() { + ne, nb = n.Applied(ce.Index) + } + // If we have at least min entries to compact, go ahead and snapshot/compact. + if nb > 0 && ne >= compactNumMin || nb > compactSizeMin { + doSnapshot(false) + } + } else if err != errConsumerClosed { + s.Errorf("Error applying consumer entries to '%s > %s': %v", ca.Client.serviceAccount(), ca.Name, err) + // Encountered an unexpected error, can't continue. + o.setWriteErr(err) + ce.ReturnToPool() + aq.recycle(&ces) + return + } + ce.ReturnToPool() + } + aq.recycle(&ces) + + case lc := <-lch: + // Not a change in leadership, just a nudge about a newly observed peer. + if lc.nudge { + if o.isMigrating() { + startMigrationMonitoring() + } + continue + } + isLeader, leaderTerm = lc.isLeader, lc.term + if recovering && !isLeader { + js.setConsumerAssignmentRecovering(ca) + } + + // Process the change. + if err := js.processConsumerLeaderChange(o, isLeader, lc.term); err == nil { + // Check our state if we are under an interest based stream. + if mset := o.getStream(); mset != nil { + var ss StreamState + mset.store.FastState(&ss) + o.checkStateForInterestStream(&ss) + } + } + + // We may receive a leader change after the consumer assignment which would cancel us + // monitoring for this closely. So re-assess our state here as well. + // Or the old leader is no longer part of the set and transferred leadership + // for this leader to resume with removal + migrating := o.isMigrating() + if migrating { + startMigrationMonitoring() + } else { + stopMigrationMonitoring() + } + case <-uch: + // keep consumer assignment current + ca = o.consumerAssignment() + + // We get this when we have a new consumer assignment caused by an update. + // We want to know if we are migrating. + migrating := o.isMigrating() + if migrating { + startMigrationMonitoring() + } else { + stopMigrationMonitoring() + } + case <-mmtc: + if !isLeader { + // We're not the leader, but check if we're out of quorum and + // need to shrink based on peer-removes. + if !js.checkEvictedPeers(n, ca.Group, fmt.Sprintf("consumer '%s > %s > %s'", o.acc.Name, ca.Stream, ca.Name)) { + stopMigrationMonitoring() + continue + } + resetMigrationMonitoring(migrateFallbackCheckInterval) + continue + } + // Reset to the slower fallback speed. + resetMigrationMonitoring(migrateFallbackCheckInterval) + status := js.runConsumerMigration(o, ca, n, leaderTerm) + // Only retry meta leader requests slowly, their reply wakes us on the fast path. + if status != nil && status.Type == MigrationStatusMeta { + resetMigrationMonitoring(migrateMetaRetryInterval) + } + // Resolve after determining the status, so that we don't set it on a stale group. + js.setMigrationStatus(o.raftGroup(), status) + + case <-t.C: + // Start forcing snapshots if they failed previously. + forceIfFailed := failedSnapshots > 0 + doSnapshot(forceIfFailed) + } + } +} + +// Migrate a consumer from peer set A to peer set B. +func (js *jetStream) runConsumerMigration(o *consumer, ca *consumerAssignment, n RaftNode, leaderTerm uint64) *DesiredClusterInfoStatus { + // Sanity-check: we're still the leader. + if leaderTerm == 0 || !n.Leader() { + return nil + } + ourPeerId, s := n.ID(), js.srv + + // Snapshot the assignment state we need up front, so the Raft reads below don't + // contend for Raft locks while holding the JetStream lock. + js.mu.RLock() + cc := js.cluster + // We are shutting down. + if cc == nil || cc.meta == nil { + js.mu.RUnlock() + return mstat(MigrationStatusUnavailable, "shutting down") + } + if ca == nil || ca.Group == nil { + js.mu.RUnlock() + return mstat(MigrationStatusUnavailable, "no consumer assignment") + } + meta := cc.meta + accName, streamName, consumerName := ca.Client.serviceAccount(), ca.Stream, ca.Name + + osa := js.streamAssignment(accName, streamName) + if osa == nil { + js.mu.RUnlock() + return mstat(MigrationStatusUnavailable, "no stream assignment") + } + + replicas := ca.Config.replicas(osa.Config) + // MUST copy, the stream assignment can be updated once we release below. + streamPeers := copyStrings(osa.Group.Peers) + current := copyStrings(ca.Group.Peers) + desiredID, desiredScaleDown, desiredPeers, needDesired := ca.Group.desiredSnapshot(leaderTerm) + js.mu.RUnlock() + + update := desiredAssignmentUpdate{Term: leaderTerm, ID: desiredID} + sendMetaUpdate := func() { + reconcile := &consumerAssignmentReconcile{Account: accName, Stream: streamName, Consumer: consumerName, desiredAssignmentUpdate: update} + s.sendInternalMsgLocked(consumerAssignmentReconcileSubj, _EMPTY_, nil, reconcile) + } + if needDesired != desiredReady { + sendMetaUpdate() + // Will only happen on legacy moves or peer sets having drifted outside desired state. + if needDesired == desiredMissing { + return mstat(MigrationStatusMeta, "requesting desired state from meta leader") + } + return mstat(MigrationStatusMeta, "recording leadership term with meta leader") + } + // A snapshot is required. Automatically installs a snapshot for a R1 scaleup. + if n.NeedSnapshot() { + snap, err := o.store.EncodedState() + if err != nil { + if errors.Is(err, ErrStoreClosed) { + return mstat(MigrationStatusUnavailable, "shutting down") + } + return mstat(MigrationStatusSnapshot, "waiting to encode state for snapshot").withErr(err) + } + if err := n.InstallSnapshot(snap, true); err != nil { + return mstat(MigrationStatusSnapshot, "waiting to install snapshot").withErr(err) + } + // The snapshot is installed, continue right away so new peers can be added + // and catch up from it without waiting for another check. + } + // If a membership change is in progress, we just wait for it to clear. + if n.MembershipChangeInProgress() { + return mstat(MigrationStatusMembership, "waiting for membership change to commit") + } + + actual := n.Peers() + actualPeers := peerIDs(actual) + + // Remove any peers that have been evicted from the cluster. + if _, status := s.removeEvictedPeers(n, meta, actual, actualPeers, current, desiredPeers, nil); status != nil { + return status + } + // Extend the actual peer set through the log. + if status := s.extendPeerSet(n, actual, actualPeers, current, desiredPeers, nil); status != nil { + return status + } + // If scaling down, we need to select where to. + if desiredScaleDown { + update.ScaleDownPeers = s.selectScaleDownPeers(ourPeerId, actual, desiredPeers, replicas) + sendMetaUpdate() + return mstat(MigrationStatusMeta, "selecting peers to scale down to") + } + + // Add peers in our desired peer set. + foundAll := true + for _, peer := range desiredPeers { + if !slices.Contains(current, peer) { + foundAll = false + break + } + } + if !foundAll { + // Only add a peer once the stream is actually hosted on it. A consumer can't + // move to a peer earlier until the stream is hosted there. + combined := current + for _, peer := range desiredPeers { + if slices.Contains(current, peer) { + continue + } + if !slices.Contains(streamPeers, peer) { + continue + } + combined = append(combined, peer) + } + if len(combined) == len(current) { + return mstat(MigrationStatusBlocked, "waiting for stream to migrate first") + } + update.MetaPeers = combined + sendMetaUpdate() + return mstat(MigrationStatusMeta, "expanding assignment with desired peers") + } + + slices.Sort(current) + slices.Sort(actualPeers) + peersMatch := slices.Equal(current, actualPeers) + + // Remove peers not in our desired peer set. + var remaining []string + for _, peer := range actualPeers { + if !slices.Contains(desiredPeers, peer) { + remaining = append(remaining, peer) + } + } + + // If the peer sets are an exact match, we can remove a peer. + if len(remaining) > 0 && peersMatch { + // Step down and perform a leader transfer if we'd remove ourselves. We are + // selected last, so leadership changes at most once, and every remaining + // member is already in the desired peer set so any successor works. + remove := s.selectPeerToRemove(ourPeerId, actual, remaining, nil) + if remove == _EMPTY_ { + return mstat(MigrationStatusBlocked, "waiting to select a peer to remove") + } + // The group left after the removal must still be able to commit. + if !s.canRemovePeer(ourPeerId, remove, actual) { + return mstat(MigrationStatusQuorum, "waiting for quorum to remove peer") + } + if remove == ourPeerId { + err := n.StepDown() + return mstat(MigrationStatusMembership, "stepping down before removing ourselves").withErr(err) + } + err := n.ProposeRemovePeer(remove) + name := s.serverNameForNode(remove) + if name == _EMPTY_ { + name = fmt.Sprintf("with id %s", remove) + } + return mstat(MigrationStatusMembership, "removing peer %s", name).withErr(err) + } + + // We're a step closer to being done. + update.MetaPeers = actualPeers + update.PeersMatch = peersMatch + sendMetaUpdate() + return mstat(MigrationStatusMeta, "waiting for peer set to settle") +} + +// Determine if we are migrating +func (o *consumer) isMigrating() bool { + if o == nil { + return false + } + + replicas, err := o.replica() + if err != nil { + return false + } + o.mu.RLock() + js, ca := o.js, o.ca + o.mu.RUnlock() + + js.mu.RLock() + defer js.mu.RUnlock() + + // During migration we will always be R>1, even when we start R1. + // So if we do not have a group or node, we know we are not migrating. + if ca == nil || ca.Group == nil || ca.Group.node == nil { + return false + } + if ca.Group.Desired != nil { + return true + } + // Without desired state, more peers than replicas is a legacy move left to finish. + // Fewer is under-replicated, healed by the meta leader; migrating would never converge. + if len(ca.Group.Peers) > replicas { + return true + } + // Final sanity check is that the actual peer set equals the one in the assignment. + peers := ca.Group.node.PeerNames() + if len(peers) > replicas { + return true + } + for _, peer := range ca.Group.Peers { + if !slices.Contains(peers, peer) { + return true + } + } + return false +} + +func (js *jetStream) applyConsumerEntries(o *consumer, ce *CommittedEntry, isLeader bool) error { + for _, e := range ce.Entries { + // Ignore if lower-level catchup is started. + // We don't need to optimize during this, all entries are handled as normal. + if e.Type == EntryCatchup { + continue + } + + if e.Type == EntrySnapshot { + if !isLeader { + // No-op needed? + state, err := decodeConsumerState(e.Data) + if err != nil { + if mset, node := o.streamAndNode(); mset != nil && node != nil { + s := js.srv + s.Errorf("JetStream cluster could not decode consumer snapshot for '%s > %s > %s' [%s]", + mset.account(), mset.name(), o, node.Group()) + } + return err + } + + if err = o.store.Update(state); err != nil && err != ErrStoreOldUpdate { + o.mu.RLock() + s, acc, mset, name := o.srv, o.acc, o.mset, o.name + o.mu.RUnlock() + if s != nil && mset != nil { + s.Warnf("Consumer '%s > %s > %s' error on store update from snapshot entry: %v", acc, mset.name(), name, err) + } + } + // Check our interest state if applicable. + if mset := o.getStream(); mset != nil { + var ss StreamState + mset.store.FastState(&ss) + // We used to register preacks here if our ack floor was higher than the last sequence. + // Now when streams catch up they properly call checkInterestState() and periodically run this as well. + // If our states drift this could have allocated lots of pre-acks. + o.checkStateForInterestStream(&ss) + } + } + + } else if e.Type == EntryRemovePeer { + js.mu.RLock() + var ourID string + if js.cluster != nil && js.cluster.meta != nil { + ourID = js.cluster.meta.ID() + } + js.mu.RUnlock() + if peer := string(e.Data); peer == ourID { + shouldRemove := true + if mset := o.getStream(); mset != nil { + if sa := mset.streamAssignment(); sa != nil && sa.Group != nil { + js.mu.RLock() + shouldRemove = !sa.Group.isMember(ourID) + js.mu.RUnlock() + } + } + if shouldRemove { + o.stopWithFlags(true, false, false, false) + } + } + } else if e.Type == EntryAddPeer { + // Ignore for now. + } else { + buf := e.Data + if len(buf) == 0 { + return errBadEntryOp + } + switch entryOp(buf[0]) { + case updateDeliveredOp: + dseq, sseq, dc, ts, err := decodeDeliveredUpdate(buf[1:]) + if err != nil { + if mset, node := o.streamAndNode(); mset != nil && node != nil { + s := js.srv + s.Errorf("JetStream cluster could not decode consumer delivered update for '%s > %s > %s' [%s]", + mset.account(), mset.name(), o, node.Group()) + } + return err + } + // Make sure to update delivered under the lock. + o.mu.Lock() + err = o.store.UpdateDelivered(dseq, sseq, dc, ts) + o.ldt = time.Now() + // Need to send message to the client, since we have quorum to do so now. + if pmsg, ok := o.pendingDeliveries[sseq]; ok { + // Copy delivery subject and sequence first, as the send returns it to the pool and clears it. + dsubj, seq := pmsg.dsubj, pmsg.seq + o.outq.send(pmsg) + delete(o.pendingDeliveries, sseq) + + // Might need to send a request timeout after sending the last replicated delivery. + if wd, ok := o.waitingDeliveries[dsubj]; ok && wd.seq == seq { + if wd.pn > 0 || wd.pb > 0 { + hdr := fmt.Appendf(nil, "NATS/1.0 408 Request Timeout\r\n%s: %d\r\n%s: %d\r\n\r\n", JSPullRequestPendingMsgs, wd.pn, JSPullRequestPendingBytes, wd.pb) + o.outq.send(newJSPubMsg(dsubj, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + } + wd.recycle() + delete(o.waitingDeliveries, dsubj) + } + } + o.mu.Unlock() + if err != nil { + return err + } + case updateAcksOp: + dseq, sseq, err := decodeAckUpdate(buf[1:]) + if err != nil { + if mset, node := o.streamAndNode(); mset != nil && node != nil { + s := js.srv + s.Errorf("JetStream cluster could not decode consumer ack update for '%s > %s > %s' [%s]", + mset.account(), mset.name(), o, node.Group()) + } + return err + } + if err := o.processReplicatedAck(dseq, sseq); err == errConsumerClosed { + return err + } + case updateSkipOp: + sseq, err := decodeSkipUpdate(buf[1:]) + if err != nil { + return err + } + o.mu.Lock() + if !o.isLeader() && sseq > o.sseq { + o.sseq = sseq + } + if o.dseq == 0 { + o.dseq = 1 + } + if o.store != nil { + o.store.UpdateStarting(sseq - 1) + } + o.mu.Unlock() + case resetSeqOp: + sseq, reply, err := decodeResetUpdate(buf[1:]) + if err != nil { + return err + } + o.mu.Lock() + recalcPending := o.resetLocalStartingSeq(sseq) + if o.store != nil { + o.store.Reset(sseq - 1) + } + // Cleanup messages that lost interest. + if o.retention == InterestPolicy { + if mset := o.mset; mset != nil { + o.mu.Unlock() + ss := mset.state() + o.checkStateForInterestStream(&ss) + o.mu.Lock() + } + } + // Recalculate pending, and re-trigger message delivery. + if !o.isLeader() { + o.mu.Unlock() + } else { + if recalcPending { + o.streamNumPending() + } + o.signalNewMessages() + s, a := o.srv, o.acc + if reply == _EMPTY_ { + o.mu.Unlock() + } else if rr, ok := o.rsm[reply]; !ok { + o.mu.Unlock() + } else { + delete(o.rsm, reply) + o.mu.Unlock() + + // Check if the reset request needs to be answered on the system account. + // This will happen for replicated sourcing consumers that get reset as part of a create/update. + if rr.internal { + a = nil + } + var resp = JSApiConsumerResetResponse{ApiResponse: ApiResponse{Type: JSApiConsumerResetResponseType}} + if resp.ConsumerInfo = setDynamicConsumerInfoMetadata(o.info()); resp.ConsumerInfo == nil { + // The consumer was closed before we could respond. + resp.Error = NewJSConsumerInvalidResetError(errConsumerClosed) + s.sendInternalAccountMsg(a, reply, s.jsonResponse(&resp)) + } else { + resp.ResetSeq = sseq + if rr.identity { + rhdr := genHeader(nil, JSStreamIdentity, o.streamIdentity()) + s.sendInternalAccountMsgWithReply(a, reply, _EMPTY_, rhdr, s.jsonResponse(&resp), false) + } else { + s.sendInternalAccountMsg(a, reply, s.jsonResponse(&resp)) + } + } + } + } + case addPendingRequest: + o.mu.Lock() + if !o.isLeader() { + if o.prm == nil { + o.prm = make(map[string]struct{}) + } + o.prm[string(buf[1:])] = struct{}{} + } + o.mu.Unlock() + case removePendingRequest: + o.mu.Lock() + if !o.isLeader() { + if o.prm != nil { + delete(o.prm, string(buf[1:])) + } + } + o.mu.Unlock() + default: + return fmt.Errorf("unknown consumer entry op type: %v", entryOp(buf[0])) + } + } + } + return nil +} + +var errConsumerClosed = errors.New("consumer closed") + +func (o *consumer) processReplicatedAck(dseq, sseq uint64) error { + o.mu.Lock() + // Update activity. + o.lat = time.Now() + + var ackAllSeqs []uint64 + if o.retention != LimitsPolicy && (o.cfg.AckPolicy == AckAll || o.cfg.AckPolicy == AckFlowControl) { + // Always use the store state, as o.asflr is skipped ahead already. + // Capture before updating store, which clears the pending below. + state, err := o.store.BorrowState() + if err == nil { + // Only need to collect if the ack covers more than the sequence itself. + if sagap := sseq - state.AckFloor.Stream; sagap > 1 { + // At most the pending entries below the ack, don't over-allocate. + ackAllSeqs = make([]uint64, 0, min(uint64(len(state.Pending)), sagap-1)) + for seq := range state.Pending { + if seq < sseq { + ackAllSeqs = append(ackAllSeqs, seq) + } + } + } + } + } + + // Do actual ack update to store. + // Always do this to have it recorded. + o.store.UpdateAcks(dseq, sseq) + + mset := o.mset + if o.closed || mset == nil { + o.mu.Unlock() + return errConsumerClosed + } + if mset.closed.Load() { + o.mu.Unlock() + return errStreamClosed + } + + // Check if we have a reply that was requested. + if reply := o.replies[sseq]; reply != _EMPTY_ { + o.outq.sendMsg(reply, nil) + delete(o.replies, sseq) + } + + if o.retention == LimitsPolicy { + o.mu.Unlock() + return nil + } + o.mu.Unlock() + + mset.ackMsg(o, sseq) + for _, seq := range ackAllSeqs { + mset.ackMsg(o, seq) + } + return nil +} + +var errBadEntryOp = errors.New("jetstream cluster bad replicated entry") +var errBadAckUpdate = errors.New("jetstream cluster bad replicated ack update") +var errBadDeliveredUpdate = errors.New("jetstream cluster bad replicated delivered update") +var errBadSkipUpdate = errors.New("jetstream cluster bad replicated skip update") +var errBadResetUpdate = errors.New("jetstream cluster bad replicated reset update") + +func decodeAckUpdate(buf []byte) (dseq, sseq uint64, err error) { + var bi, n int + if dseq, n = binary.Uvarint(buf); n <= 0 { + return 0, 0, errBadAckUpdate + } + bi += n + if sseq, n = binary.Uvarint(buf[bi:]); n <= 0 { + return 0, 0, errBadAckUpdate + } + return dseq, sseq, nil +} + +func decodeDeliveredUpdate(buf []byte) (dseq, sseq, dc uint64, ts int64, err error) { + var bi, n int + if dseq, n = binary.Uvarint(buf); n <= 0 { + return 0, 0, 0, 0, errBadDeliveredUpdate + } + bi += n + if sseq, n = binary.Uvarint(buf[bi:]); n <= 0 { + return 0, 0, 0, 0, errBadDeliveredUpdate + } + bi += n + if dc, n = binary.Uvarint(buf[bi:]); n <= 0 { + return 0, 0, 0, 0, errBadDeliveredUpdate + } + bi += n + if ts, n = binary.Varint(buf[bi:]); n <= 0 { + return 0, 0, 0, 0, errBadDeliveredUpdate + } + return dseq, sseq, dc, ts, nil +} + +func decodeSkipUpdate(buf []byte) (sseq uint64, err error) { + if len(buf) < 8 { + return 0, errBadSkipUpdate + } + return binary.LittleEndian.Uint64(buf), nil +} + +func decodeResetUpdate(buf []byte) (sseq uint64, reply string, err error) { + if len(buf) < 8 { + return 0, _EMPTY_, errBadResetUpdate + } + return binary.LittleEndian.Uint64(buf[:8]), string(buf[8:]), nil +} + +func (js *jetStream) processConsumerLeaderChange(o *consumer, isLeader bool, term uint64) error { + return js.processConsumerLeaderChangeWithAssignment(o, nil, isLeader, term) +} + +func (js *jetStream) processConsumerLeaderChangeWithAssignment(o *consumer, ca *consumerAssignment, isLeader bool, term uint64) error { + stepDownIfLeader := func() error { + if node := o.raftNode(); node != nil && isLeader { + node.StepDown() + } + return errors.New("failed to update consumer leader status") + } + + if o == nil || o.isClosed() { + return stepDownIfLeader() + } + + if ca == nil { + ca = o.consumerAssignment() + } + if ca == nil { + return stepDownIfLeader() + } + js.mu.RLock() + s, account, err := js.srv, ca.Client.serviceAccount(), ca.err + client, subject, reply, streamName, consumerName, sourcing := ca.Client, ca.Subject, ca.Reply, ca.Stream, ca.Name, ca.Config.Sourcing + hasResponded := ca.markResponded() + // A migration status is only valid for the leader and term that wrote it. + clearMigration := ca.Group.migration != nil + js.mu.RUnlock() + + if clearMigration { + js.mu.Lock() + ca.Group.migration = nil + js.mu.Unlock() + } + + acc, _ := s.LookupAccount(account) + if acc == nil { + return stepDownIfLeader() + } + + if isLeader { + // Only log if the consumer is replicated and/or durable. + // Logging about R1 ephemerals, like KV watchers, is mostly noise since the leader will always be known. + o.mu.RLock() + isReplicated, durable := o.node != nil, o.isDurable() + o.mu.RUnlock() + if isReplicated || durable { + s.Noticef("JetStream cluster new consumer leader for '%s > %s > %s'", ca.Client.serviceAccount(), streamName, consumerName) + } + s.sendConsumerLeaderElectAdvisory(o) + } else { + // We are stepping down. + // Make sure if we are doing so because we have lost quorum that we send the appropriate advisories. + if node := o.raftNode(); node != nil && !node.Quorum() && time.Since(node.Created()) > 5*time.Second { + s.sendConsumerLostQuorumAdvisory(o) + } + } + + // Tell consumer to switch leader status. + if lerr := o.setLeader(isLeader, term); lerr != nil && err == nil { + err = lerr + } + + if !isLeader || hasResponded { + if isLeader { + o.clearInitialInfo() + } + return nil + } + + var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} + if err != nil { + resp.Error = NewJSConsumerCreateError(err, Unless(err)) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } else { + // If it's a sourcing consumer, we need to respond after the consumer has been reset instead. + if sourcing { + var rresp = JSApiConsumerResetResponse{ApiResponse: ApiResponse{Type: JSApiConsumerResetResponseType}} + resetSeq, canRespond, err := o.resetStartingSeq(0, reply, true, true) + if err != nil { + rresp.Error = NewJSConsumerInvalidResetError(err) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&rresp)) + } else if canRespond { + if rresp.ConsumerInfo = setDynamicConsumerInfoMetadata(o.info()); rresp.ConsumerInfo == nil { + // The consumer was closed before we could respond. + rresp.Error = NewJSConsumerInvalidResetError(errConsumerClosed) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&rresp)) + return nil + } + rresp.ResetSeq = resetSeq + rhdr := genHeader(nil, JSStreamIdentity, o.streamIdentity()) + s.sendAPIHdrResponse(client, acc, subject, reply, _EMPTY_, rhdr, s.jsonResponse(&rresp)) + } + } else if resp.ConsumerInfo = setDynamicConsumerInfoMetadata(o.initialInfo()); resp.ConsumerInfo == nil { + // The consumer was closed before we could respond. + resp.Error = NewJSConsumerCreateError(errConsumerClosed) + s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + return nil + } else if resp.Config.Direct || resp.Config.Sourcing { + rhdr := genHeader(nil, JSStreamIdentity, o.streamIdentity()) + s.sendAPIHdrResponse(client, acc, subject, reply, _EMPTY_, rhdr, s.jsonResponse(&resp)) + } else { + s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp)) + } + o.sendCreateAdvisory() + } + + // Only send a pause advisory on consumer create if we're + // actually paused. The timer would have been kicked by now + // by the call to o.setLeader() above. + o.mu.RLock() + if isLeader && o.cfg.PauseUntil != nil && !o.cfg.PauseUntil.IsZero() && time.Now().Before(*o.cfg.PauseUntil) { + o.sendPauseAdvisoryLocked(&o.cfg) + } + o.mu.RUnlock() + + return nil +} + +// Determines if we should send lost quorum advisory. We throttle these after first one. +func (o *consumer) shouldSendLostQuorum() bool { + o.mu.Lock() + defer o.mu.Unlock() + if time.Since(o.lqsent) >= lostQuorumAdvInterval { + o.lqsent = time.Now() + return true + } + return false +} + +func (s *Server) sendConsumerLostQuorumAdvisory(o *consumer) { + if o == nil { + return + } + node, stream, consumer, acc := o.raftNode(), o.streamName(), o.String(), o.account() + if node == nil { + return + } + if !o.shouldSendLostQuorum() { + return + } + + s.Warnf("JetStream cluster consumer '%s > %s > %s' has NO quorum, stalled.", acc.GetName(), stream, consumer) + + subj := JSAdvisoryConsumerQuorumLostPre + "." + stream + "." + consumer + adv := &JSConsumerQuorumLostAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerQuorumLostAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: stream, + Consumer: consumer, + Replicas: s.replicas(node), + Domain: s.getOpts().JetStreamDomain, + } + + // 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) +} + +func (s *Server) sendConsumerLeaderElectAdvisory(o *consumer) { + if o == nil { + return + } + node, stream, consumer, acc := o.raftNode(), o.streamName(), o.String(), o.account() + if node == nil { + return + } + + subj := JSAdvisoryConsumerLeaderElectedPre + "." + stream + "." + consumer + adv := &JSConsumerLeaderElectedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSConsumerLeaderElectedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: stream, + Consumer: consumer, + Leader: s.serverNameForNode(node.GroupLeader()), + Replicas: s.replicas(node), + Domain: s.getOpts().JetStreamDomain, + } + + // 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) +} + +type streamAssignmentResult struct { + Account string `json:"account"` + Stream string `json:"stream"` + Response *JSApiStreamCreateResponse `json:"create_response,omitempty"` + Restore *JSApiStreamRestoreResponse `json:"restore_response,omitempty"` + Update bool `json:"is_update,omitempty"` +} + +// Determine if this is an insufficient resources' error type. +func isInsufficientResourcesErr(resp *JSApiStreamCreateResponse) bool { + return resp != nil && resp.Error != nil && IsNatsErr(resp.Error, JSInsufficientResourcesErr, JSMemoryResourcesExceededErr, JSStorageResourcesExceededErr) +} + +// Process error results of stream and consumer assignments. +// Success will be handled by stream leader. +func (js *jetStream) processStreamAssignmentResults(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + var result streamAssignmentResult + if err := json.Unmarshal(msg, &result); err != nil { + // TODO(dlc) - log + return + } + acc, _ := js.srv.LookupAccount(result.Account) + if acc == nil { + // TODO(dlc) - log + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + s, cc := js.srv, js.cluster + if cc == nil || cc.meta == nil { + return + } + + if sa := js.streamAssignmentOrInflight(result.Account, result.Stream); sa != nil && !sa.reassigning { + canDelete := !result.Update && time.Since(sa.Created) < 5*time.Second + + // See if we should retry in case this cluster is full but there are others. + if cfg, ci := sa.Config, sa.Client; cfg != nil && ci != nil && isInsufficientResourcesErr(result.Response) && canDelete { + // If cluster is defined we can not retry. + if cfg.Placement == nil || cfg.Placement.Cluster == _EMPTY_ { + // If we have additional clusters to try we can retry. + // We have already verified that ci != nil. + if len(ci.Alternates) > 0 { + if rg, err := js.createGroupForStream(ci, cfg); err != nil { + s.Warnf("Retrying cluster placement for stream '%s > %s' failed due to placement error: %+v", result.Account, result.Stream, err) + } else { + if org := sa.Group; org != nil && len(org.Peers) > 0 { + s.Warnf("Retrying cluster placement for stream '%s > %s' due to insufficient resources in cluster %q", + result.Account, result.Stream, s.clusterNameForNode(org.Peers[0])) + } else { + s.Warnf("Retrying cluster placement for stream '%s > %s' due to insufficient resources", result.Account, result.Stream) + } + // Pick a new preferred leader. + rg.setPreferred(s) + // Get rid of previous attempt. + if err := cc.meta.Propose(cc.term, encodeDeleteStreamAssignment(sa)); err != nil { + return + } + cc.trackInflightStreamProposal(result.Account, sa, true) + // Propose new. + nsa := sa.copyGroup() + nsa.Group, nsa.err = rg, nil + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(nsa)); err != nil { + return + } + cc.trackInflightStreamProposal(result.Account, nsa, false) + // When the new stream assignment is processed, sa.reassigning will be + // automatically set back to false. Until then, don't process any more + // assignment results. + nsa.reassigning = true + return + } + } + } + } + + // Respond to the user here. + var resp string + if result.Response != nil { + resp = s.jsonResponse(result.Response) + } else if result.Restore != nil { + resp = s.jsonResponse(result.Restore) + } + if !sa.hasResponded() || result.Update { + sa.markResponded() + js.srv.sendAPIErrResponse(sa.Client, acc, sa.Subject, sa.Reply, _EMPTY_, resp) + } + // Remove this assignment if possible. + if canDelete { + var apiErr *ApiError + if result.Response != nil { + apiErr = result.Response.Error + } else if result.Restore != nil { + apiErr = result.Restore.Error + } + s.Warnf("Stream assignment for '%s > %s' rejected by assigned member: %v", sa.Client.serviceAccount(), sa.Config.Name, apiErr) + sa.err = NewJSClusterNotAssignedError() + if err := cc.meta.Propose(cc.term, encodeDeleteStreamAssignment(sa)); err != nil { + return + } + cc.trackInflightStreamProposal(result.Account, sa, true) + } + } +} + +func (js *jetStream) processConsumerAssignmentResults(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + var result consumerAssignmentResult + if err := json.Unmarshal(msg, &result); err != nil { + // TODO(dlc) - log + return + } + acc, _ := js.srv.LookupAccount(result.Account) + if acc == nil { + // TODO(dlc) - log + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + s, cc := js.srv, js.cluster + if cc == nil || cc.meta == nil { + return + } + + if sa := js.streamAssignment(result.Account, result.Stream); sa != nil && sa.consumers != nil { + if ca := sa.consumers[result.Consumer]; ca != nil && !ca.hasResponded() { + js.srv.sendAPIErrResponse(ca.Client, acc, ca.Subject, ca.Reply, _EMPTY_, s.jsonResponse(result.Response)) + ca.markResponded() + + // Check if this failed. + // TODO(dlc) - Could have mixed results, should track per peer. + // Make sure this is recent response. + if result.Response.Error != nil && result.Response.Error != NewJSConsumerNameExistError() && time.Since(ca.Created) < 2*time.Second { + // Do not list in consumer names/lists. + s.Warnf("Consumer assignment for '%s > %s > %s' rejected by assigned member: %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, result.Response.Error) + ca.err = NewJSClusterNotAssignedError() + } + } + } +} + +type streamAssignmentReconcile struct { + Account string `json:"account"` // Account of the stream. + Stream string `json:"stream"` // The stream name itself. + desiredAssignmentUpdate +} + +type consumerAssignmentReconcile struct { + Account string `json:"account"` // Account of the consumer. + Stream string `json:"stream"` // Stream of the consumer. + Consumer string `json:"consumer"` // The consumer name itself. + desiredAssignmentUpdate +} + +type desiredAssignmentUpdate struct { + ID string `json:"id,omitempty"` // Desired state ID. Empty if there is no desired state yet. + Term uint64 `json:"term"` // Raft term of the group leader sending this update, used for fencing. + + // The below fields are mutually exclusive. + ScaleDownPeers []string `json:"scale_down_peers,omitempty"` // If the desired state was about scaledown, this is the selected peer set. + MetaPeers []string `json:"meta_peers,omitempty"` // Which peers the assignment should be updated to. + + PeersMatch bool `json:"match,omitempty"` // Actual peer set matches with the passed MetaPeers. +} + +// reconcileDesiredStreamAssignment runs on the meta leader and reconciles a stream's assignment. +func (js *jetStream) reconcileDesiredStreamAssignment(_ *subscription, _ *client, _ *Account, _, _ string, msg []byte) { + var reconcile streamAssignmentReconcile + decoder := json.NewDecoder(bytes.NewReader(msg)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&reconcile); err != nil { + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + cc := js.cluster + if cc == nil || cc.meta == nil { + return + } + osa := js.streamAssignmentOrInflight(reconcile.Account, reconcile.Stream) + if osa == nil || osa.Group == nil || osa.unsupported != nil || reconcile.Term == 0 { + return + } + + // We stage consumer updates and do them after the stream update. + var consumers []*consumerAssignment + var deleted []*consumerAssignment + + // If any consumers need to be remapped, we can't mark the stream's desired state done yet. + var done bool + + // Without desired state there's nothing to remap consumers onto yet. Remapping now + // would propose direct peer swaps without migrating them, so postpone until the next + // cycle, once desired state is initialized below and they can move through it. + noDesired := osa.Group.Desired == nil + + // A legacy in-progress move, started before the upgrade to desired state, needs + // its origin synthesized below to stay cancellable. + legacyOrigin := osa.legacyMoveOrigin() + + // If the stream is scaling down and hasn't selected its final peer set yet, + // we need to wait before we remap. + if !noDesired && !osa.Group.Desired.ScaleDown { + // Need to remap any consumers. + consumers, deleted, done = js.remapConsumerAssignments(reconcile.Account, osa) + } + + // Keep any peer that still hosts a consumer, since the stream leader can report dropping + // a peer whose consumer was assigned after it looked, and the consumer needs that peer to + // migrate its state off. + if len(reconcile.MetaPeers) > 0 { + for ca := range js.consumerAssignmentsOrInflightSeq(reconcile.Account, reconcile.Stream) { + if ca.Config == nil || ca.unsupported != nil { + continue + } + // Ephemerals we're deleting are on their way out, they don't hold a peer. + if slices.ContainsFunc(deleted, func(d *consumerAssignment) bool { return d.Name == ca.Name }) { + continue + } + for _, peer := range ca.Group.Peers { + if !slices.Contains(reconcile.MetaPeers, peer) && slices.Contains(osa.Group.Peers, peer) { + // What we apply is no longer what the group leader reported. + reconcile.MetaPeers = append(reconcile.MetaPeers, peer) + reconcile.PeersMatch = false + } + } + } + } + + ng := osa.Group.reconcileDesiredState(reconcile.desiredAssignmentUpdate, osa.Config.Replicas, done) + if ng != nil { + sa := osa.copyGroup() + sa.Group = ng + // If it was a legacy move, we need to initialize the desired state origin. + if legacyOrigin != nil && sa.Group.Desired != nil { + // Must derive the desired cluster from the destination peers, or the origin + // cluster (still held by Group.Cluster) would be committed on convergence. + if dp := sa.Group.Desired.Peers; len(dp) > 0 { + if cluster := js.srv.clusterNameForNode(dp[0]); cluster != _EMPTY_ { + sa.Group.Desired.Cluster = cluster + } + } + sa.Group.Desired.Origin = legacyOrigin + } + // Single nodes are not recorded by the NRG layer so we can rename. + // MUST do this, otherwise a scaleup afterward could potentially lead to inconsistencies. + if sa.Group.Desired == nil && len(sa.Group.Peers) == 1 { + sa.Group.Name = groupNameForStream(sa.Group.Peers, sa.Group.Storage) + } + if err := cc.meta.Propose(cc.term, encodeUpdateStreamAssignment(sa)); err != nil { + return + } + cc.trackInflightStreamProposal(reconcile.Account, sa, false) + } + + // Process any staged consumers. + for _, ca := range consumers { + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(ca)); err != nil { + return + } + cc.trackInflightConsumerProposal(reconcile.Account, reconcile.Stream, ca, false) + } + for _, ca := range deleted { + if err := cc.meta.Propose(cc.term, encodeDeleteConsumerAssignment(ca)); err != nil { + return + } + cc.trackInflightConsumerProposal(reconcile.Account, reconcile.Stream, ca, true) + } +} + +// reconcileDesiredConsumerAssignment runs on the meta leader and reconciles a consumer's assignment. +func (js *jetStream) reconcileDesiredConsumerAssignment(_ *subscription, _ *client, _ *Account, _, _ string, msg []byte) { + var reconcile consumerAssignmentReconcile + decoder := json.NewDecoder(bytes.NewReader(msg)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&reconcile); err != nil { + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + cc := js.cluster + if cc == nil || cc.meta == nil { + return + } + + osa := js.streamAssignmentOrInflight(reconcile.Account, reconcile.Stream) + if osa == nil || osa.Group == nil { + return + } + oca := js.consumerAssignmentOrInflight(reconcile.Account, reconcile.Stream, reconcile.Consumer) + if oca == nil || oca.Group == nil || oca.unsupported != nil || reconcile.Term == 0 { + return + } + // Sanity check that the consumer isn't trying to scale to a peer that isn't + // in the stream's peer assignment yet. Otherwise, we'd add a consumer peer where + // a stream isn't hosted yet. + if len(reconcile.MetaPeers) > 0 { + for _, peer := range reconcile.MetaPeers { + if slices.Contains(oca.Group.Peers, peer) { + continue + } + if !slices.Contains(osa.Group.Peers, peer) { + return + } + } + } + replicas := oca.Config.replicas(osa.Config) + ng := oca.Group.reconcileDesiredState(reconcile.desiredAssignmentUpdate, replicas, true) + if ng == nil { + return + } + ca := oca.copyGroup() + ca.Group = ng + // Single nodes are not recorded by the NRG layer so we can rename. + // MUST do this, otherwise a scaleup afterward could potentially lead to inconsistencies. + if ca.Group.Desired == nil && len(ca.Group.Peers) == 1 { + ca.Group.Name = groupNameForConsumer(ca.Group.Peers, ca.Group.Storage) + } + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(ca)); err != nil { + return + } + cc.trackInflightConsumerProposal(reconcile.Account, reconcile.Stream, ca, false) +} + +func (rg *raftGroup) reconcileDesiredState(reconcile desiredAssignmentUpdate, replicas int, done bool) *raftGroup { + // Fields are mutually exclusive. + if reconcile.ScaleDownPeers != nil && reconcile.MetaPeers != nil { + return nil + } + + // Initialize desired state if not set. + if rg.Desired == nil { + // Request is about state that we don't have. + if reconcile.ID != _EMPTY_ { + return nil + } + ng := rg.copyGroup() + newPeers := rg.Peers + // An over-replicated peer set is a legacy move being converted into + // desired state, it must keep counting as a move in flight. + legacyMove := len(newPeers) > replicas + if legacyMove { + newPeers = newPeers[len(newPeers)-replicas:] + } + ng.Peers = newPeers + ng = rg.withDesired(ng) + ng.Desired.Term = reconcile.Term + ng.Desired.Move = legacyMove + return ng + } + + // Requests from an older leadership term are stale, fence them off. Raft guarantees at most + // one leader per term, so anything below the recorded term can't be from the current leader. + if reconcile.Term < rg.Desired.Term { + return nil + } + + // Skip if this request is not about our latest desired state. + if rg.Desired.ID == _EMPTY_ || rg.Desired.ID != reconcile.ID { + return nil + } + + // Desired state is set, but a new leadership term MUST first be recorded before its updates + // are accepted. The group leader waits with sending update requests until it sees its own term. + // The desired state ID is updated to prevent the group leader from sending an update request early. + if rg.Desired.Term == 0 || reconcile.Term > rg.Desired.Term { + ng := rg.copyGroup() + ng.Desired.ID = nuid.Next() + ng.Desired.Term = reconcile.Term + return ng + } + + // A scale down requires selected peers to scale down to first. The group leader is allowed to pick + // a subset of the passed desired peer set. + if rg.Desired.ScaleDown { + if reconcile.ScaleDownPeers == nil { + return nil + } + // All scale down peers should be a subset, ignore if not. + for _, peer := range reconcile.ScaleDownPeers { + if !slices.Contains(rg.Desired.Peers, peer) { + return nil + } + } + // Reset scaledown and save the selected peers. + ng := rg.copyGroup() + ng.Desired.ID = nuid.Next() + ng.Desired.ScaleDown = false + ng.Desired.Peers = reconcile.ScaleDownPeers + return ng + } + + // Final check to make sure it's about a meta assignment peer set update. + // If not, then it's likely just a nudge to remap consumers. + if reconcile.MetaPeers == nil { + return nil + } + + // Always update the actual peers. + ng := rg.copyGroup() + prevPeers := ng.Peers + ng.Peers = reconcile.MetaPeers + + // Compare on sorted copies, so we don't clobber the peer ordering. + desiredPeers := copyStrings(rg.Desired.Peers) + metaPeers := copyStrings(reconcile.MetaPeers) + slices.Sort(desiredPeers) + slices.Sort(metaPeers) + exactMatch := slices.Equal(desiredPeers, metaPeers) + if exactMatch && reconcile.PeersMatch && done { + // We're done, reset the desired state fields and finalize the assignment. + ng.Peers = ng.Desired.Peers + ng.Cluster = ng.Desired.Cluster + ng.Preferred = _EMPTY_ + ng.Desired = nil + // Clear exclusions if converged at the configured replica count. + if len(ng.Peers) >= replicas { + ng.Excluded = nil + } + // Don't reset ScaleUp here. If it was set, we'll want each replica to know + // about it until it unsets it as part of recovery. + } else { + // Skip if the peer set is unchanged. + slices.Sort(prevPeers) + if slices.Equal(metaPeers, prevPeers) { + return nil + } + // If new peers are added, mark this assignment for scale up. + for _, peer := range metaPeers { + if !slices.Contains(prevPeers, peer) { + ng.ScaleUp = true + break + } + } + // Still converging toward the desired peer set. + ng.Desired.ID = nuid.Next() + } + return ng +} + +// consumerAssignmentCreate is a request for the meta leader to create a consumer +// assignment for a consumer that already exists on the requesting server, but that +// the meta layer doesn't know about yet. For example a consumer that came back as +// part of a stream restore. +type consumerAssignmentCreate struct { + Account string `json:"account"` // Account of the consumer. + Stream string `json:"stream"` // Stream of the consumer. + Consumer string `json:"consumer"` // The consumer name itself. + Created time.Time `json:"created"` // Creation time of the local consumer. + Config json.RawMessage `json:"config"` // Consumer config, kept raw so unknown fields can be rejected. + State *ConsumerState `json:"state,omitempty"` // Initial state to distribute with the assignment, if any. +} + +// consumerAssignmentDelete is a request for the meta leader to delete a consumer +// assignment. The account, stream, consumer name and creation time together identify +// the consumer, so a consumer that was recreated under the same name isn't deleted. +type consumerAssignmentDelete struct { + Account string `json:"account"` // Account of the consumer. + Stream string `json:"stream"` // Stream of the consumer. + Consumer string `json:"consumer"` // The consumer name itself. + Created time.Time `json:"created"` // Creation time, part of the consumer's identity. +} + +// consumerAssignmentCompressedMsg marks a consumer assignment request as being +// s2-compressed, the request is plain JSON otherwise. Deliberately not '{' or any +// leading JSON whitespace, so the first byte is an unambiguous discriminator. +const consumerAssignmentCompressedMsg byte = 1 + +// sendConsumerAssignmentCreate asks the meta leader to create a consumer assignment. +func (s *Server) sendConsumerAssignmentCreate(create *consumerAssignmentCreate) { + // Only state can be complex, so only compress when we're distributing it. + // Keeps the common request plain JSON, and readable when observing the subject. + if create.State == nil { + s.sendInternalMsgLocked(consumerAssignmentCreateSubj, _EMPTY_, nil, create) + return + } + var bb bytes.Buffer + bb.WriteByte(consumerAssignmentCompressedMsg) + s2e := s2.NewWriter(&bb) + if err := json.NewEncoder(s2e).Encode(create); err != nil { + s2e.Close() + return + } + if err := s2e.Close(); err != nil { + return + } + // Pass the bytes along, so they're sent as-is instead of being marshaled again. + s.sendInternalMsgLocked(consumerAssignmentCreateSubj, _EMPTY_, nil, bb.Bytes()) +} + +// requestConsumerAssignment asks the meta leader to assign a consumer that exists locally, +// but that the meta layer doesn't know about yet, i.e. one that came back with a stream +// restore. Retries until we observe the assignment, since the request is best-effort. +// If withState is set the consumer's current state is distributed with the assignment, which +// is needed if the consumer's group spans more peers than just us. +func (js *jetStream) requestConsumerAssignment(sa *streamAssignment, o *consumer, withState bool) { + s := js.srv + accName, streamName, name := sa.Client.serviceAccount(), sa.Config.Name, o.String() + cfg := o.config() + cfgJSON, err := json.Marshal(&cfg) + if err != nil { + s.Warnf("Could not encode config for consumer '%s > %s > %s': %v", accName, streamName, name, err) + return + } + create := &consumerAssignmentCreate{ + Account: accName, + Stream: streamName, + Consumer: name, + Created: o.createdTime(), + Config: cfgJSON, + } + if withState { + create.State, _ = o.store.State() + } + s.sendConsumerAssignmentCreate(create) + + // Check to make sure we see the assignment. + s.startGoRoutine(func() { + defer s.grWG.Done() + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + case <-s.quitCh: + return + } + js.mu.RLock() + // Give up if we're shutting down, if the stream is gone, or once we + // observe the consumer assignment we asked for. + done := js.shuttingDown || js.cluster == nil || + js.streamAssignment(accName, streamName) == nil || + js.consumerAssignment(accName, streamName, name) != nil + js.mu.RUnlock() + if done { + return + } + s.Warnf("Consumer assignment for '%s > %s > %s' has not been assigned, retrying", accName, streamName, name) + s.sendConsumerAssignmentCreate(create) + } + }) +} + +// sendConsumerAssignmentDelete asks the meta leader to delete a consumer assignment. +func (s *Server) sendConsumerAssignmentDelete(account, stream, consumer string, created time.Time) { + s.sendInternalMsgLocked(consumerAssignmentDeleteSubj, _EMPTY_, nil, &consumerAssignmentDelete{ + Account: account, + Stream: stream, + Consumer: consumer, + Created: created, + }) +} + +// processConsumerAssignmentCreate runs on the meta leader and creates an assignment for +// a consumer that already exists on the requesting server. The request is idempotent, if +// the consumer is already assigned (or a proposal is inflight) it is a no-op, so the +// requester can simply retry until it observes the assignment. +func (js *jetStream) processConsumerAssignmentCreate(_ *subscription, _ *client, _ *Account, _, _ string, msg []byte) { + if len(msg) == 0 { + return + } + var create consumerAssignmentCreate + var decoder *json.Decoder + if msg[0] == consumerAssignmentCompressedMsg { + decoder = json.NewDecoder(s2.NewReader(bytes.NewReader(msg[1:]))) + } else { + decoder = json.NewDecoder(bytes.NewReader(msg)) + } + decoder.DisallowUnknownFields() + if err := decoder.Decode(&create); err != nil { + return + } + if create.Consumer == _EMPTY_ || len(create.Config) == 0 { + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + s, cc := js.srv, js.cluster + if cc == nil || cc.meta == nil { + return + } + sa := js.streamAssignmentOrInflight(create.Account, create.Stream) + if sa == nil || sa.Group == nil || sa.Config == nil { + return + } + // Nothing to do if we know about this consumer already. + if js.consumerAssignmentOrInflight(create.Account, create.Stream, create.Consumer) != nil { + return + } + + ca := &consumerAssignment{ + Stream: create.Stream, + Name: create.Consumer, + ConfigJSON: create.Config, + Client: sa.Client, + Created: create.Created, + State: create.State, + } + // Populate ca.Config, we only got the raw config from the requester. + if err := decodeConsumerAssignmentConfig(ca); err != nil || ca.Config == nil || ca.unsupported != nil { + return + } + + rg, err := cc.createGroupForConsumer(ca.Config, sa) + if err != nil { + s.Warnf("Could not create group for consumer '%s > %s > %s': %v", + create.Account, create.Stream, create.Consumer, err) + return + } + // Pick a preferred leader. + rg.setPreferred(s) + // Inherit cluster from stream. + rg.Cluster = sa.Group.Cluster + ca.Group = rg + + // State can be complex, so make those compressed. + var entry []byte + if ca.State == nil { + entry = encodeAddConsumerAssignment(ca) + } else { + entry = encodeAddConsumerAssignmentCompressed(ca) + } + if err := cc.meta.Propose(cc.term, entry); err != nil { + return + } + cc.trackInflightConsumerProposal(create.Account, create.Stream, ca, false) +} + +// processConsumerAssignmentDelete runs on the meta leader and deletes a consumer's +// assignment. The request is idempotent, if the consumer is not assigned anymore (or a +// delete proposal is inflight) it is a no-op, so the requester can simply retry until it +// observes the assignment being gone. +func (js *jetStream) processConsumerAssignmentDelete(_ *subscription, _ *client, _ *Account, _, _ string, msg []byte) { + var del consumerAssignmentDelete + decoder := json.NewDecoder(bytes.NewReader(msg)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&del); err != nil { + return + } + if del.Consumer == _EMPTY_ { + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + cc := js.cluster + if cc == nil || cc.meta == nil { + return + } + ca := js.consumerAssignmentOrInflight(del.Account, del.Stream, del.Consumer) + // The creation time is part of the consumer's identity, only delete if the + // assignment still refers to the consumer the requester asked about. Otherwise + // we'd be deleting a new consumer that reused the name. + if ca == nil || !ca.Created.Equal(del.Created) { + return + } + cca := ca.clone() + cca.Reply = _EMPTY_ + if err := cc.meta.Propose(cc.term, encodeDeleteConsumerAssignment(cca)); err != nil { + return + } + cc.trackInflightConsumerProposal(del.Account, del.Stream, cca, true) +} + +const ( + streamAssignmentSubj = "$SYS.JSC.STREAM.ASSIGNMENT.RESULT" + streamAssignmentReconcileSubj = "$SYS.JSC.STREAM.ASSIGNMENT.RECONCILE" + consumerAssignmentSubj = "$SYS.JSC.CONSUMER.ASSIGNMENT.RESULT" + consumerAssignmentReconcileSubj = "$SYS.JSC.CONSUMER.ASSIGNMENT.RECONCILE" + consumerAssignmentCreateSubj = "$SYS.JSC.CONSUMER.ASSIGNMENT.CREATE" + consumerAssignmentDeleteSubj = "$SYS.JSC.CONSUMER.ASSIGNMENT.DELETE" +) + +// Lock should be held. +func (js *jetStream) startUpdatesSub() { + cc, s, c := js.cluster, js.srv, js.cluster.c + if cc.streamResults == nil { + cc.streamResults, _ = s.systemSubscribe(streamAssignmentSubj, _EMPTY_, false, c, js.processStreamAssignmentResults) + } + if cc.consumerResults == nil { + cc.consumerResults, _ = s.systemSubscribe(consumerAssignmentSubj, _EMPTY_, false, c, js.processConsumerAssignmentResults) + } + if cc.streamReconcile == nil { + cc.streamReconcile, _ = s.systemSubscribe(streamAssignmentReconcileSubj, _EMPTY_, false, c, js.reconcileDesiredStreamAssignment) + } + if cc.consumerReconcile == nil { + cc.consumerReconcile, _ = s.systemSubscribe(consumerAssignmentReconcileSubj, _EMPTY_, false, c, js.reconcileDesiredConsumerAssignment) + } + if cc.consumerCreate == nil { + cc.consumerCreate, _ = s.systemSubscribe(consumerAssignmentCreateSubj, _EMPTY_, false, c, js.processConsumerAssignmentCreate) + } + if cc.consumerDelete == nil { + cc.consumerDelete, _ = s.systemSubscribe(consumerAssignmentDeleteSubj, _EMPTY_, false, c, js.processConsumerAssignmentDelete) + } + if cc.stepdown == nil { + cc.stepdown, _ = s.systemSubscribe(JSApiLeaderStepDown, _EMPTY_, false, c, s.jsLeaderStepDownRequest) + } + if cc.peerRemove == nil { + cc.peerRemove, _ = s.systemSubscribe(JSApiRemoveServer, _EMPTY_, false, c, s.jsLeaderServerRemoveRequest) + } + if cc.peerEvacuate == nil { + cc.peerEvacuate, _ = s.systemSubscribe(JSApiEvacuateServer, _EMPTY_, false, c, s.jsLeaderServerEvacuateRequest) + } + if cc.peerStreamMove == nil { + cc.peerStreamMove, _ = s.systemSubscribe(JSApiServerStreamMove, _EMPTY_, false, c, s.jsLeaderServerStreamMoveRequest) + } + if cc.peerStreamCancelMove == nil { + cc.peerStreamCancelMove, _ = s.systemSubscribe(JSApiServerStreamCancelMove, _EMPTY_, false, c, s.jsLeaderServerStreamCancelMoveRequest) + } + if js.accountPurge == nil { + js.accountPurge, _ = s.systemSubscribe(JSApiAccountPurge, _EMPTY_, false, c, s.jsLeaderAccountPurgeRequest) + } +} + +// Lock should be held. +func (js *jetStream) stopUpdatesSub() { + cc := js.cluster + if cc.streamResults != nil { + cc.s.sysUnsubscribe(cc.streamResults) + cc.streamResults = nil + } + if cc.consumerResults != nil { + cc.s.sysUnsubscribe(cc.consumerResults) + cc.consumerResults = nil + } + if cc.streamReconcile != nil { + cc.s.sysUnsubscribe(cc.streamReconcile) + cc.streamReconcile = nil + } + if cc.consumerReconcile != nil { + cc.s.sysUnsubscribe(cc.consumerReconcile) + cc.consumerReconcile = nil + } + if cc.consumerCreate != nil { + cc.s.sysUnsubscribe(cc.consumerCreate) + cc.consumerCreate = nil + } + if cc.consumerDelete != nil { + cc.s.sysUnsubscribe(cc.consumerDelete) + cc.consumerDelete = nil + } + if cc.stepdown != nil { + cc.s.sysUnsubscribe(cc.stepdown) + cc.stepdown = nil + } + if cc.peerRemove != nil { + cc.s.sysUnsubscribe(cc.peerRemove) + cc.peerRemove = nil + } + if cc.peerEvacuate != nil { + cc.s.sysUnsubscribe(cc.peerEvacuate) + cc.peerEvacuate = nil + } + if cc.peerStreamMove != nil { + cc.s.sysUnsubscribe(cc.peerStreamMove) + cc.peerStreamMove = nil + } + if cc.peerStreamCancelMove != nil { + cc.s.sysUnsubscribe(cc.peerStreamCancelMove) + cc.peerStreamCancelMove = nil + } + if js.accountPurge != nil { + cc.s.sysUnsubscribe(js.accountPurge) + js.accountPurge = nil + } +} + +func (s *Server) sendDomainLeaderElectAdvisory() { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.RLock() + node := cc.meta + js.mu.RUnlock() + + if node == nil { + return + } + + adv := &JSDomainLeaderElectedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSDomainLeaderElectedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Leader: node.GroupLeader(), + Replicas: s.replicas(node), + Cluster: s.cachedClusterName(), + Domain: s.getOpts().JetStreamDomain, + } + + s.publishAdvisory(nil, JSAdvisoryDomainLeaderElected, adv) +} + +func (js *jetStream) processLeaderChange(isLeader bool, term uint64) { + if js == nil { + return + } + s := js.srv + if s == nil { + return + } + + if isLeader { + s.Noticef("Self is new JetStream cluster metadata leader") + s.sendDomainLeaderElectAdvisory() + } else { + var node string + if meta := js.getMetaGroup(); meta != nil { + node = meta.GroupLeader() + } + if node == _EMPTY_ { + s.Noticef("JetStream cluster no metadata leader") + } else if srv := js.srv.serverNameForNode(node); srv == _EMPTY_ { + s.Noticef("JetStream cluster new remote metadata leader") + } else if clst := js.srv.clusterNameForNode(node); clst == _EMPTY_ { + s.Noticef("JetStream cluster new metadata leader: %s", srv) + } else { + s.Noticef("JetStream cluster new metadata leader: %s/%s", srv, clst) + } + } + + js.mu.Lock() + defer js.mu.Unlock() + + // Update our server atomic, while holding the lock to not race with API requests. + s.isMetaLeader.Store(isLeader) + js.cluster.term = term + + // Clear replies for peer-removes. + js.cluster.peerRemoveReply = nil + + // Clear inflight proposal tracking. + js.cluster.inflightStreams = nil + js.cluster.inflightConsumers = nil + + if isLeader { + if meta := js.cluster.meta; meta != nil && meta.IsObserver() { + meta.StepDown() + return + } + } + + if isLeader { + js.startUpdatesSub() + } else { + js.stopUpdatesSub() + // TODO(dlc) - stepdown. + } + + // If we have been signaled to check the streams, this is for a bug that left stream + // assignments with no sync subject after an update and no way to sync/catchup outside of the RAFT layer. + if isLeader && js.cluster.streamsCheck { + cc := js.cluster + for acc, sa := range js.streamAssignmentsOrInflightSeqAllAccounts() { + if sa.unsupported != nil { + continue + } + if sa.Sync == _EMPTY_ { + s.Warnf("Stream assignment corrupt for stream '%s > %s'", acc, sa.Config.Name) + nsa := &streamAssignment{Group: sa.Group, Config: sa.Config, Subject: sa.Subject, Reply: sa.Reply, Client: sa.Client, Created: sa.Created} + nsa.Sync = syncSubjForStream() + if err := cc.meta.Propose(cc.term, encodeUpdateStreamAssignment(nsa)); err != nil { + return + } + cc.trackInflightStreamProposal(acc, nsa, false) + } + } + // Clear check. + cc.streamsCheck = false + } + + // Reconcile assignments for missing or stale peers, for all peers. + js.seedNewPeers(isLeader) + if isLeader { + js.reconcilePeerAssignments(nil) + } +} + +// Reconciles stream and consumer assignments with missing peers, or peers that have since +// been removed from the meta peer set. +// Lock should be held. +func (js *jetStream) reconcilePeerAssignments(peers map[string]struct{}) { + cc := js.cluster + if cc == nil || cc.meta == nil { + return + } + + // The meta peer set as we know it right now. + mp := cc.meta.Peers() + metaPeers := make(map[string]struct{}, len(mp)) + for _, p := range mp { + metaPeers[p.ID] = struct{}{} + } + + // If peers are specified, they're the only ones eligible for addition. + var ignore []string + if len(peers) > 0 { + var eligible bool + ignore = make([]string, 0, len(mp)) + for _, p := range mp { + if _, ok := peers[p.ID]; ok { + eligible = true + } else { + ignore = append(ignore, p.ID) + } + } + // None of them have joined the meta group yet, for example when a statsz + // fires before the peer joins. Nothing to do. + if !eligible { + return + } + } + + for accName, sa := range js.streamAssignmentsOrInflightSeqAllAccounts() { + if sa.unsupported != nil { + continue + } + + // Remove any stale meta peers, and add missing. + if stale := sa.Group.stalePeers(metaPeers); len(stale) > 0 || sa.missingPeers() { + if nsa, _ := cc.reassignStreamPeers(sa, stale, true, sa.Config.Replicas, ignore...); nsa != nil { + sa = nsa + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(sa)); err != nil { + return + } + cc.trackInflightStreamProposal(accName, nsa, false) + } + } + + // Now do the same for consumers. + consumers, deleted, _ := js.remapConsumerAssignments(accName, sa) + for _, ca := range consumers { + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(ca)); err != nil { + return + } + cc.trackInflightConsumerProposal(accName, sa.Config.Name, ca, false) + } + for _, ca := range deleted { + if err := cc.meta.Propose(cc.term, encodeDeleteConsumerAssignment(ca)); err != nil { + return + } + cc.trackInflightConsumerProposal(accName, sa.Config.Name, ca, true) + } + } +} + +// Lock should be held. +func (cc *jetStreamCluster) remapStreamAssignment(sa *streamAssignment, peer string, remove bool) (csa *streamAssignment, replaced bool) { + if !sa.Group.isMember(peer) && (sa.Group.Desired == nil || !slices.Contains(sa.Group.Desired.Peers, peer)) { + return nil, false + } + return cc.reassignStreamPeers(sa, []string{peer}, remove, 0) +} + +// Replaces the given peers in the group, growing it toward minReplicas if it's smaller. +// If remove is set they're dropped right away, otherwise the group migrates off them. +// Ignored peers are kept out of placement, but stay if already in the group. +// Returns nil if the peer set is unchanged or empty, replaced if all peers got a replacement. +// Lock should be held. +func (cc *jetStreamCluster) reassignStreamPeers(sa *streamAssignment, peers []string, remove bool, minReplicas int, ignore ...string) (csa *streamAssignment, replaced bool) { + // If the group is already converging toward a desired peer set, that set is what + // actually drives membership, so base the replacement on it. Otherwise we'd hand back + // a peer set that the in-flight migration reconciles right back over. + basePeers, baseCluster := sa.Group.Peers, sa.Group.Cluster + if d := sa.Group.Desired; d != nil { + basePeers, baseCluster = d.Peers, d.Cluster + } + + // Invoke placement algo passing RG peers that stay (existing) and those being removed (ignore). + retain := make([]string, 0, len(basePeers)) + for _, v := range basePeers { + if !slices.Contains(peers, v) { + retain = append(retain, v) + } + } + // Peers taken away by an operator stay out of placement until the group is healed. + // Ones that have since left the meta peer set are dropped, placement only draws from + // that set, so they can't be selected anyway and there's no point preserving them. + var excluded []string + if len(sa.Group.Excluded) > 0 && cc.meta != nil { + mp := cc.meta.Peers() + metaPeers := make(map[string]struct{}, len(mp)) + for _, p := range mp { + metaPeers[p.ID] = struct{}{} + } + for _, p := range sa.Group.Excluded { + if _, ok := metaPeers[p]; ok { + excluded = append(excluded, p) + } + } + } + // selectPeerGroup folds skip into a set, so any overlap between these is harmless. + skip := peers + if len(ignore) > 0 || len(excluded) > 0 { + skip = slices.Concat(peers, ignore, excluded) + } + // Placement is all-or-nothing, so if we can't add every peer we're missing in one + // go, add as many as we can and heal the remainder on a later pass. Walking down + // from the target means the first success is the largest group we can form. + target := max(len(basePeers), minReplicas) + var newPeers []string + var placementError *selectPeerError + for r := target; ; r-- { + newPeers, placementError = cc.selectPeerGroup(r, baseCluster, sa.Config, retain, 0, skip) + if placementError == nil || r <= len(retain)+1 { + break + } + } + + csa = sa.copyGroup() + csa.Group.Cluster = baseCluster + if placementError == nil { + csa.Group.Peers = newPeers + replaced = true + } else { + csa.Group.Peers = retain + } + csa.Group = sa.Group.withDesired(csa.Group) + // Preserve how the group is meant to converge. + if d := sa.Group.Desired; d != nil { + csa.Group.Desired.ScaleDown = d.ScaleDown + } else if sa.legacyMoveOrigin() != nil { + // A legacy move absorbed into desired state must keep counting as a move in flight. + csa.Group.Desired.Move = true + } + if remove { + removeFrom := func(from []string) []string { + return slices.DeleteFunc(from, func(p string) bool { return slices.Contains(peers, p) }) + } + csa.Group.Peers = removeFrom(csa.Group.Peers) + csa.Group.Desired.Peers = removeFrom(csa.Group.Desired.Peers) + csa.Group.Desired.addRemoved(peers) + } + // Don't allow moving to an empty set. + if len(csa.Group.Desired.Peers) == 0 { + return nil, false + } + // Don't carry a stale preferred into the remaining group. + isMember := func(peer string) bool { + return slices.Contains(csa.Group.Peers, peer) || slices.Contains(csa.Group.Desired.Peers, peer) + } + if csa.Group.Preferred != _EMPTY_ && !isMember(csa.Group.Preferred) { + csa.Group.Preferred = _EMPTY_ + } + if csa.Group.Desired.Preferred != _EMPTY_ && !isMember(csa.Group.Desired.Preferred) { + csa.Group.Desired.Preferred = _EMPTY_ + } + // Preserve excluded peer set only if the group is still missing peers. + csa.Group.Excluded = excluded + if !csa.missingPeers() { + csa.Group.Excluded = nil + } + // Nothing actually changes, don't propose a no-op migration. + if slices.Equal(csa.Group.Peers, sa.Group.Peers) && slices.Equal(csa.Group.Desired.Peers, basePeers) { + return nil, false + } + // If no peers remain, immediately jump to desired set. + if len(csa.Group.Peers) == 0 { + csa.Group.Peers = csa.Group.Desired.Peers + csa.Group.Cluster = csa.Group.Desired.Cluster + } + return csa, replaced +} + +// Reports which peers still host a consumer of this stream, and so can't +// be dropped from this group yet. +// Lock should be held. +func (sa *streamAssignment) consumerHostedPeers() []string { + var hosted []string + for _, ca := range sa.consumers { + if ca.Config == nil || ca.unsupported != nil { + continue + } + for _, peer := range ca.Group.Peers { + if !slices.Contains(hosted, peer) { + hosted = append(hosted, peer) + } + } + } + return hosted +} + +// Remaps the stream's consumers onto its target peer set. Also reports if all consumers have +// converged, meaning none need to be remapped and none are still moving toward their desired +// peer set. +// Lock should be held. +func (js *jetStream) remapConsumerAssignments(accName string, sa *streamAssignment) (consumers, deleted []*consumerAssignment, done bool) { + targetPeers := sa.targetPeers() + done = true + for ca := range js.consumerAssignmentsOrInflightSeq(accName, sa.Config.Name) { + if ca.Config == nil || ca.unsupported != nil { + continue + } + // Determine the desired replica count. + r := ca.Config.replicas(sa.Config) + // If stream is interest or workqueue policy always remaps since they require peer parity with stream. + if sa.Config.Retention != LimitsPolicy { + r = sa.Config.Replicas + } + consumerPeers := ca.Group.targetPeers() + target := r + var scaleDown bool + if ca.Group.Desired != nil { + // Still moving toward its desired peer set. + done = false + // A pending scale down must not shrink here, only drop peers that are no + // longer part of the stream. The group leader selects which peers remain. + if ca.Group.Desired.ScaleDown { + target = len(consumerPeers) + scaleDown = true + } + } + // Perform a quick check, without allocating, whether this consumer needs to be remapped. + kept := 0 + for _, p := range consumerPeers { + if slices.Contains(targetPeers, p) { + kept++ + } + } + // Backfill can only draw from the target peer set, so that bounds how far we can grow. + size := kept + if size < target { + size = min(target, max(kept, len(targetPeers))) + } else if size > target { + size = target + } + // Leave the consumer alone if its peer set is unaffected. + if kept == len(consumerPeers) && kept == size { + // If the consumer has any peers the stream no longer has, or that still need + // to be peer-removed, we can't skip. + removals := slices.ContainsFunc(ca.Group.Peers, func(p string) bool { + if !slices.Contains(sa.Group.Peers, p) { + return true + } + return sa.Group.Desired != nil && slices.Contains(sa.Group.Desired.Removed, p) && + (ca.Group.Desired == nil || !slices.Contains(ca.Group.Desired.Removed, p)) + }) + if !removals { + continue + } + } + + // Drop peers that are no longer part of the stream. If moving, the tail MUST be the new peer set. + newPeers := make([]string, 0, max(kept, size)) + for _, p := range consumerPeers { + if slices.Contains(targetPeers, p) { + newPeers = append(newPeers, p) + } + } + // Backfill from the shuffled target set until the consumer has its desired number of target peers. + if len(newPeers) < target { + backfill := copyStrings(targetPeers) + rand.Shuffle(len(backfill), func(i, j int) { backfill[i], backfill[j] = backfill[j], backfill[i] }) + for _, p := range backfill { + if len(newPeers) >= target { + break + } + if !slices.Contains(newPeers, p) { + newPeers = append(newPeers, p) + } + } + } else if len(newPeers) > target { + newPeers = newPeers[:target] + } + cca := ca.copyGroup() + // Adjust preferred as needed. + if len(consumerPeers) == 1 && kept == 1 && len(newPeers) > 1 { + // This is scale up from being a singleton, set preferred to that singleton. + cca.Group.Preferred = consumerPeers[0] + } else { + cca.Group.Preferred = _EMPTY_ + } + // Assign new peers. + cca.Group.Peers = newPeers + // Single nodes are not recorded by the NRG layer so we can rename. + if len(ca.Group.Peers) == 1 && ca.Group.Desired == nil { + cca.Group.Name = groupNameForConsumer(cca.Group.Peers, cca.Group.Storage) + } + // If the replicas was not 0 make sure it matches here. + if cca.Config.Replicas != 0 { + // Copy the config before mutating it, since only the assignment was copied. + cfg := *cca.Config + cfg.Replicas = r + cca.Config = &cfg + } + // Always use desired state, so the peers and where they must + // converge to can never diverge. + cca.Group = ca.Group.withDesired(cca.Group) + // Scaled down if we kept at least one peer, but removed others. + // A scale down that was already pending stays pending. + // If the stream has desired state it dictates where the consumer ends up, that's a move. + cca.Group.Desired.ScaleDown = scaleDown || (sa.Group.Desired == nil && kept > 0 && kept != len(consumerPeers)) + + // Drop any peers that are no longer part of the stream's peer set. + var dropped []string + cca.Group.Peers = slices.DeleteFunc(cca.Group.Peers, func(peer string) bool { + if slices.Contains(sa.Group.Peers, peer) { + return false + } + dropped = append(dropped, peer) + return true + }) + cca.Group.Desired.addRemoved(dropped) + // If a consumer lost all of its peers to removal. + if len(cca.Group.Peers) == 0 { + // Delete it if ephemeral. + if !isDurableConsumer(ca.Config) { + deleted = append(deleted, ca) + continue + } + // Durable immediately jumps to the desired peers. + cca.Group.Peers = newPeers + } + // We can not propose here before the stream itself so we collect them. + consumers = append(consumers, cca) + } + // Any consumer we remap here still needs to move onto its new peer set. + done = done && len(consumers) == 0 && len(deleted) == 0 + return consumers, deleted, done +} + +type selectPeerError struct { + excludeTag bool + offline bool + noStorage bool + uniqueTag bool + misc bool + noJsClust bool + noMatchTags map[string]struct{} + excludeTags map[string]struct{} +} + +func (e *selectPeerError) Error() string { + b := strings.Builder{} + writeBoolErrReason := func(hasErr bool, errMsg string) { + if !hasErr { + return + } + b.WriteString(", ") + b.WriteString(errMsg) + } + b.WriteString("no suitable peers for placement") + writeBoolErrReason(e.offline, "peer offline") + writeBoolErrReason(e.excludeTag, "exclude tag set") + writeBoolErrReason(e.noStorage, "insufficient storage") + writeBoolErrReason(e.uniqueTag, "server tag not unique") + writeBoolErrReason(e.misc, "miscellaneous issue") + writeBoolErrReason(e.noJsClust, "jetstream not enabled in cluster") + if len(e.noMatchTags) != 0 { + b.WriteString(", tags not matched [") + var firstTagWritten bool + for tag := range e.noMatchTags { + if firstTagWritten { + b.WriteString(", ") + } + firstTagWritten = true + b.WriteRune('\'') + b.WriteString(tag) + b.WriteRune('\'') + } + b.WriteString("]") + } + if len(e.excludeTags) != 0 { + b.WriteString(", tags excluded [") + var firstTagWritten bool + for tag := range e.excludeTags { + if firstTagWritten { + b.WriteString(", ") + } + firstTagWritten = true + b.WriteRune('\'') + b.WriteString(tag) + b.WriteRune('\'') + } + b.WriteString("]") + } + + return b.String() +} + +func (e *selectPeerError) addMissingTag(t string) { + if e.noMatchTags == nil { + e.noMatchTags = map[string]struct{}{} + } + e.noMatchTags[t] = struct{}{} +} + +func (e *selectPeerError) addExcludeTag(t string) { + if e.excludeTags == nil { + e.excludeTags = map[string]struct{}{} + } + e.excludeTags[t] = struct{}{} +} + +func (e *selectPeerError) accumulate(eAdd *selectPeerError) { + if eAdd == nil { + return + } + acc := func(val *bool, valAdd bool) { + if valAdd { + *val = valAdd + } + } + acc(&e.offline, eAdd.offline) + acc(&e.excludeTag, eAdd.excludeTag) + acc(&e.noStorage, eAdd.noStorage) + acc(&e.uniqueTag, eAdd.uniqueTag) + acc(&e.misc, eAdd.misc) + acc(&e.noJsClust, eAdd.noJsClust) + for tag := range eAdd.noMatchTags { + e.addMissingTag(tag) + } + for tag := range eAdd.excludeTags { + e.addExcludeTag(tag) + } +} + +// selectPeerGroup will select a group of peers to start a raft group. +// when peers exist already the unique tag prefix check for the replaceFirstExisting will be skipped +// js lock should be held. +func (cc *jetStreamCluster) selectPeerGroup(r int, cluster string, cfg *StreamConfig, existing []string, replaceFirstExisting int, ignore []string) ([]string, *selectPeerError) { + if cluster == _EMPTY_ || cfg == nil { + return nil, &selectPeerError{misc: true} + } + + var maxBytes uint64 + if cfg.MaxBytes > 0 { + maxBytes = uint64(cfg.MaxBytes) + } + + // Check for tags. + type tagInfo struct { + tag string + exclude bool + } + var ti []tagInfo + if cfg.Placement != nil { + ti = make([]tagInfo, 0, len(cfg.Placement.Tags)) + for _, t := range cfg.Placement.Tags { + ti = append(ti, tagInfo{ + tag: strings.TrimPrefix(t, "!"), + exclude: strings.HasPrefix(t, "!"), + }) + } + } + + // Used for weighted sorting based on availability. + type wn struct { + id string + avail uint64 + off bool + ha int + ns int + } + + var nodes []wn + // peers is a randomized list + s, peers := cc.s, cc.meta.Peers() + + uniqueTagPrefix := s.getOpts().JetStreamUniqueTag + if uniqueTagPrefix != _EMPTY_ { + for _, t := range ti { + if strings.HasPrefix(t.tag, uniqueTagPrefix) { + // disable uniqueness check if explicitly listed in tags + uniqueTagPrefix = _EMPTY_ + break + } + } + } + var uniqueTags = make(map[string]*nodeInfo) + + checkUniqueTag := func(ni *nodeInfo) (bool, *nodeInfo) { + for _, t := range ni.tags { + if strings.HasPrefix(t, uniqueTagPrefix) { + if n, ok := uniqueTags[t]; !ok { + uniqueTags[t] = ni + return true, ni + } else { + return false, n + } + } + } + // default requires the unique prefix to be present + return false, nil + } + + // Map existing. + var ep map[string]struct{} + if le := len(existing); le > 0 { + if le >= r { + return existing[:r], nil + } + ep = make(map[string]struct{}) + for i, p := range existing { + ep[p] = struct{}{} + if uniqueTagPrefix == _EMPTY_ { + continue + } + si, ok := s.nodeToInfo.Load(p) + if !ok || si == nil || i < replaceFirstExisting { + continue + } + ni := si.(nodeInfo) + // collect unique tags, but do not require them as this node is already part of the peerset + checkUniqueTag(&ni) + } + } + + // Map ignore + var ip map[string]struct{} + if li := len(ignore); li > 0 { + ip = make(map[string]struct{}) + for _, p := range ignore { + ip[p] = struct{}{} + } + } + + // Grab the number of streams and HA assets currently assigned to each peer. + // HAAssets under usage is async, so calculate here in realtime based on assignments. + peerStreams := make(map[string]int, len(peers)) + peerHA := make(map[string]int, len(peers)) + for _, asa := range cc.streams { + for _, sa := range asa { + if sa.unsupported != nil { + continue + } + isHA := len(sa.Group.Peers) > 1 + for _, peer := range sa.Group.Peers { + peerStreams[peer]++ + if isHA { + peerHA[peer]++ + } + } + } + } + + maxHaAssets := s.getOpts().JetStreamLimits.MaxHAAssets + + // An error is a result of multiple individual placement decisions. + // Which is why we keep taps on how often which one happened. + err := selectPeerError{} + + var onlinePeers int + + // Shuffle them up. + rand.Shuffle(len(peers), func(i, j int) { peers[i], peers[j] = peers[j], peers[i] }) + for _, p := range peers { + si, ok := s.nodeToInfo.Load(p.ID) + if !ok || si == nil { + err.misc = true + continue + } + ni := si.(nodeInfo) + // Only select from the designated named cluster. + if ni.cluster != cluster { + s.Debugf("Peer selection: discard %s@%s reason: not target cluster %s", ni.name, ni.cluster, cluster) + continue + } + + // If we've never heard from a server, don't consider. + if !ni.selectable() { + s.Debugf("Peer selection: discard %s@%s reason: offline", ni.name, ni.cluster) + err.offline = true + continue + } + + // If ignore skip + if _, ok := ip[p.ID]; ok { + continue + } + + // If existing also skip, we will add back in to front of the list when done. + if _, ok := ep[p.ID]; ok { + continue + } + + if ni.tags.Contains(jsExcludePlacement) { + s.Debugf("Peer selection: discard %s@%s tags: %v reason: %s present", + ni.name, ni.cluster, ni.tags, jsExcludePlacement) + err.excludeTag = true + continue + } + + if len(ti) > 0 { + matched := true + for _, t := range ti { + contains := ni.tags.Contains(t.tag) + if t.exclude && contains { + matched = false + s.Debugf("Peer selection: discard %s@%s tags: %v reason: excluded tag %s present", + ni.name, ni.cluster, ni.tags, t) + err.addExcludeTag(t.tag) + break + } else if !t.exclude && !contains { + matched = false + s.Debugf("Peer selection: discard %s@%s tags: %v reason: mandatory tag %s not present", + ni.name, ni.cluster, ni.tags, t) + err.addMissingTag(t.tag) + break + } + } + if !matched { + continue + } + } + + var available uint64 + if ni.stats != nil { + switch cfg.Storage { + case MemoryStorage: + used := ni.stats.ReservedMemory + if ni.stats.Memory > used { + used = ni.stats.Memory + } + if ni.cfg.MaxMemory > int64(used) { + available = uint64(ni.cfg.MaxMemory) - used + } + case FileStorage: + used := ni.stats.ReservedStore + if ni.stats.Store > used { + used = ni.stats.Store + } + if ni.cfg.MaxStore > int64(used) { + available = uint64(ni.cfg.MaxStore) - used + } + } + } + + // Otherwise check if we have enough room if maxBytes set. + if maxBytes > 0 && maxBytes > available { + s.Warnf("Peer selection: discard %s@%s (Max Bytes: %d) exceeds available %s storage of %d bytes", + ni.name, ni.cluster, maxBytes, cfg.Storage.String(), available) + err.noStorage = true + continue + } + // HAAssets contain _meta_ which we want to ignore, hence > and not >=. + if maxHaAssets > 0 && ni.stats != nil && ni.stats.HAAssets > maxHaAssets { + s.Warnf("Peer selection: discard %s@%s (HA Asset Count: %d) exceeds max ha asset limit of %d for stream placement", + ni.name, ni.cluster, ni.stats.HAAssets, maxHaAssets) + err.misc = true + continue + } + + if uniqueTagPrefix != _EMPTY_ { + if unique, owner := checkUniqueTag(&ni); !unique { + if owner != nil { + s.Debugf("Peer selection: discard %s@%s tags:%v reason: unique prefix %s owned by %s@%s", + ni.name, ni.cluster, ni.tags, owner.name, owner.cluster) + } else { + s.Debugf("Peer selection: discard %s@%s tags:%v reason: unique prefix %s not present", + ni.name, ni.cluster, ni.tags) + } + err.uniqueTag = true + continue + } + } + // Add to our list of potential nodes. + nodes = append(nodes, wn{p.ID, available, ni.offline, peerHA[p.ID], peerStreams[p.ID]}) + if !ni.offline { + onlinePeers++ + } + } + + // If we could not select enough peers, fail. + quorum := r/2 + 1 + missingQuorum := onlinePeers+len(existing) < quorum + if missingNodes := len(nodes) < (r - len(existing)); missingNodes || missingQuorum { + if len(peers) == 0 { + err.noJsClust = true + } else if !missingNodes && missingQuorum { + err.offline = true + } + s.Debugf("Peer selection: required %d nodes but found %d (cluster: %s replica: %d existing: %v/%d peers: %d result-peers: %d err: %+v)", + r-len(existing), len(nodes), cluster, r, existing, replaceFirstExisting, len(peers), len(nodes), err) + return nil, &err + } + // Sort based on available from most to least, breaking ties by number of total streams assigned to the peer. + slices.SortFunc(nodes, func(i, j wn) int { + // Prefer online servers to offline ones. + if i.off != j.off { + if i.off { + return 1 + } else { + return -1 + } + } + if i.avail == j.avail { + return cmp.Compare(i.ns, j.ns) + } + return -cmp.Compare(i.avail, j.avail) // reverse + }) + // If we are placing a replicated stream, let's sort based on HAAssets, as that is more important to balance. + if cfg.Replicas > 1 { + slices.SortStableFunc(nodes, func(i, j wn) int { + // Prefer online servers to offline ones. + if i.off != j.off { + if i.off { + return 1 + } else { + return -1 + } + } + return cmp.Compare(i.ha, j.ha) + }) + } + + var results []string + if len(existing) > 0 { + results = append(results, existing...) + r -= len(existing) + } + for _, r := range nodes[:r] { + results = append(results, r.id) + } + return results, nil +} + +func groupNameForStream(peers []string, storage StorageType) string { + return groupName("S", peers, storage) +} + +func groupNameForConsumer(peers []string, storage StorageType) string { + return groupName("C", peers, storage) +} + +func groupName(prefix string, peers []string, storage StorageType) string { + gns := getHash(nuid.Next()) + return fmt.Sprintf("%s-R%d%s-%s", prefix, len(peers), storage.String()[:1], gns) +} + +// returns stream count for this tier as well as applicable reservation size (not including cfg) +// jetStream read lock should be held +func (js *jetStream) tieredStreamAndReservationCount(accName, tier string, cfg *StreamConfig) (int, int64) { + var numStreams int + var reservation int64 + for sa := range js.streamAssignmentsOrInflightSeq(accName) { + // Don't count the stream toward the limit if it already exists. + if sa.Config.Name == cfg.Name { + continue + } + if tier == _EMPTY_ || isSameTier(sa.Config.Replicas, cfg.Replicas) { + numStreams++ + if sa.Config.MaxBytes > 0 && sa.Config.Storage == cfg.Storage { + reservation = addSaturate(reservation, accountReservation(tier, sa.Config.Replicas, sa.Config.MaxBytes)) + } + } + } + return numStreams, reservation +} + +// createGroupForStream will create a group for assignment for the stream. +// Lock should be held. +func (js *jetStream) createGroupForStream(ci *ClientInfo, cfg *StreamConfig) (*raftGroup, *selectPeerError) { + replicas := cfg.Replicas + if replicas == 0 { + replicas = 1 + } + + // Default connected cluster from the request origin. + cc, cluster := js.cluster, ci.Cluster + // If specified, override the default. + clusterDefined := cfg.Placement != nil && cfg.Placement.Cluster != _EMPTY_ + if clusterDefined { + cluster = cfg.Placement.Cluster + } + clusters := []string{cluster} + if !clusterDefined { + clusters = append(clusters, ci.Alternates...) + } + + // Need to create a group here. + errs := &selectPeerError{} + for _, cn := range clusters { + peers, err := cc.selectPeerGroup(replicas, cn, cfg, nil, 0, nil) + if len(peers) < replicas { + errs.accumulate(err) + continue + } + return &raftGroup{Name: groupNameForStream(peers, cfg.Storage), Storage: cfg.Storage, Peers: peers, Cluster: cn}, nil + } + return nil, errs +} + +func (acc *Account) selectLimits(replicas int) (*JetStreamAccountLimits, string, *jsAccount, *ApiError) { + // Grab our jetstream account info. + acc.mu.RLock() + jsa := acc.js + acc.mu.RUnlock() + + if jsa == nil { + return nil, _EMPTY_, nil, NewJSNotEnabledForAccountError() + } + + jsa.usageMu.RLock() + selectedLimits, tierName, ok := jsa.selectLimits(replicas) + jsa.usageMu.RUnlock() + + if !ok { + return nil, _EMPTY_, nil, NewJSNoLimitsError() + } + return &selectedLimits, tierName, jsa, nil +} + +// Read lock needs to be held +func (js *jetStream) jsClusteredStreamLimitsCheck(acc *Account, cfg *StreamConfig) *ApiError { + var replicas int + if cfg != nil { + replicas = cfg.Replicas + } + selectedLimits, tier, _, apiErr := acc.selectLimits(replicas) + if apiErr != nil { + return apiErr + } + + numStreams, reservations := js.tieredStreamAndReservationCount(acc.Name, tier, cfg) + if selectedLimits.MaxStreams > 0 && numStreams >= selectedLimits.MaxStreams { + return NewJSMaximumStreamsLimitError() + } + // Check for account limits here before proposing. + if err := js.checkAccountLimits(selectedLimits, tier, cfg, reservations); err != nil { + return NewJSStreamLimitsError(err, Unless(err)) + } + return nil +} + +func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject, reply string, rmsg []byte, config *StreamConfigRequest) { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} + + ccfg, apiErr := s.checkStreamCfg(&config.StreamConfig, acc, config.Pedantic) + if apiErr != nil { + resp.Error = apiErr + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + cfg := &ccfg + + // Now process the request and proposal. + js.mu.Lock() + defer js.mu.Unlock() + + var self *streamAssignment + var rg *raftGroup + var syncSubject string + + // Capture if we have existing/inflight assignment first. + if osa := js.streamAssignmentOrInflight(acc.Name, cfg.Name); osa != nil { + copyStreamMetadata(cfg, osa.Config) + // Set the index name on both to ensure the DeepEqual works + currentIName := make(map[string]struct{}) + for _, s := range osa.Config.Sources { + currentIName[s.iname] = struct{}{} + } + for _, s := range cfg.Sources { + s.setIndexName() + if _, ok := currentIName[s.iname]; !ok { + s.iname = _EMPTY_ + } + } + if !reflect.DeepEqual(osa.Config, cfg) { + resp.Error = NewJSStreamNameExistError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + // This is an equal assignment. + self, rg, syncSubject = osa, osa.Group, osa.Sync + } + + if cfg.Sealed { + resp.Error = NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration for create can not be sealed")) + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + // Check for subject collisions here. + if js.subjectsOverlap(acc.Name, cfg.Subjects, self) { + resp.Error = NewJSStreamSubjectOverlapError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + apiErr = js.jsClusteredStreamLimitsCheck(acc, cfg) + // Check for stream limits here before proposing. These need to be tracked from meta layer, not jsa. + if apiErr != nil { + resp.Error = apiErr + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + // Create a new one here if needed. + if rg == nil { + nrg, err := js.createGroupForStream(ci, cfg) + if err != nil { + resp.Error = NewJSClusterNoPeersError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + rg = nrg + // Pick a preferred leader. + rg.setPreferred(s) + } + + if syncSubject == _EMPTY_ { + syncSubject = syncSubjForStream() + } + // Preserve the original creation time on an idempotent create retry. + created := time.Now().UTC() + if self != nil { + created = self.Created + } + // Sync subject for post snapshot sync. + sa := &streamAssignment{Group: rg, Sync: syncSubject, Config: cfg, Subject: subject, Reply: reply, Client: ci, Created: created} + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(sa)); err != nil { + return + } + // On success, add this as an inflight proposal so we can apply limits + // on concurrent create requests while this stream assignment has + // possibly not been processed yet. + cc.trackInflightStreamProposal(acc.Name, sa, false) +} + +var ( + errReqTimeout = errors.New("timeout while waiting for response") + errReqSrvExit = errors.New("server shutdown while waiting for response") +) + +// blocking utility call to perform requests on the system account +// returns (synchronized) v or error +func sysRequest[T any](s *Server, subjFormat string, args ...any) (*T, error) { + isubj := fmt.Sprintf(subjFormat, args...) + + s.mu.Lock() + if s.sys == nil { + s.mu.Unlock() + return nil, ErrNoSysAccount + } + inbox := s.newRespInbox() + results := make(chan *T, 1) + s.sys.replies[inbox] = func(_ *subscription, _ *client, _ *Account, _, _ string, msg []byte) { + var v T + if err := json.Unmarshal(msg, &v); err != nil { + s.Warnf("Error unmarshalling response for request '%s':%v", isubj, err) + return + } + select { + case results <- &v: + default: + s.Warnf("Failed placing request response on internal channel") + } + } + s.mu.Unlock() + + s.sendInternalMsgLocked(isubj, inbox, nil, nil) + + defer func() { + s.mu.Lock() + defer s.mu.Unlock() + if s.sys != nil && s.sys.replies != nil { + delete(s.sys.replies, inbox) + } + }() + + ttl := time.NewTimer(2 * time.Second) + defer ttl.Stop() + + select { + case <-s.quitCh: + return nil, errReqSrvExit + case <-ttl.C: + return nil, errReqTimeout + case data := <-results: + return data, nil + } +} + +// jsClusteredStreamCancelMoveLocked proposes a rollback of the stream's in-flight desired +// state to its recorded origin. The caller MUST have verified an origin exists, via +// desiredOrigin. The client is responded to once the rollback proposal applies. +// Lock should be held. +func (s *Server) jsClusteredStreamCancelMoveLocked(osa *streamAssignment, accName, reply string) { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil || cc.meta == nil { + return + } + origin := osa.desiredOrigin() + if origin == nil { + return + } + // A rollback of a move still counts as a move in flight, it must keep blocking other + // moves and scales until it completes. Must capture before the group is retargeted. + moveInFlight := osa.moveInFlight() + + csa := osa.copyGroup() + // Need to respond to the client that cancels. + csa.Reply = reply + // Revert replicas, placement and retention in the config immediately. + csa.Config = osa.Config.clone() + csa.Config.Replicas = origin.Replicas + csa.Config.Placement = origin.Placement.clone() + if origin.Retention != nil { + csa.Config.Retention = *origin.Retention + } + // Move back to the initial peer set via desired state. + csa.Group.Peers = copyStrings(origin.Peers) + csa.Group.Cluster = origin.Cluster + csa.Group = osa.Group.withDesired(csa.Group) + csa.Group.Desired.Move = moveInFlight + // withDesired only carries over a prior origin, a legacy move has none yet. + // Record it, so the rollback reports the same target while it converges. + if csa.Group.Desired.Origin == nil { + csa.Group.Desired.Origin = origin + } + if err := cc.meta.Propose(cc.term, encodeUpdateStreamAssignment(csa)); err != nil { + return + } + cc.trackInflightStreamProposal(accName, csa, false) +} + +func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, subject, reply string, rmsg []byte, cfg *StreamConfig, pedantic bool) { + js := s.getJetStream() + if js == nil { + return + } + js.mu.Lock() + defer js.mu.Unlock() + s.jsClusteredStreamUpdateRequestLocked(ci, acc, subject, reply, rmsg, cfg, nil, _EMPTY_, pedantic) +} + +// peerSetCluster is the cluster peerSet is placed in, it MUST be provided if peerSet is. +// Lock should be held. +func (s *Server) jsClusteredStreamUpdateRequestLocked(ci *ClientInfo, acc *Account, subject, reply string, rmsg []byte, cfg *StreamConfig, peerSet []string, peerSetCluster string, pedantic bool) { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + meta := cc.meta + if meta == nil { + return + } + + var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + + osa := js.streamAssignmentOrInflight(acc.Name, cfg.Name) + if osa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + // Don't allow updating if all peers are offline. + if s.allPeersOffline(osa.Group) { + resp.Error = NewJSStreamOfflineError() + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + + // Update asset version metadata. + setStaticStreamMetadata(cfg) + + var newCfg *StreamConfig + if jsa := js.accounts[acc.Name]; jsa != nil { + ncfg, err := jsa.configUpdateCheckLocked(osa.Config, cfg, s, pedantic) + if err != nil { + resp.Error = NewJSStreamUpdateError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } else { + newCfg = ncfg + } + } else { + resp.Error = NewJSNotEnabledForAccountError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + // Check for mirror changes which are not allowed. + // We will allow removing the mirror config to "promote" the mirror to a normal stream. + if newCfg.Mirror != nil && !reflect.DeepEqual(newCfg.Mirror, osa.Config.Mirror) { + resp.Error = NewJSStreamMirrorNotUpdatableError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + // Check for subject collisions here. + if js.subjectsOverlap(acc.Name, newCfg.Subjects, osa) { + resp.Error = NewJSStreamSubjectOverlapError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + // In the event that some of the stream-level limits have changed, yell appropriately + // if any of the consumers exceed that limit. + oldInactiveThreshold, newInactiveThreshold := osa.Config.ConsumerLimits.InactiveThreshold, newCfg.ConsumerLimits.InactiveThreshold + oldMaxAckPending, newMaxAckPending := osa.Config.ConsumerLimits.MaxAckPending, newCfg.ConsumerLimits.MaxAckPending + updateLimits := (newInactiveThreshold > 0 && oldInactiveThreshold != newInactiveThreshold) || + (newMaxAckPending > 0 && oldMaxAckPending != newMaxAckPending) + if updateLimits { + var errorConsumers []string + for ca := range js.consumerAssignmentsOrInflightSeq(acc.Name, newCfg.Name) { + if ca.Config == nil { + continue + } + if (newInactiveThreshold > 0 && ca.Config.InactiveThreshold > newInactiveThreshold) || + (newMaxAckPending > 0 && ca.Config.MaxAckPending > newMaxAckPending) { + errorConsumers = append(errorConsumers, ca.Name) + } + } + if len(errorConsumers) > 0 { + err := fmt.Errorf("change to limits violates consumers: %s", strings.Join(errorConsumers, ", ")) + resp.Error = NewJSStreamUpdateError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + } + + // Check for a move request. + var isMoveRequest bool + if lPeerSet := len(peerSet); lPeerSet > 0 { + isMoveRequest = true + } else { + isMoveRequest = newCfg.Placement != nil && !reflect.DeepEqual(osa.Config.Placement, newCfg.Placement) + } + + // Check for replica or retention changes. + isReplicaChange := newCfg.Replicas != osa.Config.Replicas + isRetentionChange := newCfg.Retention != osa.Config.Retention + + // Combining a move and a scale in a single update is not allowed. + if isMoveRequest && (isReplicaChange || isRetentionChange) { + resp.Error = NewJSStreamMoveAndScaleError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + // A move retargets the desired peer set, so only one can be inflight at a time. + // Otherwise, peers of the abandoned target could accumulate in the assignment + // unbounded. A move can't combine with any other inflight reconfiguration across + // requests either, a scale and a retention change both converge through desired + // state. A legacy move counts as well, it is inflight but not expressed as desired + // state yet. + moveInFlight := osa.moveInFlight() + if isMoveRequest { + if moveInFlight { + resp.Error = NewJSStreamMoveInProgressError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + if osa.Group.Desired != nil { + resp.Error = NewJSStreamReconfigureInProgressError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + } else if (isReplicaChange || isRetentionChange) && moveInFlight { + // A scale can't combine with an inflight move either. Scales may freely stack + // onto an inflight scale or retention change, but not with a move. + resp.Error = NewJSStreamMoveInProgressError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + // Make copy so to not change original. + rg := osa.copyGroup().Group + + // Reset notion of scaling up, if this was done in a previous update. Must be + // preserved while a migration is inflight, so peers that create their raft + // node late keep the empty-log protection. + if rg.Desired == nil { + rg.ScaleUp = false + } + if isMoveRequest { + if len(peerSet) == 0 { + nrg, err := js.createGroupForStream(ci, newCfg) + if err != nil { + resp.Error = NewJSClusterNoPeersError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + // Overwrite to the new group, but MUST keep the same group name. + name := rg.Name + rg = nrg + rg.Name = name + } else { + if len(rg.Peers) == 1 { + rg.Preferred = peerSet[0] + } + rg.Peers = peerSet + rg.Cluster = peerSetCluster + } + rg = osa.Group.withDesired(rg) + rg.Desired.Move = true + } else if isReplicaChange { + currentPeers := rg.Peers + if osa.Group.Desired != nil { + currentPeers = osa.Group.Desired.Peers + } + isScaleUp := newCfg.Replicas > len(currentPeers) + // We are adding new peers here. + if isScaleUp { + // Check that we have the allocation available. + if err := js.jsClusteredStreamLimitsCheck(acc, newCfg); err != nil { + resp.Error = err + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + // Check if we do not have a cluster assigned, and if we do not make sure we + // try to pick one. This could happen with older streams that were assigned by + // previous servers. + if rg.Cluster == _EMPTY_ { + // Prefer placement directives if we have them. + if newCfg.Placement != nil && newCfg.Placement.Cluster != _EMPTY_ { + rg.Cluster = newCfg.Placement.Cluster + } else { + // Fall back to the cluster assignment from the client. + rg.Cluster = ci.Cluster + } + } + peers, err := cc.selectPeerGroup(newCfg.Replicas, rg.Cluster, newCfg, currentPeers, 0, nil) + if err != nil { + resp.Error = NewJSClusterNoPeersError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + // Single nodes are not recorded by the NRG layer so we can rename. + if len(osa.Group.Peers) == 1 && osa.Group.Desired == nil { + rg.Name = groupNameForStream(peers, rg.Storage) + } + if len(currentPeers) == 1 { + // This is scale up from being a singleton, set preferred to that singleton. + rg.Preferred = currentPeers[0] + } + rg.Peers = peers + rg = osa.Group.withDesired(rg) + } else { + // Mark the group as scaling down, the current leader will be preserved. + rg.Peers = currentPeers + rg = osa.Group.withDesired(rg) + rg.Desired.ScaleDown = true + } + } else { + // All other updates make sure no preferred is set. + rg.Preferred = _EMPTY_ + } + + // If we're the first to specify an origin for desired state, capture it. + rg.populateOrigin(osa) + + // A retention change must go through desired state, so consumers (if any) can be scaled first. + if isRetentionChange { + var converged bool + // Only register the retention if any consumers need to be remapped (or we already had desired state). + if rg.Desired == nil { + // Simulate remapping against the updated group. Only the group and config + // are needed, the consumers are looked up on the applied assignment. + tsa := &streamAssignment{Group: rg, Config: newCfg} + _, _, converged = js.remapConsumerAssignments(acc.Name, tsa) + } + if !converged { + rg = rg.withRetentionChange(osa, newCfg.Retention) + } + } + + syncSubject := osa.Sync + if syncSubject == _EMPTY_ { + syncSubject = syncSubjForStream() + } + sa := &streamAssignment{Group: rg, Sync: syncSubject, Created: osa.Created, Config: newCfg, Subject: subject, Reply: reply, Client: ci} + if err := meta.Propose(cc.term, encodeUpdateStreamAssignment(sa)); err != nil { + return + } + cc.trackInflightStreamProposal(acc.Name, sa, false) +} + +func (s *Server) jsClusteredStreamDeleteRequest(ci *ClientInfo, acc *Account, stream, subject, reply string, rmsg []byte) { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + if cc.meta == nil { + return + } + + osa := js.streamAssignmentOrInflight(acc.Name, stream) + if osa == nil { + var resp = JSApiStreamDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamDeleteResponseType}} + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + sa := &streamAssignment{Group: osa.Group, Config: osa.Config, Subject: subject, Reply: reply, Client: ci, Created: osa.Created} + if err := cc.meta.Propose(cc.term, encodeDeleteStreamAssignment(sa)); err != nil { + return + } + cc.trackInflightStreamProposal(acc.Name, sa, true) +} + +// Process a clustered purge request. +func (s *Server) jsClusteredStreamPurgeRequest( + ci *ClientInfo, + acc *Account, + mset *stream, + stream, subject, reply string, + rmsg []byte, + preq *JSApiStreamPurgeRequest, +) { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil || mset == nil { + return + } + + js.mu.Lock() + sa := js.streamAssignment(acc.Name, stream) + if sa == nil { + resp := JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + js.mu.Unlock() + return + } + + if n := sa.Group.node; n != nil { + sp := encodeStreamPurge(&streamPurge{Stream: stream, LastSeq: mset.state().LastSeq, Subject: subject, Reply: reply, Client: ci, Request: preq}) + js.mu.Unlock() + mset.mu.RLock() + term := mset.term + mset.mu.RUnlock() + n.Propose(term, sp) + return + } + js.mu.Unlock() + + var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} + purged, err := mset.purge(preq) + if err != nil { + resp.Error = NewJSStreamGeneralError(err, Unless(err)) + } else { + resp.Purged = purged + resp.Success = true + } + s.sendAPIResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(resp)) +} + +func (s *Server) jsClusteredStreamRestoreRequest( + ci *ClientInfo, + acc *Account, + req *JSApiStreamRestoreRequest, + subject, reply string, rmsg []byte) { + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + resp := JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}} + + // check stream config at the start of the restore process, not at the end + cfg, apiErr := s.checkStreamCfg(&req.Config, acc, false) + if apiErr != nil { + resp.Error = apiErr + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + if cc.meta == nil { + return + } + + if err := js.jsClusteredStreamLimitsCheck(acc, &cfg); err != nil { + resp.Error = err + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + if sa := js.streamAssignmentOrInflight(ci.serviceAccount(), cfg.Name); sa != nil { + resp.Error = NewJSStreamNameExistRestoreFailedError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + + // Raft group selection and placement. + rg, err := js.createGroupForStream(ci, &cfg) + if err != nil { + resp.Error = NewJSClusterNoPeersError(err) + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + // Pick a preferred leader. + rg.setPreferred(s) + sa := &streamAssignment{Group: rg, Sync: syncSubjForStream(), Config: &cfg, Subject: subject, Reply: reply, Client: ci, Created: time.Now().UTC()} + // Now add in our restore state and pre-select a peer to handle the actual receipt of the snapshot. + sa.Restore = &req.State + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(sa)); err != nil { + return + } + cc.trackInflightStreamProposal(ci.serviceAccount(), sa, false) +} + +// Determine if all peers for this group are offline. +func (s *Server) allPeersOffline(rg *raftGroup) bool { + if rg == nil { + return false + } + // Check to see if this stream has any servers online to respond. + for _, peer := range rg.Peers { + if si, ok := s.nodeToInfo.Load(peer); ok && si != nil { + if !si.(nodeInfo).offline { + return false + } + } + } + return true +} + +// sortScaleDownPeers returns the candidate peers sorted by how preferable +// they are to keep when scaling a raft group down, most preferable first: +// 1. the current leader, if known and a candidate +// 2. peers in the current peer set that are online, least lag first +// 3. peers in the current peer set that are offline, least lag first +// 4. peers not in the current peer set, these are treated as offline +func (s *Server) sortScaleDownPeers(curLeader string, current []*Peer, candidates []string) []string { + peers := make(map[string]*Peer, len(current)) + for _, p := range current { + peers[p.ID] = p + } + + // Rank each candidate: a lower tier is more preferable to keep, lag breaks + // ties within a tier. Candidate order breaks any remaining ties. + type rank struct { + tier int + lag uint64 + } + ranks := make(map[string]rank, len(candidates)) + for _, id := range candidates { + if id == curLeader { + ranks[id] = rank{tier: 0} + continue + } + p := peers[id] + if p == nil { + ranks[id] = rank{tier: 3} + continue + } + // Unknown peers are treated as offline. + offline := true + if si, ok := s.nodeToInfo.Load(id); ok && si != nil { + offline = si.(nodeInfo).offline + } + if offline { + ranks[id] = rank{tier: 2, lag: p.Lag} + } else { + ranks[id] = rank{tier: 1, lag: p.Lag} + } + } + + sorted := copyStrings(candidates) + slices.SortStableFunc(sorted, func(a, b string) int { + ra, rb := ranks[a], ranks[b] + if ra.tier != rb.tier { + return cmp.Compare(ra.tier, rb.tier) + } + return cmp.Compare(ra.lag, rb.lag) + }) + return sorted +} + +// Select the peers to keep when scaling a raft group down to replicas. +// The current leader, if known and a candidate, is always kept. Online peers +// with the least lag are preferred, but we will fall back to offline peers to +// honor the requested replica count. +func (s *Server) selectScaleDownPeers(curLeader string, current []*Peer, peers []string, replicas int) []string { + sorted := s.sortScaleDownPeers(curLeader, current, peers) + if len(sorted) > replicas { + sorted = sorted[:replicas] + } + return sorted +} + +// selectPeerToRemove picks the peer from remaining that is most preferable to +// remove during scale-down: peers unknown to the group first, then offline +// peers, then online peers with the most lag. The current leader, if among the +// candidates, is picked last so leadership is transferred at most once. +func (s *Server) selectPeerToRemove(curLeader string, current []*Peer, remaining, hosted []string) string { + // A peer that still hosts a consumer keeps serving the stream's data until the + // consumer has migrated off, so it's not up for removal yet. + remaining = slices.DeleteFunc(remaining, func(peer string) bool { + return slices.Contains(hosted, peer) + }) + if len(remaining) == 0 { + return _EMPTY_ + } + sorted := s.sortScaleDownPeers(curLeader, current, remaining) + if len(sorted) == 0 { + return _EMPTY_ + } + return sorted[len(sorted)-1] +} + +// canRemovePeer reports whether the group can still commit after removing the +// given peer. Only current peers we've heard from recently are counted as +// voters; we always count ourselves. This is the removal counterpart to the +// quorum check in selectPeerToAdd. +func (s *Server) canRemovePeer(ourPeerId, remove string, actual []*Peer) bool { + cutoff := time.Now().Add(-hbInterval * 3) + var voters int + for _, p := range actual { + if p.ID == remove { + continue + } + heard := p.ID == ourPeerId || (!p.Last.IsZero() && p.Last.After(cutoff)) + if p.Current && heard { + voters++ + } + } + return voters >= (len(actual)-1)/2+1 +} + +// selectPeerToAdd picks the peer from candidates that is most preferable to +// add during a migration: the one we heard from most recently. An unheard +// candidate is only picked if the live members still form a quorum in the +// grown group, so adding an offline peer can't stall it. Empty if no peer +// can be added safely right now. +func (s *Server) selectPeerToAdd(n RaftNode, ourPeerId string, current []*Peer, candidates []string) string { + if len(candidates) == 0 { + return _EMPTY_ + } + cutoff := time.Now().Add(-hbInterval * 3) + heard := func(ts time.Time) bool { return !ts.IsZero() && ts.After(cutoff) } + online := func(peer string) bool { + si, ok := s.nodeToInfo.Load(peer) + return ok && si != nil && !si.(nodeInfo).offline + } + // Prefer the candidate we've heard from most recently. + add, last := _EMPTY_, time.Time{} + for _, peer := range candidates { + if ts := n.LastHeardFromPeer(peer); online(peer) && heard(ts) && ts.After(last) { + add, last = peer, ts + } + } + if add != _EMPTY_ { + return add + } + // Otherwise, only add a peer if the members we've recently heard from can + // still reach quorum after the group has grown. + live := 0 + for _, p := range current { + if p.ID == ourPeerId || (online(p.ID) && heard(p.Last)) { + live++ + } + } + if quorum := (len(current)+1)/2 + 1; live >= quorum { + return candidates[0] + } + return _EMPTY_ +} + +// selectStepDownPreferred picks the peer to transfer leadership to before the +// current leader removes itself from the group: the most preferable member of +// the desired peer set. Empty if no desired peer is part of the group yet. +func (s *Server) selectStepDownPreferred(ourPeerId string, current []*Peer, desired []string) string { + var candidates []string + for _, p := range current { + if p.ID != ourPeerId && slices.Contains(desired, p.ID) { + candidates = append(candidates, p.ID) + } + } + sorted := s.sortScaleDownPeers(_EMPTY_, current, candidates) + if len(sorted) == 0 { + return _EMPTY_ + } + return sorted[0] +} + +// This will do a scatter and gather operation for all streams for this account. This is only called from metadata leader. +// This will be running in a separate Go routine. +func (s *Server) jsClusteredStreamListRequest(acc *Account, ci *ClientInfo, filter string, offset int, subject, reply string, rmsg []byte) { + defer s.grWG.Done() + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.RLock() + + var streams []*streamAssignment + for _, sa := range cc.streams[acc.Name] { + if IsNatsErr(sa.err, JSClusterNotAssignedErr) { + continue + } + + if filter != _EMPTY_ { + // These could not have subjects auto-filled in since they are raw and unprocessed. + if len(sa.Config.Subjects) == 0 { + if SubjectsCollide(filter, sa.Config.Name) { + streams = append(streams, sa) + } + } else { + for _, subj := range sa.Config.Subjects { + if SubjectsCollide(filter, subj) { + streams = append(streams, sa) + break + } + } + } + } else { + streams = append(streams, sa) + } + } + + // Needs to be sorted for offsets etc. + if len(streams) > 1 { + slices.SortFunc(streams, func(i, j *streamAssignment) int { return cmp.Compare(i.Config.Name, j.Config.Name) }) + } + + scnt := len(streams) + if offset < 0 { + offset = 0 + } + if offset > scnt { + offset = scnt + } + if offset > 0 { + streams = streams[offset:] + } + if len(streams) > JSApiListLimit { + streams = streams[:JSApiListLimit] + } + + var resp = JSApiStreamListResponse{ + ApiResponse: ApiResponse{Type: JSApiStreamListResponseType}, + Streams: make([]*StreamInfo, 0, len(streams)), + } + + js.mu.RUnlock() + + if len(streams) == 0 { + resp.Limit = JSApiListLimit + resp.Offset = offset + s.sendAPIResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(resp)) + return + } + + // Create an inbox for our responses and send out our requests. + s.mu.Lock() + inbox := s.newRespInbox() + rc := make(chan *streamInfoClusterResponse, len(streams)) + + // Store our handler. + s.sys.replies[inbox] = func(sub *subscription, _ *client, _ *Account, subject, _ string, msg []byte) { + var si streamInfoClusterResponse + if err := json.Unmarshal(msg, &si); err != nil { + s.Warnf("Error unmarshalling clustered stream info response:%v", err) + return + } + select { + case rc <- &si: + default: + s.Warnf("Failed placing remote stream info result on internal channel") + } + } + s.mu.Unlock() + + // Cleanup after. + defer func() { + s.mu.Lock() + if s.sys != nil && s.sys.replies != nil { + delete(s.sys.replies, inbox) + } + s.mu.Unlock() + }() + + var missingNames []string + sent := map[string]int{} + + // Send out our requests here. + js.mu.RLock() + for _, sa := range streams { + if s.allPeersOffline(sa.Group) { + // Place offline onto our results by hand here. + si := &StreamInfo{ + Config: *sa.Config, + Created: sa.Created, + Cluster: js.offlineClusterInfo(sa.Group), + TimeStamp: time.Now().UTC(), + } + resp.Streams = append(resp.Streams, si) + missingNames = append(missingNames, sa.Config.Name) + } else { + isubj := fmt.Sprintf(clusterStreamInfoT, sa.Client.serviceAccount(), sa.Config.Name) + s.sendInternalMsgLocked(isubj, inbox, nil, nil) + sent[sa.Config.Name] = len(sa.consumers) + } + } + // Don't hold lock. + js.mu.RUnlock() + + const timeout = 4 * time.Second + notActive := time.NewTimer(timeout) + defer notActive.Stop() + +LOOP: + for len(sent) > 0 { + select { + case <-s.quitCh: + return + case <-notActive.C: + s.Warnf("Did not receive all stream info results for %q", acc) + for sName := range sent { + missingNames = append(missingNames, sName) + } + break LOOP + case si := <-rc: + consCount := sent[si.Config.Name] + if consCount > 0 { + si.State.Consumers = consCount + } + delete(sent, si.Config.Name) + if si.OfflineReason == _EMPTY_ { + resp.Streams = append(resp.Streams, &si.StreamInfo) + } else if _, ok := resp.Offline[si.Config.Name]; !ok { + if resp.Offline == nil { + resp.Offline = make(map[string]string, 1) + } + resp.Offline[si.Config.Name] = si.OfflineReason + missingNames = append(missingNames, si.Config.Name) + } + } + } + + // Needs to be sorted as well. + if len(resp.Streams) > 1 { + slices.SortFunc(resp.Streams, func(i, j *StreamInfo) int { return cmp.Compare(i.Config.Name, j.Config.Name) }) + } + + resp.Total = scnt + resp.Limit = JSApiListLimit + resp.Offset = offset + resp.Missing = missingNames + s.sendAPIResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(resp)) +} + +// This will do a scatter and gather operation for all consumers for this stream and account. +// This will be running in a separate Go routine. +func (s *Server) jsClusteredConsumerListRequest(acc *Account, ci *ClientInfo, offset int, stream, subject, reply string, rmsg []byte) { + defer s.grWG.Done() + + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.RLock() + + var consumers []*consumerAssignment + if sas := cc.streams[acc.Name]; sas != nil { + if sa := sas[stream]; sa != nil { + // Copy over since we need to sort etc. + for _, ca := range sa.consumers { + consumers = append(consumers, ca) + } + } + } + // Needs to be sorted. + if len(consumers) > 1 { + slices.SortFunc(consumers, func(i, j *consumerAssignment) int { return cmp.Compare(i.Config.Name, j.Config.Name) }) + } + + ocnt := len(consumers) + if offset < 0 { + offset = 0 + } + if offset > ocnt { + offset = ocnt + } + if offset > 0 { + consumers = consumers[offset:] + } + if len(consumers) > JSApiListLimit { + consumers = consumers[:JSApiListLimit] + } + + // Send out our requests here. + var resp = JSApiConsumerListResponse{ + ApiResponse: ApiResponse{Type: JSApiConsumerListResponseType}, + Consumers: []*ConsumerInfo{}, + } + + js.mu.RUnlock() + + if len(consumers) == 0 { + resp.Limit = JSApiListLimit + resp.Offset = offset + s.sendAPIResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(resp)) + return + } + + // Create an inbox for our responses and send out requests. + s.mu.Lock() + inbox := s.newRespInbox() + rc := make(chan *consumerInfoClusterResponse, len(consumers)) + + // Store our handler. + s.sys.replies[inbox] = func(sub *subscription, _ *client, _ *Account, subject, _ string, msg []byte) { + var ci consumerInfoClusterResponse + if err := json.Unmarshal(msg, &ci); err != nil { + s.Warnf("Error unmarshaling clustered consumer info response:%v", err) + return + } + select { + case rc <- &ci: + default: + s.Warnf("Failed placing consumer info result on internal chan") + } + } + s.mu.Unlock() + + // Cleanup after. + defer func() { + s.mu.Lock() + if s.sys != nil && s.sys.replies != nil { + delete(s.sys.replies, inbox) + } + s.mu.Unlock() + }() + + var missingNames []string + sent := map[string]struct{}{} + + // Send out our requests here. + js.mu.RLock() + for _, ca := range consumers { + if s.allPeersOffline(ca.Group) { + // Place offline onto our results by hand here. + ci := &ConsumerInfo{ + Config: ca.Config, + Created: ca.Created, + Cluster: js.offlineClusterInfo(ca.Group), + TimeStamp: time.Now().UTC(), + } + resp.Consumers = append(resp.Consumers, ci) + missingNames = append(missingNames, ca.Name) + } else { + isubj := fmt.Sprintf(clusterConsumerInfoT, ca.Client.serviceAccount(), stream, ca.Name) + s.sendInternalMsgLocked(isubj, inbox, nil, nil) + sent[ca.Name] = struct{}{} + } + } + // Don't hold lock. + js.mu.RUnlock() + + const timeout = 4 * time.Second + notActive := time.NewTimer(timeout) + defer notActive.Stop() + +LOOP: + for len(sent) > 0 { + select { + case <-s.quitCh: + return + case <-notActive.C: + s.Warnf("Did not receive all consumer info results for '%s > %s'", acc, stream) + for cName := range sent { + missingNames = append(missingNames, cName) + } + break LOOP + case ci := <-rc: + delete(sent, ci.Name) + if ci.OfflineReason == _EMPTY_ { + resp.Consumers = append(resp.Consumers, &ci.ConsumerInfo) + } else if _, ok := resp.Offline[ci.Name]; !ok { + if resp.Offline == nil { + resp.Offline = make(map[string]string, 1) + } + resp.Offline[ci.Name] = ci.OfflineReason + missingNames = append(missingNames, ci.Name) + } + } + } + + // Needs to be sorted as well. + if len(resp.Consumers) > 1 { + slices.SortFunc(resp.Consumers, func(i, j *ConsumerInfo) int { return cmp.Compare(i.Name, j.Name) }) + } + + resp.Total = ocnt + resp.Limit = JSApiListLimit + resp.Offset = offset + resp.Missing = missingNames + s.sendAPIResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(resp)) +} + +func encodeStreamPurge(sp *streamPurge) []byte { + var bb bytes.Buffer + bb.WriteByte(byte(purgeStreamOp)) + json.NewEncoder(&bb).Encode(sp) + return bb.Bytes() +} + +func decodeStreamPurge(buf []byte) (*streamPurge, error) { + var sp streamPurge + err := json.Unmarshal(buf, &sp) + return &sp, err +} + +func (s *Server) jsClusteredConsumerDeleteRequest(ci *ClientInfo, acc *Account, stream, consumer, subject, reply string, rmsg []byte) { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + if cc.meta == nil { + return + } + + var resp = JSApiConsumerDeleteResponse{ApiResponse: ApiResponse{Type: JSApiConsumerDeleteResponseType}} + + sa := js.streamAssignmentOrInflight(acc.Name, stream) + if sa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + oca := js.consumerAssignmentOrInflight(acc.Name, stream, consumer) + if oca == nil { + resp.Error = NewJSConsumerNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) + return + } + ca := &consumerAssignment{Group: oca.Group, Stream: stream, Name: consumer, Config: oca.Config, Subject: subject, Reply: reply, Client: ci, Created: oca.Created} + if err := cc.meta.Propose(cc.term, encodeDeleteConsumerAssignment(ca)); err != nil { + return + } + cc.trackInflightConsumerProposal(acc.Name, stream, ca, true) +} + +func encodeMsgDelete(md *streamMsgDelete) []byte { + var bb bytes.Buffer + bb.WriteByte(byte(deleteMsgOp)) + json.NewEncoder(&bb).Encode(md) + return bb.Bytes() +} + +func decodeMsgDelete(buf []byte) (*streamMsgDelete, error) { + var md streamMsgDelete + err := json.Unmarshal(buf, &md) + return &md, err +} + +func (s *Server) jsClusteredMsgDeleteRequest(ci *ClientInfo, acc *Account, mset *stream, stream, subject, reply string, req *JSApiMsgDeleteRequest, rmsg []byte) { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + js.mu.Lock() + sa := js.streamAssignment(acc.Name, stream) + if sa == nil { + s.Debugf("Message delete failed, could not locate stream '%s > %s'", acc.Name, stream) + js.mu.Unlock() + return + } + + // Check for single replica items. + if n := sa.Group.node; n != nil { + md := encodeMsgDelete(&streamMsgDelete{Seq: req.Seq, NoErase: req.NoErase, Stream: stream, Subject: subject, Reply: reply, Client: ci}) + js.mu.Unlock() + mset.mu.RLock() + term := mset.term + mset.mu.RUnlock() + n.Propose(term, md) + return + } + js.mu.Unlock() + + if mset == nil { + return + } + + var err error + var removed bool + if req.NoErase { + removed, err = mset.removeMsg(req.Seq) + } else { + removed, err = mset.eraseMsg(req.Seq) + } + var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}} + if err != nil { + resp.Error = NewJSStreamMsgDeleteFailedError(err, Unless(err)) + } else if !removed { + resp.Error = NewJSSequenceNotFoundError(req.Seq) + } else { + resp.Success = true + } + s.sendAPIResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(resp)) +} + +func encodeAddStreamAssignment(sa *streamAssignment) []byte { + csa := sa.clone() + csa.Client = csa.Client.forProposal() + csa.ConfigJSON, _ = json.Marshal(sa.Config) + var bb bytes.Buffer + bb.WriteByte(byte(assignStreamOp)) + json.NewEncoder(&bb).Encode(csa) + return bb.Bytes() +} + +func encodeUpdateStreamAssignment(sa *streamAssignment) []byte { + csa := sa.clone() + csa.Client = csa.Client.forProposal() + csa.ConfigJSON, _ = json.Marshal(sa.Config) + var bb bytes.Buffer + bb.WriteByte(byte(updateStreamOp)) + json.NewEncoder(&bb).Encode(csa) + return bb.Bytes() +} + +func encodeDeleteStreamAssignment(sa *streamAssignment) []byte { + csa := sa.clone() + csa.Client = csa.Client.forProposal() + csa.ConfigJSON, _ = json.Marshal(sa.Config) + var bb bytes.Buffer + bb.WriteByte(byte(removeStreamOp)) + json.NewEncoder(&bb).Encode(csa) + return bb.Bytes() +} + +func decodeStreamAssignment(s *Server, buf []byte) (*streamAssignment, error) { + var sa streamAssignment + if err := json.Unmarshal(buf, &sa); err != nil { + return nil, err + } + if err := decodeStreamAssignmentConfig(s, &sa); err != nil { + return nil, err + } + return &sa, nil +} + +func decodeStreamAssignmentConfig(s *Server, sa *streamAssignment) error { + var unsupported bool + var cfg StreamConfig + var err error + decoder := json.NewDecoder(bytes.NewReader(sa.ConfigJSON)) + decoder.DisallowUnknownFields() + if err = decoder.Decode(&cfg); err != nil { + unsupported = true + cfg = StreamConfig{} + if err2 := json.Unmarshal(sa.ConfigJSON, &cfg); err2 != nil { + return err2 + } + } + sa.Config = &cfg + fixCfgMirrorWithDedupWindow(sa.Config) + + if unsupported || (sa.Config != nil && !supportsRequiredApiLevel(sa.Config.Metadata)) { + sa.unsupported = newUnsupportedStreamAssignment(s, sa, err) + } + return nil +} + +func encodeDeleteRange(dr *DeleteRange) []byte { + var bb bytes.Buffer + bb.WriteByte(byte(deleteRangeOp)) + json.NewEncoder(&bb).Encode(dr) + return bb.Bytes() +} + +func decodeDeleteRange(buf []byte) (*DeleteRange, error) { + var dr DeleteRange + err := json.Unmarshal(buf, &dr) + if err != nil { + return nil, err + } + return &dr, err +} + +// createGroupForConsumer will create a new group from same peer set as the stream. +func (cc *jetStreamCluster) createGroupForConsumer(cfg *ConsumerConfig, sa *streamAssignment) (*raftGroup, *selectPeerError) { + if len(sa.Group.Peers) == 0 || cfg.Replicas > len(sa.Group.Peers) { + return nil, &selectPeerError{misc: true} + } + + replicas := cfg.replicas(sa.Config) + peers := copyStrings(sa.Group.Peers) + var _ss [5]string + active := _ss[:0] + + // Calculate all active peers. + for _, peer := range peers { + if sir, ok := cc.s.nodeToInfo.Load(peer); ok && sir != nil { + if !sir.(nodeInfo).offline { + active = append(active, peer) + } + } + } + if quorum := replicas/2 + 1; quorum > len(active) { + // Not enough active to satisfy the request. + return nil, &selectPeerError{offline: true} + } + + // If we want less then our parent stream, select from active. + if replicas > 0 && replicas < len(peers) { + // Pedantic in case stream is say R5 and consumer is R3 and 3 or more offline, etc. + if len(active) < replicas { + return nil, &selectPeerError{offline: true} + } + // First shuffle the active peers and then select to account for replica = 1. + rand.Shuffle(len(active), func(i, j int) { active[i], active[j] = active[j], active[i] }) + peers = active[:replicas] + } + storage := sa.Config.Storage + if cfg.MemoryStorage { + storage = MemoryStorage + } + return &raftGroup{Name: groupNameForConsumer(peers, storage), Storage: storage, Peers: peers}, nil +} + +// jsClusteredConsumerRequest is first point of entry to create a consumer in clustered mode. +func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subject, reply string, hdr, msg []byte, req *CreateConsumerRequest) { + js, cc := s.getJetStreamCluster() + if js == nil || cc == nil { + return + } + + stream, cfg, action, pedantic := req.Stream, &req.Config, req.Action, req.Pedantic + + // If the consumer is a direct sourcing consumer, we need to "upgrade" it to be durable without AckNone. + // We only get here if the stream is not Limits-based. + if cfg.Direct && cfg.Sourcing && cfg.Name != _EMPTY_ { + cfg.Direct = false + cfg.Durable = cfg.Name + cfg.AckPolicy = AckFlowControl + cfg.AckWait = 0 + cfg.MaxDeliver = 0 + cfg.InactiveThreshold = 0 + } + + var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} + + streamCfg, ok := js.clusterStreamConfig(acc.Name, stream) + if !ok { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + selectedReplicas := cfg.replicas(&streamCfg) + selectedLimits, selectedTier, _, apiErr := acc.selectLimits(selectedReplicas) + if apiErr != nil { + resp.Error = apiErr + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + srvLim := &s.getOpts().JetStreamLimits + // Make sure we have sane defaults + if err := setConsumerConfigDefaults(cfg, &streamCfg, srvLim, selectedLimits, pedantic); err != nil { + resp.Error = err + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + if err := checkConsumerCfg(cfg, srvLim, &streamCfg, acc, selectedLimits, false); err != nil { + resp.Error = err + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + js.mu.Lock() + defer js.mu.Unlock() + + if cc.meta == nil { + return + } + + // Lookup the stream assignment. + sa := js.streamAssignmentOrInflight(acc.Name, stream) + if sa == nil { + resp.Error = NewJSStreamNotFoundError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + + // If the user provided an expected stream identity, reject the request on a mismatch. + var streamIdentity string + if req.Config.Direct || req.Config.Sourcing { + streamIdentity = sa.identity() + if reqIdentity := sliceHeader(JSStreamIdentity, hdr); len(reqIdentity) > 0 && bytesToString(reqIdentity) != streamIdentity { + resp.Error = NewJSConsumerStreamIdentityMismatchError(streamIdentity) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + // Was a consumer name provided? + var oname string + if isDurableConsumer(cfg) || cfg.Name != _EMPTY_ { + if cfg.Name != _EMPTY_ { + oname = cfg.Name + } else { + oname = cfg.Durable + } + } + + // Check for max consumers here to short circuit if possible. + // Start with limit on a stream, but if one is defined at the level of the account + // and is lower, use that limit. + if (action == ActionCreate || action == ActionCreateOrUpdate) && !cfg.Direct && !cfg.Sourcing { + // The stream limit caps every consumer of the stream. The account limit + // caps only the consumers of the selected tier, so the two need separate + // counts. + streamMaxc := sa.Config.MaxConsumers + tierMaxc := selectedLimits.MaxConsumers + // Only apply default limits if the user specified neither stream nor account limit. + // An unlimited (-1) stream or account limit counts as not set, since that is also + // the default for accounts, so the default can't be opted out of per stream or account. + if maxConsumers := s.getOpts().JetStreamLimits.DefaultMaxConsumers; maxConsumers > 0 && streamMaxc <= 0 && tierMaxc <= 0 { + streamMaxc = maxConsumers + } + if streamMaxc > 0 || tierMaxc > 0 { + // If the consumer name is specified and we think it already exists, then + // we're likely updating an existing consumer, so don't count it. Otherwise + // we will incorrectly return NewJSMaximumConsumersLimitError for an update. + if oname == _EMPTY_ || js.consumerAssignmentOrInflight(acc.Name, stream, oname) == nil { + // Don't count direct/sourcing consumers. + var total, tierTotal int + for ca := range js.consumerAssignmentsOrInflightSeq(acc.Name, stream) { + if ca.unsupported != nil { + continue + } + if ca.Config != nil && !ca.Config.Direct && !ca.Config.Sourcing { + total++ + if selectedTier == _EMPTY_ || isSameTier(ca.Config.replicas(&streamCfg), selectedReplicas) { + tierTotal++ + } + } + } + if (streamMaxc > 0 && total >= streamMaxc) || (tierMaxc > 0 && tierTotal >= tierMaxc) { + resp.Error = NewJSMaximumConsumersLimitError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + } + } + + // Also short circuit if DeliverLastPerSubject is set with no FilterSubject. + if cfg.DeliverPolicy == DeliverLastPerSubject { + if cfg.FilterSubject == _EMPTY_ && len(cfg.FilterSubjects) == 0 { + resp.Error = NewJSConsumerInvalidPolicyError(fmt.Errorf("consumer delivery policy is deliver last per subject, but FilterSubject is not set")) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + + // Setup proper default for ack wait if we are in explicit ack mode. + if cfg.AckWait == 0 && (cfg.AckPolicy == AckExplicit || cfg.AckPolicy == AckAll) { + cfg.AckWait = JsAckWaitDefault + } + // Setup default of -1, meaning no limit for MaxDeliver. + if cfg.MaxDeliver == 0 { + cfg.MaxDeliver = -1 + } + // Set proper default for max ack pending if we are ack explicit and none has been set. + if cfg.AckPolicy == AckExplicit && cfg.MaxAckPending == 0 { + cfg.MaxAckPending = JsDefaultMaxAckPending + } + + if cfg.PriorityPolicy == PriorityPinnedClient && cfg.PinnedTTL == 0 { + cfg.PinnedTTL = JsDefaultPinnedTTL + } + + var ca *consumerAssignment + // See if we have an existing one already under same durable name or + // if name was set by the user. + if oname != _EMPTY_ { + if ca = js.consumerAssignmentOrInflight(acc.Name, stream, oname); ca != nil { + // Provided config might miss metadata, copy from existing config. + copyConsumerMetadata(cfg, ca.Config) + + // If a durable sourcing consumer is used, we need to reset the deliver policy. + if cfg.Sourcing && cfg.Durable != _EMPTY_ { + cfg.DeliverPolicy = ca.Config.DeliverPolicy + cfg.OptStartSeq = ca.Config.OptStartSeq + cfg.OptStartTime = ca.Config.OptStartTime + } + + if action == ActionCreate && !reflect.DeepEqual(cfg, ca.Config) { + resp.Error = NewJSConsumerAlreadyExistsError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Do quick sanity check on new cfg to prevent here if possible. + if err := acc.checkNewConsumerConfig(ca.Config, cfg); err != nil { + resp.Error = NewJSConsumerCreateError(err, Unless(err)) + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Don't allow updating if all peers are offline. + if s.allPeersOffline(ca.Group) { + resp.Error = NewJSConsumerOfflineError() + s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil, errRespDelay) + return + } + } else { + // Initialize/update asset version metadata. + // First time creating this consumer, or updating. + setStaticConsumerMetadata(cfg) + } + } + + // Initialize/update asset version metadata. + // But only if we're not creating, should only update it the first time + // to be idempotent with versions where there's no versioning metadata. + if action != ActionCreate { + setStaticConsumerMetadata(cfg) + } + + // If this is new consumer. + if ca == nil { + if action == ActionUpdate { + resp.Error = NewJSConsumerDoesNotExistError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + rg, err := cc.createGroupForConsumer(cfg, sa) + if err != nil { + resp.Error = NewJSInsufficientResourcesError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + // Pick a preferred leader. + rg.setPreferred(s) + + // Inherit cluster from stream. + rg.Cluster = sa.Group.Cluster + + // We need to set the ephemeral here before replicating. + if !isDurableConsumer(cfg) { + if cfg.Name != _EMPTY_ { + oname = cfg.Name + } else { + // Make sure name is unique. + for { + oname = createConsumerName() + if js.consumerAssignmentOrInflight(acc.Name, stream, oname) != nil { + continue + } + break + } + } + } + if len(rg.Peers) > 1 { + if maxHaAssets := s.getOpts().JetStreamLimits.MaxHAAssets; maxHaAssets != 0 { + for _, peer := range rg.Peers { + if ni, ok := s.nodeToInfo.Load(peer); ok { + ni := ni.(nodeInfo) + if stats := ni.stats; stats != nil && stats.HAAssets > maxHaAssets { + resp.Error = NewJSInsufficientResourcesError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + s.Warnf("%s@%s (HA Asset Count: %d) exceeds max ha asset limit of %d"+ + " for (durable) consumer %s placement on stream %s", + ni.name, ni.cluster, ni.stats.HAAssets, maxHaAssets, oname, stream) + return + } + } + } + } + } + + // Check if we are work queue policy. + // We will do pre-checks here to avoid thrashing meta layer. + if sa.Config.Retention == WorkQueuePolicy && !cfg.Direct && !cfg.Sourcing { + if cfg.AckPolicy != AckExplicit && cfg.AckPolicy != AckFlowControl { + resp.Error = NewJSConsumerWQRequiresExplicitAckError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if cfg.DeliverPolicy != DeliverAll { + resp.Error = NewJSConsumerWQConsumerNotDeliverAllError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + subjects := gatherSubjectFilters(cfg.FilterSubject, cfg.FilterSubjects) + for oca := range js.consumerAssignmentsOrInflightSeq(acc.Name, stream) { + if oca.Name == oname || oca.Config.Direct || oca.Config.Sourcing { + continue + } + if len(subjects) == 0 { + resp.Error = NewJSConsumerWQMultipleUnfilteredError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + for _, psubj := range gatherSubjectFilters(oca.Config.FilterSubject, oca.Config.FilterSubjects) { + for _, subj := range subjects { + if SubjectsCollide(subj, psubj) { + resp.Error = NewJSConsumerWQConsumerNotUniqueError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + } + } + } + } + + ca = &consumerAssignment{ + Group: rg, + Stream: stream, + Name: oname, + Config: cfg, + Subject: subject, + Reply: reply, + Client: ci, + Created: time.Now().UTC(), + } + } else { + // If the consumer already exists then don't allow updating the PauseUntil, just set + // it back to whatever the current configured value is. + cfg.PauseUntil = ca.Config.PauseUntil + + nca := ca.copyGroup() + + // Reset notion of scaling up, if this was done in a previous update. Must be + // preserved while a migration is inflight, so peers that create their raft + // node late keep the empty-log protection. + if nca.Group.Desired == nil { + nca.Group.ScaleUp = false + } + + rBefore := nca.Config.replicas(sa.Config) + rAfter := cfg.replicas(sa.Config) + + if rBefore < rAfter { + newPeerSet := nca.Group.Peers + // Scale up by adding new members from the stream peer set that are not yet in the consumer peer set. + streamPeerSet := copyStrings(sa.Group.Peers) + + // Respond with error when there is a config mismatch between the intended config and expected peer size. + if len(streamPeerSet) < rAfter { + resp.Error = NewJSConsumerReplicasExceedsStreamError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + rand.Shuffle(rAfter, func(i, j int) { streamPeerSet[i], streamPeerSet[j] = streamPeerSet[j], streamPeerSet[i] }) + for _, p := range streamPeerSet { + found := false + for _, sp := range newPeerSet { + if sp == p { + found = true + break + } + } + if !found { + newPeerSet = append(newPeerSet, p) + if len(newPeerSet) == rAfter { + break + } + } + } + // Single nodes are not recorded by the NRG layer so we can rename. + if len(ca.Group.Peers) == 1 && ca.Group.Desired == nil { + nca.Group.Name = groupNameForConsumer(newPeerSet, nca.Group.Storage) + } + nca.Group.Peers = newPeerSet + nca.Group = ca.Group.withDesired(nca.Group) + } else if rBefore > rAfter { + // Mark the group as scaling down, the current leader will be preserved. + nca.Group = ca.Group.withDesired(nca.Group) + nca.Group.Desired.ScaleDown = true + } + + // Update config and client info on copy of existing. + nca.Config = cfg + nca.Client = ci + nca.Subject = subject + nca.Reply = reply + ca = nca + } + + // Do formal proposal. + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(ca)); err != nil { + return + } + cc.trackInflightConsumerProposal(acc.Name, stream, ca, false) +} + +func encodeAddConsumerAssignment(ca *consumerAssignment) []byte { + cca := ca.clone() + cca.Client = cca.Client.forProposal() + cca.ConfigJSON, _ = json.Marshal(ca.Config) + var bb bytes.Buffer + bb.WriteByte(byte(assignConsumerOp)) + json.NewEncoder(&bb).Encode(cca) + return bb.Bytes() +} + +func encodeDeleteConsumerAssignment(ca *consumerAssignment) []byte { + cca := ca.clone() + cca.Client = cca.Client.forProposal() + cca.ConfigJSON, _ = json.Marshal(ca.Config) + var bb bytes.Buffer + bb.WriteByte(byte(removeConsumerOp)) + json.NewEncoder(&bb).Encode(cca) + return bb.Bytes() +} + +func decodeConsumerAssignment(buf []byte) (*consumerAssignment, error) { + var ca consumerAssignment + if err := json.Unmarshal(buf, &ca); err != nil { + return nil, err + } + if err := decodeConsumerAssignmentConfig(&ca); err != nil { + return nil, err + } + return &ca, nil +} + +func decodeConsumerAssignmentConfig(ca *consumerAssignment) error { + var unsupported bool + var cfg ConsumerConfig + var err error + decoder := json.NewDecoder(bytes.NewReader(ca.ConfigJSON)) + decoder.DisallowUnknownFields() + if err = decoder.Decode(&cfg); err != nil { + unsupported = true + cfg = ConsumerConfig{} + if err2 := json.Unmarshal(ca.ConfigJSON, &cfg); err2 != nil { + return err2 + } + } + ca.Config = &cfg + if unsupported || (ca.Config != nil && !supportsRequiredApiLevel(ca.Config.Metadata)) { + ca.unsupported = newUnsupportedConsumerAssignment(ca, err) + } + return nil +} + +func encodeAddConsumerAssignmentCompressed(ca *consumerAssignment) []byte { + cca := ca.clone() + cca.Client = cca.Client.forProposal() + cca.ConfigJSON, _ = json.Marshal(ca.Config) + var bb bytes.Buffer + bb.WriteByte(byte(assignCompressedConsumerOp)) + s2e := s2.NewWriter(&bb) + json.NewEncoder(s2e).Encode(cca) + s2e.Close() + return bb.Bytes() +} + +func decodeConsumerAssignmentCompressed(buf []byte) (*consumerAssignment, error) { + var ca consumerAssignment + bb := bytes.NewBuffer(buf) + s2d := s2.NewReader(bb) + decoder := json.NewDecoder(s2d) + if err := decoder.Decode(&ca); err != nil { + return nil, err + } + if err := decodeConsumerAssignmentConfig(&ca); err != nil { + return nil, err + } + return &ca, nil +} + +var errBadStreamMsg = errors.New("jetstream cluster bad replicated stream msg") + +func decodeStreamMsg(buf []byte) (subject, reply string, hdr, msg []byte, lseq uint64, ts int64, sourced bool, err error) { + var le = binary.LittleEndian + if len(buf) < 26 { + return _EMPTY_, _EMPTY_, nil, nil, 0, 0, false, errBadStreamMsg + } + lseq = le.Uint64(buf) + buf = buf[8:] + ts = int64(le.Uint64(buf)) + buf = buf[8:] + sl := int(le.Uint16(buf)) + buf = buf[2:] + if len(buf) < sl { + return _EMPTY_, _EMPTY_, nil, nil, 0, 0, false, errBadStreamMsg + } + subject = string(buf[:sl]) + buf = buf[sl:] + if len(buf) < 2 { + return _EMPTY_, _EMPTY_, nil, nil, 0, 0, false, errBadStreamMsg + } + rl := int(le.Uint16(buf)) + buf = buf[2:] + if len(buf) < rl { + return _EMPTY_, _EMPTY_, nil, nil, 0, 0, false, errBadStreamMsg + } + reply = string(buf[:rl]) + buf = buf[rl:] + if len(buf) < 2 { + return _EMPTY_, _EMPTY_, nil, nil, 0, 0, false, errBadStreamMsg + } + hl := int(le.Uint16(buf)) + buf = buf[2:] + if len(buf) < hl { + return _EMPTY_, _EMPTY_, nil, nil, 0, 0, false, errBadStreamMsg + } + if hdr = buf[:hl]; len(hdr) == 0 { + hdr = nil + } + buf = buf[hl:] + if len(buf) < 4 { + return _EMPTY_, _EMPTY_, nil, nil, 0, 0, false, errBadStreamMsg + } + ml := int(le.Uint32(buf)) + buf = buf[4:] + // ml is read as a uint32 but held in an int; on 32-bit builds a length with + // the high bit set becomes negative, which slips past len(buf) < ml and then + // panics on buf[:ml]. Reject a negative length so the bound holds everywhere. + if ml < 0 || len(buf) < ml { + return _EMPTY_, _EMPTY_, nil, nil, 0, 0, false, errBadStreamMsg + } + if msg = buf[:ml]; len(msg) == 0 { + msg = nil + } + buf = buf[ml:] + if len(buf) > 0 { + flags, _ := binary.Uvarint(buf) + sourced = flags&msgFlagFromSourceOrMirror != 0 + } + return subject, reply, hdr, msg, lseq, ts, sourced, nil +} + +func decodeBatchMsg(buf []byte) (batchId string, batchSeq uint64, op entryOp, mbuf []byte, err error) { + var le = binary.LittleEndian + if len(buf) < 2 { + return _EMPTY_, 0, 0, nil, errBadStreamMsg + } + bl := int(le.Uint16(buf)) + buf = buf[2:] + if len(buf) < bl { + return _EMPTY_, 0, 0, nil, errBadStreamMsg + } + batchId = string(buf[:bl]) + buf = buf[bl:] + var n int + batchSeq, n = binary.Uvarint(buf) + if n <= 0 { + return _EMPTY_, 0, 0, nil, errBadStreamMsg + } + buf = buf[n:] + if len(buf) < 1 { + return _EMPTY_, 0, 0, nil, errBadStreamMsg + } + op = entryOp(buf[0]) + mbuf = buf[1:] + return batchId, batchSeq, op, mbuf, nil +} + +// Flags for encodeStreamMsg/decodeStreamMsg. +const ( + msgFlagFromSourceOrMirror uint64 = 1 << iota +) + +func encodeStreamMsg(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, sourced bool) []byte { + return encodeStreamMsgAllowCompress(subject, reply, hdr, msg, lseq, ts, sourced) +} + +func encodeStreamMsgAllowCompress(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, sourced bool) []byte { + return encodeStreamMsgAllowCompressAndBatch(subject, reply, hdr, msg, lseq, ts, sourced, _EMPTY_, 0, false) +} + +// Threshold for compression. +// TODO(dlc) - Eventually make configurable. +const compressThreshold = 8192 // 8k + +// If allowed and contents over the threshold we will compress. +func encodeStreamMsgAllowCompressAndBatch(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, sourced bool, batchId string, batchSeq uint64, batchCommit bool) []byte { + // Clip the subject, reply, header and msgs down. Operate on + // uint64 lengths to avoid overflowing. + slen := min(uint64(len(subject)), math.MaxUint16) + rlen := min(uint64(len(reply)), math.MaxUint16) + hlen := min(uint64(len(hdr)), math.MaxUint16) + mlen := min(uint64(len(msg)), math.MaxUint32) + total := slen + rlen + hlen + mlen + + shouldCompress := total > compressThreshold + elen := int(1 + 8 + 8 + total) + elen += (2 + 2 + 2 + 4 + 8) // Encoded lengths, 4bytes, flags are up to 8 bytes + + blen := min(uint64(len(batchId)), math.MaxUint16) + if batchId != _EMPTY_ { + elen += int(2 + blen + 8) // length of batchId, batchId itself, batchSeq (up to 8 bytes) + } + + var flags uint64 + if sourced { + flags |= msgFlagFromSourceOrMirror + } + + var le = binary.LittleEndian + var opIndex int + buf := make([]byte, 1, elen) + if batchId != _EMPTY_ { + if batchCommit { + buf[0] = byte(batchCommitMsgOp) + } else { + buf[0] = byte(batchMsgOp) + } + buf = le.AppendUint16(buf, uint16(blen)) + buf = append(buf, batchId[:blen]...) + buf = binary.AppendUvarint(buf, batchSeq) + opIndex = len(buf) + buf = append(buf, byte(streamMsgOp)) + } else { + buf[opIndex] = byte(streamMsgOp) + } + + buf = le.AppendUint64(buf, lseq) + buf = le.AppendUint64(buf, uint64(ts)) + buf = le.AppendUint16(buf, uint16(slen)) + buf = append(buf, subject[:slen]...) + buf = le.AppendUint16(buf, uint16(rlen)) + buf = append(buf, reply[:rlen]...) + buf = le.AppendUint16(buf, uint16(hlen)) + buf = append(buf, hdr[:hlen]...) + buf = le.AppendUint32(buf, uint32(mlen)) + buf = append(buf, msg[:mlen]...) + buf = binary.AppendUvarint(buf, flags) + + // Check if we should compress. + if shouldCompress { + nbuf := make([]byte, s2.MaxEncodedLen(elen)) + if opIndex > 0 { + copy(nbuf[:opIndex], buf[:opIndex]) + } + nbuf[opIndex] = byte(compressedStreamMsgOp) + ebuf := s2.Encode(nbuf[opIndex+1:], buf[opIndex+1:]) + // Only pay the cost of decode on the other side if we compressed. + // S2 will allow us to try without major penalty for non-compressable data. + if len(ebuf) < len(buf) { + buf = nbuf[:len(ebuf)+opIndex+1] + } + } + + return buf +} + +// StreamSnapshot is used for snapshotting and out of band catch up in clustered mode. +// Legacy, only used for decoding old snapshots, see decodeStreamSnapshot. +type streamSnapshot struct { + Msgs uint64 `json:"messages"` + Bytes uint64 `json:"bytes"` + FirstSeq uint64 `json:"first_seq"` + LastSeq uint64 `json:"last_seq"` + Failed uint64 `json:"clfs"` + Deleted []uint64 `json:"deleted,omitempty"` +} + +func decodeStreamSnapshot(data []byte) (*StreamReplicatedState, error) { + if IsEncodedStreamState(data) { + return DecodeStreamState(data) + } + + var snap streamSnapshot + if err := json.Unmarshal(data, &snap); err != nil { + return nil, err + } + state := &StreamReplicatedState{ + Msgs: snap.Msgs, + Bytes: snap.Bytes, + FirstSeq: snap.FirstSeq, + LastSeq: snap.LastSeq, + Failed: snap.Failed, + } + if len(snap.Deleted) > 0 { + state.Deleted = append(state.Deleted, DeleteSlice(snap.Deleted)) + } + return state, nil +} + +// Grab a snapshot of a stream for clustered mode. +func (mset *stream) stateSnapshot() []byte { + mset.mu.RLock() + defer mset.mu.RUnlock() + return mset.stateSnapshotLocked() +} + +// Grab a snapshot of a stream for clustered mode. +// Snapshots always use the binary encoding, which all supported server +// versions (2.10.0+) can decode. The legacy JSON encoding remains supported +// on the decode side only, see decodeStreamSnapshot. +// Lock should be held. +func (mset *stream) stateSnapshotLocked() []byte { + // Only include the sourcing state once enabled, a peer that doesn't accept + // the encoding rejects the whole snapshot. + withSources := mset.srv.getOpts().getFeatureFlag(FeatureFlagJsSnapshotSources) + snap, err := mset.store.EncodedStreamState(mset.getCLFS(), withSources) + if err != nil { + return nil + } + return snap +} + +// To warn when we are getting too far behind from what has been proposed vs what has been committed. +const streamLagWarnThreshold = 10_000 + +// processClusteredInboundMsg will propose the inbound message to the underlying raft group. +func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg []byte, mt *msgTrace, sourced bool) (retErr error) { + // For possible error response. + var response []byte + + 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, term := mset.srv, mset.js, mset.jsa, mset.cfg.Storage, mset.cfg.Replicas, mset.tier, mset.outq, mset.node, mset.term + maxMsgSize, lseq := int(mset.cfg.MaxMsgSize), mset.lseq + isLeader, isSealed, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules := mset.isLeader(), mset.cfg.Sealed, mset.cfg.AllowRollup, mset.cfg.DenyPurge, mset.cfg.AllowMsgTTL, mset.cfg.AllowMsgCounter, mset.cfg.AllowMsgSchedules + + // 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() + + // This should not happen but possible now that we allow scale up, and scale down where this could trigger. + // + // We also invoke this in clustering mode for message tracing when not + // performing message delivery. + if node == nil || mt.traceOnly() { + return mset.processJetStreamMsg(subject, reply, hdr, msg, 0, 0, mt, sourced, true) + } + + // 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() + } + + // Bail here if sealed. + if isSealed { + var resp = JSPubAckResponse{PubAck: &PubAck{Stream: mset.name()}, Error: NewJSStreamSealedError()} + b, _ := json.Marshal(resp) + mset.outq.sendMsg(reply, b) + return NewJSStreamSealedError() + } + + // Check here pre-emptively if we have exceeded this server limits. + if js.limitsExceeded(stype) { + s.resourcesExceededError(stype) + if canRespond { + b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: NewJSInsufficientResourcesError()}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, b, nil, 0)) + } + // Stepdown regardless. + if node := mset.raftNode(); node != nil { + node.StepDown() + } + return 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()) + if canRespond { + var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} + resp.Error = err + response, _ = json.Marshal(resp) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) + } + return 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, name) + s.RateLimitWarnf("%s", err.Error()) + if canRespond { + var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} + resp.Error = NewJSStreamMessageExceedsMaximumError() + response, _ = json.Marshal(resp) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) + } + return err + } + + // 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, true) + } + + var ( + dseq uint64 + apiErr *ApiError + err error + ) + diff := &batchStagedDiff{} + if hdr, msg, dseq, apiErr, err = checkMsgHeadersPreClusteredProposal(diff, mset, csubject, subject, hdr, msg, sourced, name, jsa, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules, discard, discardNewPer, maxMsgSize, maxMsgs, maxMsgsPer, maxBytes); err != nil { + mset.clMu.Unlock() + if err == errMsgIdDuplicate && dseq > 0 { + var buf [256]byte + pubAck := append(buf[:0], mset.pubAck...) + response = append(pubAck, strconv.FormatUint(dseq, 10)...) + response = append(response, ",\"duplicate\": true}"...) + outq.sendMsg(reply, response) + return err + } + if canRespond { + var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} + resp.Error = apiErr + response, _ = json.Marshal(resp) + outq.sendMsg(reply, response) + } + return err + } + + err = commitSingleMsg(diff, mset, subject, reply, hdr, msg, name, jsa, mt, node, term, r, lseq) + mset.clMu.Unlock() + return err +} + +func (mset *stream) getAndDeleteMsgTrace(lseq uint64) *msgTrace { + if mset == nil { + return nil + } + mset.clMu.Lock() + mt, ok := mset.mt[lseq] + if ok { + delete(mset.mt, lseq) + } + mset.clMu.Unlock() + return mt +} + +// For requesting messages post raft snapshot to catch up streams post server restart. +// Any deleted msgs etc will be handled inline on catchup. +type streamSyncRequest struct { + Peer string `json:"peer,omitempty"` + FirstSeq uint64 `json:"first_seq"` + LastSeq uint64 `json:"last_seq"` + DeleteRangesOk bool `json:"delete_ranges"` + MinApplied uint64 `json:"min_applied"` +} + +// clusterStreamInfoRequest is the optional body on a cluster stream info request. +// Without it only the leader answers, with the full stream info. With State set +// every member answers with its stream state. +type clusterStreamInfoRequest struct { + State bool `json:"state,omitempty"` +} + +// clusterStreamInfoResponse is one member's answer, saying which member it is. +type clusterStreamInfoResponse struct { + Peer string `json:"peer"` + State *StreamState `json:"state,omitempty"` +} + +// Given a stream state that represents a snapshot, calculate the sync request based on our current state. +// Stream lock must be held. +func (mset *stream) calculateSyncRequest(state *StreamState, snap *StreamReplicatedState, index uint64) *streamSyncRequest { + // Shouldn't happen, but consequences are pretty bad if we have the lock held and + // our caller tries to take the lock again on panic defer, as in processSnapshot. + if state == nil || snap == nil || mset.node == nil { + return nil + } + // Quick check if we are already caught up. + if state.LastSeq >= snap.LastSeq { + return nil + } + return &streamSyncRequest{FirstSeq: state.LastSeq + 1, LastSeq: snap.LastSeq, Peer: mset.node.ID(), DeleteRangesOk: true, MinApplied: index} +} + +// processSnapshotDeletes will update our current store based on the snapshot +// but only processing deletes and new FirstSeq / purges. +func (mset *stream) processSnapshotDeletes(snap *StreamReplicatedState) error { + mset.mu.Lock() + var state StreamState + mset.store.FastState(&state) + // Always adjust if FirstSeq has moved beyond our state. + var didReset bool + if snap.FirstSeq > state.FirstSeq { + if _, err := mset.store.Compact(snap.FirstSeq); err != nil { + mset.mu.Unlock() + return err + } + mset.store.FastState(&state) + mset.lseq = state.LastSeq + mset.clearAllPreAcksBelowFloor(state.FirstSeq) + didReset = true + } + s := mset.srv + mset.mu.Unlock() + + if didReset { + s.Warnf("Catchup for stream '%s > %s' resetting first sequence: %d on catchup request", + mset.account(), mset.name(), snap.FirstSeq) + } + + if len(snap.Deleted) > 0 { + return mset.store.SyncDeleted(snap.Deleted) + } + return nil +} + +// catchupPeer tracks a peer the leader is catching up out of band. +type catchupPeer struct { + lag uint64 // Lag is the number of messages that still need to be sent to the peer. + quit chan struct{} // Quit channel is closed to cancel the runCatchup serving this peer. +} + +// setCatchupPeer records that this peer is catching up, and returns its quit channel. +func (mset *stream) setCatchupPeer(peer string, lag uint64) <-chan struct{} { + if peer == _EMPTY_ { + return nil + } + mset.mu.Lock() + defer mset.mu.Unlock() + if mset.catchups == nil { + mset.catchups = make(map[string]*catchupPeer) + } + // Cancel if there was a previous catchup for this peer. + if cp := mset.catchups[peer]; cp != nil && cp.quit != nil { + close(cp.quit) + } + quit := make(chan struct{}) + mset.catchups[peer] = &catchupPeer{lag: lag, quit: quit} + return quit +} + +// Will decrement by one. +func (mset *stream) updateCatchupPeer(peer string) { + if peer == _EMPTY_ { + return + } + mset.mu.Lock() + if cp := mset.catchups[peer]; cp != nil && cp.lag > 0 { + cp.lag-- + } + mset.mu.Unlock() +} + +func (mset *stream) decrementCatchupPeer(peer string, num uint64) { + if peer == _EMPTY_ { + return + } + mset.mu.Lock() + if cp := mset.catchups[peer]; cp != nil && cp.lag > 0 { + if cp.lag >= num { + cp.lag -= num + } else { + cp.lag = 0 + } + } + mset.mu.Unlock() +} + +// detachCatchupPeer detaches the quit channel for the peer, leaving it tracked after exiting runCatchup. +func (mset *stream) detachCatchupPeer(peer string, quit <-chan struct{}) { + mset.mu.Lock() + if cp := mset.catchups[peer]; cp != nil && cp.quit != nil && cp.quit == quit { + cp.quit = nil + } + mset.mu.Unlock() +} + +// clearCatchupPeer stops tracking the peer if its quit channel matches. +func (mset *stream) clearCatchupPeer(peer string, quit <-chan struct{}) { + mset.mu.Lock() + if cp := mset.catchups[peer]; cp != nil && cp.quit != nil && cp.quit == quit { + mset.clearCatchupPeerLocked(peer) + } + mset.mu.Unlock() +} + +// Lock should be held. +func (mset *stream) clearCatchupPeerLocked(peer string) { + if cp := mset.catchups[peer]; cp != nil { + if cp.quit != nil { + close(cp.quit) + cp.quit = nil + } + delete(mset.catchups, peer) + } + if len(mset.catchups) == 0 { + mset.catchups = nil + } +} + +// Lock should be held. +func (mset *stream) clearAllCatchupPeers() { + for _, cp := range mset.catchups { + if cp.quit != nil { + close(cp.quit) + cp.quit = nil + } + } + mset.catchups = nil +} + +func (mset *stream) lagForCatchupPeer(peer string) uint64 { + mset.mu.RLock() + defer mset.mu.RUnlock() + if cp := mset.catchups[peer]; cp != nil { + return cp.lag + } + return 0 +} + +func (mset *stream) hasCatchupPeers() bool { + mset.mu.RLock() + defer mset.mu.RUnlock() + return len(mset.catchups) > 0 +} + +func (mset *stream) catchupPeers() []string { + mset.mu.RLock() + defer mset.mu.RUnlock() + return slices.Collect(maps.Keys(mset.catchups)) +} + +// peerPositions is what the members of a group have told the leader about their stream state. +// Owned by the stream's monitor routine, so it needs no locking. +type peerPositions struct { + bar uint64 // Minimum sequence peers must store to be considered caught up. + asked time.Time // When we last asked, so we don't request on every tick. + seqs map[string]uint64 // Where each peer last said its store was. + retried bool // Whether the free first retry is used up, later ones wait out migratePosAskInterval. + reply string // Inbox the answers come back on, empty while not listening. + sub *subscription // Subscription used to receive replies. +} + +// isCaughtUp reports whether a peer has caught up to at least the recorded bar. +func (pos *peerPositions) isCaughtUp(peer string) bool { + seq, ok := pos.seqs[peer] + return ok && seq >= pos.bar +} + +// record folds an answer in, reporting whether a peer we could not vouch for now is. +func (pos *peerPositions) record(resp *clusterStreamInfoResponse) bool { + if resp.Peer == _EMPTY_ || resp.State == nil { + return false + } + was := pos.isCaughtUp(resp.Peer) + if pos.seqs == nil { + pos.seqs = make(map[string]uint64) + } + pos.seqs[resp.Peer] = resp.State.LastSeq + return !was && pos.isCaughtUp(resp.Peer) +} + +// request asks every member about their stream state, unless we asked recently. +func (pos *peerPositions) request(mset *stream, accName, streamName string) { + if pos.reply == _EMPTY_ { + return + } + if pos.asked.IsZero() { + pos.bar = mset.state().LastSeq + } else if !pos.retried { + // The first retry isn't delayed, a fast catch up must not be penalized for the full timeout. + pos.retried = true + } else if time.Since(pos.asked) < migratePosAskInterval { + // Wait for timeout. + return + } + pos.asked = time.Now() + req, err := json.Marshal(&clusterStreamInfoRequest{State: true}) + if err != nil { + return + } + mset.srv.sendInternalMsgLocked(fmt.Sprintf(clusterStreamInfoT, accName, streamName), pos.reply, nil, req) +} + +func (mset *stream) setCatchingUp() { + mset.catchup.Store(true) +} + +func (mset *stream) clearCatchingUp() { + mset.catchup.Store(false) +} + +func (mset *stream) isCatchingUp() bool { + return mset.catchup.Load() +} + +// Determine if a non-leader is current. +// Lock should be held. +func (mset *stream) isCurrent() bool { + if mset.node == nil { + return true + } + return mset.node.Current() && !mset.catchup.Load() +} + +// Maximum requests for the whole server that can be in flight at the same time. +const maxConcurrentSyncRequests = 32 + +var ( + errCatchupCorruptSnapshot = errors.New("corrupt stream snapshot detected") + errCatchupStalled = errors.New("catchup stalled") + errCatchupStreamStopped = errors.New("stream has been stopped") // when a catchup is terminated due to the stream going away. + errCatchupBadMsg = errors.New("bad catchup msg") + errCatchupWrongSeqForSkip = errors.New("wrong sequence for skipped msg") + errCatchupAbortedNoLeader = errors.New("catchup aborted, no leader") + errCatchupTooManyRetries = errors.New("catchup failed, too many retries") +) + +// Catchup inactivity timers. +const ( + defaultStreamCatchupStartInterval = 5 * time.Second + defaultStreamCatchupActivityInterval = 10 * time.Second +) + +var ( + streamCatchupStartInterval = defaultStreamCatchupStartInterval + streamCatchupActivityInterval = defaultStreamCatchupActivityInterval +) + +// Process a stream snapshot. +func (mset *stream) processSnapshot(snap *StreamReplicatedState, index uint64) (e error) { + // Adopt the leader's sourcing state, we can't always derive it locally. + mset.store.ApplySourcesState(snap.Sources) + + // Update any deletes, etc. + if err := mset.processSnapshotDeletes(snap); err != nil { + return err + } + mset.setCLFS(snap.Failed) + + mset.mu.Lock() + var state StreamState + mset.store.FastState(&state) + sreq := mset.calculateSyncRequest(&state, snap, index) + + if mset.sa == nil || mset.node == nil { + mset.mu.Unlock() + return errCatchupStreamStopped + } + s, js, subject, n, st := mset.srv, mset.js, mset.sa.Sync, mset.node, mset.cfg.Storage + qname := fmt.Sprintf("[ACC:%s] stream '%s' snapshot", mset.acc.Name, mset.cfg.Name) + mset.mu.Unlock() + + // Always try to resume applies, we might be paused already if we timed out of processing the snapshot previously. + defer func() { + // Don't bother resuming if server or stream is gone. + if e != errCatchupStreamStopped && e != ErrServerNotRunning { + n.ResumeApply() + } + }() + + // Adopt the leader's sourcing state, we can't always derive it locally. + // Do it at the very end, so we catch up messages first (if any) and apply the snapshot state after. + defer mset.store.ApplySourcesState(snap.Sources) + + // Bug that would cause this to be empty on stream update. + if subject == _EMPTY_ { + return errCatchupCorruptSnapshot + } + + // No catchup is needed, flush any snapshot deletes before returning. + if sreq == nil { + return mset.flushAllPending() + } + + // We need to catch up, but are already exceeding limits. + if js.limitsExceeded(st) { + s.resourcesExceededError(st) + return NewJSInsufficientResourcesError() + } + + // Pause the apply channel for our raft group while we catch up. + if err := n.PauseApply(); err != nil { + // The only reason PauseApply can fail is due to errAlreadyLeader. + // We step down to get someone else to become the leader that can catch us up. + // Ignore the error since we could have already stepped down before us doing so here. + _ = n.StepDown() + // Now try pausing again and continue to catchup. + if err = n.PauseApply(); err != nil { + return err + } + } + + // Set our catchup state. + mset.setCatchingUp() + defer mset.clearCatchingUp() + + var sub *subscription + var err error + + startInterval := streamCatchupStartInterval + activityInterval := streamCatchupActivityInterval + notActive := time.NewTimer(startInterval) + defer notActive.Stop() + + defer func() { + if sub != nil { + s.sysUnsubscribe(sub) + } + // Make sure any consumers are updated for the pending amounts. + mset.mu.Lock() + for _, o := range mset.consumers { + o.mu.Lock() + o.streamNumPending() + o.mu.Unlock() + } + mset.mu.Unlock() + + // If we are interest based make sure to check our ack floor state. + // We will delay a bit to allow consumer states to also catchup. + if mset.isInterestRetention() { + fire := time.Duration(rand.IntN(10)+5) * time.Second + time.AfterFunc(fire, mset.checkInterestState) + } + }() + + var releaseSem bool + releaseSyncOutSem := func() { + if !releaseSem { + return + } + // Need to use select for the server shutdown case. + select { + case s.syncOutSem <- struct{}{}: + default: + } + releaseSem = false + } + // On exit, we will release our semaphore if we acquired it. + defer releaseSyncOutSem() + + // Do not let this go on forever. + start := time.Now() + const maxRetries = 3 + var numRetries int + +RETRY: + // On retry, we need to release the semaphore we got. Call will be no-op + // if releaseSem boolean has not been set to true on successfully getting + // the semaphore. + releaseSyncOutSem() + + if n.Leaderless() { + // Prevent us from spinning if we've installed a snapshot from a leader but there's no leader online. + // We wait a bit to check if a leader has come online in the meantime, if so we can continue. + var canContinue bool + if numRetries == 0 { + time.Sleep(startInterval) + canContinue = !n.Leaderless() + } + if !canContinue { + return fmt.Errorf("%w for stream '%s > %s'", errCatchupAbortedNoLeader, mset.account(), mset.name()) + } + } + + // If we have a sub clear that here. + if sub != nil { + s.sysUnsubscribe(sub) + sub = nil + } + + if !s.isRunning() { + return ErrServerNotRunning + } + + numRetries++ + if numRetries > maxRetries { + // Force a hard reset here. + return errCatchupTooManyRetries + } + + // Block here if we have too many requests in flight. + <-s.syncOutSem + releaseSem = true + + // We may have been blocked for a bit, so the reset needs to ensure that we + // consume the already fired timer. + if !notActive.Stop() { + select { + case <-notActive.C: + default: + } + } + notActive.Reset(startInterval) + + // Grab sync request again on failures. + if sreq == nil { + mset.mu.RLock() + var state StreamState + mset.store.FastState(&state) + sreq = mset.calculateSyncRequest(&state, snap, index) + mset.mu.RUnlock() + if sreq == nil { + return mset.flushAllPending() + } + } + + // Used to transfer message from the wire to another Go routine internally. + type im struct { + msg []byte + reply string + } + // This is used to notify the leader that it should stop the runCatchup + // because we are either bailing out or going to retry due to an error. + var lastReply string + notifyLeaderStopCatchup := func(reply string, err error) error { + if reply == _EMPTY_ { + return err + } + s.sendInternalMsgLocked(reply, _EMPTY_, nil, err.Error()) + return err + } + + msgsQ := newIPQueue[*im](s, qname) + defer msgsQ.unregister() + + // Send our catchup request here. + reply := syncReplySubject() + sub, err = s.sysSubscribe(reply, func(_ *subscription, _ *client, _ *Account, _, reply string, msg []byte) { + // Make copy since we are using a buffer from the inbound client/route. + msgsQ.push(&im{copyBytes(msg), reply}) + }) + if err != nil { + s.Errorf("Could not subscribe to stream catchup: %v", err) + goto RETRY + } + + // Send our sync request. + b, _ := json.Marshal(sreq) + s.sendInternalMsgLocked(subject, reply, nil, b) + + // Remember when we sent this out to avoid loop spins on errors below. + reqSendTime := time.Now() + + // Clear our sync request. + sreq = nil + + // Run our own select loop here. + for qch := n.QuitC(); ; { + select { + case <-msgsQ.ch: + notActive.Reset(activityInterval) + + mrecs := msgsQ.pop() + for _, mrec := range mrecs { + msg := mrec.msg + if mrec.reply != _EMPTY_ { + lastReply = mrec.reply + } + // Check for eof signaling. + if len(msg) == 0 { + msgsQ.recycle(&mrecs) + + // Sanity check that we've received all data expected by the snapshot. + mset.mu.RLock() + lseq := mset.lseq + mset.mu.RUnlock() + if lseq >= snap.LastSeq { + // We MUST ensure all data is flushed up to this point, if the store hadn't already. + // Because the snapshot needs to represent what has been persisted. + err = mset.flushAllPending() + if err == nil { + s.Noticef("Catchup for stream '%s > %s' complete (took %v)", mset.account(), mset.name(), time.Since(start).Round(time.Millisecond)) + } else { + s.Noticef("Catchup for stream '%s > %s' errored: %v (took %v)", mset.account(), mset.name(), err, time.Since(start).Round(time.Millisecond)) + } + return err + } + + // Make sure we do not spin and make things worse. + const minRetryWait = 2 * time.Second + elapsed := time.Since(reqSendTime) + if elapsed < minRetryWait { + select { + case <-s.quitCh: + return notifyLeaderStopCatchup(lastReply, ErrServerNotRunning) + case <-qch: + return notifyLeaderStopCatchup(lastReply, errCatchupStreamStopped) + case <-time.After(minRetryWait - elapsed): + } + } + goto RETRY + } + if _, err := mset.processCatchupMsg(msg); err == nil { + if mrec.reply != _EMPTY_ { + s.sendInternalMsgLocked(mrec.reply, _EMPTY_, nil, nil) + } + } else if isOutOfSpaceErr(err) { + _ = notifyLeaderStopCatchup(mrec.reply, err) + msgsQ.recycle(&mrecs) + return err + } else if err == NewJSInsufficientResourcesError() { + _ = notifyLeaderStopCatchup(mrec.reply, err) + if mset.js.limitsExceeded(st) { + s.resourcesExceededError(st) + } else { + s.Warnf("Catchup for stream '%s > %s' errored, account resources exceeded: %v", mset.account(), mset.name(), err) + } + msgsQ.recycle(&mrecs) + return err + } else if err == errCatchupBadMsg { + // A bad/corrupt catchup message (e.g. a failed s2 decode) is a + // deterministic failure: requesting a fresh sync from the leader + // returns the same bytes, so retrying would churn indefinitely. + _ = notifyLeaderStopCatchup(mrec.reply, err) + s.Warnf("Catchup for stream '%s > %s' errored, bad message: %v", mset.account(), mset.name(), err) + msgsQ.recycle(&mrecs) + return err + } else { + _ = notifyLeaderStopCatchup(mrec.reply, err) + s.Warnf("Catchup for stream '%s > %s' errored, will retry: %v", mset.account(), mset.name(), err) + msgsQ.recycle(&mrecs) + + // Make sure we do not spin and make things worse. + const minRetryWait = 2 * time.Second + elapsed := time.Since(reqSendTime) + if elapsed < minRetryWait { + select { + case <-s.quitCh: + return ErrServerNotRunning + case <-qch: + return errCatchupStreamStopped + case <-time.After(minRetryWait - elapsed): + } + } + goto RETRY + } + } + notActive.Reset(activityInterval) + msgsQ.recycle(&mrecs) + case <-notActive.C: + if mrecs := msgsQ.pop(); len(mrecs) > 0 { + mrec := mrecs[0] + _ = notifyLeaderStopCatchup(mrec.reply, errCatchupStalled) + msgsQ.recycle(&mrecs) + } + s.Warnf("Catchup for stream '%s > %s' stalled", mset.account(), mset.name()) + // Sanity check that we've not become leader. Shouldn't be possible + // since we haven't applied the snapshot yet. + if n.State() == Leader { + n.StepDown() + } + goto RETRY + case <-s.quitCh: + return notifyLeaderStopCatchup(lastReply, ErrServerNotRunning) + case <-qch: + return notifyLeaderStopCatchup(lastReply, errCatchupStreamStopped) + } + } +} + +// processCatchupMsg will be called to process out of band catchup msgs from a sync request. +func (mset *stream) processCatchupMsg(msg []byte) (uint64, error) { + if len(msg) == 0 { + return 0, errCatchupBadMsg + } + op := entryOp(msg[0]) + if op != streamMsgOp && op != compressedStreamMsgOp && op != deleteRangeOp { + return 0, errCatchupBadMsg + } + + mbuf := msg[1:] + if op == deleteRangeOp { + dr, err := decodeDeleteRange(mbuf) + if err != nil { + return 0, errCatchupBadMsg + } + // Handle the delete range. + // Make sure the sequences match up properly. + mset.mu.Lock() + lseq := dr.First + dr.Num - 1 + if err = mset.store.SkipMsgs(dr.First, dr.Num); err != nil { + mset.mu.Unlock() + return 0, errCatchupWrongSeqForSkip + } + mset.clearAllPreAcksInRange(dr.First, lseq) + mset.lseq = lseq + mset.mu.Unlock() + return lseq, nil + } + + if op == compressedStreamMsgOp { + var err error + mbuf, err = s2.Decode(nil, mbuf) + if err != nil { + return 0, errCatchupBadMsg + } + } + + subj, _, hdr, msg, seq, ts, _, err := decodeStreamMsg(mbuf) + if err != nil { + return 0, errCatchupBadMsg + } + + mset.mu.Lock() + st := mset.cfg.Storage + if mset.hasAllPreAcks(seq, subj) { + mset.clearAllPreAcks(seq) + // Mark this to be skipped + subj, ts = _EMPTY_, 0 + } + mset.mu.Unlock() + + // Since we're clustered we do not want to check limits based on tier here and possibly introduce skew. + if mset.js.limitsExceeded(st) { + return 0, NewJSInsufficientResourcesError() + } + + // Find the message TTL if any. + // TODO(nat): If the TTL isn't valid by this stage then there isn't really a + // lot we can do about it, as we'd break the catchup if we reject the message. + ttl, _ := getMessageTTL(hdr) + + // Put into our store + // Messages to be skipped have no subject or timestamp. + // TODO(dlc) - formalize with skipMsgOp + if subj == _EMPTY_ && ts == 0 { + if _, err = mset.store.SkipMsg(seq); err != nil { + return 0, errCatchupWrongSeqForSkip + } + } else if err := mset.store.StoreRawMsg(subj, hdr, msg, seq, ts, ttl, false); err != nil { + return 0, err + } + + mset.mu.Lock() + defer mset.mu.Unlock() + // Update our lseq. + mset.lseq = seq + + // Check for MsgId and if we have one here make sure to update our internal map. + if len(hdr) > 0 { + if msgId := getMsgId(hdr); msgId != _EMPTY_ { + mset.ddMu.Lock() + mset.storeMsgIdLocked(&ddentry{msgId, seq, ts}) + mset.ddMu.Unlock() + } + } + + return seq, nil +} + +// flushAllPending will flush any pending writes as a result of installing a snapshot or performing catchup. +func (mset *stream) flushAllPending() error { + return mset.store.FlushAllPending() +} + +func (mset *stream) flushForScaleDown() error { + if fs, ok := mset.store.(*fileStore); ok { + return fs.flushForScaleDown() + } + return nil +} + +func (mset *stream) handleClusterSyncRequest(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + var sreq streamSyncRequest + if err := json.Unmarshal(msg, &sreq); err != nil { + // Log error. + return + } + mset.srv.startGoRoutine(func() { mset.runCatchup(reply, &sreq) }) +} + +// Lock should be held. +func (js *jetStream) offlineClusterInfo(rg *raftGroup) *ClusterInfo { + s := js.srv + + ci := &ClusterInfo{Name: s.ClusterName(), RaftGroup: rg.Name} + for _, peer := range rg.Peers { + if sir, ok := s.nodeToInfo.Load(peer); ok && sir != nil { + si := sir.(nodeInfo) + pi := &PeerInfo{Peer: peer, Name: si.name, Current: false, Offline: true} + ci.Replicas = append(ci.Replicas, pi) + } + } + return ci +} + +// clusterInfo will report on the status of the raft group. +func (js *jetStream) clusterInfo(rg *raftGroup) *ClusterInfo { + if js == nil { + return nil + } + js.mu.RLock() + s := js.srv + if rg == nil || (rg.node == nil && rg.Desired == nil) { + js.mu.RUnlock() + return &ClusterInfo{ + Name: s.cachedClusterName(), + Leader: s.Name(), + } + } + // Capture what we need and let go of the lock to ensure that + // contention on Raft locks can't happen while holding JS lock. + n := rg.node + rgName := rg.Name + rgPeers := copyStrings(rg.Peers) + var ( + desired *DesiredClusterInfo + desiredPeers []string + ) + if d := rg.Desired; d != nil { + desired = &DesiredClusterInfo{ + Created: d.Created, + Name: d.Cluster, + } + // If desired state can be rolled back, include what can be rolled back to. + // Must copy, the origin is owned by the meta layer but reported without its lock. + if d.Origin != nil { + desired.Origin = &DesiredClusterInfoOrigin{ + Replicas: d.Origin.Replicas, + Placement: d.Origin.Placement.clone(), + } + if r := d.Origin.Retention; r != nil { + retention := *r + desired.Origin.Retention = &retention + } + } + // Don't populate peers if scaling down, since the peers aren't the desired set, it's + // the set that the group leader selects the peers to scale down to from. + if !d.ScaleDown { + desiredPeers = copyStrings(d.Peers) + } + } + // The group leader can have a status before desired state exists, most importantly + // when it's still requesting it from the meta leader. Report that on its own. + if status := rg.migration; status != nil { + if desired == nil { + desired = &DesiredClusterInfo{} + } + cp := *status + desired.Status = &cp + } + js.mu.RUnlock() + + ci := &ClusterInfo{ + Name: s.cachedClusterName(), + RaftGroup: rgName, + } + + ourID := s.Node() + id := ourID + if n == nil { + // Desired state exists, but our Raft node hasn't started yet. We have no + // group state to name a leader from, so don't suppress ourselves as a replica. + id = _EMPTY_ + } else { + ci.Leader = s.serverNameForNode(n.GroupLeader()) + ci.LeaderSince = n.LeaderSince() + ci.SystemAcc = n.IsSystemAccount() + ci.TrafficAcc = n.GetTrafficAccountName() + + // If we are leaderless, do not suppress putting us in the peer list. + if ci.Leader == _EMPTY_ { + id = _EMPTY_ + } + + now := time.Now() + for _, rp := range n.Peers() { + // The peer is either in the actual or desired peer set. + if rp.ID != id && (slices.Contains(rgPeers, rp.ID) || slices.Contains(desiredPeers, rp.ID)) { + var lastSeen time.Duration + if now.After(rp.Last) && !rp.Last.IsZero() { + lastSeen = now.Sub(rp.Last) + } + current := rp.Current + if current && lastSeen > lostQuorumInterval { + current = false + } + // Create a peer info with common settings if the peer has not been seen + // yet (which can happen after the whole cluster is stopped and only some + // of the nodes are restarted). + pi := &PeerInfo{ + Current: current, + Offline: true, + Active: lastSeen, + Lag: rp.Lag, + Peer: rp.ID, + } + // If node is found, complete/update the settings. + if sir, ok := s.nodeToInfo.Load(rp.ID); ok && sir != nil { + si := sir.(nodeInfo) + pi.Name, pi.Offline, pi.cluster = si.name, si.offline, si.cluster + } else { + // If not, then add a name that indicates that the server name + // is unknown at this time, and clear the lag since it is misleading + // (the node may not have that much lag). + // Note: We return now the Peer ID in PeerInfo, so the "(peerID: %s)" + // would technically not be required, but keeping it for now. + pi.Name, pi.Lag = fmt.Sprintf("Server name unknown at this time (peerID: %s)", rp.ID), 0 + } + ci.Replicas = append(ci.Replicas, pi) + } + } + } + + generatePeer := func(peer string) *DesiredPeerInfo { + pi := &DesiredPeerInfo{Offline: true, Peer: peer} + if sir, ok := s.nodeToInfo.Load(peer); ok && sir != nil { + si := sir.(nodeInfo) + pi.Name, pi.Offline = si.name, si.offline + } else { + pi.Name = fmt.Sprintf("Server name unknown at this time (peerID: %s)", peer) + } + return pi + } + if desired != nil { + ci.Desired = desired + for _, peer := range desiredPeers { + ci.Desired.Replicas = append(ci.Desired.Replicas, generatePeer(peer)) + } + } + for _, peer := range rgPeers { + // Skip if the peer is already present. + if peer == id || slices.ContainsFunc(ci.Replicas, func(info *PeerInfo) bool { return info.Peer == peer }) { + continue + } + pi := generatePeer(peer) + p := &PeerInfo{Name: pi.Name, Offline: pi.Offline, Peer: pi.Peer} + // We know the peer is part of the assignment, but if we have a Raft node it + // wasn't reported as one of its peers, so it hasn't joined the group (yet). + p.Pending = n != nil + // Report ourselves current if there's no Raft node. + if n == nil && peer == ourID { + p.Current, p.Offline = true, false + } + ci.Replicas = append(ci.Replicas, p) + } + // Order the result based on the name so that we get something consistent + // when doing repeated stream info in the CLI, etc... + slices.SortFunc(ci.Replicas, func(i, j *PeerInfo) int { return cmp.Compare(i.Name, j.Name) }) + return ci +} + +func (mset *stream) checkClusterInfo(ci *ClusterInfo) { + for _, r := range ci.Replicas { + peer := getHash(r.Name) + if lag := mset.lagForCatchupPeer(peer); lag > 0 { + r.Current = false + r.Lag = lag + } + } +} + +// Return a list of alternates, ranked by preference order to the request, of stream mirrors. +// This allows clients to select or get more information about read replicas that could be a +// better option to connect to versus the original source. +func (js *jetStream) streamAlternates(ci *ClientInfo, stream string) []StreamAlternate { + if js == nil { + return nil + } + + js.mu.RLock() + defer js.mu.RUnlock() + + s, cc := js.srv, js.cluster + // Track our domain. + domain := s.getOpts().JetStreamDomain + + // No clustering just return nil. + if cc == nil { + return nil + } + acc, _ := s.LookupAccount(ci.serviceAccount()) + if acc == nil { + return nil + } + + // Collect our ordering first for clusters. + weights := make(map[string]int) + all := []string{ci.Cluster} + all = append(all, ci.Alternates...) + + for i := 0; i < len(all); i++ { + weights[all[i]] = len(all) - i + } + + var alts []StreamAlternate + for _, sa := range cc.streams[acc.Name] { + if sa.unsupported != nil { + continue + } + // Add in ourselves and any mirrors. + if sa.Config.Name == stream || (sa.Config.Mirror != nil && sa.Config.Mirror.Name == stream) { + alts = append(alts, StreamAlternate{Name: sa.Config.Name, Domain: domain, Cluster: sa.Group.Cluster}) + } + } + // If just us don't fill in. + if len(alts) == 1 { + return nil + } + + // Sort based on our weights that originate from the request itself. + // reverse sort + slices.SortFunc(alts, func(i, j StreamAlternate) int { return -cmp.Compare(weights[i.Cluster], weights[j.Cluster]) }) + + return alts +} + +// Internal request for stream info, this is coming on the wire so do not block here. +func (mset *stream) handleClusterStreamInfoRequest(_ *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + // An empty body is a request for the full stream info from the leader. + var req clusterStreamInfoRequest + if len(msg) > 0 && json.Unmarshal(msg, &req) == nil && req.State { + go mset.processClusterStreamStateRequest(reply) + return + } + go mset.processClusterStreamInfoRequest(reply) +} + +// Answer with where our stream store actually is. state() is a FastState and takes no +// stream lock, so we answer even while our own monitor routine is stuck on a catchup. +func (mset *stream) processClusterStreamStateRequest(reply string) { + mset.mu.RLock() + sysc, node := mset.sysc, mset.node + mset.mu.RUnlock() + if sysc == nil || node == nil { + return + } + state := mset.state() + sysc.sendInternalMsg(reply, _EMPTY_, nil, &clusterStreamInfoResponse{Peer: node.ID(), State: &state}) +} + +func (mset *stream) processClusterStreamInfoRequest(reply string) { + mset.mu.RLock() + sysc, js, sa, config := mset.sysc, mset.srv.js.Load(), mset.sa, mset.cfg + isLeader := mset.isLeader() + mset.mu.RUnlock() + + // By design all members will receive this. Normally we only want the leader answering. + // But if we have stalled and lost quorom all can respond. + if sa != nil && !js.isGroupLeaderless(sa.Group) && !isLeader { + return + } + + // If we are not the leader let someone else possibly respond first. + if !isLeader { + time.Sleep(500 * time.Millisecond) + } + + // Report the config as requested, the stream can still be running at its origin. + config = js.targetStreamConfig(mset, config) + + si := &StreamInfo{ + Created: mset.createdTime(), + State: mset.state(), + Config: config, + Cluster: js.clusterInfo(mset.raftGroup()), + Sources: mset.sourcesInfo(), + Mirror: mset.mirrorInfo(), + TimeStamp: time.Now().UTC(), + } + + // Check for out of band catchups. + if mset.hasCatchupPeers() { + mset.checkClusterInfo(si.Cluster) + } + + sysc.sendInternalMsg(reply, _EMPTY_, nil, si) +} + +// 64MB for now, for the total server. This is max we will blast out if asked to +// do so to another server for purposes of catchups. +// This number should be ok on 1Gbit interface. +const defaultMaxTotalCatchupOutBytes = int64(64 * 1024 * 1024) + +// Current total outstanding catchup bytes. +func (s *Server) gcbTotal() int64 { + s.gcbMu.RLock() + defer s.gcbMu.RUnlock() + return s.gcbOut +} + +// Returns true if Current total outstanding catchup bytes is below +// the maximum configured. +func (s *Server) gcbBelowMax() bool { + s.gcbMu.RLock() + defer s.gcbMu.RUnlock() + return s.gcbOut <= s.gcbOutMax +} + +// Adds `sz` to the server's total outstanding catchup bytes and to `localsz` +// under the gcbMu lock. The `localsz` points to the local outstanding catchup +// bytes of the runCatchup go routine of a given stream. +func (s *Server) gcbAdd(localsz *int64, sz int64) { + s.gcbMu.Lock() + atomic.AddInt64(localsz, sz) + s.gcbOut += sz + if s.gcbOut >= s.gcbOutMax && s.gcbKick == nil { + s.gcbKick = make(chan struct{}) + } + s.gcbMu.Unlock() +} + +// Removes `sz` from the server's total outstanding catchup bytes and from +// `localsz`, but only if `localsz` is non 0, which would signal that gcSubLast +// has already been invoked. See that function for details. +// Must be invoked under the gcbMu lock. +func (s *Server) gcbSubLocked(localsz *int64, sz int64) { + if atomic.LoadInt64(localsz) == 0 { + return + } + atomic.AddInt64(localsz, -sz) + s.gcbOut -= sz + if s.gcbKick != nil && s.gcbOut < s.gcbOutMax { + close(s.gcbKick) + s.gcbKick = nil + } +} + +// Locked version of gcbSubLocked() +func (s *Server) gcbSub(localsz *int64, sz int64) { + s.gcbMu.Lock() + s.gcbSubLocked(localsz, sz) + s.gcbMu.Unlock() +} + +// Similar to gcbSub() but reset `localsz` to 0 at the end under the gcbMu lock. +// This will signal further calls to gcbSub() for this `localsz` pointer that +// nothing should be done because runCatchup() has exited and any remaining +// outstanding bytes value has already been decremented. +func (s *Server) gcbSubLast(localsz *int64) { + s.gcbMu.Lock() + s.gcbSubLocked(localsz, *localsz) + *localsz = 0 + s.gcbMu.Unlock() +} + +// Returns our kick chan, or nil if it does not exist. +func (s *Server) cbKickChan() <-chan struct{} { + s.gcbMu.RLock() + defer s.gcbMu.RUnlock() + return s.gcbKick +} + +func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) { + s := mset.srv + defer s.grWG.Done() + + const maxOutBytes = int64(64 * 1024 * 1024) // 64MB for now, these are all internal, from server to server + const maxOutMsgs = int32(256 * 1024) // 256k in case we have lots of small messages or skip msgs. + outb := int64(0) + outm := int32(0) + + // On abnormal exit make sure to update global total. + defer s.gcbSubLast(&outb) + + // Flow control processing. + ackReplySize := func(subj string) int64 { + if li := strings.LastIndexByte(subj, btsep); li > 0 && li < len(subj) { + return parseAckReplyNum(subj[li+1:]) + } + return 0 + } + + nextBatchC := make(chan struct{}, 4) + nextBatchC <- struct{}{} + remoteQuitCh := make(chan struct{}) + var remoteQuitOnce sync.Once + + activityInterval := streamCatchupActivityInterval + notActive := time.NewTimer(activityInterval) + defer notActive.Stop() + + // Setup ackReply for flow control. + ackReply := syncAckSubject() + ackSub, _ := s.sysSubscribe(ackReply, func(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + if len(msg) > 0 { + s.Warnf("Catchup for stream '%s > %s' was aborted on the remote due to: %q", + mset.account(), mset.name(), msg) + s.sysUnsubscribe(sub) + // The unsubscribe is skipped during shutdown, so guard against a repeated abort. + remoteQuitOnce.Do(func() { close(remoteQuitCh) }) + return + } + sz := ackReplySize(subject) + s.gcbSub(&outb, sz) + atomic.AddInt32(&outm, -1) + mset.updateCatchupPeer(sreq.Peer) + // Kick ourselves and anyone else who might have stalled on global state. + select { + case nextBatchC <- struct{}{}: + default: + } + // Reset our activity + notActive.Reset(activityInterval) + }) + defer s.sysUnsubscribe(ackSub) + ackReplyT := strings.ReplaceAll(ackReply, ".*", ".%d") + + // Grab our state. + var state StreamState + // mset.store never changes after being set, don't need lock. + mset.store.FastState(&state) + + // Setup sequences to walk through. + seq, last := sreq.FirstSeq, sreq.LastSeq + + // The follower received a snapshot from another leader, and we've become leader since. + // We have an up-to-date log but could be behind on applies. We must wait until we've reached the minimum required. + // The follower will automatically retry after a timeout, so we can safely return here. + if node := mset.raftNode(); node != nil { + index, _, applied := node.Progress() + // Only skip if our log has enough entries, and they could be applied in the future. + if index >= sreq.MinApplied && applied < sreq.MinApplied { + return + } + // We know here we've either applied enough entries, or our log doesn't have enough entries. + // In the latter case the request expects us to have more. Just continue and value availability here. + // This should only be possible if the logs have already desynced, and we shouldn't have become leader + // in the first place. Not much we can do here in this (hypothetical) scenario. + + // Do another quick sanity check that we actually have enough data to satisfy the request. + // If not, let's step down and hope a new leader can correct this. + if state.LastSeq < last { + s.Warnf("Catchup for stream '%s > %s' skipped, requested sequence %d was larger than current state: %+v", + mset.account(), mset.name(), seq, state) + node.StepDown() + return + } + } + + start := time.Now() + quit := mset.setCatchupPeer(sreq.Peer, last-seq) + + var spb int + const minWait = 5 * time.Second + + sendNextBatchAndContinue := func(qch chan struct{}) bool { + // Check if we know we will not enter the loop because we are done. + if seq > last { + s.Noticef("Catchup for stream '%s > %s' complete (took %v)", mset.account(), mset.name(), time.Since(start).Round(time.Millisecond)) + // EOF + s.sendInternalMsgLocked(sendSubject, _EMPTY_, nil, nil) + return false + } + + // If we already sent a batch, we will try to make sure we can at least send a minimum + // batch before sending the next batch. + if spb > 0 { + // Wait til we can send at least 4k + const minBatchWait = int32(4 * 1024) + mw := time.NewTimer(minWait) + for done := maxOutMsgs-atomic.LoadInt32(&outm) > minBatchWait; !done; { + select { + case <-nextBatchC: + done = maxOutMsgs-atomic.LoadInt32(&outm) > minBatchWait + if !done { + // Wait for a small bit. + time.Sleep(100 * time.Millisecond) + } else { + // GC friendly. + mw.Stop() + } + case <-mw.C: + done = true + case <-s.quitCh: + return false + case <-qch: + return false + case <-remoteQuitCh: + return false + case <-quit: + return false + } + } + spb = 0 + } + + // Send an encoded msg. + sendEM := func(em []byte) { + // Place size in reply subject for flow control. + l := int64(len(em)) + reply := fmt.Sprintf(ackReplyT, l) + s.gcbAdd(&outb, l) + atomic.AddInt32(&outm, 1) + s.sendInternalMsgLocked(sendSubject, reply, nil, em) + spb++ + } + + // If we support gap markers. + var dr DeleteRange + drOk := sreq.DeleteRangesOk + + // Will send our delete range. + // Should already be checked for being valid. + sendDR := func() { + if dr.Num == 1 { + // Send like a normal skip msg. + sendEM(encodeStreamMsg(_EMPTY_, _EMPTY_, nil, nil, dr.First, 0, false)) + } else { + // We have a run, send a gap record. We send these without reply or tracking. + s.sendInternalMsgLocked(sendSubject, _EMPTY_, nil, encodeDeleteRange(&dr)) + // Clear out the pending for catchup. + mset.decrementCatchupPeer(sreq.Peer, dr.Num) + } + // Reset always. + dr.First, dr.Num = 0, 0 + } + + // See if we should use LoadNextMsg instead of walking sequence by sequence if we have an order magnitude more interior deletes. + // Only makes sense with delete range capabilities. + useLoadNext := drOk && (uint64(state.NumDeleted) > 2*state.Msgs || state.NumDeleted > 1_000_000) + + var smv StoreMsg + for ; seq <= last && atomic.LoadInt64(&outb) <= maxOutBytes && atomic.LoadInt32(&outm) <= maxOutMsgs && s.gcbBelowMax(); seq++ { + var sm *StoreMsg + var err error + // If we should use load next do so here. + if useLoadNext { + var nseq uint64 + sm, nseq, err = mset.store.LoadNextMsg(fwcs, true, seq, &smv) + if err == nil && nseq > seq { + // If we jumped over the requested last sequence, clamp it down. + // Otherwise, we would send too much to the follower. + if nseq > last { + nseq = last + sm = nil + } + dr.First, dr.Num = seq, nseq-seq + // Jump ahead + seq = nseq + } else if err == ErrStoreEOF { + dr.First, dr.Num = seq, last-seq + // Clear EOF here for normal processing. + err = nil + // Jump ahead + seq = last + } + } else { + sm, err = mset.store.LoadMsg(seq, &smv) + } + + // if this is not a deleted msg, bail out. + if err != nil && err != ErrStoreMsgNotFound && err != errDeletedMsg { + if err == ErrStoreEOF { + var state StreamState + mset.store.FastState(&state) + if seq > state.LastSeq { + // The snapshot has a larger last sequence then we have. This could be due to a truncation + // when trying to recover after corruption, still not 100% sure. Could be off by 1 too somehow, + // but tested a ton of those with no success. + s.Warnf("Catchup for stream '%s > %s' completed (took %v), but requested sequence %d was larger than current state: %+v", + mset.account(), mset.name(), time.Since(start).Round(time.Millisecond), seq, state) + // Try our best to redo our invalidated snapshot as well. + mset.mu.Lock() + if n := mset.node; n != nil { + if err := mset.flushAllPending(); err == nil { + n.InstallSnapshot(mset.stateSnapshotLocked(), true) + } + } + mset.mu.Unlock() + // If we allow gap markers check if we have one pending. + if drOk && dr.First > 0 { + sendDR() + } + // Signal EOF + s.sendInternalMsgLocked(sendSubject, _EMPTY_, nil, nil) + return false + } + } + s.Warnf("Error loading message for catchup '%s > %s': %v", mset.account(), mset.name(), err) + return false + } + + if sm != nil { + // If we allow gap markers check if we have one pending. + if drOk && dr.First > 0 { + sendDR() + } + // Send the normal message now. + sendEM(encodeStreamMsgAllowCompress(sm.subj, _EMPTY_, sm.hdr, sm.msg, sm.seq, sm.ts, false)) + } else { + if drOk { + if dr.First == 0 { + dr.First, dr.Num = seq, 1 + } else { + dr.Num++ + } + } else { + // Skip record for deleted msg. + sendEM(encodeStreamMsg(_EMPTY_, _EMPTY_, nil, nil, seq, 0, false)) + } + } + + // Check if we are done. + if seq == last { + // Need to see if we have a pending delete range. + if drOk && dr.First > 0 { + sendDR() + } + s.Noticef("Catchup for stream '%s > %s' complete (took %v)", mset.account(), mset.name(), time.Since(start).Round(time.Millisecond)) + // EOF + s.sendInternalMsgLocked(sendSubject, _EMPTY_, nil, nil) + return false + } + select { + case <-remoteQuitCh: + return false + case <-quit: + return false + default: + } + } + if drOk && dr.First > 0 { + sendDR() + } + return true + } + + // Check is this stream got closed. + mset.mu.RLock() + qch := mset.qch + mset.mu.RUnlock() + if qch == nil { + return + } + + // Run as long as we are still active and need catchup. + // FIXME(dlc) - Purge event? Stream delete? + retryTimer := time.NewTimer(500 * time.Millisecond) + defer stopAndClearTimer(&retryTimer) + for { + // Get this each time, will be non-nil if globally blocked and we will close to wake everyone up. + cbKick := s.cbKickChan() + + select { + case <-s.quitCh: + return + case <-qch: + return + case <-remoteQuitCh: + mset.clearCatchupPeer(sreq.Peer, quit) + return + case <-quit: + // Superseded by a newer request from this peer, or the peer left the group. + return + case <-notActive.C: + s.Warnf("Catchup for stream '%s > %s' stalled", mset.account(), mset.name()) + // Do NOT clear the catchup peer on a transient inactivity stall, this allows the + // follower to retry without us losing track of it requiring catchup. + mset.detachCatchupPeer(sreq.Peer, quit) + return + case <-nextBatchC: + if !sendNextBatchAndContinue(qch) { + mset.clearCatchupPeer(sreq.Peer, quit) + return + } + case <-cbKick: + if !sendNextBatchAndContinue(qch) { + mset.clearCatchupPeer(sreq.Peer, quit) + return + } + case <-retryTimer.C: + if !sendNextBatchAndContinue(qch) { + mset.clearCatchupPeer(sreq.Peer, quit) + return + } + } + retryTimer.Reset(500 * time.Millisecond) + } +} + +const jscAllSubj = "$JSC.>" + +func syncSubjForStream() string { + return syncSubject("$JSC.SYNC") +} + +func syncReplySubject() string { + return syncSubject("$JSC.R") +} + +// infoReplySubject returns a reply subject for an internally-issued JetStream +// API request. With js_api_reply_v2 enabled, it scopes the subject to the +// requested API resource so leafnode permissions do not need to grant access +// to every opaque $JSC.R reply. +func (s *Server) infoReplySubject(domain, account, stream, consumer string) string { + if !s.getOpts().getFeatureFlag(FeatureFlagJsAPIReplyFormatV2) { + return syncSubject("$JSC.R") + } + if domain == _EMPTY_ { + domain = "_" + } + return syncSubject(fmt.Sprintf("$JSC.R.%s.%s.%s.%s", domain, getHash(account), stream, consumer)) +} + +func syncAckSubject() string { + return syncSubject("$JSC.ACK") + ".*" +} + +func syncSubject(pre string) string { + var sb strings.Builder + sb.WriteString(pre) + sb.WriteByte(btsep) + + var b [replySuffixLen]byte + rn := rand.Int64() + for i, l := 0, rn; i < len(b); i++ { + b[i] = digits[l%base] + l /= base + } + + sb.Write(b[:]) + return sb.String() +} + +const ( + clusterStreamInfoT = "$JSC.SI.%s.%s" + clusterConsumerInfoT = "$JSC.CI.%s.%s.%s" + jsaUpdatesSubT = "$JSC.ARU.%s.*" + jsaUpdatesPubT = "$JSC.ARU.%s.%s" +) diff --git a/server/monitor.go b/server/monitor.go new file mode 100644 index 0000000..afb5e53 --- /dev/null +++ b/server/monitor.go @@ -0,0 +1,4383 @@ +// Copyright 2013-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 ( + "bytes" + "cmp" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/json" + "errors" + "expvar" + "fmt" + "maps" + "math" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "runtime/debug" + "runtime/pprof" + "slices" + "sort" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/nats-io/jwt/v2" + "github.com/nats-io/nats-server/v2/server/pse" +) + +// Connz represents detailed information on current client connections. +type Connz struct { + ID string `json:"server_id"` + Now time.Time `json:"now"` + NumConns int `json:"num_connections"` + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Conns []*ConnInfo `json:"connections"` +} + +// ConnzOptions are the options passed to Connz() +type ConnzOptions struct { + // Sort indicates how the results will be sorted. Check SortOpt for possible values. + // Only the sort by connection ID (ByCid) is ascending, all others are descending. + Sort SortOpt `json:"sort"` + + // Username indicates if user names should be included in the results. + Username bool `json:"auth"` + + // Subscriptions indicates if subscriptions should be included in the results. + Subscriptions bool `json:"subscriptions"` + + // SubscriptionsDetail indicates if subscription details should be included in the results + SubscriptionsDetail bool `json:"subscriptions_detail"` + + // Offset is used for pagination. Connz() only returns connections starting at this + // offset from the global results. + Offset int `json:"offset"` + + // Limit is the maximum number of connections that should be returned by Connz(). + Limit int `json:"limit"` + + // Filter for this explicit client connection. + CID uint64 `json:"cid"` + + // Filter for this explicit client connection based on the MQTT client ID + MQTTClient string `json:"mqtt_client"` + + // Filter by connection state. + State ConnState `json:"state"` + + // The below options only apply if auth is true. + + // Filter by username. + User string `json:"user"` + + // Filter by account. + Account string `json:"acc"` + + // Filter by subject interest + FilterSubject string `json:"filter_subject"` +} + +// ConnState is for filtering states of connections. We will only have two, open and closed. +type ConnState int + +const ( + // ConnOpen filters on open clients. + ConnOpen = ConnState(iota) + // ConnClosed filters on closed clients. + ConnClosed + // ConnAll returns all clients. + ConnAll +) + +// ConnInfo has detailed information on a per connection basis. +type ConnInfo struct { + Cid uint64 `json:"cid"` + Kind string `json:"kind,omitempty"` + Type string `json:"type,omitempty"` + IP string `json:"ip"` + Port int `json:"port"` + Start time.Time `json:"start"` + LastActivity time.Time `json:"last_activity"` + Stop *time.Time `json:"stop,omitempty"` + Reason string `json:"reason,omitempty"` + RTT string `json:"rtt,omitempty"` + Uptime string `json:"uptime"` + Idle string `json:"idle"` + Pending int `json:"pending_bytes"` + InMsgs int64 `json:"in_msgs"` + OutMsgs int64 `json:"out_msgs"` + InBytes int64 `json:"in_bytes"` + OutBytes int64 `json:"out_bytes"` + Stalls int64 `json:"stalls,omitempty"` + NumSubs uint32 `json:"subscriptions"` + Name string `json:"name,omitempty"` + Lang string `json:"lang,omitempty"` + Version string `json:"version,omitempty"` + TLSVersion string `json:"tls_version,omitempty"` + TLSCipher string `json:"tls_cipher_suite,omitempty"` + TLSPeerCerts []*TLSPeerCert `json:"tls_peer_certs,omitempty"` + TLSFirst bool `json:"tls_first,omitempty"` + AuthorizedUser string `json:"authorized_user,omitempty"` + Account string `json:"account,omitempty"` + Subs []string `json:"subscriptions_list,omitempty"` + SubsDetail []SubDetail `json:"subscriptions_list_detail,omitempty"` + JWT string `json:"jwt,omitempty"` + IssuerKey string `json:"issuer_key,omitempty"` + NameTag string `json:"name_tag,omitempty"` + Tags jwt.TagList `json:"tags,omitempty"` + MQTTClient string `json:"mqtt_client,omitempty"` // This is the MQTT client id + Proxy *ProxyInfo `json:"proxy,omitempty"` + + // Internal + rtt int64 // For fast sorting +} + +// ProxyInfo represents the information about this proxied connection. +type ProxyInfo struct { + Key string `json:"key"` +} + +// TLSPeerCert contains basic information about a TLS peer certificate +type TLSPeerCert struct { + Subject string `json:"subject,omitempty"` + SubjectPKISha256 string `json:"spki_sha256,omitempty"` + CertSha256 string `json:"cert_sha256,omitempty"` +} + +// DefaultConnListSize is the default size of the connection list. +const DefaultConnListSize = 1024 + +// DefaultSubListSize is the default size of the subscriptions list. +const DefaultSubListSize = 1024 + +const defaultStackBufSize = 10000 + +func newSubsDetailList(client *client) []SubDetail { + subsDetail := make([]SubDetail, 0, len(client.subs)) + for _, sub := range client.subs { + subsDetail = append(subsDetail, newClientSubDetail(sub)) + } + return subsDetail +} + +func newSubsList(client *client) []string { + subs := make([]string, 0, len(client.subs)) + for _, sub := range client.subs { + subs = append(subs, string(sub.subject)) + } + return subs +} + +func redactBearerJWT(userJWT string) string { + if userJWT == _EMPTY_ { + return _EMPTY_ + } + uc, err := jwt.DecodeUserClaims(userJWT) + if err == nil && uc != nil && uc.BearerToken { + return _EMPTY_ + } + return userJWT +} + +// Connz returns a Connz struct containing information about connections. +func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) { + var ( + sortOpt = ByCid + auth bool + subs bool + subsDet bool + offset int + limit = DefaultConnListSize + cid = uint64(0) + state = ConnOpen + user string + acc string + a *Account + filter string + mqttCID string + ) + + if opts != nil { + // If no sort option given or sort is by uptime, then sort by cid + if opts.Sort != _EMPTY_ { + sortOpt = opts.Sort + if !sortOpt.IsValid() { + return nil, fmt.Errorf("invalid sorting option: %s", sortOpt) + } + } + + // Auth specifics. + auth = opts.Username + user = opts.User + acc = opts.Account + mqttCID = opts.MQTTClient + + subs = opts.Subscriptions + subsDet = opts.SubscriptionsDetail + offset = opts.Offset + if offset < 0 { + offset = 0 + } + limit = opts.Limit + if limit <= 0 { + limit = DefaultConnListSize + } + // state + state = opts.State + + // ByStop only makes sense on closed connections + if sortOpt == ByStop && state != ConnClosed { + return nil, fmt.Errorf("sort by stop only valid on closed connections") + } + // ByReason is the same. + if sortOpt == ByReason && state != ConnClosed { + return nil, fmt.Errorf("sort by reason only valid on closed connections") + } + // If searching by CID + if opts.CID > 0 { + cid = opts.CID + limit = 1 + } + // If filtering by subject. + if opts.FilterSubject != _EMPTY_ && opts.FilterSubject != fwcs { + if acc == _EMPTY_ { + return nil, fmt.Errorf("filter by subject only valid with account filtering") + } + filter = opts.FilterSubject + } + } + + c := &Connz{ + Offset: offset, + Limit: limit, + Now: time.Now().UTC(), + } + + // Open clients + var openClients []*client + // Selected client + var cidClient *client + // Hold for closed clients if requested. + var closedClients []*closedClient + + var clist map[uint64]*client + + if acc != _EMPTY_ { + var err error + a, err = s.lookupAccount(acc) + if err != nil { + return c, nil + } + a.mu.RLock() + clist = make(map[uint64]*client, a.numLocalConnections()) + for c := range a.clients { + if c.kind == CLIENT || c.kind == LEAF { + clist[c.cid] = c + } + } + a.mu.RUnlock() + } + + // Snapshot server connection state. + s.mu.RLock() + // Default to all client unless filled in above. + if clist == nil { + clist = make(map[uint64]*client, len(s.clients)+len(s.leafs)) + maps.Copy(clist, s.clients) + maps.Copy(clist, s.leafs) + } + + // copy the server id for monitoring + c.ID = s.info.ID + // select client by CID + if cid > 0 { + cidClient = s.clients[cid] + } + s.mu.RUnlock() + + // Number of total clients. The resulting ConnInfo array + // may be smaller if pagination is used. + switch state { + case ConnOpen: + c.Total = len(clist) + case ConnClosed: + closedClients = s.closed.closedClients() + c.Total = len(closedClients) + case ConnAll: + c.Total = len(clist) + closedClients = s.closed.closedClients() + c.Total += len(closedClients) + } + + // We may need to filter these connections. + if acc != _EMPTY_ && len(closedClients) > 0 { + var ccc []*closedClient + for _, cc := range closedClients { + if cc.acc != acc { + continue + } + ccc = append(ccc, cc) + } + c.Total -= (len(closedClients) - len(ccc)) + closedClients = ccc + } + + totalClients := c.Total + if cid > 0 { // Meaning we only want 1. + totalClients = 1 + } + if state == ConnOpen || state == ConnAll { + openClients = make([]*client, 0, totalClients) + } + + // Data structures for results. + var conns []ConnInfo // Limits allocs for actual ConnInfos. + var pconns ConnInfos + + switch state { + case ConnOpen: + conns = make([]ConnInfo, totalClients) + pconns = make(ConnInfos, totalClients) + case ConnClosed: + pconns = make(ConnInfos, totalClients) + case ConnAll: + conns = make([]ConnInfo, cap(openClients)) + pconns = make(ConnInfos, totalClients) + } + + // Search by individual CID. + if cid > 0 { + // Let's first check if user also selects on ConnOpen or ConnAll + // and look for opened connections. + if state == ConnOpen || state == ConnAll { + if cidClient != nil { + openClients = append(openClients, cidClient) + closedClients = nil + } + } + // If we did not find, and the user selected for ConnClosed or ConnAll, + // look for closed connections. + if len(openClients) == 0 && (state == ConnClosed || state == ConnAll) { + copyClosed := closedClients + closedClients = nil + for _, cc := range copyClosed { + if cc.Cid == cid { + closedClients = []*closedClient{cc} + break + } + } + } + } else { + // Gather all open clients. + if state == ConnOpen || state == ConnAll { + for _, client := range clist { + // Snapshot each under the client lock. + client.mu.RLock() + var cAccName string + if client.acc != nil { + cAccName = client.acc.Name + } + cAuthUser := client.getRawAuthUser() + cMQTTID := client.getMQTTClientID() + client.mu.RUnlock() + + // If we have an account specified we need to filter. + if acc != _EMPTY_ && cAccName != acc { + continue + } + // Do user filtering second + if user != _EMPTY_ && cAuthUser != user { + continue + } + // Do mqtt client ID filtering next + if mqttCID != _EMPTY_ && cMQTTID != mqttCID { + continue + } + openClients = append(openClients, client) + } + } + } + + // Filter by subject now if needed. We do this outside of server lock. + if filter != _EMPTY_ { + var oc []*client + for _, c := range openClients { + c.mu.Lock() + for _, sub := range c.subs { + if SubjectsCollide(filter, string(sub.subject)) { + oc = append(oc, c) + break + } + } + c.mu.Unlock() + openClients = oc + } + } + + // Just return with empty array if nothing here. + if len(openClients) == 0 && len(closedClients) == 0 { + c.Conns = ConnInfos{} + return c, nil + } + + // Now whip through and generate ConnInfo entries + // Open Clients + i := 0 + for _, client := range openClients { + client.mu.Lock() + ci := &conns[i] + ci.fill(client, client.nc, c.Now, auth) + // Fill in subscription data if requested. + if len(client.subs) > 0 { + if subsDet { + ci.SubsDetail = newSubsDetailList(client) + } else if subs { + ci.Subs = newSubsList(client) + } + } + // Fill in user if auth requested. + if auth { + ci.AuthorizedUser = client.getRawAuthUser() + if name := client.acc.GetName(); name != globalAccountName { + ci.Account = name + } + ci.JWT = client.opts.JWT + ci.IssuerKey = issuerForClient(client) + ci.Tags = client.tags + ci.NameTag = client.acc.getNameTag() + } + client.mu.Unlock() + ci.JWT = redactBearerJWT(ci.JWT) + pconns[i] = ci + i++ + } + // Closed Clients + var needCopy bool + if subs || subsDet || auth { + needCopy = true + } + for _, cc := range closedClients { + // If we have an account specified we need to filter. + if acc != _EMPTY_ && cc.acc != acc { + continue + } + // Do user filtering second + if user != _EMPTY_ && cc.user != user { + continue + } + // Do mqtt client ID filtering next + if mqttCID != _EMPTY_ && cc.MQTTClient != mqttCID { + continue + } + // Copy if needed for any changes to the ConnInfo + if needCopy { + cx := *cc + cc = &cx + } + // Fill in subscription data if requested. + if len(cc.subs) > 0 { + if subsDet { + cc.SubsDetail = cc.subs + } else if subs { + cc.Subs = make([]string, 0, len(cc.subs)) + for _, sub := range cc.subs { + cc.Subs = append(cc.Subs, sub.Subject) + } + } + } + // Fill in user if auth requested. + if auth { + cc.AuthorizedUser = cc.user + if cc.acc != _EMPTY_ && (cc.acc != globalAccountName) { + cc.Account = cc.acc + if acc, err := s.LookupAccount(cc.acc); err == nil { + cc.NameTag = acc.getNameTag() + } + } + cc.JWT = redactBearerJWT(cc.JWT) + } + pconns[i] = &cc.ConnInfo + i++ + } + + // This will trip if we have filtered out client connections. + if len(pconns) != i { + pconns = pconns[:i] + totalClients = i + } + + switch sortOpt { + case ByCid, ByStart: + sort.Sort(SortByCid{pconns}) + case BySubs: + sort.Sort(sort.Reverse(SortBySubs{pconns})) + case ByPending: + sort.Sort(sort.Reverse(SortByPending{pconns})) + case ByOutMsgs: + sort.Sort(sort.Reverse(SortByOutMsgs{pconns})) + case ByInMsgs: + sort.Sort(sort.Reverse(SortByInMsgs{pconns})) + case ByOutBytes: + sort.Sort(sort.Reverse(SortByOutBytes{pconns})) + case ByInBytes: + sort.Sort(sort.Reverse(SortByInBytes{pconns})) + case ByLast: + sort.Sort(sort.Reverse(SortByLast{pconns})) + case ByIdle: + sort.Sort(sort.Reverse(SortByIdle{pconns, c.Now})) + case ByUptime: + sort.Sort(SortByUptime{pconns, time.Now()}) + case ByStop: + sort.Sort(sort.Reverse(SortByStop{pconns})) + case ByReason: + sort.Sort(SortByReason{pconns}) + case ByRTT: + sort.Sort(sort.Reverse(SortByRTT{pconns})) + } + + // Make sure these are sane. + maxIndex := totalClients + minoff := min(max(c.Offset, 0), maxIndex) + maxoff := minoff + min(c.Limit, maxIndex-minoff) + + // Now pare down to the requested size. + // TODO(dlc) - for very large number of connections we + // could save the whole list in a hash, send hash on first + // request and allow users to use has for subsequent pages. + // Low TTL, say < 1sec. + c.Conns = pconns[minoff:maxoff] + c.NumConns = len(c.Conns) + + return c, nil +} + +// Fills in the ConnInfo from the client. +// client should be locked. +func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool) { + // For fast sort if required. + rtt := client.getRTT() + ci.rtt = int64(rtt) + + ci.Cid = client.cid + ci.MQTTClient = client.getMQTTClientID() + ci.Kind = client.kindString() + ci.Type = client.clientTypeString() + ci.Start = client.start + ci.LastActivity = client.last + ci.Uptime = myUptime(now.Sub(client.start)) + ci.Idle = myUptime(now.Sub(client.last)) + ci.RTT = rtt.String() + ci.OutMsgs = client.outMsgs + ci.OutBytes = client.outBytes + ci.NumSubs = uint32(len(client.subs)) + ci.Pending = int(client.out.pb) + ci.Name = client.opts.Name + ci.Lang = client.opts.Lang + ci.Version = client.opts.Version + // inMsgs and inBytes are updated outside of the client's lock, so + // we need to use atomic here. + ci.InMsgs = atomic.LoadInt64(&client.inMsgs) + ci.InBytes = atomic.LoadInt64(&client.inBytes) + ci.Stalls = atomic.LoadInt64(&client.stalls) + ci.Proxy = createProxyInfo(client) + + // If the connection is gone, too bad, we won't set TLSVersion and TLSCipher. + // Exclude clients that are still doing handshake so we don't block in + // ConnectionState(). + if client.flags.isSet(handshakeComplete) && nc != nil { + if conn, ok := nc.(*tls.Conn); ok { + cs := conn.ConnectionState() + ci.TLSVersion = tlsVersion(cs.Version) + ci.TLSCipher = tls.CipherSuiteName(cs.CipherSuite) + if auth && len(cs.PeerCertificates) > 0 { + ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates) + } + ci.TLSFirst = client.flags.isSet(didTLSFirst) + } + } + + if client.port != 0 { + ci.Port = int(client.port) + ci.IP = client.host + } +} + +// If this client came from a trusted proxy, this will return a ProxyInfo +// to be used in ConnInfo or LeafInfo. +// +// Client lock must be held on entry. +func createProxyInfo(c *client) *ProxyInfo { + if c.proxyKey == _EMPTY_ { + return nil + } + return &ProxyInfo{Key: c.proxyKey} +} + +func makePeerCerts(pc []*x509.Certificate) []*TLSPeerCert { + res := make([]*TLSPeerCert, len(pc)) + for i, c := range pc { + tmp := sha256.Sum256(c.RawSubjectPublicKeyInfo) + ssha := hex.EncodeToString(tmp[:]) + tmp = sha256.Sum256(c.Raw) + csha := hex.EncodeToString(tmp[:]) + res[i] = &TLSPeerCert{Subject: c.Subject.String(), SubjectPKISha256: ssha, CertSha256: csha} + } + return res +} + +// Assume lock is held +func (c *client) getRTT() time.Duration { + if c.rtt == 0 { + // If a real client, go ahead and send ping now to get a value + // for RTT. For tests and telnet, or if client is closing, etc skip. + if c.opts.Lang != _EMPTY_ { + c.sendRTTPingLocked() + } + return 0 + } + var rtt time.Duration + if c.rtt > time.Microsecond && c.rtt < time.Millisecond { + rtt = c.rtt.Truncate(time.Microsecond) + } else { + rtt = c.rtt.Truncate(time.Nanosecond) + } + return rtt +} + +func decodeBool(w http.ResponseWriter, r *http.Request, param string) (bool, error) { + str := r.URL.Query().Get(param) + if str == _EMPTY_ { + return false, nil + } + val, err := strconv.ParseBool(str) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Error decoding boolean for '%s': %v", param, err))) + return false, err + } + return val, nil +} + +func decodeUint64(w http.ResponseWriter, r *http.Request, param string) (uint64, error) { + str := r.URL.Query().Get(param) + if str == _EMPTY_ { + return 0, nil + } + val, err := strconv.ParseUint(str, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Error decoding uint64 for '%s': %v", param, err))) + return 0, err + } + return val, nil +} + +func decodeInt(w http.ResponseWriter, r *http.Request, param string) (int, error) { + str := r.URL.Query().Get(param) + if str == _EMPTY_ { + return 0, nil + } + val, err := strconv.Atoi(str) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf("Error decoding int for '%s': %v", param, err))) + return 0, err + } + return val, nil +} + +func decodeState(w http.ResponseWriter, r *http.Request) (ConnState, error) { + str := r.URL.Query().Get("state") + if str == _EMPTY_ { + return ConnOpen, nil + } + switch strings.ToLower(str) { + case "open": + return ConnOpen, nil + case "closed": + return ConnClosed, nil + case "any", "all": + return ConnAll, nil + } + // We do not understand intended state here. + w.WriteHeader(http.StatusBadRequest) + err := fmt.Errorf("Error decoding state for %s", str) + w.Write([]byte(err.Error())) + return 0, err +} + +func decodeSubs(w http.ResponseWriter, r *http.Request) (subs bool, subsDet bool, err error) { + subsDet = strings.ToLower(r.URL.Query().Get("subs")) == "detail" + if !subsDet { + subs, err = decodeBool(w, r, "subs") + } + return +} + +// HandleConnz process HTTP requests for connection information. +func (s *Server) HandleConnz(w http.ResponseWriter, r *http.Request) { + sortOpt := SortOpt(r.URL.Query().Get("sort")) + auth, err := decodeBool(w, r, "auth") + if err != nil { + return + } + subs, subsDet, err := decodeSubs(w, r) + if err != nil { + return + } + offset, err := decodeInt(w, r, "offset") + if err != nil { + return + } + limit, err := decodeInt(w, r, "limit") + if err != nil { + return + } + cid, err := decodeUint64(w, r, "cid") + if err != nil { + return + } + state, err := decodeState(w, r) + if err != nil { + return + } + + user := r.URL.Query().Get("user") + acc := r.URL.Query().Get("acc") + mqttCID := r.URL.Query().Get("mqtt_client") + + connzOpts := &ConnzOptions{ + Sort: sortOpt, + Username: auth, + Subscriptions: subs, + SubscriptionsDetail: subsDet, + Offset: offset, + Limit: limit, + CID: cid, + MQTTClient: mqttCID, + State: state, + User: user, + Account: acc, + } + + s.mu.Lock() + s.httpReqStats[ConnzPath]++ + s.mu.Unlock() + + c, err := s.Connz(connzOpts) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + b, err := json.MarshalIndent(c, "", " ") + if err != nil { + s.Errorf("Error marshaling response to /connz request: %v", err) + } + + // Handle response + ResponseHandler(w, r, b) +} + +// Routez represents detailed information on current client connections. +type Routez struct { + ID string `json:"server_id"` + Name string `json:"server_name"` + Now time.Time `json:"now"` + Import *SubjectPermission `json:"import,omitempty"` + Export *SubjectPermission `json:"export,omitempty"` + NumRoutes int `json:"num_routes"` + Routes []*RouteInfo `json:"routes"` +} + +// RoutezOptions are options passed to Routez +type RoutezOptions struct { + // Subscriptions indicates that Routez will return a route's subscriptions + Subscriptions bool `json:"subscriptions"` + // SubscriptionsDetail indicates if subscription details should be included in the results + SubscriptionsDetail bool `json:"subscriptions_detail"` +} + +// RouteInfo has detailed information on a per connection basis. +type RouteInfo struct { + Rid uint64 `json:"rid"` + RemoteID string `json:"remote_id"` + RemoteName string `json:"remote_name"` + DidSolicit bool `json:"did_solicit"` + IsConfigured bool `json:"is_configured"` + IP string `json:"ip"` + Port int `json:"port"` + Start time.Time `json:"start"` + LastActivity time.Time `json:"last_activity"` + RTT string `json:"rtt,omitempty"` + Uptime string `json:"uptime"` + Idle string `json:"idle"` + Import *SubjectPermission `json:"import,omitempty"` + Export *SubjectPermission `json:"export,omitempty"` + Pending int `json:"pending_size"` + InMsgs int64 `json:"in_msgs"` + OutMsgs int64 `json:"out_msgs"` + InBytes int64 `json:"in_bytes"` + OutBytes int64 `json:"out_bytes"` + NumSubs uint32 `json:"subscriptions"` + Subs []string `json:"subscriptions_list,omitempty"` + SubsDetail []SubDetail `json:"subscriptions_list_detail,omitempty"` + Account string `json:"account,omitempty"` + Compression string `json:"compression,omitempty"` +} + +// Routez returns a Routez struct containing information about routes. +func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) { + rs := &Routez{ + Now: time.Now().UTC(), + Routes: []*RouteInfo{}, + } + + if routezOpts == nil { + routezOpts = &RoutezOptions{} + } + + s.mu.Lock() + rs.NumRoutes = s.numRoutes() + + // copy the server id for monitoring + rs.ID = s.info.ID + + // Check for defined permissions for all connected routes. + if perms := s.getOpts().Cluster.Permissions; perms != nil { + rs.Import = perms.Import + rs.Export = perms.Export + } + rs.Name = s.info.Name + + addRoute := func(r *client) { + r.mu.Lock() + ri := &RouteInfo{ + Rid: r.cid, + RemoteID: r.route.remoteID, + RemoteName: r.route.remoteName, + DidSolicit: r.route.didSolicit, + IsConfigured: r.route.routeType == Explicit, + InMsgs: atomic.LoadInt64(&r.inMsgs), + OutMsgs: r.outMsgs, + InBytes: atomic.LoadInt64(&r.inBytes), + OutBytes: r.outBytes, + NumSubs: uint32(len(r.subs)), + Import: r.opts.Import, + Pending: int(r.out.pb), + Export: r.opts.Export, + RTT: r.getRTT().String(), + Start: r.start, + LastActivity: r.last, + Uptime: myUptime(rs.Now.Sub(r.start)), + Idle: myUptime(rs.Now.Sub(r.last)), + Account: string(r.route.accName), + Compression: r.route.compression, + } + + if len(r.subs) > 0 { + if routezOpts.SubscriptionsDetail { + ri.SubsDetail = newSubsDetailList(r) + } else if routezOpts.Subscriptions { + ri.Subs = newSubsList(r) + } + } + + switch conn := r.nc.(type) { + case *net.TCPConn, *tls.Conn: + addr := conn.RemoteAddr().(*net.TCPAddr) + ri.Port = addr.Port + ri.IP = addr.IP.String() + } + r.mu.Unlock() + rs.Routes = append(rs.Routes, ri) + } + + // Walk the list + s.forEachRoute(func(r *client) { + addRoute(r) + }) + s.mu.Unlock() + return rs, nil +} + +// HandleRoutez process HTTP requests for route information. +func (s *Server) HandleRoutez(w http.ResponseWriter, r *http.Request) { + subs, subsDetail, err := decodeSubs(w, r) + if err != nil { + return + } + + opts := RoutezOptions{Subscriptions: subs, SubscriptionsDetail: subsDetail} + + s.mu.Lock() + s.httpReqStats[RoutezPath]++ + s.mu.Unlock() + + // As of now, no error is ever returned. + rs, _ := s.Routez(&opts) + b, err := json.MarshalIndent(rs, "", " ") + if err != nil { + s.Errorf("Error marshaling response to /routez request: %v", err) + } + + // Handle response + ResponseHandler(w, r, b) +} + +// Subsz represents detail information on current connections. +type Subsz struct { + ID string `json:"server_id"` + Now time.Time `json:"now"` + *SublistStats + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Subs []SubDetail `json:"subscriptions_list,omitempty"` +} + +// SubszOptions are the options passed to Subsz. +// As of now, there are no options defined. +type SubszOptions struct { + // Offset is used for pagination. Subsz() only returns connections starting at this + // offset from the global results. + Offset int `json:"offset"` + + // Limit is the maximum number of subscriptions that should be returned by Subsz(). + Limit int `json:"limit"` + + // Subscriptions indicates if subscription details should be included in the results. + Subscriptions bool `json:"subscriptions"` + + // Filter based on this account name. + Account string `json:"account,omitempty"` + + // Test the list against this subject. Needs to be literal since it signifies a publish subject. + // We will only return subscriptions that would match if a message was sent to this subject. + Test string `json:"test,omitempty"` +} + +// SubDetail is for verbose information for subscriptions. +type SubDetail struct { + Account string `json:"account,omitempty"` + AccountTag string `json:"account_tag,omitempty"` + Subject string `json:"subject"` + Queue string `json:"qgroup,omitempty"` + Sid string `json:"sid"` + Msgs int64 `json:"msgs"` + Max int64 `json:"max,omitempty"` + Cid uint64 `json:"cid"` +} + +// Subscription client should be locked and guaranteed to be present. +func newSubDetail(sub *subscription) SubDetail { + sd := newClientSubDetail(sub) + sd.Account = sub.client.acc.GetName() + sd.AccountTag = sub.client.acc.getNameTag() + return sd +} + +// For subs details under clients. +func newClientSubDetail(sub *subscription) SubDetail { + return SubDetail{ + Subject: string(sub.subject), + Queue: string(sub.queue), + Sid: string(sub.sid), + Msgs: sub.nm, + Max: sub.max, + Cid: sub.client.cid, + } +} + +// Subsz returns a Subsz struct containing subjects statistics +func (s *Server) Subsz(opts *SubszOptions) (*Subsz, error) { + var ( + subdetail bool + test bool + offset int + testSub string + filterAcc string + limit = DefaultSubListSize + ) + + if opts != nil { + subdetail = opts.Subscriptions + offset = opts.Offset + if offset < 0 { + offset = 0 + } + limit = opts.Limit + if limit <= 0 { + limit = DefaultSubListSize + } + if opts.Test != _EMPTY_ { + testSub = opts.Test + test = true + if !IsValidLiteralSubject(testSub) { + return nil, fmt.Errorf("invalid test subject, must be valid publish subject: %s", testSub) + } + } + if opts.Account != _EMPTY_ { + filterAcc = opts.Account + } + } + + slStats := &SublistStats{} + + // FIXME(dlc) - Make account aware. + sz := &Subsz{ + ID: s.info.ID, + Now: time.Now().UTC(), + SublistStats: slStats, + Total: 0, + Offset: offset, + Limit: limit, + Subs: nil, + } + + if subdetail { + var raw [4096]*subscription + subs := raw[:0] + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + if filterAcc != _EMPTY_ && acc.GetName() != filterAcc { + return true + } + slStats.add(acc.sl.Stats()) + acc.sl.localSubs(&subs, false) + return true + }) + + details := make([]SubDetail, 0, len(subs)) + i := 0 + // TODO(dlc) - may be inefficient and could just do normal match when total subs is large and filtering. + for _, sub := range subs { + // Check for filter + if test && !matchLiteral(testSub, string(sub.subject)) { + continue + } + if sub.client == nil { + continue + } + sub.client.mu.Lock() + details = append(details, newSubDetail(sub)) + sub.client.mu.Unlock() + i++ + } + // Make sure these are sane. + maxIndex := i + minoff := min(max(sz.Offset, 0), maxIndex) + maxoff := minoff + min(sz.Limit, maxIndex-minoff) + sz.Subs = details[minoff:maxoff] + sz.Total = len(details) + } else { + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + if filterAcc != _EMPTY_ && acc.GetName() != filterAcc { + return true + } + slStats.add(acc.sl.Stats()) + return true + }) + } + + return sz, nil +} + +// HandleSubsz processes HTTP requests for subjects stats. +func (s *Server) HandleSubsz(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + s.httpReqStats[SubszPath]++ + s.mu.Unlock() + + subs, err := decodeBool(w, r, "subs") + if err != nil { + return + } + offset, err := decodeInt(w, r, "offset") + if err != nil { + return + } + limit, err := decodeInt(w, r, "limit") + if err != nil { + return + } + testSub := r.URL.Query().Get("test") + // Filtered account. + filterAcc := r.URL.Query().Get("acc") + + subszOpts := &SubszOptions{ + Subscriptions: subs, + Offset: offset, + Limit: limit, + Account: filterAcc, + Test: testSub, + } + + st, err := s.Subsz(subszOpts) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + var b []byte + b, err = json.MarshalIndent(st, "", " ") + if err != nil { + s.Errorf("Error marshaling response to /subscriptionsz request: %v", err) + } + + // Handle response + ResponseHandler(w, r, b) +} + +// HandleStacksz processes HTTP requests for getting stacks +func (s *Server) HandleStacksz(w http.ResponseWriter, r *http.Request) { + // Do not get any lock here that would prevent getting the stacks + // if we were to have a deadlock somewhere. + var defaultBuf [defaultStackBufSize]byte + size := defaultStackBufSize + buf := defaultBuf[:size] + n := 0 + for { + n = runtime.Stack(buf, true) + if n < size { + break + } + size *= 2 + buf = make([]byte, size) + } + // Handle response + ResponseHandler(w, r, buf[:n]) +} + +type IpqueueszStatusIPQ struct { + Pending int `json:"pending"` + InProgress int `json:"in_progress,omitempty"` +} + +type IpqueueszStatus map[string]IpqueueszStatusIPQ + +func (s *Server) Ipqueuesz(opts *IpqueueszOptions) *IpqueueszStatus { + all, qfilter := opts.All, opts.Filter + queues := IpqueueszStatus{} + s.ipQueues.Range(func(k, v any) bool { + var pending, inProgress int + name := k.(string) + queue, ok := v.(interface { + len() int + inProgress() int64 + }) + if ok { + pending = queue.len() + inProgress = int(queue.inProgress()) + } + if !all && (pending == 0 && inProgress == 0) { + return true + } else if qfilter != _EMPTY_ && !strings.Contains(name, qfilter) { + return true + } + queues[name] = IpqueueszStatusIPQ{Pending: pending, InProgress: inProgress} + return true + }) + return &queues +} + +func (s *Server) HandleIPQueuesz(w http.ResponseWriter, r *http.Request) { + all, err := decodeBool(w, r, "all") + if err != nil { + return + } + qfilter := r.URL.Query().Get("queues") + + queues := s.Ipqueuesz(&IpqueueszOptions{ + All: all, + Filter: qfilter, + }) + + b, _ := json.MarshalIndent(queues, "", " ") + ResponseHandler(w, r, b) +} + +// Varz will output server information on the monitoring port at /varz. +type Varz struct { + ID string `json:"server_id"` // ID is the unique server ID generated at start + Name string `json:"server_name"` // Name is the configured server name, equals ID when not set + Version string `json:"version"` // Version is the version of the running server + Proto int `json:"proto"` // Proto is the protocol version this server supports + GitCommit string `json:"git_commit,omitempty"` // GitCommit is the git repository commit hash that the build corresponds with + GoVersion string `json:"go"` // GoVersion is the version of Go used to build this binary + Host string `json:"host"` // Host is the hostname the server runs on + Port int `json:"port"` // Port is the port the server listens on for client connections + AuthRequired bool `json:"auth_required,omitempty"` // AuthRequired indicates if users are required to authenticate to join the server + TLSRequired bool `json:"tls_required,omitempty"` // TLSRequired indicates if connections must use TLS when connecting to this server + TLSVerify bool `json:"tls_verify,omitempty"` // TLSVerify indicates if full TLS verification will be performed + TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"` // TLSOCSPPeerVerify indicates if the OCSP protocol will be used to verify peers + IP string `json:"ip,omitempty"` // IP is the IP address the server listens on if set + ClientConnectURLs []string `json:"connect_urls,omitempty"` // ClientConnectURLs is the list of URLs NATS clients can use to connect to this server + WSConnectURLs []string `json:"ws_connect_urls,omitempty"` // WSConnectURLs is the list of URLs websocket clients can use to connect to this server + MaxConn int `json:"max_connections"` // MaxConn is the maximum amount of connections the server can accept + MaxSubs int `json:"max_subscriptions,omitempty"` // MaxSubs is the maximum amount of subscriptions the server can manage + PingInterval time.Duration `json:"ping_interval"` // PingInterval is the interval the server will send PING messages during periods of inactivity on a connection + MaxPingsOut int `json:"ping_max"` // MaxPingsOut is the number of unanswered PINGs after which the connection will be considered stale + HTTPHost string `json:"http_host"` // HTTPHost is the HTTP host monitoring connections are accepted on + HTTPPort int `json:"http_port"` // HTTPPort is the port monitoring connections are accepted on + HTTPBasePath string `json:"http_base_path"` // HTTPBasePath is the path prefix for access to monitor endpoints + HTTPSPort int `json:"https_port"` // HTTPSPort is the HTTPS host monitoring connections are accepted on` + AuthTimeout float64 `json:"auth_timeout"` // AuthTimeout is the amount of seconds connections have to complete authentication + MaxControlLine int32 `json:"max_control_line"` // MaxControlLine is the amount of bytes a signal control message may be + MaxPayload int `json:"max_payload"` // MaxPayload is the maximum amount of bytes a message may have as payload + MaxPending int64 `json:"max_pending"` // MaxPending is the maximum amount of unprocessed bytes a connection may have + Cluster ClusterOptsVarz `json:"cluster,omitempty"` // Cluster is the Cluster state + Gateway GatewayOptsVarz `json:"gateway,omitempty"` // Gateway is the Super Cluster state + LeafNode LeafNodeOptsVarz `json:"leaf,omitempty"` // LeafNode is the Leafnode state + MQTT MQTTOptsVarz `json:"mqtt,omitempty"` // MQTT is the MQTT state + Websocket WebsocketOptsVarz `json:"websocket,omitempty"` // Websocket is the Websocket client state + JetStream JetStreamVarz `json:"jetstream,omitempty"` // JetStream is the JetStream state + TLSTimeout float64 `json:"tls_timeout"` // TLSTimeout is how long TLS operations have to complete + WriteDeadline time.Duration `json:"write_deadline"` // WriteDeadline is the maximum time writes to sockets have to complete + WriteTimeout string `json:"write_timeout,omitempty"` // WriteTimeout is the closure policy for write deadline errors + Start time.Time `json:"start"` // Start is time when the server was started + Now time.Time `json:"now"` // Now is the current time of the server + Uptime string `json:"uptime"` // Uptime is how long the server has been running + Mem int64 `json:"mem"` // Mem is the resident memory allocation + Cores int `json:"cores"` // Cores is the number of cores the process has access to + MaxProcs int `json:"gomaxprocs"` // MaxProcs is the configured GOMAXPROCS value + MemLimit int64 `json:"gomemlimit,omitempty"` // MemLimit is the configured GOMEMLIMIT value + CPU float64 `json:"cpu"` // CPU is the current total CPU usage + Connections int `json:"connections"` // Connections is the current connected connections + TotalConnections uint64 `json:"total_connections"` // TotalConnections is the total connections the server have ever handled + Routes int `json:"routes"` // Routes is the number of connected route servers + Remotes int `json:"remotes"` // Remotes is the configured route remote endpoints + Leafs int `json:"leafnodes"` // Leafs is the number connected leafnode clients + InMsgs int64 `json:"in_msgs"` // InMsgs is the total number of messages this server received. This includes messages from the clients, routers, gateways and leaf nodes + InBytes int64 `json:"in_bytes"` // InBytes is the total number of bytes this server received. This includes messages from the clients, routers, gateways and leaf nodes + InClientMsgs int64 `json:"in_client_msgs"` // InClientMsgs is the number of messages this server received from the clients + InClientBytes int64 `json:"in_client_bytes"` // InClientBytes is the number of bytes this server received from the clients + OutMsgs int64 `json:"out_msgs"` // OutMsgs is the total number of message this server sent. This includes messages sent to the clients, routers, gateways and leaf nodes + OutBytes int64 `json:"out_bytes"` // OutBytes is the total number of bytes this server sent. This includes messages sent to the clients, routers, gateways and leaf nodes + OutClientMsgs int64 `json:"out_client_msgs"` // OutClientMsgs is the number of messages this server sent to the clients + OutClientBytes int64 `json:"out_client_bytes"` // OutClientBytes is the number of bytes this server sent to the clients + SlowConsumers int64 `json:"slow_consumers"` // SlowConsumers is the total count of clients that were disconnected since start due to being slow consumers + StaleConnections int64 `json:"stale_connections"` // StaleConnections is the total count of stale connections that were detected + StalledClients int64 `json:"stalled_clients"` // StalledClients is the total number of times that clients have been stalled. + Subscriptions uint32 `json:"subscriptions"` // Subscriptions is the count of active subscriptions + HTTPReqStats map[string]uint64 `json:"http_req_stats"` // HTTPReqStats is the number of requests each HTTP endpoint received + ConfigLoadTime time.Time `json:"config_load_time"` // ConfigLoadTime is the time the configuration was loaded or reloaded + ConfigDigest string `json:"config_digest"` // ConfigDigest is a calculated hash of the current configuration + Tags jwt.TagList `json:"tags,omitempty"` // Tags are the tags assigned to the server in configuration + Metadata map[string]string `json:"metadata,omitempty"` // Metadata is the metadata assigned to the server in configuration + FeatureFlags map[string]bool `json:"feature_flags,omitempty"` // FeatureFlags is the feature flags enabled/disabled in configuration + TrustedOperatorsJwt []string `json:"trusted_operators_jwt,omitempty"` // TrustedOperatorsJwt is the JWTs for all trusted operators + TrustedOperatorsClaim []*jwt.OperatorClaims `json:"trusted_operators_claim,omitempty"` // TrustedOperatorsClaim is the decoded claims for each trusted operator + SystemAccount string `json:"system_account,omitempty"` // SystemAccount is the name of the System account + PinnedAccountFail uint64 `json:"pinned_account_fails,omitempty"` // PinnedAccountFail is how often user logon fails due to the issuer account not being pinned. + OCSPResponseCache *OCSPResponseCacheVarz `json:"ocsp_peer_cache,omitempty"` // OCSPResponseCache is the state of the OCSP cache + SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats"` // SlowConsumersStats are statistics about all detected Slow Consumer + StaleConnectionStats *StaleConnectionStats `json:"stale_connection_stats,omitempty"` // StaleConnectionStats are statistics about all detected Stale Connections + DiskIOWaitStats *DiskIOWaitStats `json:"disk_io_wait_stats"` // DiskIOWaitStats are statistics about disk I/O semaphore contention + Proxies *ProxiesOptsVarz `json:"proxies,omitempty"` // Proxies hold information about network proxy devices + TLSCertNotAfter time.Time `json:"tls_cert_not_after,omitzero"` // TLSCertNotAfter is the expiration date of the TLS certificate of this server +} + +// JetStreamVarz contains basic runtime information about jetstream +type JetStreamVarz struct { + Config *JetStreamConfig `json:"config,omitempty"` // Config is the active JetStream configuration + Stats *JetStreamStats `json:"stats,omitempty"` // Stats is the statistics for the JetStream server + Meta *MetaClusterInfo `json:"meta,omitempty"` // Meta is information about the JetStream metalayer + Limits *JSLimitOpts `json:"limits,omitempty"` // Limits are the configured JetStream limits +} + +// ClusterOptsVarz contains monitoring cluster information +type ClusterOptsVarz struct { + Name string `json:"name,omitempty"` // Name is the configured cluster name + Host string `json:"addr,omitempty"` // Host is the host the cluster listens on for connections + Port int `json:"cluster_port,omitempty"` // Port is the port the cluster listens on for connections + AuthTimeout float64 `json:"auth_timeout,omitempty"` // AuthTimeout is the time cluster connections have to complete authentication + URLs []string `json:"urls,omitempty"` // URLs is the list of cluster URLs + TLSTimeout float64 `json:"tls_timeout,omitempty"` // TLSTimeout is how long TLS operations have to complete + TLSRequired bool `json:"tls_required,omitempty"` // TLSRequired indicates if TLS is required for connections + TLSVerify bool `json:"tls_verify,omitempty"` // TLSVerify indicates if full verification of TLS connections is performed + PoolSize int `json:"pool_size,omitempty"` // PoolSize is the configured route connection pool size + WriteDeadline time.Duration `json:"write_deadline,omitempty"` // WriteDeadline is the maximum time writes to sockets have to complete + WriteTimeout string `json:"write_timeout,omitempty"` // WriteTimeout is the closure policy for write deadline errors + TLSCertNotAfter time.Time `json:"tls_cert_not_after,omitzero"` // TLSCertNotAfter is the expiration date of the TLS certificate +} + +// GatewayOptsVarz contains monitoring gateway information +type GatewayOptsVarz struct { + Name string `json:"name,omitempty"` // Name is the configured cluster name + Host string `json:"host,omitempty"` // Host is the host the gateway listens on for connections + Port int `json:"port,omitempty"` // Port is the post gateway connections listens on + AuthTimeout float64 `json:"auth_timeout,omitempty"` // AuthTimeout is the time cluster connections have to complete authentication + TLSTimeout float64 `json:"tls_timeout,omitempty"` // TLSTimeout is how long TLS operations have to complete + TLSRequired bool `json:"tls_required,omitempty"` // TLSRequired indicates if TLS is required for connections + TLSVerify bool `json:"tls_verify,omitempty"` // TLSVerify indicates if full verification of TLS connections is performed + Advertise string `json:"advertise,omitempty"` // Advertise is the URL advertised to remote gateway clients + ConnectRetries int `json:"connect_retries,omitempty"` // ConnectRetries is how many connection attempts the route will make + Gateways []RemoteGatewayOptsVarz `json:"gateways,omitempty"` // Gateways is state of configured gateway remotes + RejectUnknown bool `json:"reject_unknown,omitempty"` // RejectUnknown indicates if unknown cluster connections will be rejected + WriteDeadline time.Duration `json:"write_deadline,omitempty"` // WriteDeadline is the maximum time writes to sockets have to complete + WriteTimeout string `json:"write_timeout,omitempty"` // WriteTimeout is the closure policy for write deadline errors + TLSCertNotAfter time.Time `json:"tls_cert_not_after,omitzero"` // TLSCertNotAfter is the expiration date of the TLS certificaet +} + +// RemoteGatewayOptsVarz contains monitoring remote gateway information +type RemoteGatewayOptsVarz struct { + Name string `json:"name"` // Name is the name of the remote gateway + TLSTimeout float64 `json:"tls_timeout,omitempty"` // TLSTimeout is how long TLS operations have to complete + URLs []string `json:"urls,omitempty"` // URLs is the list of Gateway URLs +} + +// LeafNodeOptsVarz contains monitoring leaf node information +type LeafNodeOptsVarz struct { + Host string `json:"host,omitempty"` // Host is the host the server listens on + Port int `json:"port,omitempty"` // Port is the port the server listens on + AuthTimeout float64 `json:"auth_timeout,omitempty"` // AuthTimeout is the time Leafnode connections have to complete authentication + TLSTimeout float64 `json:"tls_timeout,omitempty"` // TLSTimeout is how long TLS operations have to complete + TLSRequired bool `json:"tls_required,omitempty"` // TLSRequired indicates if TLS is required for connections + TLSVerify bool `json:"tls_verify,omitempty"` // TLSVerify indicates if full verification of TLS connections is performed + Remotes []RemoteLeafOptsVarz `json:"remotes,omitempty"` // Remotes is state of configured Leafnode remotes + TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"` // TLSOCSPPeerVerify indicates if OCSP verification will be performed + WriteDeadline time.Duration `json:"write_deadline,omitempty"` // WriteDeadline is the maximum time writes to sockets have to complete + WriteTimeout string `json:"write_timeout,omitempty"` // WriteTimeout is the closure policy for write deadline errors + TLSCertNotAfter time.Time `json:"tls_cert_not_after,omitzero"` // TLSCertNotAfter is the expiration date of the TLS certificate +} + +// DenyRules Contains lists of subjects not allowed to be imported/exported +type DenyRules struct { + Exports []string `json:"exports,omitempty"` // Exports are denied exports + Imports []string `json:"imports,omitempty"` // Imports are denied imports +} + +// RemoteLeafOptsVarz contains monitoring remote leaf node information +type RemoteLeafOptsVarz struct { + LocalAccount string `json:"local_account,omitempty"` // LocalAccount is the local account this leaf is logged into + TLSTimeout float64 `json:"tls_timeout,omitempty"` // TLSTimeout is how long TLS operations have to complete + URLs []string `json:"urls,omitempty"` // URLs is the list of URLs for the remote Leafnode connection + Deny *DenyRules `json:"deny,omitempty"` // Deny is the configured import and exports that the Leafnode may not access + TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"` // TLSOCSPPeerVerify indicates if OCSP verification will be done +} + +// MQTTOptsVarz contains monitoring MQTT information +type MQTTOptsVarz struct { + Host string `json:"host,omitempty"` // Host is the host the server listens on + Port int `json:"port,omitempty"` // Port is the port the server listens on + NoAuthUser string `json:"no_auth_user,omitempty"` // NoAuthUser is the user that will be used for unauthenticated connections + AuthTimeout float64 `json:"auth_timeout,omitempty"` // AuthTimeout is how long authentication has to complete + TLSMap bool `json:"tls_map,omitempty"` // TLSMap indicates if TLS Mapping is enabled + TLSTimeout float64 `json:"tls_timeout,omitempty"` // TLSTimeout is how long TLS operations have to complete + TLSPinnedCerts []string `json:"tls_pinned_certs,omitempty"` // TLSPinnedCerts is the list of certificates pinned to this connection + JsDomain string `json:"js_domain,omitempty"` // JsDomain is the JetStream domain used for MQTT state + AckWait time.Duration `json:"ack_wait,omitempty"` // AckWait is how long the internal JetStream state store will allow acks to complete + MaxAckPending uint16 `json:"max_ack_pending,omitempty"` // MaxAckPending is how many outstanding acks the internal JetStream state store will allow + TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"` // TLSOCSPPeerVerify indicates if OCSP verification will be done + TLSCertNotAfter time.Time `json:"tls_cert_not_after,omitzero"` // TLSCertNotAfter is the expiration date of the TLS certificate +} + +// WebsocketOptsVarz contains monitoring websocket information +type WebsocketOptsVarz struct { + Host string `json:"host,omitempty"` // Host is the host the server listens on + Port int `json:"port,omitempty"` // Port is the port the server listens on + Advertise string `json:"advertise,omitempty"` // Advertise is the connection URL the server advertises + NoAuthUser string `json:"no_auth_user,omitempty"` // NoAuthUser is the user that will be used for unauthenticated connections + JWTCookie string `json:"jwt_cookie,omitempty"` // JWTCookie is the name of a cookie the server will read for the connection JWT + HandshakeTimeout time.Duration `json:"handshake_timeout,omitempty"` // HandshakeTimeout is how long the connection has to complete the websocket setup + AuthTimeout float64 `json:"auth_timeout,omitempty"` // AuthTimeout is how long authentication has to complete + NoTLS bool `json:"no_tls,omitempty"` // NoTLS indicates if TLS is disabled + TLSMap bool `json:"tls_map,omitempty"` // TLSMap indicates if TLS Mapping is enabled + TLSPinnedCerts []string `json:"tls_pinned_certs,omitempty"` // TLSPinnedCerts is the list of certificates pinned to this connection + SameOrigin bool `json:"same_origin,omitempty"` // SameOrigin indicates if same origin connections are allowed + AllowedOrigins []string `json:"allowed_origins,omitempty"` // AllowedOrigins list of configured trusted origins + Compression bool `json:"compression,omitempty"` // Compression indicates if compression is supported + TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"` // TLSOCSPPeerVerify indicates if OCSP verification will be done + TLSCertNotAfter time.Time `json:"tls_cert_not_after,omitzero"` // TLSCertNotAfter is the expiration date of the TLS certificate +} + +// OCSPResponseCacheVarz contains OCSP response cache information +type OCSPResponseCacheVarz struct { + Type string `json:"cache_type,omitempty"` // Type is the kind of cache being used + Hits int64 `json:"cache_hits,omitempty"` // Hits is how many times the cache was able to answer a request + Misses int64 `json:"cache_misses,omitempty"` // Misses is how many times the cache failed to answer a request + Responses int64 `json:"cached_responses,omitempty"` // Responses is how many responses are currently stored in the cache + Revokes int64 `json:"cached_revoked_responses,omitempty"` // Revokes is how many of the stored cache entries are revokes + Goods int64 `json:"cached_good_responses,omitempty"` // Goods is how many of the stored cache entries are good responses + Unknowns int64 `json:"cached_unknown_responses,omitempty"` // Unknowns is how many of the stored cache entries are unknown responses +} + +// ProxiesOptsVarz contains proxies information +type ProxiesOptsVarz struct { + Trusted []*ProxyOptsVarz `json:"trusted,omitempty"` // Trusted holds a list of trusted proxies +} + +// ProxyOptsVarz contains proxy information +type ProxyOptsVarz struct { + Key string `json:"key"` // Key is the public key of the proxy +} + +// VarzOptions are the options passed to Varz(). +// Currently, there are no options defined. +type VarzOptions struct{} + +// SlowConsumersStats contains information about the slow consumers from different type of connections. +type SlowConsumersStats struct { + Clients uint64 `json:"clients"` // Clients is how many Clients were slow consumers + Routes uint64 `json:"routes"` // Routes is how many Routes were slow consumers + Gateways uint64 `json:"gateways"` // Gateways is how many Gateways were slow consumers + Leafs uint64 `json:"leafs"` // Leafs is how many Leafnodes were slow consumers +} + +// StaleConnectionStats contains information about the stale connections from different type of connections. +type StaleConnectionStats struct { + Clients uint64 `json:"clients"` // Clients is how many Client connections became stale connections + Routes uint64 `json:"routes"` // Routes is how many Route connections became stale connections + Gateways uint64 `json:"gateways"` // Gateways is how many Gateway connections became stale connections + Leafs uint64 `json:"leafs"` // Leafs is how many Leafnode connections became stale connections +} + +// DiskIOWaitStats contains information about disk I/O semaphore contention. +type DiskIOWaitStats struct { + Waiters int64 `json:"waiters"` // Waiters is the number of goroutines waiting on the dios + Waits uint64 `json:"waits"` // Waits is the number of dios acquires that had to wait + WaitTime uint64 `json:"wait_time"` // WaitTime is the cumulative time spent waiting for dios + MaxWaitTime uint64 `json:"max_wait_time"` // MaxWaitTime is the longest observed wait +} + +func myUptime(d time.Duration) string { + // Just use total seconds for uptime, and display days / years + tsecs := d / time.Second + tmins := tsecs / 60 + thrs := tmins / 60 + tdays := thrs / 24 + tyrs := tdays / 365 + + if tyrs > 0 { + return fmt.Sprintf("%dy%dd%dh%dm%ds", tyrs, tdays%365, thrs%24, tmins%60, tsecs%60) + } + if tdays > 0 { + return fmt.Sprintf("%dd%dh%dm%ds", tdays, thrs%24, tmins%60, tsecs%60) + } + if thrs > 0 { + return fmt.Sprintf("%dh%dm%ds", thrs, tmins%60, tsecs%60) + } + if tmins > 0 { + return fmt.Sprintf("%dm%ds", tmins, tsecs%60) + } + return fmt.Sprintf("%ds", tsecs) +} + +func tlsCertNotAfter(config *tls.Config) time.Time { + if config == nil || len(config.Certificates) == 0 { + return time.Time{} + } + cert := config.Certificates[0] + leaf := cert.Leaf + if leaf == nil { + var err error + leaf, err = x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return time.Time{} + } + } + return leaf.NotAfter +} + +// HandleRoot will show basic info and links to others handlers. +func (s *Server) HandleRoot(w http.ResponseWriter, r *http.Request) { + // This feels dumb to me, but is required: https://code.google.com/p/go/issues/detail?id=4799 + if r.URL.Path != s.httpBasePath { + http.NotFound(w, r) + return + } + s.mu.Lock() + s.httpReqStats[RootPath]++ + s.mu.Unlock() + + // Calculate source url. If git set go directly to that tag, otherwise just main. + var srcUrl string + if gitCommit == _EMPTY_ { + srcUrl = "https://github.com/nats-io/nats-server" + } else if serverVersion != _EMPTY_ { + srcUrl = fmt.Sprintf("https://github.com/nats-io/nats-server/tree/%s", serverVersion) + } else { + srcUrl = fmt.Sprintf("https://github.com/nats-io/nats-server/tree/%s", gitCommit) + } + + fmt.Fprintf(w, ` + + + + + + + + v%s + + +
+ General %s + JetStream %s + Connections %s + Accounts %s + Account Stats %s + Subscriptions %s + Routes %s + LeafNodes %s + Gateways %s + Raft Groups %s + Health Probe %s + Expvar %s + Help + +`, + srcUrl, + VERSION, + s.basePath(VarzPath), VarzPath, + s.basePath(JszPath), JszPath, + s.basePath(ConnzPath), ConnzPath, + s.basePath(AccountzPath), AccountzPath, + s.basePath(AccountStatzPath), AccountStatzPath, + s.basePath(SubszPath), SubszPath, + s.basePath(RoutezPath), RoutezPath, + s.basePath(LeafzPath), LeafzPath, + s.basePath(GatewayzPath), GatewayzPath, + s.basePath(RaftzPath), RaftzPath, + s.basePath(HealthzPath), HealthzPath, + s.basePath(ExpvarzPath), ExpvarzPath, + ) +} + +func (s *Server) updateJszVarz(js *jetStream, v *JetStreamVarz, doConfig bool) { + if doConfig { + js.mu.RLock() + // We want to snapshot the config since it will then be available outside + // of the js lock. So make a copy first, then point to this copy. + cfg := js.config + v.Config = &cfg + js.mu.RUnlock() + } + v.Stats = js.usageStats() + v.Limits = &s.getOpts().JetStreamLimits + if mg := js.getMetaGroup(); mg != nil { + if ci := s.raftNodeToClusterInfo(mg); ci != nil { + v.Meta = &MetaClusterInfo{ + Name: ci.Name, + Leader: ci.Leader, + Replicas: ci.Replicas, + Size: mg.ClusterSize(), + QuorumNeeded: mg.QuorumNeeded(), + Rescue: mg.InRescue(), + } + if ci.Leader != _EMPTY_ { + v.Meta.Peer = getHash(ci.Leader) + } + if ipq := s.jsAPIRoutedReqs; ipq != nil { + v.Meta.PendingRequests = ipq.len() + } + if ipq := s.jsAPIRoutedInfoReqs; ipq != nil { + v.Meta.PendingInfos = ipq.len() + } + v.Meta.Pending = v.Meta.PendingRequests + v.Meta.PendingInfos + v.Meta.Snapshot = s.metaClusterSnapshotStats(js, mg) + } + } +} + +// Varz returns a Varz struct containing the server information. +func (s *Server) Varz(varzOpts *VarzOptions) (*Varz, error) { + var rss, vss int64 + var pcpu float64 + + // We want to do that outside of the lock. + pse.ProcUsage(&pcpu, &rss, &vss) + + s.mu.RLock() + // We need to create a new instance of Varz (with no reference + // whatsoever to anything stored in the server) since the user + // has access to the returned value. + v := s.createVarz(pcpu, rss) + s.mu.RUnlock() + + if js := s.getJetStream(); js != nil { + s.updateJszVarz(js, &v.JetStream, true) + } + + return v, nil +} + +// Returns a Varz instance. +// Server lock is held on entry. +func (s *Server) createVarz(pcpu float64, rss int64) *Varz { + info := s.info + opts := s.getOpts() + c := &opts.Cluster + gw := &opts.Gateway + ln := &opts.LeafNode + mqtt := &opts.MQTT + ws := &opts.Websocket + clustTlsReq := c.TLSConfig != nil + gatewayTlsReq := gw.TLSConfig != nil + leafTlsReq := ln.TLSConfig != nil + leafTlsVerify := leafTlsReq && ln.TLSConfig.ClientAuth == tls.RequireAndVerifyClientCert + leafTlsOCSPPeerVerify := s.ocspPeerVerify && leafTlsReq && ln.tlsConfigOpts.OCSPPeerConfig != nil && ln.tlsConfigOpts.OCSPPeerConfig.Verify + mqttTlsOCSPPeerVerify := s.ocspPeerVerify && mqtt.TLSConfig != nil && mqtt.tlsConfigOpts.OCSPPeerConfig != nil && mqtt.tlsConfigOpts.OCSPPeerConfig.Verify + wsTlsOCSPPeerVerify := s.ocspPeerVerify && ws.TLSConfig != nil && ws.tlsConfigOpts.OCSPPeerConfig != nil && ws.tlsConfigOpts.OCSPPeerConfig.Verify + varz := &Varz{ + ID: info.ID, + Version: info.Version, + Proto: info.Proto, + GitCommit: info.GitCommit, + GoVersion: info.GoVersion, + Name: info.Name, + Host: info.Host, + Port: info.Port, + IP: info.IP, + HTTPHost: opts.HTTPHost, + HTTPPort: opts.HTTPPort, + HTTPBasePath: opts.HTTPBasePath, + HTTPSPort: opts.HTTPSPort, + Cluster: ClusterOptsVarz{ + Name: info.Cluster, + Host: c.Host, + Port: c.Port, + AuthTimeout: c.AuthTimeout, + TLSTimeout: c.TLSTimeout, + TLSRequired: clustTlsReq, + TLSVerify: clustTlsReq, + PoolSize: opts.Cluster.PoolSize, + WriteDeadline: opts.Cluster.WriteDeadline, + WriteTimeout: opts.Cluster.WriteTimeout.String(), + }, + Gateway: GatewayOptsVarz{ + Name: gw.Name, + Host: gw.Host, + Port: gw.Port, + AuthTimeout: gw.AuthTimeout, + TLSTimeout: gw.TLSTimeout, + TLSRequired: gatewayTlsReq, + TLSVerify: gatewayTlsReq, + Advertise: gw.Advertise, + ConnectRetries: gw.ConnectRetries, + Gateways: []RemoteGatewayOptsVarz{}, + RejectUnknown: gw.RejectUnknown, + WriteDeadline: opts.Cluster.WriteDeadline, + WriteTimeout: opts.Cluster.WriteTimeout.String(), + }, + LeafNode: LeafNodeOptsVarz{ + Host: ln.Host, + Port: ln.Port, + AuthTimeout: ln.AuthTimeout, + TLSTimeout: ln.TLSTimeout, + TLSRequired: leafTlsReq, + TLSVerify: leafTlsVerify, + TLSOCSPPeerVerify: leafTlsOCSPPeerVerify, + Remotes: []RemoteLeafOptsVarz{}, + WriteDeadline: opts.Cluster.WriteDeadline, + WriteTimeout: opts.Cluster.WriteTimeout.String(), + }, + MQTT: MQTTOptsVarz{ + Host: mqtt.Host, + Port: mqtt.Port, + NoAuthUser: mqtt.NoAuthUser, + AuthTimeout: mqtt.AuthTimeout, + TLSMap: mqtt.TLSMap, + TLSTimeout: mqtt.TLSTimeout, + JsDomain: mqtt.JsDomain, + AckWait: mqtt.AckWait, + MaxAckPending: mqtt.MaxAckPending, + TLSOCSPPeerVerify: mqttTlsOCSPPeerVerify, + }, + Websocket: WebsocketOptsVarz{ + Host: ws.Host, + Port: ws.Port, + Advertise: ws.Advertise, + NoAuthUser: ws.NoAuthUser, + JWTCookie: ws.JWTCookie, + AuthTimeout: ws.AuthTimeout, + NoTLS: ws.NoTLS, + TLSMap: ws.TLSMap, + SameOrigin: ws.SameOrigin, + AllowedOrigins: copyStrings(ws.AllowedOrigins), + Compression: ws.Compression, + HandshakeTimeout: ws.HandshakeTimeout, + TLSOCSPPeerVerify: wsTlsOCSPPeerVerify, + }, + Start: s.start.UTC(), + MaxSubs: opts.MaxSubs, + Cores: runtime.NumCPU(), + MaxProcs: runtime.GOMAXPROCS(0), + TrustedOperatorsJwt: opts.operatorJWT, + TrustedOperatorsClaim: opts.TrustedOperators, + } + if mm := debug.SetMemoryLimit(-1); mm < math.MaxInt64 { + varz.MemLimit = mm + } + // If this is a leaf without cluster, reset the cluster name (that is otherwise + // set to the server name). + if s.leafNoCluster { + varz.Cluster.Name = _EMPTY_ + } + if len(opts.Routes) > 0 { + varz.Cluster.URLs = urlsToStrings(opts.Routes) + } + if l := len(gw.Gateways); l > 0 { + rgwa := make([]RemoteGatewayOptsVarz, l) + for i, r := range gw.Gateways { + rgwa[i] = RemoteGatewayOptsVarz{ + Name: r.Name, + TLSTimeout: r.TLSTimeout, + } + } + varz.Gateway.Gateways = rgwa + } + // Finish setting it up with fields that can be updated during + // configuration reload and runtime. + s.updateVarzConfigReloadableFields(varz) + s.updateVarzRuntimeFields(varz, true, pcpu, rss) + return varz +} + +// Builds the list of remote leafnodes for Varz from the given options. +// Server lock is held on entry. +func (s *Server) varzLeafNodeRemotes(opts *Options) []RemoteLeafOptsVarz { + l := len(opts.LeafNode.Remotes) + if l == 0 { + return nil + } + rlna := make([]RemoteLeafOptsVarz, l) + for i, r := range opts.LeafNode.Remotes { + var deny *DenyRules + if len(r.DenyImports) > 0 || len(r.DenyExports) > 0 { + deny = &DenyRules{ + Imports: r.DenyImports, + Exports: r.DenyExports, + } + } + remoteTlsOCSPPeerVerify := s.ocspPeerVerify && r.tlsConfigOpts != nil && r.tlsConfigOpts.OCSPPeerConfig != nil && r.tlsConfigOpts.OCSPPeerConfig.Verify + + rlna[i] = RemoteLeafOptsVarz{ + LocalAccount: r.LocalAccount, + URLs: urlsToStrings(r.URLs), + TLSTimeout: r.TLSTimeout, + Deny: deny, + TLSOCSPPeerVerify: remoteTlsOCSPPeerVerify, + } + } + return rlna +} + +func urlsToStrings(urls []*url.URL) []string { + sURLs := make([]string, len(urls)) + for i, u := range urls { + sURLs[i] = u.Host + } + return sURLs +} + +// Invoked during configuration reload once options have possibly be changed +// and config load time has been set. If s.varz has not been initialized yet +// (because no pooling of /varz has been made), this function does nothing. +// Server lock is held on entry. +func (s *Server) updateVarzConfigReloadableFields(v *Varz) { + if v == nil { + return + } + opts := s.getOpts() + info := &s.info + v.AuthRequired = info.AuthRequired + v.TLSRequired = info.TLSRequired + v.TLSVerify = info.TLSVerify + v.MaxConn = opts.MaxConn + v.PingInterval = opts.PingInterval + v.MaxPingsOut = opts.MaxPingsOut + v.AuthTimeout = opts.AuthTimeout + v.MaxControlLine = opts.MaxControlLine + v.MaxPayload = int(opts.MaxPayload) + v.MaxPending = opts.MaxPending + v.TLSTimeout = opts.TLSTimeout + v.WriteDeadline = opts.WriteDeadline + v.WriteTimeout = opts.WriteTimeout.String() + v.ConfigLoadTime = s.configTime.UTC() + v.ConfigDigest = opts.configDigest + v.Tags = opts.Tags + v.Metadata = opts.Metadata + v.FeatureFlags = opts.getMergedFeatureFlags() + // Update route URLs if applicable + if s.varzUpdateRouteURLs { + v.Cluster.URLs = urlsToStrings(opts.Routes) + s.varzUpdateRouteURLs = false + } + if s.sys != nil && s.sys.account != nil { + v.SystemAccount = s.sys.account.GetName() + } + v.MQTT.TLSPinnedCerts = getPinnedCertsAsSlice(opts.MQTT.TLSPinnedCerts) + v.Websocket.TLSPinnedCerts = getPinnedCertsAsSlice(opts.Websocket.TLSPinnedCerts) + + v.TLSOCSPPeerVerify = s.ocspPeerVerify && v.TLSRequired && s.opts.tlsConfigOpts != nil && s.opts.tlsConfigOpts.OCSPPeerConfig != nil && s.opts.tlsConfigOpts.OCSPPeerConfig.Verify + + v.TLSCertNotAfter = tlsCertNotAfter(opts.TLSConfig) + v.Cluster.TLSCertNotAfter = tlsCertNotAfter(opts.Cluster.TLSConfig) + v.Gateway.TLSCertNotAfter = tlsCertNotAfter(opts.Gateway.TLSConfig) + v.LeafNode.TLSCertNotAfter = tlsCertNotAfter(opts.LeafNode.TLSConfig) + v.LeafNode.Remotes = s.varzLeafNodeRemotes(opts) + v.MQTT.TLSCertNotAfter = tlsCertNotAfter(opts.MQTT.TLSConfig) + v.Websocket.TLSCertNotAfter = tlsCertNotAfter(opts.Websocket.TLSConfig) + + if opts.Proxies != nil { + if v.Proxies == nil { + v.Proxies = &ProxiesOptsVarz{} + } + trusted := make([]*ProxyOptsVarz, 0, len(opts.Proxies.Trusted)) + for _, t := range opts.Proxies.Trusted { + trusted = append(trusted, &ProxyOptsVarz{Key: t.Key}) + } + v.Proxies.Trusted = trusted + } else { + v.Proxies = nil + } + + if cfg := v.JetStream.Config; cfg != nil { + if opts.JetStreamMaxMemory > 0 { + cfg.MaxMemory = opts.JetStreamMaxMemory + } + if opts.JetStreamMaxStore > 0 { + cfg.MaxStore = opts.JetStreamMaxStore + } + } +} + +func getPinnedCertsAsSlice(certs PinnedCertSet) []string { + if len(certs) == 0 { + return nil + } + res := make([]string, 0, len(certs)) + for cn := range certs { + res = append(res, cn) + } + return res +} + +// Updates the runtime Varz fields, that is, fields that change during +// runtime and that should be updated any time Varz() or polling of /varz +// is done. +// Server lock is held on entry. +func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64, rss int64) { + v.Now = time.Now().UTC() + v.Uptime = myUptime(time.Since(s.start)) + v.Mem = rss + v.CPU = pcpu + if l := len(s.info.ClientConnectURLs); l > 0 { + v.ClientConnectURLs = append([]string(nil), s.info.ClientConnectURLs...) + } + if l := len(s.info.WSConnectURLs); l > 0 { + v.WSConnectURLs = append([]string(nil), s.info.WSConnectURLs...) + } + v.Connections = len(s.clients) + v.TotalConnections = s.totalClients + v.Routes = s.numRoutes() + v.Remotes = s.numRemotes() + v.Leafs = len(s.leafs) + v.InMsgs = atomic.LoadInt64(&s.inMsgs) + v.InBytes = atomic.LoadInt64(&s.inBytes) + v.OutMsgs = atomic.LoadInt64(&s.outMsgs) + v.OutBytes = atomic.LoadInt64(&s.outBytes) + v.InClientMsgs = atomic.LoadInt64(&s.inClientMsgs) + v.InClientBytes = atomic.LoadInt64(&s.inClientBytes) + v.OutClientMsgs = atomic.LoadInt64(&s.outClientMsgs) + v.OutClientBytes = atomic.LoadInt64(&s.outClientBytes) + v.SlowConsumers = atomic.LoadInt64(&s.slowConsumers) + v.StalledClients = atomic.LoadInt64(&s.stalls) + v.SlowConsumersStats = &SlowConsumersStats{ + Clients: s.NumSlowConsumersClients(), + Routes: s.NumSlowConsumersRoutes(), + Gateways: s.NumSlowConsumersGateways(), + Leafs: s.NumSlowConsumersLeafs(), + } + v.StaleConnections = atomic.LoadInt64(&s.staleConnections) + v.StaleConnectionStats = &StaleConnectionStats{ + Clients: s.NumStaleConnectionsClients(), + Routes: s.NumStaleConnectionsRoutes(), + Gateways: s.NumStaleConnectionsGateways(), + Leafs: s.NumStaleConnectionsLeafs(), + } + v.PinnedAccountFail = atomic.LoadUint64(&s.pinnedAccFail) + + // Make sure to reset in case we are re-using. + v.Subscriptions = 0 + s.accounts.Range(func(k, val any) bool { + acc := val.(*Account) + v.Subscriptions += acc.sl.Count() + return true + }) + + v.HTTPReqStats = make(map[string]uint64, len(s.httpReqStats)) + for key, val := range s.httpReqStats { + v.HTTPReqStats[key] = val + } + + // Update Gateway remote urls if applicable + gw := s.gateway + gw.RLock() + if gw.enabled { + for i := 0; i < len(v.Gateway.Gateways); i++ { + g := &v.Gateway.Gateways[i] + rgw := gw.remotes[g.Name] + if rgw != nil { + rgw.RLock() + // forceUpdate is needed if user calls Varz() programmatically, + // since we need to create a new instance every time and the + // gateway's varzUpdateURLs may have been set to false after + // a web /varz inspection. + if forceUpdate || rgw.varzUpdateURLs { + // Make reuse of backend array + g.URLs = g.URLs[:0] + // rgw.urls is a map[string]*url.URL where the key is + // already in the right format (host:port, without any + // user info present). + for u := range rgw.urls { + g.URLs = append(g.URLs, u) + } + rgw.varzUpdateURLs = false + } + rgw.RUnlock() + } else if g.Name == gw.name && len(gw.ownCfgURLs) > 0 { + // This is a remote that correspond to this very same server. + // We report the URLs that were configured (if any). + // Since we don't support changes to the gateway configuration + // at this time, we could do this only if g.URLs has not been already + // set, but let's do it regardless in case we add support for + // gateway config reload. + g.URLs = g.URLs[:0] + g.URLs = append(g.URLs, gw.ownCfgURLs...) + } + } + } + gw.RUnlock() + + if s.ocsprc != nil && s.ocsprc.Type() != "none" { + stats := s.ocsprc.Stats() + if stats != nil { + v.OCSPResponseCache = &OCSPResponseCacheVarz{ + s.ocsprc.Type(), + stats.Hits, + stats.Misses, + stats.Responses, + stats.Revokes, + stats.Goods, + stats.Unknowns, + } + } + } + v.DiskIOWaitStats = diskIOWaitStats(s.dios) +} + +func diskIOWaitStats(d *diskIOSemaphore) *DiskIOWaitStats { + if d == nil { + return &DiskIOWaitStats{} + } + return &DiskIOWaitStats{ + Waiters: d.waiters.Load(), + Waits: d.waits.Load(), + WaitTime: d.waitNanos.Load(), + MaxWaitTime: d.maxWaitNanos.Load(), + } +} + +// HandleVarz will process HTTP requests for server information. +func (s *Server) HandleVarz(w http.ResponseWriter, r *http.Request) { + var rss, vss int64 + var pcpu float64 + + // We want to do that outside of the lock. + pse.ProcUsage(&pcpu, &rss, &vss) + + // In response to http requests, we want to minimize mem copies + // so we use an object stored in the server. Creating/collecting + // server metrics is done under server lock, but we don't want + // to marshal under that lock. Still, we need to prevent concurrent + // http requests to /varz to update s.varz while marshal is + // happening, so we need a new lock that serialize those http + // requests and include marshaling. + s.varzMu.Lock() + + // Use server lock to create/update the server's varz object. + s.mu.Lock() + var created bool + s.httpReqStats[VarzPath]++ + if s.varz == nil { + s.varz = s.createVarz(pcpu, rss) + created = true + } else { + s.updateVarzRuntimeFields(s.varz, false, pcpu, rss) + } + s.mu.Unlock() + // Since locking is jetStream -> Server, need to update jetstream + // varz outside of server lock. + + if js := s.getJetStream(); js != nil { + var v JetStreamVarz + // Work on stack variable + s.updateJszVarz(js, &v, created) + // Now update server's varz + s.mu.RLock() + sv := &s.varz.JetStream + if created { + sv.Config = v.Config + } + sv.Stats = v.Stats + sv.Meta = v.Meta + sv.Limits = v.Limits + s.mu.RUnlock() + } + + // Do the marshaling outside of server lock, but under varzMu lock. + b, err := json.MarshalIndent(s.varz, "", " ") + s.varzMu.Unlock() + + if err != nil { + s.Errorf("Error marshaling response to /varz request: %v", err) + } + + // Handle response + ResponseHandler(w, r, b) +} + +// GatewayzOptions are the options passed to Gatewayz() +type GatewayzOptions struct { + // Name will output only remote gateways with this name + Name string `json:"name"` + + // Accounts indicates if accounts with its interest should be included in the results. + Accounts bool `json:"accounts"` + + // AccountName will limit the list of accounts to that account name (makes Accounts implicit) + AccountName string `json:"account_name"` + + // AccountSubscriptions indicates if subscriptions should be included in the results. + // Note: This is used only if `Accounts` or `AccountName` are specified. + AccountSubscriptions bool `json:"subscriptions"` + + // AccountSubscriptionsDetail indicates if subscription details should be included in the results. + // Note: This is used only if `Accounts` or `AccountName` are specified. + AccountSubscriptionsDetail bool `json:"subscriptions_detail"` +} + +// Gatewayz represents detailed information on Gateways +type Gatewayz struct { + ID string `json:"server_id"` + Now time.Time `json:"now"` + Name string `json:"name,omitempty"` + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + OutboundGateways map[string]*RemoteGatewayz `json:"outbound_gateways"` + InboundGateways map[string][]*RemoteGatewayz `json:"inbound_gateways"` +} + +// RemoteGatewayz represents information about an outbound connection to a gateway +type RemoteGatewayz struct { + IsConfigured bool `json:"configured"` + Connection *ConnInfo `json:"connection,omitempty"` + Accounts []*AccountGatewayz `json:"accounts,omitempty"` +} + +// AccountGatewayz represents interest mode for this account +type AccountGatewayz struct { + Name string `json:"name"` + InterestMode string `json:"interest_mode"` + NoInterestCount int `json:"no_interest_count,omitempty"` + InterestOnlyThreshold int `json:"interest_only_threshold,omitempty"` + TotalSubscriptions int `json:"num_subs,omitempty"` + NumQueueSubscriptions int `json:"num_queue_subs,omitempty"` + Subs []string `json:"subscriptions_list,omitempty"` + SubsDetail []SubDetail `json:"subscriptions_list_detail,omitempty"` +} + +// Gatewayz returns a Gatewayz struct containing information about gateways. +func (s *Server) Gatewayz(opts *GatewayzOptions) (*Gatewayz, error) { + srvID := s.ID() + now := time.Now().UTC() + gw := s.gateway + gw.RLock() + if !gw.enabled || gw.info == nil { + gw.RUnlock() + gwz := &Gatewayz{ + ID: srvID, + Now: now, + OutboundGateways: map[string]*RemoteGatewayz{}, + InboundGateways: map[string][]*RemoteGatewayz{}, + } + return gwz, nil + } + // Here gateways are enabled, so fill up more. + gwz := &Gatewayz{ + ID: srvID, + Now: now, + Name: gw.name, + Host: gw.info.Host, + Port: gw.info.Port, + } + gw.RUnlock() + + gwz.OutboundGateways = s.createOutboundsRemoteGatewayz(opts, now) + gwz.InboundGateways = s.createInboundsRemoteGatewayz(opts, now) + + return gwz, nil +} + +// Based on give options struct, returns if there is a filtered +// Gateway Name and if we should do report Accounts. +// Note that if Accounts is false but AccountName is not empty, +// then Accounts is implicitly set to true. +func getMonitorGWOptions(opts *GatewayzOptions) (string, bool) { + var name string + var accs bool + if opts != nil { + if opts.Name != _EMPTY_ { + name = opts.Name + } + accs = opts.Accounts + if !accs && opts.AccountName != _EMPTY_ { + accs = true + } + } + return name, accs +} + +// Returns a map of gateways outbound connections. +// Based on options, will include a single or all gateways, +// with no/single/or all accounts interest information. +func (s *Server) createOutboundsRemoteGatewayz(opts *GatewayzOptions, now time.Time) map[string]*RemoteGatewayz { + targetGWName, doAccs := getMonitorGWOptions(opts) + + if targetGWName != _EMPTY_ { + c := s.getOutboundGatewayConnection(targetGWName) + if c == nil { + return nil + } + outbounds := make(map[string]*RemoteGatewayz, 1) + _, rgw := createOutboundRemoteGatewayz(c, opts, now, doAccs) + outbounds[targetGWName] = rgw + return outbounds + } + + var connsa [16]*client + var conns = connsa[:0] + + s.getOutboundGatewayConnections(&conns) + + outbounds := make(map[string]*RemoteGatewayz, len(conns)) + for _, c := range conns { + name, rgw := createOutboundRemoteGatewayz(c, opts, now, doAccs) + if rgw != nil { + outbounds[name] = rgw + } + } + return outbounds +} + +// Returns a RemoteGatewayz for a given outbound gw connection +func createOutboundRemoteGatewayz(c *client, opts *GatewayzOptions, now time.Time, doAccs bool) (string, *RemoteGatewayz) { + var name string + var rgw *RemoteGatewayz + + c.mu.Lock() + if c.gw != nil { + rgw = &RemoteGatewayz{} + if doAccs { + rgw.Accounts = createOutboundAccountsGatewayz(opts, c.gw) + } + if c.gw.cfg != nil { + rgw.IsConfigured = !c.gw.cfg.isImplicit() + } + rgw.Connection = &ConnInfo{} + rgw.Connection.fill(c, c.nc, now, false) + name = c.gw.name + } + c.mu.Unlock() + + return name, rgw +} + +// Returns the list of accounts for this outbound gateway connection. +// Based on the options, it will be a single or all accounts for +// this outbound. +func createOutboundAccountsGatewayz(opts *GatewayzOptions, gw *gateway) []*AccountGatewayz { + if gw.outsim == nil { + return nil + } + + var accName string + if opts != nil { + accName = opts.AccountName + } + if accName != _EMPTY_ { + ei, ok := gw.outsim.Load(accName) + if !ok { + return nil + } + a := createAccountOutboundGatewayz(opts, accName, ei) + return []*AccountGatewayz{a} + } + + accs := make([]*AccountGatewayz, 0, 4) + gw.outsim.Range(func(k, v any) bool { + name := k.(string) + a := createAccountOutboundGatewayz(opts, name, v) + accs = append(accs, a) + return true + }) + return accs +} + +// Returns an AccountGatewayz for this gateway outbound connection +func createAccountOutboundGatewayz(opts *GatewayzOptions, name string, ei any) *AccountGatewayz { + a := &AccountGatewayz{ + Name: name, + InterestOnlyThreshold: gatewayMaxRUnsubBeforeSwitch, + } + if ei != nil { + e := ei.(*outsie) + e.RLock() + a.InterestMode = e.mode.String() + a.NoInterestCount = len(e.ni) + a.NumQueueSubscriptions = e.qsubs + a.TotalSubscriptions = int(e.sl.Count()) + if opts.AccountSubscriptions || opts.AccountSubscriptionsDetail { + var subsa [4096]*subscription + subs := subsa[:0] + e.sl.All(&subs) + if opts.AccountSubscriptions { + a.Subs = make([]string, 0, len(subs)) + } else { + a.SubsDetail = make([]SubDetail, 0, len(subs)) + } + for _, sub := range subs { + if opts.AccountSubscriptions { + a.Subs = append(a.Subs, string(sub.subject)) + } else { + a.SubsDetail = append(a.SubsDetail, newClientSubDetail(sub)) + } + } + } + e.RUnlock() + } else { + a.InterestMode = Optimistic.String() + } + return a +} + +// Returns a map of gateways inbound connections. +// Each entry is an array of RemoteGatewayz since a given server +// may have more than one inbound from the same remote gateway. +// Based on options, will include a single or all gateways, +// with no/single/or all accounts interest information. +func (s *Server) createInboundsRemoteGatewayz(opts *GatewayzOptions, now time.Time) map[string][]*RemoteGatewayz { + targetGWName, doAccs := getMonitorGWOptions(opts) + + var connsa [16]*client + var conns = connsa[:0] + s.getInboundGatewayConnections(&conns) + + m := make(map[string][]*RemoteGatewayz) + for _, c := range conns { + c.mu.Lock() + if c.gw != nil && (targetGWName == _EMPTY_ || targetGWName == c.gw.name) { + igws := m[c.gw.name] + if igws == nil { + igws = make([]*RemoteGatewayz, 0, 2) + } + rgw := &RemoteGatewayz{} + if doAccs { + rgw.Accounts = createInboundAccountsGatewayz(opts, c.gw) + } + rgw.Connection = &ConnInfo{} + rgw.Connection.fill(c, c.nc, now, false) + igws = append(igws, rgw) + m[c.gw.name] = igws + } + c.mu.Unlock() + } + return m +} + +// Returns the list of accounts for this inbound gateway connection. +// Based on the options, it will be a single or all accounts for +// this inbound. +func createInboundAccountsGatewayz(opts *GatewayzOptions, gw *gateway) []*AccountGatewayz { + if gw.insim == nil { + return nil + } + + var accName string + if opts != nil { + accName = opts.AccountName + } + if accName != _EMPTY_ { + e, ok := gw.insim[accName] + if !ok { + return nil + } + a := createInboundAccountGatewayz(accName, e) + return []*AccountGatewayz{a} + } + + accs := make([]*AccountGatewayz, 0, 4) + for name, e := range gw.insim { + a := createInboundAccountGatewayz(name, e) + accs = append(accs, a) + } + return accs +} + +// Returns an AccountGatewayz for this gateway inbound connection +func createInboundAccountGatewayz(name string, e *insie) *AccountGatewayz { + a := &AccountGatewayz{ + Name: name, + InterestOnlyThreshold: gatewayMaxRUnsubBeforeSwitch, + } + if e != nil { + a.InterestMode = e.mode.String() + a.NoInterestCount = len(e.ni) + } else { + a.InterestMode = Optimistic.String() + } + return a +} + +// HandleGatewayz process HTTP requests for route information. +func (s *Server) HandleGatewayz(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + s.httpReqStats[GatewayzPath]++ + s.mu.Unlock() + + subs, subsDet, err := decodeSubs(w, r) + if err != nil { + return + } + accs, err := decodeBool(w, r, "accs") + if err != nil { + return + } + gwName := r.URL.Query().Get("gw_name") + accName := r.URL.Query().Get("acc_name") + if accName != _EMPTY_ { + accs = true + } + + opts := &GatewayzOptions{ + Name: gwName, + Accounts: accs, + AccountName: accName, + AccountSubscriptions: subs, + AccountSubscriptionsDetail: subsDet, + } + gw, err := s.Gatewayz(opts) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + b, err := json.MarshalIndent(gw, "", " ") + if err != nil { + s.Errorf("Error marshaling response to /gatewayz request: %v", err) + } + + // Handle response + ResponseHandler(w, r, b) +} + +// Leafz represents detailed information on Leafnodes. +type Leafz struct { + ID string `json:"server_id"` + Now time.Time `json:"now"` + NumLeafs int `json:"leafnodes"` + Leafs []*LeafInfo `json:"leafs"` +} + +// LeafzOptions are options passed to Leafz +type LeafzOptions struct { + // Subscriptions indicates that Leafz will return a leafnode's subscriptions + Subscriptions bool `json:"subscriptions"` + Account string `json:"account"` +} + +// LeafInfo has detailed information on each remote leafnode connection. +type LeafInfo struct { + ID uint64 `json:"id"` + Name string `json:"name"` + IsSpoke bool `json:"is_spoke"` + IsIsolated bool `json:"is_isolated,omitempty"` + Account string `json:"account"` + IP string `json:"ip"` + Port int `json:"port"` + RTT string `json:"rtt,omitempty"` + InMsgs int64 `json:"in_msgs"` + OutMsgs int64 `json:"out_msgs"` + InBytes int64 `json:"in_bytes"` + OutBytes int64 `json:"out_bytes"` + NumSubs uint32 `json:"subscriptions"` + Subs []string `json:"subscriptions_list,omitempty"` + Compression string `json:"compression,omitempty"` + Proxy *ProxyInfo `json:"proxy,omitempty"` +} + +// Leafz returns a Leafz structure containing information about leafnodes. +func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) { + // Grab leafnodes + var lconns []*client + s.mu.Lock() + if len(s.leafs) > 0 { + lconns = make([]*client, 0, len(s.leafs)) + for _, ln := range s.leafs { + if opts != nil && opts.Account != _EMPTY_ { + ln.mu.Lock() + ok := ln.acc.Name == opts.Account + ln.mu.Unlock() + if !ok { + continue + } + } + lconns = append(lconns, ln) + } + } + s.mu.Unlock() + + leafnodes := make([]*LeafInfo, 0, len(lconns)) + + if len(lconns) > 0 { + for _, ln := range lconns { + ln.mu.Lock() + lni := &LeafInfo{ + ID: ln.cid, + Name: ln.leaf.remoteServer, + IsSpoke: ln.isSpokeLeafNode(), + IsIsolated: ln.leaf.isolated, + Account: ln.acc.Name, + IP: ln.host, + Port: int(ln.port), + RTT: ln.getRTT().String(), + InMsgs: atomic.LoadInt64(&ln.inMsgs), + OutMsgs: ln.outMsgs, + InBytes: atomic.LoadInt64(&ln.inBytes), + OutBytes: ln.outBytes, + NumSubs: uint32(len(ln.subs)), + Compression: ln.leaf.compression, + Proxy: createProxyInfo(ln), + } + if opts != nil && opts.Subscriptions { + lni.Subs = make([]string, 0, len(ln.subs)) + for _, sub := range ln.subs { + lni.Subs = append(lni.Subs, string(sub.subject)) + } + } + ln.mu.Unlock() + leafnodes = append(leafnodes, lni) + } + } + + return &Leafz{ + ID: s.ID(), + Now: time.Now().UTC(), + NumLeafs: len(leafnodes), + Leafs: leafnodes, + }, nil +} + +// HandleLeafz process HTTP requests for leafnode information. +func (s *Server) HandleLeafz(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + s.httpReqStats[LeafzPath]++ + s.mu.Unlock() + + subs, err := decodeBool(w, r, "subs") + if err != nil { + return + } + l, err := s.Leafz(&LeafzOptions{subs, r.URL.Query().Get("acc")}) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + b, err := json.MarshalIndent(l, "", " ") + if err != nil { + s.Errorf("Error marshaling response to /leafz request: %v", err) + } + + // Handle response + ResponseHandler(w, r, b) +} + +// Leafz represents detailed information on Leafnodes. +type AccountStatz struct { + ID string `json:"server_id"` + Now time.Time `json:"now"` + Accounts []*AccountStat `json:"account_statz"` +} + +// AccountStatzOptions are options passed to account stats requests. +type AccountStatzOptions struct { + Accounts []string `json:"accounts"` + IncludeUnused bool `json:"include_unused"` +} + +// Leafz returns a AccountStatz structure containing summary information about accounts. +func (s *Server) AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error) { + stz := &AccountStatz{ + ID: s.ID(), + Now: time.Now().UTC(), + Accounts: []*AccountStat{}, + } + if opts == nil || len(opts.Accounts) == 0 { + s.accounts.Range(func(key, a any) bool { + acc := a.(*Account) + acc.mu.RLock() + if (opts != nil && opts.IncludeUnused) || acc.numLocalConnections() != 0 || acc.numLocalLeafNodes() != 0 { + stz.Accounts = append(stz.Accounts, acc.statz()) + } + acc.mu.RUnlock() + return true + }) + } else { + for _, a := range opts.Accounts { + if acc, ok := s.accounts.Load(a); ok { + acc := acc.(*Account) + acc.mu.RLock() + if opts.IncludeUnused || acc.numLocalConnections() != 0 || acc.numLocalLeafNodes() != 0 { + stz.Accounts = append(stz.Accounts, acc.statz()) + } + acc.mu.RUnlock() + } + } + } + return stz, nil +} + +// HandleAccountStatz process HTTP requests for statz information of all accounts. +func (s *Server) HandleAccountStatz(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + s.httpReqStats[AccountStatzPath]++ + s.mu.Unlock() + + unused, err := decodeBool(w, r, "unused") + if err != nil { + return + } + + l, err := s.AccountStatz(&AccountStatzOptions{IncludeUnused: unused}) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + b, err := json.MarshalIndent(l, "", " ") + if err != nil { + s.Errorf("Error marshaling response to %s request: %v", AccountStatzPath, err) + return + } + + // Handle response + ResponseHandler(w, r, b) +} + +// ResponseHandler handles responses for monitoring routes. +func ResponseHandler(w http.ResponseWriter, r *http.Request, data []byte) { + handleResponse(http.StatusOK, w, r, data) +} + +// handleResponse handles responses for monitoring routes with a specific HTTP status code. +func handleResponse(code int, w http.ResponseWriter, _ *http.Request, data []byte) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.WriteHeader(code) + w.Write(data) +} + +func (reason ClosedState) String() string { + switch reason { + case ClientClosed: + return "Client Closed" + case AuthenticationTimeout: + return "Authentication Timeout" + case AuthenticationViolation: + return "Authentication Failure" + case TLSHandshakeError: + return "TLS Handshake Failure" + case SlowConsumerPendingBytes: + return "Slow Consumer (Pending Bytes)" + case SlowConsumerWriteDeadline: + return "Slow Consumer (Write Deadline)" + case WriteError: + return "Write Error" + case ReadError: + return "Read Error" + case ParseError: + return "Parse Error" + case StaleConnection: + return "Stale Connection" + case ProtocolViolation: + return "Protocol Violation" + case BadClientProtocolVersion: + return "Bad Client Protocol Version" + case WrongPort: + return "Incorrect Port" + case MaxConnectionsExceeded: + return "Maximum Connections Exceeded" + case MaxAccountConnectionsExceeded: + return "Maximum Account Connections Exceeded" + case MaxPayloadExceeded: + return "Maximum Message Payload Exceeded" + case MaxControlLineExceeded: + return "Maximum Control Line Exceeded" + case MaxSubscriptionsExceeded: + return "Maximum Subscriptions Exceeded" + case DuplicateRoute: + return "Duplicate Route" + case RouteRemoved: + return "Route Removed" + case ServerShutdown: + return "Server Shutdown" + case AuthenticationExpired: + return "Authentication Expired" + case WrongGateway: + return "Wrong Gateway" + case MissingAccount: + return "Missing Account" + case Revocation: + return "Credentials Revoked" + case InternalClient: + return "Internal Client" + case MsgHeaderViolation: + return "Message Header Violation" + case NoRespondersRequiresHeaders: + return "No Responders Requires Headers" + case ClusterNameConflict: + return "Cluster Name Conflict" + case DuplicateRemoteLeafnodeConnection: + return "Duplicate Remote LeafNode Connection" + case DuplicateClientID: + return "Duplicate Client ID" + case DuplicateServerName: + return "Duplicate Server Name" + case MinimumVersionRequired: + return "Minimum Version Required" + case ClusterNamesIdentical: + return "Cluster Names Identical" + case Kicked: + return "Kicked" + case ProxyNotTrusted: + return "Proxy Not Trusted" + case ProxyRequired: + return "Proxy Required" + } + + return "Unknown State" +} + +// AccountzOptions are options passed to Accountz +type AccountzOptions struct { + // Account indicates that Accountz will return details for the account + Account string `json:"account"` +} + +func newExtServiceLatency(l *serviceLatency) *jwt.ServiceLatency { + if l == nil { + return nil + } + return &jwt.ServiceLatency{ + Sampling: jwt.SamplingRate(l.sampling), + Results: jwt.Subject(l.subject), + } +} + +type ExtImport struct { + jwt.Import + Invalid bool `json:"invalid"` + Share bool `json:"share"` + Tracking bool `json:"tracking"` + TrackingHdr http.Header `json:"tracking_header,omitempty"` + Latency *jwt.ServiceLatency `json:"latency,omitempty"` + M1 *ServiceLatency `json:"m1,omitempty"` +} + +type ExtExport struct { + jwt.Export + ApprovedAccounts []string `json:"approved_accounts,omitempty"` + RevokedAct map[string]time.Time `json:"revoked_activations,omitempty"` +} + +type ExtVrIssues struct { + Description string `json:"description"` + Blocking bool `json:"blocking"` + Time bool `json:"time_check"` +} + +type ExtMap map[string][]*MapDest + +type AccountInfo struct { + AccountName string `json:"account_name"` + LastUpdate time.Time `json:"update_time,omitempty"` + IsSystem bool `json:"is_system,omitempty"` + Expired bool `json:"expired"` + Complete bool `json:"complete"` + JetStream bool `json:"jetstream_enabled"` + LeafCnt int `json:"leafnode_connections"` + ClientCnt int `json:"client_connections"` + SubCnt uint32 `json:"subscriptions"` + Mappings ExtMap `json:"mappings,omitempty"` + Exports []ExtExport `json:"exports,omitempty"` + Imports []ExtImport `json:"imports,omitempty"` + Jwt string `json:"jwt,omitempty"` + IssuerKey string `json:"issuer_key,omitempty"` + NameTag string `json:"name_tag,omitempty"` + Tags jwt.TagList `json:"tags,omitempty"` + Claim *jwt.AccountClaims `json:"decoded_jwt,omitempty"` + Vr []ExtVrIssues `json:"validation_result_jwt,omitempty"` + RevokedUser map[string]time.Time `json:"revoked_user,omitempty"` + Sublist *SublistStats `json:"sublist_stats,omitempty"` + Responses map[string]ExtImport `json:"responses,omitempty"` +} + +type Accountz struct { + ID string `json:"server_id"` + Now time.Time `json:"now"` + SystemAccount string `json:"system_account,omitempty"` + Accounts []string `json:"accounts,omitempty"` + Account *AccountInfo `json:"account_detail,omitempty"` +} + +// HandleAccountz process HTTP requests for account information. +func (s *Server) HandleAccountz(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + s.httpReqStats[AccountzPath]++ + s.mu.Unlock() + if l, err := s.Accountz(&AccountzOptions{r.URL.Query().Get("acc")}); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + } else if b, err := json.MarshalIndent(l, "", " "); err != nil { + s.Errorf("Error marshaling response to %s request: %v", AccountzPath, err) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + } else { + ResponseHandler(w, r, b) // Handle response + } +} + +func (s *Server) Accountz(optz *AccountzOptions) (*Accountz, error) { + a := &Accountz{ + ID: s.ID(), + Now: time.Now().UTC(), + } + if sacc := s.SystemAccount(); sacc != nil { + a.SystemAccount = sacc.GetName() + } + if optz == nil || optz.Account == _EMPTY_ { + a.Accounts = []string{} + s.accounts.Range(func(key, value any) bool { + a.Accounts = append(a.Accounts, key.(string)) + return true + }) + return a, nil + } + aInfo, err := s.accountInfo(optz.Account) + if err != nil { + return nil, err + } + a.Account = aInfo + return a, nil +} + +func newExtImport(v *serviceImport) ExtImport { + imp := ExtImport{ + Invalid: true, + Import: jwt.Import{Type: jwt.Service}, + } + if v != nil { + imp.Share = v.share + imp.Tracking = v.tracking + imp.Invalid = v.invalid + imp.Import = jwt.Import{ + Subject: jwt.Subject(v.to), + Account: v.acc.Name, + Type: jwt.Service, + // Deprecated so we duplicate. Use LocalSubject. + To: jwt.Subject(v.from), + LocalSubject: jwt.RenamingSubject(v.from), + } + imp.TrackingHdr = v.trackingHdr + imp.Latency = newExtServiceLatency(v.latency) + if v.m1 != nil { + m1 := *v.m1 + imp.M1 = &m1 + } + } + return imp +} + +func (s *Server) accountInfo(accName string) (*AccountInfo, error) { + var a *Account + if v, ok := s.accounts.Load(accName); !ok { + return nil, fmt.Errorf("Account %s does not exist", accName) + } else { + a = v.(*Account) + } + isSys := a == s.SystemAccount() + a.mu.RLock() + defer a.mu.RUnlock() + var vrIssues []ExtVrIssues + claim, _ := jwt.DecodeAccountClaims(a.claimJWT) // ignore error + if claim != nil { + vr := jwt.ValidationResults{} + claim.Validate(&vr) + vrIssues = make([]ExtVrIssues, len(vr.Issues)) + for i, v := range vr.Issues { + vrIssues[i] = ExtVrIssues{v.Description, v.Blocking, v.TimeCheck} + } + } + collectRevocations := func(revocations map[string]int64) map[string]time.Time { + l := len(revocations) + if l == 0 { + return nil + } + rev := make(map[string]time.Time, l) + for k, v := range revocations { + rev[k] = time.Unix(v, 0) + } + return rev + } + exports := []ExtExport{} + for k, v := range a.exports.services { + e := ExtExport{ + Export: jwt.Export{ + Subject: jwt.Subject(k), + Type: jwt.Service, + }, + ApprovedAccounts: []string{}, + } + if v != nil { + e.Latency = newExtServiceLatency(v.latency) + e.TokenReq = v.tokenReq + e.ResponseType = jwt.ResponseType(v.respType.String()) + for name := range v.approved { + e.ApprovedAccounts = append(e.ApprovedAccounts, name) + } + e.RevokedAct = collectRevocations(v.actsRevoked) + } + exports = append(exports, e) + } + for k, v := range a.exports.streams { + e := ExtExport{ + Export: jwt.Export{ + Subject: jwt.Subject(k), + Type: jwt.Stream, + }, + ApprovedAccounts: []string{}, + } + if v != nil { + e.TokenReq = v.tokenReq + for name := range v.approved { + e.ApprovedAccounts = append(e.ApprovedAccounts, name) + } + e.RevokedAct = collectRevocations(v.actsRevoked) + } + exports = append(exports, e) + } + imports := []ExtImport{} + for _, v := range a.imports.streams { + imp := ExtImport{ + Invalid: true, + Import: jwt.Import{Type: jwt.Stream}, + } + if v != nil { + imp.Invalid = v.invalid + imp.Import = jwt.Import{ + Subject: jwt.Subject(v.from), + Account: v.acc.Name, + Type: jwt.Stream, + LocalSubject: jwt.RenamingSubject(v.to), + } + } + imports = append(imports, imp) + } + for _, sis := range a.imports.services { + for _, v := range sis { + imports = append(imports, newExtImport(v)) + } + } + responses := map[string]ExtImport{} + for k, v := range a.exports.responses { + responses[k] = newExtImport(v) + } + mappings := ExtMap{} + for _, m := range a.mappings { + var dests []*MapDest + var src string + if m == nil { + src = "nil" + if _, ok := mappings[src]; ok { // only set if not present (keep orig in case nil is used) + continue + } + dests = append(dests, &MapDest{}) + } else { + src = m.src + for _, d := range m.dests { + dests = append(dests, &MapDest{d.tr.dest, d.weight, _EMPTY_}) + } + for c, cd := range m.cdests { + for _, d := range cd { + dests = append(dests, &MapDest{d.tr.dest, d.weight, c}) + } + } + } + mappings[src] = dests + } + return &AccountInfo{ + AccountName: accName, + LastUpdate: a.updated.UTC(), + IsSystem: isSys, + Expired: a.expired.Load(), + Complete: !a.incomplete, + JetStream: a.js != nil, + LeafCnt: a.numLocalLeafNodes(), + ClientCnt: a.numLocalConnections(), + SubCnt: a.sl.Count(), + Mappings: mappings, + Exports: exports, + Imports: imports, + Jwt: a.claimJWT, + IssuerKey: a.Issuer, + NameTag: a.getNameTagLocked(), + Tags: a.tags, + Claim: claim, + Vr: vrIssues, + RevokedUser: collectRevocations(a.usersRevoked), + Sublist: a.sl.Stats(), + Responses: responses, + }, nil +} + +// JSzOptions are options passed to Jsz +type JSzOptions struct { + Account string `json:"account,omitempty"` + Accounts bool `json:"accounts,omitempty"` + Streams bool `json:"streams,omitempty"` + Consumer bool `json:"consumer,omitempty"` + DirectConsumer bool `json:"direct_consumer,omitempty"` + Config bool `json:"config,omitempty"` + LeaderOnly bool `json:"leader_only,omitempty"` + Offset int `json:"offset,omitempty"` + Limit int `json:"limit,omitempty"` + RaftGroups bool `json:"raft,omitempty"` + StreamLeaderOnly bool `json:"stream_leader_only,omitempty"` +} + +// HealthzOptions are options passed to Healthz +type HealthzOptions struct { + // Deprecated: Use JSEnabledOnly instead + JSEnabled bool `json:"js-enabled,omitempty"` + JSEnabledOnly bool `json:"js-enabled-only,omitempty"` + JSServerOnly bool `json:"js-server-only,omitempty"` + JSMetaOnly bool `json:"js-meta-only,omitempty"` + Account string `json:"account,omitempty"` + Stream string `json:"stream,omitempty"` + Consumer string `json:"consumer,omitempty"` + Details bool `json:"details,omitempty"` +} + +// ProfilezOptions are options passed to Profilez +type ProfilezOptions struct { + Name string `json:"name"` + Debug int `json:"debug"` + Duration time.Duration `json:"duration,omitempty"` +} + +// IpqueueszOptions are options passed to Ipqueuesz +type IpqueueszOptions struct { + All bool `json:"all"` + Filter string `json:"filter"` +} + +// RaftzOptions are options passed to Raftz +type RaftzOptions struct { + AccountFilter string `json:"account"` + GroupFilter string `json:"group"` +} + +// StreamDetail shows information about the stream state and its consumers. +type StreamDetail struct { + Name string `json:"name"` + Created time.Time `json:"created"` + Cluster *ClusterInfo `json:"cluster,omitempty"` + Config *StreamConfig `json:"config,omitempty"` + State StreamState `json:"state,omitempty"` + Consumer []*ConsumerInfo `json:"consumer_detail,omitempty"` + DirectConsumer []*ConsumerInfo `json:"direct_consumer_detail,omitempty"` + Mirror *StreamSourceInfo `json:"mirror,omitempty"` + Sources []*StreamSourceInfo `json:"sources,omitempty"` + RaftGroup string `json:"stream_raft_group,omitempty"` + ConsumerRaftGroups []*RaftGroupDetail `json:"consumer_raft_groups,omitempty"` +} + +// RaftGroupDetail shows information details about the Raft group. +type RaftGroupDetail struct { + Name string `json:"name"` + RaftGroup string `json:"raft_group,omitempty"` +} + +type AccountDetail struct { + Name string `json:"name"` + Id string `json:"id"` + JetStreamStats + Streams []StreamDetail `json:"stream_detail,omitempty"` +} + +// MetaSnapshotStats shows information about meta snapshots. +type MetaSnapshotStats struct { + PendingEntries uint64 `json:"pending_entries"` // PendingEntries is the count of pending entries in the meta layer + PendingSize uint64 `json:"pending_size"` // PendingSize is the size in bytes of pending entries in the meta layer + LastTime time.Time `json:"last_time,omitempty"` // LastTime is when the last meta snapshot was taken + LastDuration time.Duration `json:"last_duration,omitempty"` // LastDuration is how long the last meta snapshot took +} + +// metaClusterSnapshotStats returns snapshot statistics for the meta group. +func (s *Server) metaClusterSnapshotStats(js *jetStream, mg RaftNode) *MetaSnapshotStats { + entries, bytes := mg.Size() + snap := &MetaSnapshotStats{ + PendingEntries: entries, + PendingSize: bytes, + } + + js.mu.RLock() + cluster := js.cluster + js.mu.RUnlock() + + if cluster != nil { + timeNanos := atomic.LoadInt64(&cluster.lastMetaSnapTime) + durationNanos := atomic.LoadInt64(&cluster.lastMetaSnapDuration) + if timeNanos > 0 { + snap.LastTime = time.Unix(0, timeNanos).UTC() + } + if durationNanos > 0 { + snap.LastDuration = time.Duration(durationNanos) + } + } + + return snap +} + +// MetaClusterInfo shows information about the meta group. +type MetaClusterInfo struct { + Name string `json:"name,omitempty"` // Name is the name of the cluster + Leader string `json:"leader,omitempty"` // Leader is the server name of the cluster leader + Peer string `json:"peer,omitempty"` // Peer is unique ID of the leader + Replicas []*PeerInfo `json:"replicas,omitempty"` // Replicas is a list of known peers + Size int `json:"cluster_size"` // Size is the known size of the cluster + QuorumNeeded int `json:"quorum_needed"` // QuorumNeeded is this server's current effective quorum size + Rescue bool `json:"rescue,omitempty"` // Rescue indicates the quorum is unsafely lowered by an active rescue + Pending int `json:"pending"` // Pending is how many RAFT messages are not yet processed + PendingRequests int `json:"pending_requests"` // PendingRequests is how many CRUD operations are queued for processing + PendingInfos int `json:"pending_infos"` // PendingInfos is how many info operations are queued for processing + Snapshot *MetaSnapshotStats `json:"snapshot"` // Snapshot contains meta snapshot statistics +} + +// JSInfo has detailed information on JetStream. +type JSInfo struct { + JetStreamStats + ID string `json:"server_id"` + Now time.Time `json:"now"` + Disabled bool `json:"disabled,omitempty"` + Config JetStreamConfig `json:"config,omitempty"` + Limits *JSLimitOpts `json:"limits,omitempty"` + Streams int `json:"streams"` + StreamsLeader int `json:"streams_leader,omitempty"` + Consumers int `json:"consumers"` + ConsumersLeader int `json:"consumers_leader,omitempty"` + Messages uint64 `json:"messages"` + Bytes uint64 `json:"bytes"` + Meta *MetaClusterInfo `json:"meta_cluster,omitempty"` + AccountDetails []*AccountDetail `json:"account_details,omitempty"` + Total int `json:"total"` +} + +func (s *Server) accountDetail(jsa *jsAccount, optStreams, optConsumers, optDirectConsumers, optCfg, optRaft, optStreamLeader bool) *AccountDetail { + jsa.mu.RLock() + acc := jsa.account + name := acc.GetName() + id := name + if acc.nameTag != _EMPTY_ { + name = acc.nameTag + } + jsa.usageMu.RLock() + totalMem, totalStore := jsa.storageTotals() + detail := AccountDetail{ + Name: name, + Id: id, + JetStreamStats: JetStreamStats{ + Memory: totalMem, + Store: totalStore, + API: JetStreamAPIStats{ + Total: jsa.apiTotal, + Errors: jsa.apiErrors, + }, + }, + Streams: make([]StreamDetail, 0, len(jsa.streams)), + } + if reserved, ok := jsa.limits[_EMPTY_]; ok { + detail.JetStreamStats.ReservedMemory = uint64(reserved.MaxMemory) + detail.JetStreamStats.ReservedStore = uint64(reserved.MaxStore) + } + jsa.usageMu.RUnlock() + + var streams []*stream + if optStreams { + for _, stream := range jsa.streams { + streams = append(streams, stream) + } + } + jsa.mu.RUnlock() + + if js := s.getJetStream(); js != nil && optStreams { + for _, stream := range streams { + rgroup := stream.raftGroup() + ci := js.clusterInfo(rgroup) + var cfg *StreamConfig + if optCfg { + // Report the config as requested, the stream can still be running at its origin. + // Must be consistent with the desired state reported as part of the cluster info. + c := js.targetStreamConfig(stream, stream.config()) + cfg = &c + } + // Skip if we are only looking for stream leaders. + if optStreamLeader && ci != nil && ci.Leader != s.Name() { + continue + } + sdet := StreamDetail{ + Name: stream.name(), + Created: stream.createdTime(), + State: stream.state(), + Cluster: ci, + Config: cfg, + Mirror: stream.mirrorInfo(), + Sources: stream.sourcesInfo(), + } + if optRaft && rgroup != nil { + sdet.RaftGroup = rgroup.Name + sdet.ConsumerRaftGroups = make([]*RaftGroupDetail, 0) + } + if optConsumers { + for _, consumer := range stream.getPublicConsumers() { + cInfo := consumer.info() + if cInfo == nil { + continue + } + if !optCfg { + cInfo.Config = nil + } + sdet.Consumer = append(sdet.Consumer, cInfo) + if optRaft { + crgroup := consumer.raftGroup() + if crgroup != nil { + sdet.ConsumerRaftGroups = append(sdet.ConsumerRaftGroups, + &RaftGroupDetail{cInfo.Name, crgroup.Name}, + ) + } + } + } + if optDirectConsumers { + for _, consumer := range stream.getDirectConsumers() { + cInfo := consumer.info() + if cInfo == nil { + continue + } + if !optCfg { + cInfo.Config = nil + } + sdet.DirectConsumer = append(sdet.DirectConsumer, cInfo) + } + } + } + detail.Streams = append(detail.Streams, sdet) + } + } + return &detail +} + +func (s *Server) JszAccount(opts *JSzOptions) (*AccountDetail, error) { + js := s.getJetStream() + if js == nil { + return nil, fmt.Errorf("jetstream not enabled") + } + acc := opts.Account + account, ok := s.accounts.Load(acc) + if !ok { + return nil, fmt.Errorf("account %q not found", acc) + } + js.mu.RLock() + jsa, ok := js.accounts[account.(*Account).Name] + js.mu.RUnlock() + if !ok { + return nil, fmt.Errorf("account %q not jetstream enabled", acc) + } + return s.accountDetail(jsa, opts.Streams, opts.Consumer, opts.DirectConsumer, opts.Config, opts.RaftGroups, opts.StreamLeaderOnly), nil +} + +// helper to get cluster info from node via dummy group +func (s *Server) raftNodeToClusterInfo(node RaftNode) *ClusterInfo { + if node == nil { + return nil + } + peers := node.Peers() + peerList := make([]string, len(peers)) + for i, p := range peers { + peerList[i] = p.ID + } + group := &raftGroup{ + Name: _EMPTY_, + Peers: peerList, + node: node, + } + return s.getJetStream().clusterInfo(group) +} + +// Jsz returns a Jsz structure containing information about JetStream. +func (s *Server) Jsz(opts *JSzOptions) (*JSInfo, error) { + // set option defaults + if opts == nil { + opts = &JSzOptions{} + } + if opts.Offset < 0 { + opts.Offset = 0 + } + if opts.Limit == 0 { + opts.Limit = 1024 + } + if opts.Consumer { + opts.Streams = true + } + if opts.Streams && opts.Account == _EMPTY_ { + opts.Accounts = true + } + + jsi := &JSInfo{ + ID: s.ID(), + Now: time.Now().UTC(), + } + + js := s.getJetStream() + if js == nil || !js.isEnabled() { + if opts.LeaderOnly { + return nil, fmt.Errorf("%w: not leader", errSkipZreq) + } + + jsi.Disabled = true + return jsi, nil + } + + jsi.Limits = &s.getOpts().JetStreamLimits + + js.mu.RLock() + isLeader := js.cluster == nil || js.cluster.isLeader() + js.mu.RUnlock() + + if opts.LeaderOnly && !isLeader { + return nil, fmt.Errorf("%w: not leader", errSkipZreq) + } + + var accounts []*jsAccount + + js.mu.RLock() + jsi.Config = js.config + for _, info := range js.accounts { + accounts = append(accounts, info) + } + js.mu.RUnlock() + + jsi.Total = len(accounts) + + if mg := js.getMetaGroup(); mg != nil { + if ci := s.raftNodeToClusterInfo(mg); ci != nil { + jsi.Meta = &MetaClusterInfo{ + Name: ci.Name, + Leader: ci.Leader, + Replicas: ci.Replicas, + Size: mg.ClusterSize(), + QuorumNeeded: mg.QuorumNeeded(), + Rescue: mg.InRescue(), + } + if ci.Leader != _EMPTY_ { + jsi.Meta.Peer = getHash(ci.Leader) + } + if ipq := s.jsAPIRoutedReqs; ipq != nil { + jsi.Meta.PendingRequests = ipq.len() + } + if ipq := s.jsAPIRoutedInfoReqs; ipq != nil { + jsi.Meta.PendingInfos = ipq.len() + } + jsi.Meta.Pending = jsi.Meta.PendingRequests + jsi.Meta.PendingInfos + jsi.Meta.Snapshot = s.metaClusterSnapshotStats(js, mg) + } + } + + jsi.JetStreamStats = *js.usageStats() + + // If a specific account is requested, track the index. + filterIdx := -1 + + // Calculate the stats of all accounts and streams regardless of the filtering. + for i, jsa := range accounts { + if jsa.acc().GetName() == opts.Account { + filterIdx = i + } + + jsa.mu.RLock() + streams := make([]*stream, 0, len(jsa.streams)) + for _, stream := range jsa.streams { + streams = append(streams, stream) + } + jsa.mu.RUnlock() + + jsi.Streams += len(streams) + for _, stream := range streams { + streamState := stream.state() + jsi.Messages += streamState.Msgs + jsi.Bytes += streamState.Bytes + jsi.Consumers += streamState.Consumers + if opts.RaftGroups { + if node := stream.raftNode(); node == nil || node.Leader() { + jsi.StreamsLeader++ + } + for _, consumer := range stream.getPublicConsumers() { + if node := consumer.raftNode(); node == nil || node.Leader() { + jsi.ConsumersLeader++ + } + } + } + } + } + + // Targeted account takes precedence. + if filterIdx >= 0 { + accounts = accounts[filterIdx : filterIdx+1] + } else if opts.Accounts { + + if opts.Limit > 0 { + // Sort by name for a consistent read (barring any concurrent changes) + slices.SortFunc(accounts, func(i, j *jsAccount) int { return cmp.Compare(i.acc().Name, j.acc().Name) }) + + // Offset larger than the number of accounts. + offset := min(opts.Offset, len(accounts)) + accounts = accounts[offset:] + + limit := min(opts.Limit, len(accounts)) + accounts = accounts[:limit] + } + } else { + accounts = nil + } + + if len(accounts) > 0 { + jsi.AccountDetails = make([]*AccountDetail, 0, len(accounts)) + + for _, jsa := range accounts { + detail := s.accountDetail(jsa, opts.Streams, opts.Consumer, opts.DirectConsumer, opts.Config, opts.RaftGroups, opts.StreamLeaderOnly) + jsi.AccountDetails = append(jsi.AccountDetails, detail) + } + } + + return jsi, nil +} + +// HandleJsz process HTTP requests for jetstream information. +func (s *Server) HandleJsz(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + s.httpReqStats[JszPath]++ + s.mu.Unlock() + accounts, err := decodeBool(w, r, "accounts") + if err != nil { + return + } + streams, err := decodeBool(w, r, "streams") + if err != nil { + return + } + consumers, err := decodeBool(w, r, "consumers") + if err != nil { + return + } + directConsumers, err := decodeBool(w, r, "direct-consumers") + if err != nil { + return + } + config, err := decodeBool(w, r, "config") + if err != nil { + return + } + offset, err := decodeInt(w, r, "offset") + if err != nil { + return + } + limit, err := decodeInt(w, r, "limit") + if err != nil { + return + } + leader, err := decodeBool(w, r, "leader-only") + if err != nil { + return + } + rgroups, err := decodeBool(w, r, "raft") + if err != nil { + return + } + + sleader, err := decodeBool(w, r, "stream-leader-only") + if err != nil { + return + } + + l, err := s.Jsz(&JSzOptions{ + Account: r.URL.Query().Get("acc"), + Accounts: accounts, + Streams: streams, + Consumer: consumers, + DirectConsumer: directConsumers, + Config: config, + LeaderOnly: leader, + Offset: offset, + Limit: limit, + RaftGroups: rgroups, + StreamLeaderOnly: sleader, + }) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + b, err := json.MarshalIndent(l, "", " ") + if err != nil { + s.Errorf("Error marshaling response to /jsz request: %v", err) + } + + // Handle response + ResponseHandler(w, r, b) +} + +type HealthStatus struct { + Status string `json:"status"` + StatusCode int `json:"status_code,omitempty"` + Error string `json:"error,omitempty"` + Errors []HealthzError `json:"errors,omitempty"` +} + +type HealthzError struct { + Type HealthZErrorType `json:"type"` + Account string `json:"account,omitempty"` + Stream string `json:"stream,omitempty"` + Consumer string `json:"consumer,omitempty"` + Error string `json:"error,omitempty"` +} + +type HealthZErrorType int + +const ( + HealthzErrorConn HealthZErrorType = iota + HealthzErrorBadRequest + HealthzErrorJetStream + HealthzErrorAccount + HealthzErrorStream + HealthzErrorConsumer +) + +func (t HealthZErrorType) String() string { + switch t { + case HealthzErrorConn: + return "CONNECTION" + case HealthzErrorBadRequest: + return "BAD_REQUEST" + case HealthzErrorJetStream: + return "JETSTREAM" + case HealthzErrorAccount: + return "ACCOUNT" + case HealthzErrorStream: + return "STREAM" + case HealthzErrorConsumer: + return "CONSUMER" + default: + return "unknown" + } +} + +func (t HealthZErrorType) MarshalJSON() ([]byte, error) { + return json.Marshal(t.String()) +} + +func (t *HealthZErrorType) UnmarshalJSON(data []byte) error { + switch string(data) { + case `"CONNECTION"`: + *t = HealthzErrorConn + case `"BAD_REQUEST"`: + *t = HealthzErrorBadRequest + case `"JETSTREAM"`: + *t = HealthzErrorJetStream + case `"ACCOUNT"`: + *t = HealthzErrorAccount + case `"STREAM"`: + *t = HealthzErrorStream + case `"CONSUMER"`: + *t = HealthzErrorConsumer + default: + return fmt.Errorf("unknown healthz error type %q", data) + } + return nil +} + +// https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check +func (s *Server) HandleHealthz(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + s.httpReqStats[HealthzPath]++ + s.mu.Unlock() + + jsEnabled, err := decodeBool(w, r, "js-enabled") + if err != nil { + return + } + if jsEnabled { + s.Warnf("Healthcheck: js-enabled deprecated, use js-enabled-only instead") + } + jsEnabledOnly, err := decodeBool(w, r, "js-enabled-only") + if err != nil { + return + } + jsServerOnly, err := decodeBool(w, r, "js-server-only") + if err != nil { + return + } + jsMetaOnly, err := decodeBool(w, r, "js-meta-only") + if err != nil { + return + } + + includeDetails, err := decodeBool(w, r, "details") + if err != nil { + return + } + + hs := s.healthz(&HealthzOptions{ + JSEnabled: jsEnabled, + JSEnabledOnly: jsEnabledOnly, + JSServerOnly: jsServerOnly, + JSMetaOnly: jsMetaOnly, + Account: r.URL.Query().Get("account"), + Stream: r.URL.Query().Get("stream"), + Consumer: r.URL.Query().Get("consumer"), + Details: includeDetails, + }) + + code := hs.StatusCode + if hs.Error != _EMPTY_ { + s.Warnf("Healthcheck failed: %q", hs.Error) + } else if len(hs.Errors) != 0 { + s.Warnf("Healthcheck failed: %d errors", len(hs.Errors)) + } + // Remove StatusCode from JSON representation when responding via HTTP + // since this is already in the response. + hs.StatusCode = 0 + b, err := json.Marshal(hs) + if err != nil { + s.Errorf("Error marshaling response to /healthz request: %v", err) + } + + handleResponse(code, w, r, b) +} + +// Generate health status. +func (s *Server) healthz(opts *HealthzOptions) *HealthStatus { + var health = &HealthStatus{Status: "ok"} + + // set option defaults + if opts == nil { + opts = &HealthzOptions{} + } + details := opts.Details + defer func() { + // for response with details enabled, set status to either "error" or "ok" + if details { + if len(health.Errors) != 0 { + health.Status = "error" + } else { + health.Status = "ok" + } + } + // if no specific status code was set, set it based on the presence of errors + if health.StatusCode == 0 { + if health.Error != _EMPTY_ || len(health.Errors) != 0 { + health.StatusCode = http.StatusServiceUnavailable + } else { + health.StatusCode = http.StatusOK + } + } + }() + + if opts.Account == _EMPTY_ && opts.Stream != _EMPTY_ { + health.StatusCode = http.StatusBadRequest + if !details { + health.Status = "error" + health.Error = fmt.Sprintf("%q must not be empty when checking stream health", "account") + } else { + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorBadRequest, + Error: fmt.Sprintf("%q must not be empty when checking stream health", "account"), + }) + } + return health + } + + if opts.Stream == _EMPTY_ && opts.Consumer != _EMPTY_ { + health.StatusCode = http.StatusBadRequest + if !details { + health.Status = "error" + health.Error = fmt.Sprintf("%q must not be empty when checking consumer health", "stream") + } else { + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorBadRequest, + Error: fmt.Sprintf("%q must not be empty when checking consumer health", "stream"), + }) + } + return health + } + + if err := s.readyForConnections(time.Millisecond); err != nil { + health.StatusCode = http.StatusInternalServerError + health.Status = "error" + if !details { + health.Error = err.Error() + } else { + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorConn, + Error: err.Error(), + }) + } + return health + } + + // If JSServerOnly is true, then do not check further accounts, streams and consumers. + if opts.JSServerOnly { + return health + } + + sopts := s.getOpts() + + // If JS is not enabled in the config, we stop. + if !sopts.JetStream { + return health + } + + // Access the Jetstream state to perform additional checks. + js := s.getJetStream() + const na = "unavailable" + if !js.isEnabled() { + health.StatusCode = http.StatusServiceUnavailable + health.Status = na + if !details { + health.Error = NewJSNotEnabledError().Error() + } else { + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorJetStream, + Error: NewJSNotEnabledError().Error(), + }) + } + return health + } + // Only check if JS is enabled, skip meta and asset check. + if opts.JSEnabledOnly || opts.JSEnabled { + return health + } + + // Clustered JetStream + js.mu.RLock() + cc := js.cluster + js.mu.RUnlock() + + // Currently single server we make sure the streams were recovered. + if cc == nil { + sdir := js.config.StoreDir + // Whip through account folders and pull each stream name. + fis, _ := os.ReadDir(sdir) + var accFound, streamFound, consumerFound bool + for _, fi := range fis { + if fi.Name() == snapStagingDir { + continue + } + if opts.Account != _EMPTY_ { + if fi.Name() != opts.Account { + continue + } + accFound = true + } + acc, err := s.LookupAccount(fi.Name()) + // Expired accounts are not a JetStream health problem; skip them when + // scanning all accounts. Still surface an error if this account was + // explicitly requested — including the err==nil + IsExpired() case. + expired := (err != nil && errors.Is(err, ErrAccountExpired)) || (err == nil && acc.IsExpired()) + if expired { + if opts.Account == _EMPTY_ { + continue + } + msg := fmt.Sprintf("JetStream account '%s' is expired", fi.Name()) + if !details { + health.Status = na + health.Error = msg + return health + } + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorAccount, + Account: fi.Name(), + Error: msg, + }) + // Return so later stream/consumer not-found checks do not + // replace this with a misleading 404 for assets we skipped. + return health + } + if err != nil { + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream account '%s' could not be resolved", fi.Name()) + return health + } + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorAccount, + Account: fi.Name(), + Error: fmt.Sprintf("JetStream account '%s' could not be resolved", fi.Name()), + }) + continue + } + sfis, _ := os.ReadDir(filepath.Join(sdir, fi.Name(), "streams")) + for _, sfi := range sfis { + if opts.Stream != _EMPTY_ { + if sfi.Name() != opts.Stream { + continue + } + streamFound = true + } + stream := sfi.Name() + s, err := acc.lookupStream(stream) + if err != nil { + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream stream '%s > %s' could not be recovered", acc, stream) + return health + } + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorStream, + Account: acc.Name, + Stream: stream, + Error: fmt.Sprintf("JetStream stream '%s > %s' could not be recovered", acc, stream), + }) + continue + } + if streamWerr := s.getWriteErr(); streamWerr != nil { + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream stream '%s > %s' write error: %v", acc, stream, streamWerr) + return health + } + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorStream, + Account: acc.Name, + Stream: stream, + Error: fmt.Sprintf("JetStream stream '%s > %s' write error: %v", acc, stream, streamWerr), + }) + continue + } + if streamFound { + // if consumer option is passed, verify that the consumer exists on stream + if opts.Consumer != _EMPTY_ { + if s.lookupConsumer(opts.Consumer) != nil { + consumerFound = true + } + } + break + } + } + if accFound { + break + } + } + if opts.Account != _EMPTY_ && !accFound { + health.StatusCode = http.StatusNotFound + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream account %q not found", opts.Account) + } else { + health.Errors = []HealthzError{ + { + Type: HealthzErrorAccount, + Account: opts.Account, + Error: fmt.Sprintf("JetStream account %q not found", opts.Account), + }, + } + } + return health + } + if opts.Stream != _EMPTY_ && !streamFound { + health.StatusCode = http.StatusNotFound + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream stream %q not found on account %q", opts.Stream, opts.Account) + } else { + health.Errors = []HealthzError{ + { + Type: HealthzErrorStream, + Account: opts.Account, + Stream: opts.Stream, + Error: fmt.Sprintf("JetStream stream %q not found on account %q", opts.Stream, opts.Account), + }, + } + } + return health + } + if opts.Consumer != _EMPTY_ && !consumerFound { + health.StatusCode = http.StatusNotFound + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream consumer %q not found for stream %q on account %q", opts.Consumer, opts.Stream, opts.Account) + } else { + health.Errors = []HealthzError{ + { + Type: HealthzErrorConsumer, + Account: opts.Account, + Stream: opts.Stream, + Consumer: opts.Consumer, + Error: fmt.Sprintf("JetStream consumer %q not found for stream %q on account %q", opts.Consumer, opts.Stream, opts.Account), + }, + } + } + } + return health + } + + // If we are here we want to check for any assets assigned to us. + var meta RaftNode + js.mu.RLock() + meta = cc.meta + js.mu.RUnlock() + + // Check meta layer health. + var metaNoLeader, metaClosed, metaUnhealthy bool + var metaWerr error + if meta != nil { + metaNoLeader = meta.GroupLeader() == _EMPTY_ + metaClosed = meta.State() == Closed + metaUnhealthy = !meta.Healthy() + metaWerr = meta.GetWriteErr() + } + // Surface an application-level meta write error (e.g. an undecodable meta entry) + // in addition to the node-level write error above. + metaApplyErr := js.getMetaWriteErr() + metaRecovering := js.isMetaRecovering() + if meta == nil || metaNoLeader || metaClosed || metaUnhealthy || metaWerr != nil || metaApplyErr != nil || metaRecovering { + var desc string + if metaWerr != nil { + desc = fmt.Sprintf("JetStream meta layer write error: %v", metaWerr) + } else if metaApplyErr != nil { + desc = fmt.Sprintf("JetStream meta layer apply error: %v", metaApplyErr) + } else if metaClosed { + desc = "JetStream meta layer is not running" + } else if meta != nil && metaRecovering { + desc = "JetStream is still recovering meta layer" + } else if meta == nil || metaNoLeader { + desc = "JetStream has not established contact with a meta leader" + } else { + desc = "JetStream is not current with the meta leader" + } + if !details { + health.Status = na + health.Error = desc + } else { + health.Errors = []HealthzError{ + { + Type: HealthzErrorJetStream, + Error: desc, + }, + } + } + return health + } + + // Skips doing full healthz and only checks the meta leader. + if opts.JSMetaOnly { + return health + } + + // Range across all accounts, the streams assigned to them, and the consumers. + // If they are assigned to this server check their status. + ourID := meta.ID() + + // Copy the meta layer so we do not need to hold the js read lock for an extended period of time. + var streams map[string]map[string]*streamAssignment + js.mu.RLock() + if opts.Account == _EMPTY_ { + // Collect all relevant streams and consumers. + streams = make(map[string]map[string]*streamAssignment, len(cc.streams)) + for acc, asa := range cc.streams { + nasa := make(map[string]*streamAssignment) + for stream, sa := range asa { + // If we are a member and we are not being restored, select for check. + if sa.Group.isMember(ourID) && sa.Restore == nil { + csa := sa.copyGroup() + csa.consumers = make(map[string]*consumerAssignment) + for consumer, ca := range sa.consumers { + if ca.Group.isMember(ourID) { + // Use original here. Not a copy. + csa.consumers[consumer] = ca + } + } + nasa[stream] = csa + } + } + streams[acc] = nasa + } + } else { + streams = make(map[string]map[string]*streamAssignment, 1) + asa, ok := cc.streams[opts.Account] + if !ok { + health.StatusCode = http.StatusNotFound + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream account %q not found", opts.Account) + } else { + health.Errors = []HealthzError{ + { + Type: HealthzErrorAccount, + Account: opts.Account, + Error: fmt.Sprintf("JetStream account %q not found", opts.Account), + }, + } + } + js.mu.RUnlock() + return health + } + nasa := make(map[string]*streamAssignment) + if opts.Stream != _EMPTY_ { + sa, ok := asa[opts.Stream] + if !ok || !sa.Group.isMember(ourID) { + health.StatusCode = http.StatusNotFound + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream stream %q not found on account %q", opts.Stream, opts.Account) + } else { + health.Errors = []HealthzError{ + { + Type: HealthzErrorStream, + Account: opts.Account, + Stream: opts.Stream, + Error: fmt.Sprintf("JetStream stream %q not found on account %q", opts.Stream, opts.Account), + }, + } + } + js.mu.RUnlock() + return health + } + csa := sa.copyGroup() + csa.consumers = make(map[string]*consumerAssignment) + var consumerFound bool + for consumer, ca := range sa.consumers { + if opts.Consumer != _EMPTY_ { + if consumer != opts.Consumer || !ca.Group.isMember(ourID) { + continue + } + consumerFound = true + } + // If we are a member and we are not being restored, select for check. + if sa.Group.isMember(ourID) && sa.Restore == nil { + csa.consumers[consumer] = ca + } + if consumerFound { + break + } + } + if opts.Consumer != _EMPTY_ && !consumerFound { + health.StatusCode = http.StatusNotFound + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream consumer %q not found for stream %q on account %q", opts.Consumer, opts.Stream, opts.Account) + } else { + health.Errors = []HealthzError{ + { + Type: HealthzErrorConsumer, + Account: opts.Account, + Stream: opts.Stream, + Consumer: opts.Consumer, + Error: fmt.Sprintf("JetStream consumer %q not found for stream %q on account %q", opts.Consumer, opts.Stream, opts.Account), + }, + } + } + js.mu.RUnlock() + return health + } + nasa[opts.Stream] = csa + } else { + for stream, sa := range asa { + // If we are a member and we are not being restored, select for check. + if sa.Group.isMember(ourID) && sa.Restore == nil { + csa := sa.copyGroup() + csa.consumers = make(map[string]*consumerAssignment) + for consumer, ca := range sa.consumers { + if ca.Group.isMember(ourID) { + csa.consumers[consumer] = ca + } + } + nasa[stream] = csa + } + } + } + streams[opts.Account] = nasa + } + js.mu.RUnlock() + + // Use our copy to traverse so we do not need to hold the js lock. + for accName, asa := range streams { + acc, err := s.LookupAccount(accName) + // Expired accounts are not a JetStream health problem; skip them when + // scanning all accounts. Still surface an error if this account was + // explicitly requested — including the err==nil + IsExpired() case. + expired := (err != nil && errors.Is(err, ErrAccountExpired)) || (err == nil && acc.IsExpired()) + if expired { + if opts.Account == _EMPTY_ { + continue + } + msg := fmt.Sprintf("JetStream account %q is expired", accName) + if !details { + health.Status = na + health.Error = msg + return health + } + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorAccount, + Account: accName, + Error: msg, + }) + // Return so later health checks do not obscure the expired account. + return health + } + if err != nil && len(asa) > 0 { + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream can not lookup account %q: %v", accName, err) + return health + } + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorAccount, + Account: accName, + Error: fmt.Sprintf("JetStream can not lookup account %q: %v", accName, err), + }) + continue + } + + for stream, sa := range asa { + if sa != nil && sa.unsupported != nil { + continue + } + // Make sure we can look up + if err := js.isStreamHealthy(acc, sa); err != nil { + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream stream '%s > %s' is not current: %s", accName, stream, err) + return health + } + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorStream, + Account: accName, + Stream: stream, + Error: fmt.Sprintf("JetStream stream '%s > %s' is not current: %s", accName, stream, err), + }) + continue + } + mset, _ := acc.lookupStream(stream) + // Now check consumers. + for consumer, ca := range sa.consumers { + if err := js.isConsumerHealthy(mset, sa, consumer, ca); err != nil { + if !details { + health.Status = na + health.Error = fmt.Sprintf("JetStream consumer '%s > %s > %s' is not current: %s", acc, stream, consumer, err) + return health + } + health.Errors = append(health.Errors, HealthzError{ + Type: HealthzErrorConsumer, + Account: accName, + Stream: stream, + Consumer: consumer, + Error: fmt.Sprintf("JetStream consumer '%s > %s > %s' is not current: %s", acc, stream, consumer, err), + }) + } + } + } + } + // Success. + return health +} + +// Healthz returns the health status of the server. +func (s *Server) Healthz(opts *HealthzOptions) *HealthStatus { + return s.healthz(opts) +} + +type ExpvarzStatus struct { + Memstats json.RawMessage `json:"memstats"` + Cmdline json.RawMessage `json:"cmdline"` +} + +func (s *Server) expvarz(_ *ExpvarzEventOptions) *ExpvarzStatus { + var stat ExpvarzStatus + + const memStatsKey = "memstats" + const cmdLineKey = "cmdline" + + expvar.Do(func(v expvar.KeyValue) { + switch v.Key { + case memStatsKey: + stat.Memstats = json.RawMessage(v.Value.String()) + + case cmdLineKey: + stat.Cmdline = json.RawMessage(v.Value.String()) + } + }) + + return &stat +} + +type ProfilezStatus struct { + Profile []byte `json:"profile"` + Error string `json:"error"` +} + +func (s *Server) profilez(opts *ProfilezOptions) *ProfilezStatus { + var buffer bytes.Buffer + switch opts.Name { + case _EMPTY_: + return &ProfilezStatus{ + Error: "Profile name not specified", + } + case "cpu": + if opts.Duration <= 0 || opts.Duration > 15*time.Second { + return &ProfilezStatus{ + Error: fmt.Sprintf("Duration %s should be between 0s and 15s", opts.Duration), + } + } + if err := pprof.StartCPUProfile(&buffer); err != nil { + return &ProfilezStatus{ + Error: fmt.Sprintf("Failed to start CPU profile: %s", err), + } + } + time.Sleep(opts.Duration) + pprof.StopCPUProfile() + default: + profile := pprof.Lookup(opts.Name) + if profile == nil { + return &ProfilezStatus{ + Error: fmt.Sprintf("Profile %q not found", opts.Name), + } + } + if err := profile.WriteTo(&buffer, opts.Debug); err != nil { + return &ProfilezStatus{ + Error: fmt.Sprintf("Profile %q error: %s", opts.Name, err), + } + } + } + return &ProfilezStatus{ + Profile: buffer.Bytes(), + } +} + +type RaftzGroup struct { + ID string `json:"id"` + State string `json:"state"` + Size int `json:"size"` + QuorumNeeded int `json:"quorum_needed"` + Observer bool `json:"observer,omitempty"` + Paused bool `json:"paused,omitempty"` + Overrun bool `json:"overrun,omitempty"` + OverrunCount uint64 `json:"overrun_count,omitempty"` + Committed uint64 `json:"committed"` + Applied uint64 `json:"applied"` + CatchingUp bool `json:"catching_up,omitempty"` + Leader string `json:"leader,omitempty"` + LeaderSince *time.Time `json:"leader_since,omitempty"` + EverHadLeader bool `json:"ever_had_leader"` + Term uint64 `json:"term"` + Vote string `json:"voted_for,omitempty"` + PTerm uint64 `json:"pterm"` + PIndex uint64 `json:"pindex"` + SystemAcc bool `json:"system_account"` + TrafficAcc string `json:"traffic_account"` + IPQPropLen int `json:"ipq_proposal_len"` + IPQEntryLen int `json:"ipq_entry_len"` + IPQRespLen int `json:"ipq_resp_len"` + IPQApplyLen int `json:"ipq_apply_len"` + WAL StreamState `json:"wal"` + WALError error `json:"wal_error,omitempty"` + Peers map[string]RaftzGroupPeer `json:"peers"` +} + +type RaftzGroupPeer struct { + Name string `json:"name"` + Known bool `json:"known"` + LastReplicatedIndex uint64 `json:"last_replicated_index,omitempty"` + LastSeen string `json:"last_seen,omitempty"` +} + +type RaftzStatus map[string]map[string]RaftzGroup + +func (s *Server) HandleRaftz(w http.ResponseWriter, r *http.Request) { + if s.raftNodes == nil { + w.WriteHeader(404) + w.Write([]byte("No Raft nodes registered")) + return + } + + groups := s.Raftz(&RaftzOptions{ + AccountFilter: r.URL.Query().Get("acc"), + GroupFilter: r.URL.Query().Get("group"), + }) + + if groups == nil { + w.WriteHeader(404) + w.Write([]byte("No Raft nodes returned, check supplied filters")) + return + } + + b, _ := json.MarshalIndent(groups, "", " ") + ResponseHandler(w, r, b) +} + +func (s *Server) Raftz(opts *RaftzOptions) *RaftzStatus { + afilter, gfilter := opts.AccountFilter, opts.GroupFilter + + if afilter == _EMPTY_ { + if sys := s.SystemAccount(); sys != nil { + afilter = sys.Name + } else { + return nil + } + } + + groups := map[string]RaftNode{} + infos := RaftzStatus{} // account -> group ID + + s.rnMu.RLock() + if gfilter != _EMPTY_ { + if rg, ok := s.raftNodes[gfilter]; ok && rg != nil { + if n, ok := rg.(*raft); ok { + if n.accName == afilter { + groups[gfilter] = rg + } + } + } + } else { + for name, rg := range s.raftNodes { + if rg == nil { + continue + } + if n, ok := rg.(*raft); ok { + if n.accName != afilter { + continue + } + groups[name] = rg + } + } + } + s.rnMu.RUnlock() + + for name, rg := range groups { + n, ok := rg.(*raft) + if n == nil || !ok { + continue + } + if _, ok := infos[n.accName]; !ok { + infos[n.accName] = map[string]RaftzGroup{} + } + // Only take the lock once, using the public RaftNode functions would + // cause us to take and release the locks over and over again. + n.RLock() + info := RaftzGroup{ + ID: n.id, + State: RaftState(n.state.Load()).String(), + Size: n.csz, + QuorumNeeded: n.qn, + Observer: n.observer, + Paused: n.paused, + Overrun: n.quorumPaused || n.isLeaderOverrun(), + OverrunCount: n.overrunCount, + Committed: n.commit, + Applied: n.applied, + CatchingUp: n.catchup != nil, + Leader: n.leader, + LeaderSince: n.leaderSince.Load(), + EverHadLeader: n.pleader.Load(), + Term: n.term, + Vote: n.vote, + PTerm: n.pterm, + PIndex: n.pindex, + SystemAcc: n.IsSystemAccount(), + TrafficAcc: n.t.Account().GetName(), + IPQPropLen: n.prop.len(), + IPQEntryLen: n.entry.len(), + IPQRespLen: n.resp.len(), + IPQApplyLen: n.apply.len(), + WALError: n.werr, + Peers: map[string]RaftzGroupPeer{}, + } + n.wal.FastState(&info.WAL) + for id, p := range n.peers { + if id == n.id { + continue + } + kp := n.membChange == nil || n.membChange.peer != id + peer := RaftzGroupPeer{ + Name: s.serverNameForNode(id), + Known: kp, + LastReplicatedIndex: p.li, + } + if !p.ts.IsZero() { + peer.LastSeen = time.Since(p.ts).String() + } + info.Peers[id] = peer + } + n.RUnlock() + infos[n.accName][name] = info + } + + return &infos +} diff --git a/server/mqtt.go b/server/mqtt.go new file mode 100644 index 0000000..0b1c72f --- /dev/null +++ b/server/mqtt.go @@ -0,0 +1,6477 @@ +// Copyright 2020-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 ( + "bytes" + "crypto/tls" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "slices" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + "unicode/utf8" + + "github.com/nats-io/jwt/v2" + "github.com/nats-io/nats-server/v2/server/gsl" + "github.com/nats-io/nats-server/v2/server/stree" + "github.com/nats-io/nuid" +) + +// References to "spec" here is from https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.pdf + +const ( + mqttPacketConnect = byte(0x10) + mqttPacketConnectAck = byte(0x20) + mqttPacketPub = byte(0x30) + mqttPacketPubAck = byte(0x40) + mqttPacketPubRec = byte(0x50) + mqttPacketPubRel = byte(0x60) + mqttPacketPubComp = byte(0x70) + mqttPacketSub = byte(0x80) + mqttPacketSubAck = byte(0x90) + mqttPacketUnsub = byte(0xa0) + mqttPacketUnsubAck = byte(0xb0) + mqttPacketPing = byte(0xc0) + mqttPacketPingResp = byte(0xd0) + mqttPacketDisconnect = byte(0xe0) + mqttPacketMask = byte(0xf0) + mqttPacketFlagMask = byte(0x0f) + + mqttProtoLevel = byte(0x4) + + // Connect flags + mqttConnFlagReserved = byte(0x1) + mqttConnFlagCleanSession = byte(0x2) + mqttConnFlagWillFlag = byte(0x04) + mqttConnFlagWillQoS = byte(0x18) + mqttConnFlagWillRetain = byte(0x20) + mqttConnFlagPasswordFlag = byte(0x40) + mqttConnFlagUsernameFlag = byte(0x80) + + // Publish flags + mqttPubFlagRetain = byte(0x01) + mqttPubFlagQoS = byte(0x06) + mqttPubFlagDup = byte(0x08) + mqttPubFlags = mqttPubFlagRetain | mqttPubFlagQoS | mqttPubFlagDup // 0x0f, the fixed-header flags nibble + mqttPubQos1 = byte(0x1 << 1) + mqttPubQoS2 = byte(0x2 << 1) + + // Subscribe flags + mqttSubscribeFlags = byte(0x2) + mqttSubAckFailure = byte(0x80) + + // Unsubscribe flags + mqttUnsubscribeFlags = byte(0x2) + + // ConnAck returned codes + mqttConnAckRCConnectionAccepted = byte(0x0) + mqttConnAckRCUnacceptableProtocolVersion = byte(0x1) + mqttConnAckRCIdentifierRejected = byte(0x2) + mqttConnAckRCServerUnavailable = byte(0x3) + mqttConnAckRCBadUserOrPassword = byte(0x4) + mqttConnAckRCNotAuthorized = byte(0x5) + mqttConnAckRCQoS2WillRejected = byte(0x10) + + // Maximum payload size of a control packet + mqttMaxPayloadSize = 0xFFFFFFF + + // Topic/Filter characters + mqttTopicLevelSep = '/' + mqttSingleLevelWC = '+' + mqttMultiLevelWC = '#' + mqttReservedPre = '$' + + // This is appended to the sid of a subscription that is + // created on the upper level subject because of the MQTT + // wildcard '#' semantic. + mqttMultiLevelSidSuffix = " fwc" + + // This is the prefix used for all subjects used by MQTT code. + mqttPrefix = "$MQTT." + + // This is the prefix for NATS subscriptions subjects associated as delivery + // subject of JS consumer. We want to make them unique so will prevent users + // MQTT subscriptions to start with this. + mqttSubPrefix = mqttPrefix + "sub." + + // Stream name for MQTT messages on a given account + mqttStreamName = "$MQTT_msgs" + mqttStreamSubjectPrefix = mqttPrefix + "msgs." + + // Stream name for MQTT retained messages on a given account + mqttRetainedMsgsStreamName = "$MQTT_rmsgs" + mqttRetainedMsgsStreamSubject = mqttPrefix + "rmsgs." + + // Stream name for MQTT sessions on a given account + mqttSessStreamName = "$MQTT_sess" + mqttSessStreamSubjectPrefix = mqttPrefix + "sess." + + // Stream name prefix for MQTT sessions on a given account + mqttSessionsStreamNamePrefix = "$MQTT_sess_" + + // Stream name and subject for incoming MQTT QoS2 messages + mqttQoS2IncomingMsgsStreamName = "$MQTT_qos2in" + mqttQoS2IncomingMsgsStreamSubjectPrefix = mqttPrefix + "qos2.in." + + // Stream name and subjects for outgoing MQTT QoS (PUBREL) messages + mqttOutStreamName = "$MQTT_out" + mqttOutSubjectPrefix = mqttPrefix + "out." + mqttPubRelSubjectPrefix = mqttPrefix + "out.pubrel." + mqttPubRelDeliverySubjectPrefix = mqttPrefix + "deliver.pubrel." + mqttPubRelConsumerDurablePrefix = "$MQTT_PUBREL_" + + // As per spec, MQTT server may not redeliver QoS 1 and 2 messages to + // clients, except after client reconnects. However, NATS Server will + // redeliver unacknowledged messages after this default interval. This can + // be changed with the server.Options.MQTT.AckWait option. + mqttDefaultAckWait = 30 * time.Second + + // This is the default for the outstanding number of pending QoS 1 + // messages sent to a session with QoS 1 subscriptions. + mqttDefaultMaxAckPending = 1024 + + // A session's list of subscriptions cannot have a cumulative MaxAckPending + // of more than this limit. + mqttMaxAckTotalLimit = 0xFFFF + + // Prefix of the reply subject for JS API requests. + mqttJSARepliesPrefix = mqttPrefix + "JSA." + + // Those are tokens that are used for the reply subject of JS API requests. + // For instance "$MQTT.JSA..SC." is the reply subject + // for a request to create a stream (where is the server name hash), + // while "$MQTT.JSA..SL." is for a stream lookup, etc... + mqttJSAIdTokenPos = 3 + mqttJSATokenPos = 4 + mqttJSAClientIDPos = 5 + mqttJSAStreamCreate = "SC" + mqttJSAStreamUpdate = "SU" + mqttJSAStreamLookup = "SL" + mqttJSAStreamDel = "SD" + mqttJSAConsumerCreate = "CC" + mqttJSAConsumerLookup = "CL" + mqttJSAConsumerDel = "CD" + mqttJSAMsgStore = "MS" + mqttJSAMsgLoad = "ML" + mqttJSAMsgDelete = "MD" + mqttJSASessPersist = "SP" + mqttJSARetainedMsgDel = "RD" + mqttJSAStreamNames = "SN" + + // This is how long to keep a client in the flappers map before closing the + // connection. This prevent quick reconnect from those clients that keep + // wanting to connect with a client ID already in use. + mqttSessFlappingJailDur = time.Second + + // This is how frequently the timer to cleanup the sessions flappers map is firing. + mqttSessFlappingCleanupInterval = 5 * time.Second + + // Default retry delay if transfer of old session streams to new one fails + mqttDefaultTransferRetry = 5 * time.Second + + // For Websocket URLs + mqttWSPath = "/mqtt" + + mqttInitialPubHeader = 16 // An overkill, should need 7 bytes max + mqttProcessSubTooLong = 100 * time.Millisecond + mqttDefaultRetainedCacheTTL = 2 * time.Minute + mqttRetainedTransferTimeout = 10 * time.Second + mqttDefaultJSAPITimeout = 5 * time.Second + mqttRetainedFlagDelMarker = '-' +) + +const ( + sparkbNBIRTH = "NBIRTH" + sparkbDBIRTH = "DBIRTH" + sparkbNDEATH = "NDEATH" + sparkbDDEATH = "DDEATH" +) + +var ( + sparkbNamespaceTopicPrefix = []byte("spBv1.0/") + sparkbCertificatesTopicPrefix = []byte("$sparkplug/certificates/") +) + +var ( + mqttPingResponse = []byte{mqttPacketPingResp, 0x0} + mqttProtoName = []byte("MQTT") + mqttOldProtoName = []byte("MQIsdp") + mqttSessJailDur = mqttSessFlappingJailDur + mqttFlapCleanItvl = mqttSessFlappingCleanupInterval + mqttRetainedCacheTTL = mqttDefaultRetainedCacheTTL +) + +var ( + errMQTTNotWebsocketPort = errors.New("MQTT clients over websocket must connect to the Websocket port, not the MQTT port") + errMQTTTopicFilterCannotBeEmpty = errors.New("topic filter cannot be empty") + errMQTTMalformedVarInt = errors.New("malformed variable int") + errMQTTSecondConnectPacket = errors.New("received a second CONNECT packet") + errMQTTServerNameMustBeSet = errors.New("mqtt requires server name to be explicitly set") + errMQTTUserMixWithUsersNKeys = errors.New("mqtt authentication username not compatible with presence of users/nkeys") + errMQTTTokenMixWIthUsersNKeys = errors.New("mqtt authentication token not compatible with presence of users/nkeys") + errMQTTAckWaitMustBePositive = errors.New("ack wait must be a positive value") + errMQTTJSAPITimeoutMustBePositive = errors.New("JS API timeout must be a positive value") + errMQTTStandaloneNeedsJetStream = errors.New("mqtt requires JetStream to be enabled if running in standalone mode") + errMQTTConnFlagReserved = errors.New("connect flags reserved bit not set to 0") + errMQTTWillAndRetainFlag = errors.New("if Will flag is set to 0, Will Retain flag must be 0 too") + errMQTTPasswordFlagAndNoUser = errors.New("password flag set but username flag is not") + errMQTTCIDEmptyNeedsCleanFlag = errors.New("when client ID is empty, clean session flag must be set to 1") + errMQTTEmptyWillTopic = errors.New("empty Will topic not allowed") + errMQTTEmptyUsername = errors.New("empty user name not allowed") + errMQTTTopicIsEmpty = errors.New("topic cannot be empty") + errMQTTPacketIdentifierIsZero = errors.New("packet identifier cannot be 0") + errMQTTUnsupportedCharacters = errors.New("character not supported for MQTT topics") + errMQTTInvalidSession = errors.New("invalid MQTT session") + errMQTTInvalidRetainFlags = errors.New("invalid retained message flags") + errMQTTInvalidRetainedMessage = errors.New("invalid retained message") + errMQTTSessionCollision = errors.New("stored session does not match client ID") + errMQTTInvalidPublishLength = errors.New("invalid publish message, variable header exceeds remaining length") + errMQTTAckPipelineStopped = errors.New("QoS1 PUBACK pipeline has shut down while admitting a message, " + + "abandoning the wait for its JetStream ack; failing the connection, " + + "the client will re-send unacknowledged PUBLISH packets on reconnect") +) + +type srvMQTT struct { + listener net.Listener + listenerErr error + authOverride bool + sessmgr mqttSessionManager +} + +type mqttSessionManager struct { + mu sync.RWMutex + sessions map[string]*mqttAccountSessionManager // key is account name +} + +type mqttAccountSessionManager struct { + mu sync.RWMutex + sessions map[string]*mqttSession // key is MQTT client ID + sessByHash map[string]*mqttSession // key is MQTT client ID hash + sessLocked map[string]struct{} // key is MQTT client ID and indicate that a session can not be taken by a new client at this time + flappers map[string]time.Time // When connection connects with client ID already in use + flapTimer *time.Timer // Timer to perform some cleanup of the flappers map + retmsgs *stree.SubjectTree[mqttRetainedMsgRef] // retained message metadata + rmsCache *sync.Map // map[subject]mqttRetainedMsg + jsa mqttJSA + domainTk string // Domain (with trailing "."), or possibly empty. This is added to session subject. +} + +type mqttJSAResponse struct { + reply string // will be used to map to the original request in jsa.NewRequestExMulti + value any +} + +type mqttJSA struct { + mu sync.Mutex + id string + c *client + sendq *ipQueue[*mqttJSPubMsg] + rplyr string + replies sync.Map // [string]chan *mqttJSAResponse + nuid *nuid.NUID + quitCh chan struct{} + domain string // Domain or possibly empty. This is added to session subject. + domainSet bool // covers if domain was set, even to empty + timeout time.Duration +} + +type mqttJSPubMsg struct { + subj string + reply string + hdr int + msg []byte +} + +type mqttRetMsgDel struct { + Subject string `json:"subject"` + Seq uint64 `json:"seq"` +} + +type mqttSession struct { + // subsMu is a "quick" version of the session lock, sufficient for the QoS0 + // callback. It only guarantees that a new subscription is initialized, and + // its retained messages if any have been queued up for delivery. The QoS12 + // callback uses the session lock. + mu sync.Mutex + subsMu sync.RWMutex + + id string // client ID + idHash string // client ID hash + c *client + jsa *mqttJSA + subs map[string]byte // Key is MQTT SUBSCRIBE filter, value is the subscription QoS + cons map[string]*ConsumerConfig + pubRelConsumer *ConsumerConfig + pubRelSubscribed bool + pubRelDeliverySubject string + pubRelDeliverySubjectB []byte + pubRelSubject string + seq uint64 + + // pendingPublish maps packet identifiers (PI) to JetStream ACK subjects for + // QoS1 and 2 PUBLISH messages pending delivery to the session's client. + pendingPublish map[uint16]*mqttPending + + // pendingPubRel maps PIs to JetStream ACK subjects for QoS2 PUBREL + // messages pending delivery to the session's client. + pendingPubRel map[uint16]*mqttPending + + // cpending maps delivery attempts (that come with a JS ACK subject) to + // existing PIs. + cpending map[string]map[uint64]uint16 // composite key: jsDur, sseq + + // "Last used" publish packet identifier (PI). starting point searching for the next available. + last_pi uint16 + + // Maximum number of pending acks for this session. + maxp uint16 + tmaxack int + clean bool + domainTk string +} + +type mqttPersistedSession struct { + Origin string `json:"origin,omitempty"` + ID string `json:"id,omitempty"` + Clean bool `json:"clean,omitempty"` + Subs map[string]byte `json:"subs,omitempty"` + Cons map[string]*ConsumerConfig `json:"cons,omitempty"` + PubRel *ConsumerConfig `json:"pubrel,omitempty"` +} + +type mqttRetainedMsg struct { + Origin string `json:"origin,omitempty"` + Subject string `json:"subject,omitempty"` + Topic string `json:"topic,omitempty"` + Msg []byte `json:"msg,omitempty"` + Flags byte `json:"flags,omitempty"` + Source string `json:"source,omitempty"` + + expiresFromCache time.Time +} + +type mqttRetainedMsgRef struct { + sseq uint64 +} + +// mqttSub contains fields associated with a MQTT subscription, and is added to +// the main subscription struct for MQTT message delivery subscriptions. The +// delivery callbacks may get invoked before sub.mqtt is set up, so they should +// acquire either sess.mu or sess.subsMu before accessing it. +type mqttSub struct { + // The sub's QOS and the JS durable name. They can change when + // re-subscribing, and are used in the delivery callbacks. They can be + // quickly accessed using sess.subsMu.RLock, or under the main session lock. + qos byte + jsDur string + + // closed marks the subscription as torn down (QoS downgrade to 0, or + // unsubscribe) so QoS 1/2 delivery callbacks stop tracking new messages for + // it. Guarded like qos/jsDur (sess.mu or sess.subsMu). Unlike clearing + // sub.mqtt, this keeps the struct valid for an in-flight enqueue. + closed bool + + // Pending serialization of retained messages to be sent when subscription + // is registered. The sub's delivery callbacks must wait until `prm` is + // ready (can block on sess.mu for that, too). + prm [][]byte + + // If this subscription needs to be checked for being reserved. E.g. '#' or + // '*' or '*/'. It is set up at the time of subscription and is immutable + // after that. + reserved bool +} + +type mqtt struct { + r *mqttReader + cp *mqttConnectProto + pp *mqttPublish + asm *mqttAccountSessionManager // quick reference to account session manager, immutable after processConnect() + sess *mqttSession // quick reference to session, immutable after processConnect() + cid string // client ID + + // Pipelines the PUBACKs of inbound QoS1 PUBLISH messages. Owned by the + // readLoop goroutine, no lock. + acks *mqttAckPipeline + + // rejectQoS2Pub tells the MQTT client to not accept QoS2 PUBLISH, instead + // error and terminate the connection. + rejectQoS2Pub bool + + // downgradeQOS2Sub tells the MQTT client to downgrade QoS2 SUBSCRIBE + // requests to QoS1. + downgradeQoS2Sub bool +} + +type mqttPending struct { + sseq uint64 // stream sequence + jsAckSubject string // the ACK subject to send the ack to + jsDur string // JS durable name +} + +type mqttConnectProto struct { + rd time.Duration + will *mqttWill + flags byte +} + +type mqttIOReader interface { + io.Reader + SetReadDeadline(time.Time) error +} + +type mqttReader struct { + reader mqttIOReader + buf []byte + pos int + pstart int + pbuf []byte +} + +type mqttWriter struct { + bytes.Buffer +} + +type mqttWill struct { + topic []byte + subject []byte + mapped []byte + message []byte + qos byte + retain bool +} + +type mqttFilter struct { + filter string + qos byte + // Used only for tracing and should not be used after parsing of (un)sub protocols. + ttopic []byte +} + +type mqttPublish struct { + topic []byte + subject []byte + mapped []byte + msg []byte + sz int + pi uint16 + flags byte +} + +// When we re-encode incoming MQTT PUBLISH messages for NATS delivery, we add +// the following headers: +// - "Nmqtt-Pub" (*always) indicates that the message originated from MQTT, and +// contains the original message QoS. +// - "Nmqtt-Subject" contains the original MQTT subject from mqttParsePub. +// - "Nmqtt-Mapped" contains the mapping during mqttParsePub. +// +// When we submit a PUBREL for delivery, we add a "Nmqtt-PubRel" header that +// contains the PI. +const ( + // NATS header that indicates that the message originated from MQTT and + // stores the published message QOS. + mqttNatsHeader = "Nmqtt-Pub" + // A staged QoS2 message's mqttNatsHeader value carries a second byte after + // the QoS: the MQTT PUBLISH flags nibble (mqttPubFlags) as one hex char, + // e.g. "25" for a retained QoS2 message (0x5 = retain|QoS2). The value is a + // persisted, cross-version contract: byte 0 stays the bare QoS forever + // (older servers read only it), a missing flags byte reads as no flags, and + // extensions may only append bytes (a second hex char = a full private + // byte), never change the meaning of existing ones. + + // NATS headers to store retained message metadata (along with the original + // message as binary). + mqttNatsRetainedMessageTopic = "Nmqtt-RTopic" + mqttNatsRetainedMessageOrigin = "Nmqtt-ROrigin" + mqttNatsRetainedMessageFlags = "Nmqtt-RFlags" + mqttNatsRetainedMessageSource = "Nmqtt-RSource" + + // NATS header that indicates that the message is an MQTT PubRel and stores + // the PI. + mqttNatsPubRelHeader = "Nmqtt-PubRel" + + // NATS headers to store the original MQTT subject and the subject mapping. + mqttNatsHeaderSubject = "Nmqtt-Subject" + mqttNatsHeaderMapped = "Nmqtt-Mapped" +) + +type mqttParsedPublishNATSHeader struct { + qos byte + retained bool + subject []byte + mapped []byte +} + +func (s *Server) startMQTT() { + if s.isShuttingDown() { + return + } + + sopts := s.getOpts() + o := &sopts.MQTT + + var hl net.Listener + var err error + + port := o.Port + if port == -1 { + port = 0 + } + hp := net.JoinHostPort(o.Host, strconv.Itoa(port)) + s.mu.Lock() + s.mqtt.sessmgr.sessions = make(map[string]*mqttAccountSessionManager) + hl, err = natsListen("tcp", hp) + s.mqtt.listenerErr = err + if err != nil { + s.mu.Unlock() + s.Fatalf("Unable to listen for MQTT connections: %v", err) + return + } + if port == 0 { + o.Port = hl.Addr().(*net.TCPAddr).Port + } + s.mqtt.listener = hl + scheme := "mqtt" + if o.TLSConfig != nil { + scheme = "tls" + } + s.Noticef("Listening for MQTT clients on %s://%s:%d", scheme, o.Host, o.Port) + go s.acceptConnections(hl, "MQTT", func(conn net.Conn) { s.createMQTTClient(conn, nil) }, nil) + s.mu.Unlock() +} + +// This is similar to createClient() but has some modifications specifi to MQTT clients. +// The comments have been kept to minimum to reduce code size. Check createClient() for +// more details. +func (s *Server) createMQTTClient(conn net.Conn, ws *websocket) *client { + opts := s.getOpts() + + maxPay := int32(opts.MaxPayload) + maxSubs := int32(opts.MaxSubs) + if maxSubs == 0 { + maxSubs = -1 + } + now := time.Now() + + mqtt := &mqtt{ + rejectQoS2Pub: opts.MQTT.rejectQoS2Pub, + downgradeQoS2Sub: opts.MQTT.downgradeQoS2Sub, + } + c := &client{srv: s, nc: conn, mpay: maxPay, msubs: maxSubs, start: now, last: now, mqtt: mqtt, ws: ws} + c.headers = true + c.mqtt.pp = &mqttPublish{} + // MQTT clients don't send NATS CONNECT protocols. So make it an "echo" + // client, but disable verbose and pedantic (by not setting them). + c.opts.Echo = true + + c.registerWithAccount(s.globalAccount()) + + s.mu.Lock() + // Check auth, override if applicable. + authRequired := s.info.AuthRequired || s.mqtt.authOverride + s.totalClients++ + s.mu.Unlock() + + c.mu.Lock() + if authRequired { + c.flags.set(expectConnect) + } + c.initClient() + c.Debugf("Client connection created") + c.mu.Unlock() + + s.mu.Lock() + if !s.isRunning() || s.ldm { + if s.isShuttingDown() { + conn.Close() + } + s.mu.Unlock() + return c + } + + if opts.MaxConn < 0 || (opts.MaxConn > 0 && len(s.clients) >= opts.MaxConn) { + s.mu.Unlock() + c.maxConnExceeded() + return nil + } + s.clients[c.cid] = c + + // Websocket TLS handshake is already done when getting to this function. + tlsRequired := opts.MQTT.TLSConfig != nil && ws == nil + s.mu.Unlock() + + c.mu.Lock() + + // In case connection has already been closed + if c.isClosed() { + c.mu.Unlock() + c.closeConnection(WriteError) + return nil + } + + var pre []byte + if tlsRequired && opts.AllowNonTLS { + pre = make([]byte, 4) + c.nc.SetReadDeadline(time.Now().Add(secondsToDuration(opts.MQTT.TLSTimeout))) + n, _ := io.ReadFull(c.nc, pre[:]) + c.nc.SetReadDeadline(time.Time{}) + pre = pre[:n] + if n > 0 && pre[0] == 0x16 { + tlsRequired = true + } else { + tlsRequired = false + } + } + + if tlsRequired { + if len(pre) > 0 { + c.nc = &tlsMixConn{c.nc, bytes.NewBuffer(pre)} + pre = nil + } + + // Perform server-side TLS handshake. + if err := c.doTLSServerHandshake(tlsHandshakeMQTT, opts.MQTT.TLSConfig, opts.MQTT.TLSTimeout, opts.MQTT.TLSPinnedCerts); err != nil { + c.mu.Unlock() + return nil + } + } + + if authRequired { + timeout := opts.AuthTimeout + // Possibly override with MQTT specific value. + if opts.MQTT.AuthTimeout != 0 { + timeout = opts.MQTT.AuthTimeout + } + c.setAuthTimer(secondsToDuration(timeout)) + } + + // No Ping timer for MQTT clients... + + s.startGoRoutine(func() { c.readLoop(pre) }) + s.startGoRoutine(func() { c.writeLoop() }) + + if tlsRequired { + c.Debugf("TLS handshake complete") + cs := c.nc.(*tls.Conn).ConnectionState() + c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite)) + } + + c.mu.Unlock() + + return c +} + +// Given the mqtt options, we check if any auth configuration +// has been provided. If so, possibly create users/nkey users and +// store them in s.mqtt.users/nkeys. +// Also update a boolean that indicates if auth is required for +// mqtt clients. +// Server lock is held on entry. +func (s *Server) mqttConfigAuth(opts *MQTTOpts) { + mqtt := &s.mqtt + // If any of those is specified, we consider that there is an override. + mqtt.authOverride = opts.Username != _EMPTY_ || opts.Token != _EMPTY_ || opts.NoAuthUser != _EMPTY_ +} + +// Validate the mqtt related options. +func validateMQTTOptions(o *Options) error { + mo := &o.MQTT + // If no port is defined, we don't care about other options + if mo.Port == 0 { + return nil + } + // We have to force the server name to be explicitly set and be unique when + // in cluster mode. + if o.ServerName == _EMPTY_ && (o.Cluster.Port != 0 || o.Gateway.Port != 0) { + return errMQTTServerNameMustBeSet + } + // If there is a NoAuthUser, we need to have Users defined and + // the user to be present. + if mo.NoAuthUser != _EMPTY_ { + if err := validateNoAuthUser(o, mo.NoAuthUser); err != nil { + return err + } + } + // Token/Username not possible if there are users/nkeys + if len(o.Users) > 0 || len(o.Nkeys) > 0 { + if mo.Username != _EMPTY_ { + return errMQTTUserMixWithUsersNKeys + } + if mo.Token != _EMPTY_ { + return errMQTTTokenMixWIthUsersNKeys + } + } + if mo.AckWait < 0 { + return errMQTTAckWaitMustBePositive + } + if mo.JSAPITimeout < 0 { + return errMQTTJSAPITimeoutMustBePositive + } + // If strictly standalone and there is no JS enabled, then it won't work... + // For leafnodes, we could either have remote(s) and it would be ok, or no + // remote but accept from a remote side that has "hub" property set, which + // then would ok too. So we fail only if we have no leafnode config at all. + if !o.JetStream && o.Cluster.Port == 0 && o.Gateway.Port == 0 && + o.LeafNode.Port == 0 && len(o.LeafNode.Remotes) == 0 { + return errMQTTStandaloneNeedsJetStream + } + if err := validatePinnedCerts(mo.TLSPinnedCerts); err != nil { + return fmt.Errorf("mqtt: %v", err) + } + if mo.ConsumerReplicas > 0 && mo.StreamReplicas > 0 && mo.ConsumerReplicas > mo.StreamReplicas { + return fmt.Errorf("mqtt: consumer_replicas (%v) cannot be higher than stream_replicas (%v)", + mo.ConsumerReplicas, mo.StreamReplicas) + } + return nil +} + +// Returns true if this connection is from a MQTT client. +// Lock held on entry. +func (c *client) isMqtt() bool { + return c.mqtt != nil +} + +// If this is an MQTT client, returns the session client ID, +// otherwise returns the empty string. +// Lock held on entry +func (c *client) getMQTTClientID() string { + if !c.isMqtt() { + return _EMPTY_ + } + return c.mqtt.cid +} + +// Parse protocols inside the given buffer. +// This is invoked from the readLoop. +func (c *client) mqttParse(buf []byte) error { + c.mu.Lock() + s := c.srv + trace := c.trace + connected := c.flags.isSet(connectReceived) + mqtt := c.mqtt + r := mqtt.r + var rd time.Duration + if mqtt.cp != nil { + rd = mqtt.cp.rd + if rd > 0 { + r.reader.SetReadDeadline(time.Time{}) + } + } + hasMappings := c.in.flags.isSet(hasMappings) + c.mu.Unlock() + + r.reset(buf) + + var err error + var b byte + var pl int + var complete bool + + for err == nil && r.hasMore() { + + // Keep track of the starting of the packet, in case we have a partial + r.pstart = r.pos + + // Read packet type and flags + if b, err = r.readByte("packet type"); err != nil { + break + } + + // Packet type + pt := b & mqttPacketMask + + // If client was not connected yet, the first packet must be + // a mqttPacketConnect otherwise we fail the connection. + if !connected && pt != mqttPacketConnect { + // If the buffer indicates that it may be a websocket handshake + // but the client is not websocket, it means that the client + // connected to the MQTT port instead of the Websocket port. + if bytes.HasPrefix(buf, []byte("GET ")) && !c.isWebsocket() { + err = errMQTTNotWebsocketPort + } else { + err = fmt.Errorf("the first packet should be a CONNECT (%v), got %v", mqttPacketConnect, pt) + } + break + } + if err = mqttCheckFixedHeaderFlags(pt, b&mqttPacketFlagMask); err != nil { + break + } + + maxLen := int32(jwt.NoLimit) + if !connected { + maxLen = atomic.LoadInt32(&c.mpay) + } + pl, complete, err = r.readPacketLen(maxLen) + if err != nil || !complete { + if err == ErrMaxPayload { + c.maxPayloadViolation(pl, maxLen) + } + break + } + if err = mqttCheckRemainingLength(pt, pl); err != nil { + break + } + + switch pt { + // Packets that we receive back when we act as the "sender": PUBACK, + // PUBREC, PUBCOMP. + case mqttPacketPubAck: + var pi uint16 + pi, err = mqttParsePIPacket(r) + if trace { + c.traceInOp("PUBACK", errOrTrace(err, fmt.Sprintf("pi=%v", pi))) + } + if err == nil { + err = c.mqttProcessPubAck(pi) + } + + case mqttPacketPubRec: + var pi uint16 + pi, err = mqttParsePIPacket(r) + if trace { + c.traceInOp("PUBREC", errOrTrace(err, fmt.Sprintf("pi=%v", pi))) + } + if err == nil { + err = c.mqttProcessPubRec(pi) + } + + case mqttPacketPubComp: + var pi uint16 + pi, err = mqttParsePIPacket(r) + if trace { + c.traceInOp("PUBCOMP", errOrTrace(err, fmt.Sprintf("pi=%v", pi))) + } + if err == nil { + c.mqttProcessPubComp(pi) + } + + // Packets where we act as the "receiver": PUBLISH, PUBREL, SUBSCRIBE, UNSUBSCRIBE. + case mqttPacketPub: + pp := c.mqtt.pp + pp.flags = b & mqttPacketFlagMask + err = c.mqttParsePub(r, pl, pp, hasMappings) + if trace { + c.traceInOp("PUBLISH", errOrTrace(err, mqttPubTrace(pp))) + if err == nil { + c.mqttTraceMsg(pp.msg) + } + } + if err == nil { + err = s.mqttProcessPub(c, pp, trace) + } + + case mqttPacketPubRel: + var pi uint16 + pi, err = mqttParsePIPacket(r) + if trace { + c.traceInOp("PUBREL", errOrTrace(err, fmt.Sprintf("pi=%v", pi))) + } + if err == nil { + err = s.mqttProcessPubRel(c, pi, trace) + } + + case mqttPacketSub: + var pi uint16 // packet identifier + var filters []*mqttFilter + var subs []*subscription + pi, filters, err = c.mqttParseSubs(r, b, pl) + if trace { + c.traceInOp("SUBSCRIBE", errOrTrace(err, mqttSubscribeTrace(pi, filters))) + } + if err == nil { + subs, err = c.mqttProcessSubs(filters) + if err == nil && trace { + c.traceOutOp("SUBACK", []byte(fmt.Sprintf("pi=%v", pi))) + } + } + if err == nil { + c.mqttEnqueueSubAck(pi, filters) + c.mqttSendRetainedMsgsToNewSubs(subs) + } + + case mqttPacketUnsub: + var pi uint16 // packet identifier + var filters []*mqttFilter + pi, filters, err = c.mqttParseUnsubs(r, b, pl) + if trace { + c.traceInOp("UNSUBSCRIBE", errOrTrace(err, mqttUnsubscribeTrace(pi, filters))) + } + if err == nil { + err = c.mqttProcessUnsubs(filters) + if err == nil && trace { + c.traceOutOp("UNSUBACK", []byte(fmt.Sprintf("pi=%v", pi))) + } + } + if err == nil { + c.mqttEnqueueUnsubAck(pi) + } + + // Packets that we get both as a receiver and sender: PING, CONNECT, DISCONNECT + case mqttPacketPing: + if trace { + c.traceInOp("PINGREQ", nil) + } + c.mqttEnqueuePingResp() + if trace { + c.traceOutOp("PINGRESP", nil) + } + + case mqttPacketConnect: + // It is an error to receive a second connect packet + if connected { + err = errMQTTSecondConnectPacket + break + } + var rc byte + var cp *mqttConnectProto + var sessp bool + rc, cp, err = c.mqttParseConnect(r, hasMappings) + // Add the client id to the client's string, regardless of error. + // We may still get the client_id if the call above fails somewhere + // after parsing the client ID itself. + c.ncs.Store(fmt.Sprintf("%s - %q", c, c.mqtt.cid)) + if trace && cp != nil { + c.traceInOp("CONNECT", errOrTrace(err, c.mqttConnectTrace(cp))) + } + if rc != 0 { + c.mqttEnqueueConnAck(rc, sessp) + if trace { + c.traceOutOp("CONNACK", []byte(fmt.Sprintf("sp=%v rc=%v", sessp, rc))) + } + } else if err == nil { + if err = s.mqttProcessConnect(c, cp, trace); err != nil { + err = fmt.Errorf("unable to connect: %v", err) + } else { + // Add this debug statement so users running in Debug mode + // will have the client id printed here for the first time. + c.Debugf("Client connected") + connected = true + rd = cp.rd + } + } + + case mqttPacketDisconnect: + if trace { + c.traceInOp("DISCONNECT", nil) + } + // Normal disconnect, we need to discard the will. + // Spec [MQTT-3.1.2-8] + c.mu.Lock() + if c.mqtt.cp != nil { + c.mqtt.cp.will = nil + } + c.mu.Unlock() + s.mqttHandleClosedClient(c) + c.closeConnection(ClientClosed) + return nil + + default: + err = fmt.Errorf("received unknown packet type %d", pt>>4) + } + } + if err == nil && rd > 0 { + r.reader.SetReadDeadline(time.Now().Add(rd)) + } + return err +} + +func mqttCheckFixedHeaderFlags(packetType, flags byte) error { + var expected byte + switch packetType { + case mqttPacketConnect, mqttPacketPubAck, mqttPacketPubRec, mqttPacketPubComp, + mqttPacketPing, mqttPacketDisconnect: + expected = 0 + case mqttPacketPubRel, mqttPacketSub, mqttPacketUnsub: + expected = 0x2 + case mqttPacketPub: + return nil + default: + return nil + } + if flags != expected { + return fmt.Errorf("invalid fixed header flags %x for packet type %x", flags, packetType) + } + return nil +} + +func mqttCheckRemainingLength(packetType byte, pl int) error { + var expected int + switch packetType { + case mqttPacketConnect, mqttPacketPub, mqttPacketSub, mqttPacketUnsub: + return nil + case mqttPacketPubAck, mqttPacketPubRec, mqttPacketPubRel, mqttPacketPubComp: + expected = 2 + case mqttPacketPing, mqttPacketDisconnect: + expected = 0 + default: + return nil + } + if pl != expected { + return fmt.Errorf("invalid remaining length %d for packet type %x", pl, packetType) + } + return nil +} + +func (c *client) mqttTraceMsg(msg []byte) { + maxTrace := c.srv.getOpts().MaxTracedMsgLen + if maxTrace > 0 && len(msg) > maxTrace { + c.Tracef("<<- MSG_PAYLOAD: [\"%s...\"]", msg[:maxTrace]) + } else { + c.Tracef("<<- MSG_PAYLOAD: [%q]", msg) + } +} + +// The MQTT client connection has been closed, or the DISCONNECT packet was received. +// For a "clean" session, we will delete the session, otherwise, simply removing +// the binding. We will also send the "will" message if applicable. +// +// Runs from the client's readLoop. +// No lock held on entry. +func (s *Server) mqttHandleClosedClient(c *client) { + c.mu.Lock() + asm := c.mqtt.asm + sess := c.mqtt.sess + c.mu.Unlock() + + // Dropping pending entries is safe: the client re-sends unacknowledged + // PUBLISH packets on reconnect, Spec [MQTT-4.4.0-1]. + if pipe := c.mqtt.acks; pipe != nil { + pipe.shutdown() + } + + // If asm or sess are nil, it means that we have failed a client + // before it was associated with a session, so nothing more to do. + if asm == nil || sess == nil { + return + } + + // Add this session to the locked map for the rest of the execution. + if err := asm.lockSession(sess, c); err != nil { + return + } + defer asm.unlockSession(sess) + + asm.mu.Lock() + // Clear the client from the session, but session may stay. + sess.mu.Lock() + sess.c = nil + doClean := sess.clean + sess.mu.Unlock() + // If it was a clean session, then we remove from the account manager, + // and we will call clear() outside of any lock. + if doClean { + asm.removeSession(sess, false) + } + // Remove in case it was in the flappers map. + asm.removeSessFromFlappers(sess.id) + asm.mu.Unlock() + + // This needs to be done outside of any lock. + if doClean { + if err := sess.clear(true); err != nil { + c.Errorf(err.Error()) + } + } + + // Now handle the "will". This function will be a no-op if there is no "will" to send. + s.mqttHandleWill(c) +} + +// Updates the MaxAckPending for all MQTT sessions, updating the +// JetStream consumers and updating their max ack pending and forcing +// a expiration of pending messages. +// +// Runs from a server configuration reload routine. +// No lock held on entry. +func (s *Server) mqttUpdateMaxAckPending(newmaxp uint16) { + msm := &s.mqtt.sessmgr + s.accounts.Range(func(k, _ any) bool { + accName := k.(string) + msm.mu.RLock() + asm := msm.sessions[accName] + msm.mu.RUnlock() + if asm == nil { + // Move to next account + return true + } + asm.mu.RLock() + for _, sess := range asm.sessions { + sess.mu.Lock() + sess.maxp = newmaxp + sess.mu.Unlock() + } + asm.mu.RUnlock() + return true + }) +} + +func (s *Server) mqttGetJSAForAccount(acc string) *mqttJSA { + sm := &s.mqtt.sessmgr + + sm.mu.RLock() + asm := sm.sessions[acc] + sm.mu.RUnlock() + + if asm == nil { + return nil + } + + asm.mu.RLock() + jsa := &asm.jsa + asm.mu.RUnlock() + return jsa +} + +func (s *Server) mqttStoreQoSMsgForAccountOnNewSubject(hdr int, msg []byte, acc, subject string) { + if s == nil || hdr <= 0 { + return + } + h := mqttParsePublishNATSHeader(msg[:hdr]) + if h == nil || h.qos == 0 { + return + } + jsa := s.mqttGetJSAForAccount(acc) + if jsa == nil { + return + } + jsa.storeMsg(mqttStreamSubjectPrefix+subject, hdr, msg) +} + +// Encodes the MQTT PUBLISH flags nibble (mqttPubFlags) as one hex char, the +// flags byte that follows the QoS in a mqttNatsHeader value. +func mqttNatsHeaderEncodeFlags(ppFlags byte) byte { + return "0123456789abcdef"[ppFlags&mqttPubFlags] +} + +// Decodes the MQTT flags nibble carried after the QoS in a mqttNatsHeader +// value; values without the flags byte read as no flags set. Callers test the +// result with the mqttPubFlag* bits. +func mqttNatsHeaderDecodeFlags(value []byte) byte { + if len(value) < 2 { + return 0 + } + switch c := value[1]; { + case c >= '0' && c <= '9': + return c - '0' + case c >= 'a' && c <= 'f': + return c - 'a' + 10 + } + return 0 +} + +func mqttParsePublishNATSHeader(headerBytes []byte) *mqttParsedPublishNATSHeader { + if len(headerBytes) == 0 { + return nil + } + + pubValue := getHeader(mqttNatsHeader, headerBytes) + if len(pubValue) == 0 { + return nil + } + return &mqttParsedPublishNATSHeader{ + qos: pubValue[0] - '0', + retained: mqttIsRetained(mqttNatsHeaderDecodeFlags(pubValue)), + subject: getHeader(mqttNatsHeaderSubject, headerBytes), + mapped: getHeader(mqttNatsHeaderMapped, headerBytes), + } +} + +func mqttParsePubRelNATSHeader(headerBytes []byte) uint16 { + if len(headerBytes) == 0 { + return 0 + } + + pubrelValue := getHeader(mqttNatsPubRelHeader, headerBytes) + if len(pubrelValue) == 0 { + return 0 + } + pi, _ := strconv.ParseUint(string(pubrelValue), 10, 16) + return uint16(pi) +} + +// Returns the MQTT sessions manager for a given account. +// If new, creates the required JetStream streams/consumers +// for handling of sessions and messages. +func (s *Server) getOrCreateMQTTAccountSessionManager(c *client) (*mqttAccountSessionManager, error) { + sm := &s.mqtt.sessmgr + + c.mu.Lock() + acc := c.acc + c.mu.Unlock() + accName := acc.GetName() + + sm.mu.RLock() + asm, ok := sm.sessions[accName] + sm.mu.RUnlock() + + if ok { + return asm, nil + } + + // We will pass the quitCh to the account session manager if we happen to create it. + s.mu.Lock() + quitCh := s.quitCh + s.mu.Unlock() + + // Not found, now take the write lock and check again + sm.mu.Lock() + defer sm.mu.Unlock() + asm, ok = sm.sessions[accName] + if ok { + return asm, nil + } + // Need to create one here. + asm, err := s.mqttCreateAccountSessionManager(acc, quitCh) + if err != nil { + return nil, err + } + sm.sessions[accName] = asm + return asm, nil +} + +// Creates JS streams/consumers for handling of sessions and messages for this account. +// +// Global session manager lock is held on entry. +func (s *Server) mqttCreateAccountSessionManager(acc *Account, quitCh chan struct{}) (*mqttAccountSessionManager, error) { + var err error + + accName := acc.GetName() + + opts := s.getOpts() + c := s.createInternalAccountClient() + c.acc = acc + + id := s.NodeName() + + mqttJSAPITimeout := opts.MQTT.JSAPITimeout + if mqttJSAPITimeout == 0 { + mqttJSAPITimeout = mqttDefaultJSAPITimeout + } + + replicas := opts.MQTT.StreamReplicas + if replicas <= 0 { + replicas = s.mqttDetermineReplicas() + } + qname := fmt.Sprintf("[ACC:%s] MQTT ", accName) + as := &mqttAccountSessionManager{ + sessions: make(map[string]*mqttSession), + sessByHash: make(map[string]*mqttSession), + sessLocked: make(map[string]struct{}), + flappers: make(map[string]time.Time), + jsa: mqttJSA{ + id: id, + c: c, + rplyr: mqttJSARepliesPrefix + id + ".", + sendq: newIPQueue[*mqttJSPubMsg](s, qname+"send"), + nuid: nuid.New(), + quitCh: quitCh, + timeout: mqttJSAPITimeout, + }, + rmsCache: &sync.Map{}, + } + // TODO record domain name in as here + + // The domain to communicate with may be required for JS calls. + // Search from specific (per account setting) to generic (mqtt setting) + if opts.JsAccDefaultDomain != nil { + if d, ok := opts.JsAccDefaultDomain[accName]; ok { + if d != _EMPTY_ { + as.jsa.domain = d + } + as.jsa.domainSet = true + } + // in case domain was set to empty, check if there are more generic domain overwrites + } + if as.jsa.domain == _EMPTY_ { + if d := opts.MQTT.JsDomain; d != _EMPTY_ { + as.jsa.domain = d + as.jsa.domainSet = true + } + } + // We need to include the domain in the subject prefix used to store sessions in the $MQTT_sess stream. + if as.jsa.domainSet { + if as.jsa.domain != _EMPTY_ { + as.domainTk = as.jsa.domain + "." + } + } else if d := s.getOpts().JetStreamDomain; d != _EMPTY_ { + as.domainTk = d + "." + } + if as.jsa.domainSet { + s.Noticef("Creating MQTT streams/consumers with replicas %v for account %q in domain %q", replicas, accName, as.jsa.domain) + } else { + s.Noticef("Creating MQTT streams/consumers with replicas %v for account %q", replicas, accName) + } + + var subs []*subscription + var success bool + closeCh := make(chan struct{}) + + defer func() { + if success { + return + } + for _, sub := range subs { + c.processUnsub(sub.sid) + } + close(closeCh) + }() + + // We create all subscriptions before starting the go routine that will do + // sends otherwise we could get races. + // Note that using two different clients (one for the subs, one for the + // sends) would cause other issues such as registration of recent subs in + // the "sub" client would be invisible to the check for GW routed replies + // (shouldMapReplyForGatewaySend) since the client there would be the "sender". + + jsa := &as.jsa + sid := int64(1) + // This is a subscription that will process all JS API replies. We could split to + // individual subscriptions if needed, but since there is a bit of common code, + // that seemed like a good idea to be all in one place. + if err := as.createSubscription(jsa.rplyr+">", + as.processJSAPIReplies, &sid, &subs); err != nil { + return nil, err + } + + // We will listen for replies to session persist requests so that we can + // detect the use of a session with the same client ID anywhere in the cluster. + // `$MQTT.JSA.{js-id}.SP.{client-id-hash}.{uuid}` + if err := as.createSubscription(mqttJSARepliesPrefix+"*."+mqttJSASessPersist+".*.*", + as.processSessionPersist, &sid, &subs); err != nil { + return nil, err + } + + // We create the subscription on "$MQTT.sub." to limit the subjects + // that a user would allow permissions on. + rmsubj := mqttSubPrefix + nuid.Next() + if err := as.createSubscription(rmsubj, as.processRetainedMsg, &sid, &subs); err != nil { + return nil, err + } + + // Create a subscription to be notified of retained messages delete requests. + rmdelsubj := mqttJSARepliesPrefix + "*." + mqttJSARetainedMsgDel + if err := as.createSubscription(rmdelsubj, as.processRetainedMsgDel, &sid, &subs); err != nil { + return nil, err + } + + // No more creation of subscriptions past this point otherwise RACEs may happen. + + // Start the go routine that will send JS API requests. + s.startGoRoutine(func() { + defer s.grWG.Done() + as.sendJSAPIrequests(s, c, accName, closeCh) + }) + + // Start the go routine that will clean up cached retained messages that expired. + s.startGoRoutine(func() { + defer s.grWG.Done() + as.cleanupRetainedMessageCache(s, closeCh) + }) + + lookupStream := func(stream, txt string) (*StreamInfo, error) { + si, err := jsa.lookupStream(stream) + if err != nil { + if IsNatsErr(err, JSStreamNotFoundErr) { + return nil, nil + } + return nil, fmt.Errorf("lookup %s stream for account %q: %v", txt, accName, err) + } + if opts.MQTT.StreamReplicas == 0 { + return si, nil + } + sr := 1 + if si.Cluster != nil { + sr += len(si.Cluster.Replicas) + } + if replicas != sr { + s.Warnf("MQTT %s stream replicas mismatch: current is %v but configuration is %v for '%s > %s'", + txt, sr, replicas, accName, stream) + } + return si, nil + } + + if si, err := lookupStream(mqttSessStreamName, "sessions"); err != nil { + return nil, err + } else if si == nil { + // Create the stream for the sessions. + cfg := &StreamConfig{ + Name: mqttSessStreamName, + Subjects: []string{mqttSessStreamSubjectPrefix + as.domainTk + ">"}, + Storage: FileStorage, + Retention: LimitsPolicy, + Replicas: replicas, + MaxMsgsPer: 1, + } + if _, created, err := jsa.createStream(cfg); err == nil && created { + as.transferUniqueSessStreamsToMuxed(s) + } else if isErrorOtherThan(err, JSStreamNameExistErr) { + return nil, fmt.Errorf("create sessions stream for account %q: %v", accName, err) + } + } + + if si, err := lookupStream(mqttStreamName, "messages"); err != nil { + return nil, err + } else if si == nil { + // Create the stream for the messages. + cfg := &StreamConfig{ + Name: mqttStreamName, + Subjects: []string{mqttStreamSubjectPrefix + ">"}, + Storage: FileStorage, + Retention: InterestPolicy, + Replicas: replicas, + } + if _, _, err := jsa.createStream(cfg); isErrorOtherThan(err, JSStreamNameExistErr) { + return nil, fmt.Errorf("create messages stream for account %q: %v", accName, err) + } + } + + if si, err := lookupStream(mqttQoS2IncomingMsgsStreamName, "QoS2 incoming messages"); err != nil { + return nil, err + } else if si == nil { + // Create the stream for the incoming QoS2 messages that have not been + // PUBREL-ed by the sender. Subject is + // "$MQTT.qos2..", the .PI is to achieve exactly + // once for each PI. + cfg := &StreamConfig{ + Name: mqttQoS2IncomingMsgsStreamName, + Subjects: []string{mqttQoS2IncomingMsgsStreamSubjectPrefix + ">"}, + Storage: FileStorage, + Retention: LimitsPolicy, + Discard: DiscardNew, + MaxMsgsPer: 1, + DiscardNewPer: true, + Replicas: replicas, + } + if _, _, err := jsa.createStream(cfg); isErrorOtherThan(err, JSStreamNameExistErr) { + return nil, fmt.Errorf("create QoS2 incoming messages stream for account %q: %v", accName, err) + } + } + + if si, err := lookupStream(mqttOutStreamName, "QoS2 outgoing PUBREL"); err != nil { + return nil, err + } else if si == nil { + // Create the stream for the incoming QoS2 messages that have not been + // PUBREL-ed by the sender. NATS messages are submitted as + // "$MQTT.pubrel." + cfg := &StreamConfig{ + Name: mqttOutStreamName, + Subjects: []string{mqttOutSubjectPrefix + ">"}, + Storage: FileStorage, + Retention: InterestPolicy, + Replicas: replicas, + } + if _, _, err := jsa.createStream(cfg); isErrorOtherThan(err, JSStreamNameExistErr) { + return nil, fmt.Errorf("create QoS2 outgoing PUBREL stream for account %q: %v", accName, err) + } + } + + // This is the only case where we need "si" after lookup/create + needToTransfer := true + si, err := lookupStream(mqttRetainedMsgsStreamName, "retained messages") + switch { + case err != nil: + return nil, err + + case si == nil: + // Create the stream for retained messages. + cfg := &StreamConfig{ + Name: mqttRetainedMsgsStreamName, + Subjects: []string{mqttRetainedMsgsStreamSubject + ">"}, + Storage: FileStorage, + Retention: LimitsPolicy, + Replicas: replicas, + MaxMsgsPer: 1, + } + // We will need "si" outside of this block. + si, _, err = jsa.createStream(cfg) + if err != nil { + if isErrorOtherThan(err, JSStreamNameExistErr) { + return nil, fmt.Errorf("create retained messages stream for account %q: %v", accName, err) + } + // Suppose we had a race and the stream was actually created by another + // node, we really need "si" after that, so lookup the stream again here. + si, err = lookupStream(mqttRetainedMsgsStreamName, "retained messages") + if err != nil { + return nil, err + } + } + needToTransfer = false + + default: + needToTransfer = si.Config.MaxMsgsPer != 1 + } + // Guard before dereferencing si.Config below. + if si == nil { + return nil, fmt.Errorf("could not look up or create the retained messages stream for account %q", accName) + } + + // Doing this check outside of above if/else due to possible race when + // creating the stream. + wantedSubj := mqttRetainedMsgsStreamSubject + ">" + if len(si.Config.Subjects) != 1 || si.Config.Subjects[0] != wantedSubj { + // Update only the Subjects at this stage, not MaxMsgsPer yet. + si.Config.Subjects = []string{wantedSubj} + if si, err = jsa.updateStream(&si.Config); err != nil { + return nil, fmt.Errorf("failed to update stream config: %w", err) + } + } + + transferRMS := func() error { + if !needToTransfer { + return nil + } + + as.transferRetainedToPerKeySubjectStream(s) + + // We need another lookup to have up-to-date si.State values in order + // to load all retained messages. + si, err = lookupStream(mqttRetainedMsgsStreamName, "retained messages") + if err != nil { + return err + } + needToTransfer = false + return nil + } + + // Attempt to transfer all "single subject" retained messages to new + // subjects. It may fail, will log its own error; ignore it the first time + // and proceed to updating MaxMsgsPer. Then we invoke transferRMS() again, + // which will get another chance to resolve the error; if not we bail there. + if err = transferRMS(); err != nil { + return nil, err + } + // Guard before dereferencing si.Config below. + if si == nil { + return nil, fmt.Errorf("could not look up the retained messages stream for account %q", accName) + } + + // Now, if the stream does not have MaxMsgsPer set to 1, and there are no + // more messages on the single $MQTT.rmsgs subject, update the stream again. + if si.Config.MaxMsgsPer != 1 { + si.Config.MaxMsgsPer = 1 + // We will need an up-to-date si, so don't use local variable here. + if si, err = jsa.updateStream(&si.Config); err != nil { + return nil, fmt.Errorf("failed to update stream config: %w", err) + } + } + + // If we failed the first time, there is now at most one lingering message + // in the old subject. Try again (it will be a NO-OP if succeeded the first + // time). + if err = transferRMS(); err != nil { + return nil, err + } + + // Opportunistically delete the old (legacy) consumer, from v2.10.10 and + // before. Ignore any errors that might arise. + rmLegacyDurName := mqttRetainedMsgsStreamName + "_" + jsa.id + jsa.deleteConsumer(mqttRetainedMsgsStreamName, rmLegacyDurName, true) + + // Create a new, uniquely names consumer for retained messages for this + // server. The prior one will expire eventually. + ccfg := &CreateConsumerRequest{ + Stream: mqttRetainedMsgsStreamName, + Config: ConsumerConfig{ + Name: mqttRetainedMsgsStreamName + "_" + nuid.Next(), + FilterSubject: mqttRetainedMsgsStreamSubject + ">", + DeliverSubject: rmsubj, + ReplayPolicy: ReplayInstant, + AckPolicy: AckNone, + InactiveThreshold: 5 * time.Minute, + }, + } + if _, err := jsa.createEphemeralConsumer(ccfg); err != nil { + return nil, fmt.Errorf("create retained messages consumer for account %q: %v", accName, err) + } + + // Set this so that on defer we don't cleanup. + success = true + + return as, nil +} + +func (s *Server) mqttDetermineReplicas() int { + // If not clustered, then replica will be 1. + if !s.JetStreamIsClustered() { + return 1 + } + opts := s.getOpts() + replicas := 0 + for _, u := range opts.Routes { + host := u.Hostname() + // If this is an IP just add one. + if net.ParseIP(host) != nil { + replicas++ + } else { + addrs, _ := net.LookupHost(host) + replicas += len(addrs) + } + } + if replicas < 1 { + replicas = 1 + } else if replicas > 3 { + replicas = 3 + } + return replicas +} + +////////////////////////////////////////////////////////////////////////////// +// +// JS APIs related functions +// +////////////////////////////////////////////////////////////////////////////// + +func (jsa *mqttJSA) newRequest(kind, subject string, hdr int, msg []byte) (any, error) { + return jsa.newRequestEx(kind, subject, _EMPTY_, hdr, msg) +} + +func (jsa *mqttJSA) prefixDomain(subject string) string { + if jsa.domain != _EMPTY_ { + // rewrite js api prefix with domain + if sub := strings.TrimPrefix(subject, JSApiPrefix+"."); sub != subject { + subject = fmt.Sprintf("$JS.%s.API.%s", jsa.domain, sub) + } + } + return subject +} + +func (jsa *mqttJSA) newRequestEx(kind, subject, cidHash string, hdr int, msg []byte) (any, error) { + responses, err := jsa.newRequestExMulti(kind, subject, cidHash, []int{hdr}, [][]byte{msg}) + if err != nil { + return nil, err + } + if len(responses) != 1 { + return nil, fmt.Errorf("unreachable: invalid number of responses (%d)", len(responses)) + } + return responses[0].value, nil +} + +// newRequestExMulti sends multiple messages on the same subject and waits for +// all responses. It returns the same number of responses in the same order as +// msgs parameter. In case of a timeout it returns an error as well as all +// responses received as a sparsely populated array, matching msgs, with nils +// for the values that have not yet been received. +// +// Note that each response may represent an error and should be inspected as +// such by the caller. +func (jsa *mqttJSA) newRequestExMulti(kind, subject, cidHash string, hdrs []int, msgs [][]byte) ([]*mqttJSAResponse, error) { + if len(hdrs) != len(msgs) { + return nil, fmt.Errorf("unreachable: invalid number of messages (%d) or header offsets (%d)", len(msgs), len(hdrs)) + } + responseCh := make(chan *mqttJSAResponse, len(msgs)) + + // Generate and queue all outgoing requests, have all results reported to + // responseCh, and store a map of reply subjects to the original subjects' + // indices. + r2i := map[string]int{} + for i, msg := range msgs { + hdr := hdrs[i] + var sb strings.Builder + // Either we use nuid.Next() which uses a global lock, or our own nuid object, but + // then it needs to be "write" protected. This approach will reduce across account + // contention since we won't use the global nuid's lock. + jsa.mu.Lock() + uid := jsa.nuid.Next() + sb.WriteString(jsa.rplyr) + jsa.mu.Unlock() + + sb.WriteString(kind) + sb.WriteByte(btsep) + if cidHash != _EMPTY_ { + sb.WriteString(cidHash) + sb.WriteByte(btsep) + } + sb.WriteString(uid) + reply := sb.String() + + // Add responseCh to the reply channel map. It will be cleaned out on + // timeout (see below), or in processJSAPIReplies upon receiving the + // response. + jsa.replies.Store(reply, responseCh) + + subject = jsa.prefixDomain(subject) + jsa.sendq.push(&mqttJSPubMsg{ + subj: subject, + reply: reply, + hdr: hdr, + msg: msg, + }) + r2i[reply] = i + } + + // Wait for all responses to come back, or for the timeout to expire. We + // don't want to use time.After() which causes memory growth because the + // timer can't be stopped and will need to expire to then be garbage + // collected. + c := 0 + responses := make([]*mqttJSAResponse, len(msgs)) + start := time.Now() + t := time.NewTimer(jsa.timeout) + defer t.Stop() + for { + select { + case r := <-responseCh: + i := r2i[r.reply] + responses[i] = r + c++ + if c == len(msgs) { + return responses, nil + } + + case <-jsa.quitCh: + return nil, ErrServerNotRunning + + case <-t.C: + var reply string + now := time.Now() + for reply = range r2i { // preserve the last value for Errorf + jsa.replies.Delete(reply) + } + + if len(msgs) == 1 { + return responses, fmt.Errorf("timeout after %v: request type %q on %q (reply=%q)", now.Sub(start), kind, subject, reply) + } else { + return responses, fmt.Errorf("timeout after %v: request type %q on %q: got %d out of %d", now.Sub(start), kind, subject, c, len(msgs)) + } + } + } +} + +func (jsa *mqttJSA) sendAck(ackSubject string) { + // Send to the ack subject with no payload. + jsa.sendMsg(ackSubject, nil) +} + +func (jsa *mqttJSA) sendMsg(subj string, msg []byte) { + if subj == _EMPTY_ { + return + } + // We pass -1 for the hdr so that the send loop does not need to + // add the "client info" header. This is not a JS API request per se. + jsa.sendq.push(&mqttJSPubMsg{subj: subj, msg: msg, hdr: -1}) +} + +func (jsa *mqttJSA) createEphemeralConsumer(cfg *CreateConsumerRequest) (*JSApiConsumerCreateResponse, error) { + cfgb, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + subj := fmt.Sprintf(JSApiConsumerCreateT, cfg.Stream) + ccri, err := jsa.newRequest(mqttJSAConsumerCreate, subj, 0, cfgb) + if err != nil { + return nil, err + } + ccr := ccri.(*JSApiConsumerCreateResponse) + return ccr, ccr.ToError() +} + +func (jsa *mqttJSA) createDurableConsumer(cfg *CreateConsumerRequest) (*JSApiConsumerCreateResponse, error) { + cfgb, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + subj := fmt.Sprintf(JSApiDurableCreateT, cfg.Stream, cfg.Config.Durable) + ccri, err := jsa.newRequest(mqttJSAConsumerCreate, subj, 0, cfgb) + if err != nil { + return nil, err + } + ccr := ccri.(*JSApiConsumerCreateResponse) + return ccr, ccr.ToError() +} + +// if noWait is specified, does not wait for the JS response, returns nil +func (jsa *mqttJSA) deleteConsumer(streamName, consName string, noWait bool) (*JSApiConsumerDeleteResponse, error) { + subj := fmt.Sprintf(JSApiConsumerDeleteT, streamName, consName) + if noWait { + jsa.sendMsg(subj, nil) + return nil, nil + } + + cdri, err := jsa.newRequest(mqttJSAConsumerDel, subj, 0, nil) + if err != nil { + return nil, err + } + cdr := cdri.(*JSApiConsumerDeleteResponse) + return cdr, cdr.ToError() +} + +func (jsa *mqttJSA) createStream(cfg *StreamConfig) (*StreamInfo, bool, error) { + cfgb, err := json.Marshal(cfg) + if err != nil { + return nil, false, err + } + scri, err := jsa.newRequest(mqttJSAStreamCreate, fmt.Sprintf(JSApiStreamCreateT, cfg.Name), 0, cfgb) + if err != nil { + return nil, false, err + } + scr := scri.(*JSApiStreamCreateResponse) + if err = scr.ToError(); err != nil { + return nil, false, err + } + if scr.StreamInfo == nil { + return nil, false, fmt.Errorf("invalid stream create response: missing stream info") + } + return scr.StreamInfo, scr.DidCreate, nil +} + +func (jsa *mqttJSA) updateStream(cfg *StreamConfig) (*StreamInfo, error) { + cfgb, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + scri, err := jsa.newRequest(mqttJSAStreamUpdate, fmt.Sprintf(JSApiStreamUpdateT, cfg.Name), 0, cfgb) + if err != nil { + return nil, err + } + scr := scri.(*JSApiStreamUpdateResponse) + if err = scr.ToError(); err != nil { + return nil, err + } + if scr.StreamInfo == nil { + return nil, fmt.Errorf("invalid stream update response: missing stream info") + } + return scr.StreamInfo, nil +} + +func (jsa *mqttJSA) lookupStream(name string) (*StreamInfo, error) { + slri, err := jsa.newRequest(mqttJSAStreamLookup, fmt.Sprintf(JSApiStreamInfoT, name), 0, nil) + if err != nil { + return nil, err + } + slr := slri.(*JSApiStreamInfoResponse) + if err = slr.ToError(); err != nil { + return nil, err + } + if slr.StreamInfo == nil { + return nil, NewJSStreamNotFoundError() + } + return slr.StreamInfo, nil +} + +func (jsa *mqttJSA) deleteStream(name string) (bool, error) { + sdri, err := jsa.newRequest(mqttJSAStreamDel, fmt.Sprintf(JSApiStreamDeleteT, name), 0, nil) + if err != nil { + return false, err + } + sdr := sdri.(*JSApiStreamDeleteResponse) + return sdr.Success, sdr.ToError() +} + +func (jsa *mqttJSA) loadLastMsgFor(streamName string, subject string) (*StoredMsg, error) { + mreq := &JSApiMsgGetRequest{LastFor: subject} + req, err := json.Marshal(mreq) + if err != nil { + return nil, err + } + lmri, err := jsa.newRequest(mqttJSAMsgLoad, fmt.Sprintf(JSApiMsgGetT, streamName), 0, req) + if err != nil { + return nil, err + } + lmr := lmri.(*JSApiMsgGetResponse) + if err = lmr.ToError(); err != nil { + return nil, err + } + if lmr.Message == nil { + return nil, NewJSNoMessageFoundError() + } + return lmr.Message, nil +} + +func (jsa *mqttJSA) loadLastMsgForMulti(streamName string, subjects []string) ([]*JSApiMsgGetResponse, error) { + marshaled := make([][]byte, 0, len(subjects)) + headerBytes := make([]int, 0, len(subjects)) + for _, subject := range subjects { + mreq := &JSApiMsgGetRequest{LastFor: subject} + bb, err := json.Marshal(mreq) + if err != nil { + return nil, err + } + marshaled = append(marshaled, bb) + headerBytes = append(headerBytes, 0) + } + + all, err := jsa.newRequestExMulti(mqttJSAMsgLoad, fmt.Sprintf(JSApiMsgGetT, streamName), _EMPTY_, headerBytes, marshaled) + // all has the same order as subjects, preserve it as we unmarshal + responses := make([]*JSApiMsgGetResponse, len(all)) + for i, v := range all { + if v != nil { + responses[i] = v.value.(*JSApiMsgGetResponse) + } + } + return responses, err +} + +func (jsa *mqttJSA) loadNextMsgFor(streamName string, subject string) (*StoredMsg, error) { + mreq := &JSApiMsgGetRequest{NextFor: subject} + req, err := json.Marshal(mreq) + if err != nil { + return nil, err + } + lmri, err := jsa.newRequest(mqttJSAMsgLoad, fmt.Sprintf(JSApiMsgGetT, streamName), 0, req) + if err != nil { + return nil, err + } + lmr := lmri.(*JSApiMsgGetResponse) + if err = lmr.ToError(); err != nil { + return nil, err + } + if lmr.Message == nil { + return nil, NewJSNoMessageFoundError() + } + return lmr.Message, nil +} + +func (jsa *mqttJSA) loadMsg(streamName string, seq uint64) (*StoredMsg, error) { + mreq := &JSApiMsgGetRequest{Seq: seq} + req, err := json.Marshal(mreq) + if err != nil { + return nil, err + } + lmri, err := jsa.newRequest(mqttJSAMsgLoad, fmt.Sprintf(JSApiMsgGetT, streamName), 0, req) + if err != nil { + return nil, err + } + lmr := lmri.(*JSApiMsgGetResponse) + if err := lmr.ToError(); err != nil { + return nil, err + } + if lmr.Message == nil { + return nil, NewJSNoMessageFoundError() + } + return lmr.Message, nil +} + +func (jsa *mqttJSA) storeMsgNoWait(subject string, hdrLen int, msg []byte) { + jsa.sendq.push(&mqttJSPubMsg{ + subj: subject, + msg: msg, + hdr: hdrLen, + }) +} + +func (jsa *mqttJSA) storeMsg(subject string, headers int, msg []byte) (*JSPubAckResponse, error) { + smri, err := jsa.newRequest(mqttJSAMsgStore, subject, headers, msg) + if err != nil { + return nil, err + } + smr := smri.(*JSPubAckResponse) + return smr, smr.ToError() +} + +// Like storeMsg, but does not wait: processJSAPIReplies completes ack.done +// when the reply arrives. A caller that gives up waiting must delete +// ack.reply from jsa.replies. +func (jsa *mqttJSA) storeMsgAsync(subject string, hdr int, msg []byte, ack *mqttPipelinedAck) { + var sb strings.Builder + jsa.mu.Lock() + uid := jsa.nuid.Next() + sb.WriteString(jsa.rplyr) + jsa.mu.Unlock() + sb.WriteString(mqttJSAMsgStore) + sb.WriteByte(btsep) + sb.WriteString(uid) + ack.reply = sb.String() + + jsa.replies.Store(ack.reply, ack) + jsa.sendq.push(&mqttJSPubMsg{ + subj: subject, + reply: ack.reply, + hdr: hdr, + msg: msg, + }) +} + +func (jsa *mqttJSA) storeSessionMsg(domainTk, cidHash string, hdr int, msg []byte) (*JSPubAckResponse, error) { + // Compute subject where the session is being stored + subject := mqttSessStreamSubjectPrefix + domainTk + cidHash + + // Passing cidHash will add it to the JS reply subject, so that we can use + // it in processSessionPersist. + smri, err := jsa.newRequestEx(mqttJSASessPersist, subject, cidHash, hdr, msg) + if err != nil { + return nil, err + } + smr := smri.(*JSPubAckResponse) + return smr, smr.ToError() +} + +func (jsa *mqttJSA) loadSessionMsg(domainTk, cidHash string) (*StoredMsg, error) { + subject := mqttSessStreamSubjectPrefix + domainTk + cidHash + return jsa.loadLastMsgFor(mqttSessStreamName, subject) +} + +func (jsa *mqttJSA) deleteMsg(stream string, seq uint64, wait bool) error { + dreq := JSApiMsgDeleteRequest{Seq: seq, NoErase: true} + req, _ := json.Marshal(dreq) + subj := jsa.prefixDomain(fmt.Sprintf(JSApiMsgDeleteT, stream)) + if !wait { + jsa.sendq.push(&mqttJSPubMsg{ + subj: subj, + msg: req, + }) + return nil + } + dmi, err := jsa.newRequest(mqttJSAMsgDelete, subj, 0, req) + if err != nil { + return err + } + dm := dmi.(*JSApiMsgDeleteResponse) + return dm.ToError() +} + +////////////////////////////////////////////////////////////////////////////// +// +// Account Sessions Manager related functions +// +////////////////////////////////////////////////////////////////////////////// + +// Returns true if `err` is not nil and does not match the api error with ErrorIdentifier id +func isErrorOtherThan(err error, id ErrorIdentifier) bool { + return err != nil && !IsNatsErr(err, id) +} + +// Process JS API replies. +// +// Can run from various go routines (consumer's loop, system send loop, etc..). +func (as *mqttAccountSessionManager) processJSAPIReplies(_ *subscription, pc *client, _ *Account, subject, _ string, msg []byte) { + token := tokenAt(subject, mqttJSATokenPos) + if token == _EMPTY_ { + return + } + jsa := &as.jsa + // Claim atomically: dispatch at most once, even on duplicate replies. + chi, ok := jsa.replies.LoadAndDelete(subject) + if !ok { + return + } + out := func(value any) { + switch to := chi.(type) { + case chan *mqttJSAResponse: + to <- &mqttJSAResponse{reply: subject, value: value} + case *mqttPipelinedAck: + // An "MS" reply is always a *JSPubAckResponse; done is + // buffered(1) and dispatched at most once, cannot block. + to.done <- value.(*JSPubAckResponse).ToError() + default: + // Leave a clue; the waiter will hang until its timeout. + pc.Warnf("Unexpected type %T registered for JS API reply %q", chi, subject) + } + } + switch token { + case mqttJSAStreamCreate: + var resp = &JSApiStreamCreateResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAStreamUpdate: + var resp = &JSApiStreamUpdateResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAStreamLookup: + var resp = &JSApiStreamInfoResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAStreamDel: + var resp = &JSApiStreamDeleteResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAConsumerCreate: + var resp = &JSApiConsumerCreateResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAConsumerDel: + var resp = &JSApiConsumerDeleteResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAMsgStore, mqttJSASessPersist: + var resp = &JSPubAckResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAMsgLoad: + var resp = &JSApiMsgGetResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAStreamNames: + var resp = &JSApiStreamNamesResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + case mqttJSAMsgDelete: + var resp = &JSApiMsgDeleteResponse{} + if err := json.Unmarshal(msg, resp); err != nil { + resp.Error = NewJSInvalidJSONError(err) + } + out(resp) + default: + pc.Warnf("Unknown reply code %q", token) + } +} + +// This will both load all retained messages and process updates from the cluster. +// +// Run from various go routines (JS consumer, etc..). +// No lock held on entry. +func (as *mqttAccountSessionManager) processRetainedMsg(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + h, m := c.msgParts(rmsg) + // We need to strip the trailing "\r\n". + if l := len(m); l >= LEN_CR_LF { + m = m[:l-LEN_CR_LF] + } + rm, err := mqttDecodeRetainedMessage(subject, h, m) + if err != nil { + return + } + if strings.IndexByte(rm.Subject, 0x7f) >= 0 { + c.Warnf("Skipping retained message for subject %q: unsupported character 0x7f", rm.Subject) + return + } + // The as.jsa.id is immutable, so no need to have a rlock here. + local := rm.Origin == as.jsa.id + // Get the stream sequence for this message. + seq, _, _, _, _ := ackReplyInfo(reply) + if len(m) == 0 { + // An empty payload means that we need to remove the retained message. + rmSeq := as.removeRetainedMsg(rm.Subject, 0) + if local { + if rmSeq > 0 { + // This is for backward compatibility reasons. + // Should be removed in a future release. + as.notifyRetainedMsgDeleted(rm.Subject, rmSeq) + } + // Delete this very message we just processed, we don't need it anymore. + as.deleteRetainedMsg(seq) + } + } else { + // Add this retained message. The `rm.Msg` references some buffer that we + // don't own. But addRetainedMsg() will take care of making a copy of + // `rm.Msg` it `rm` ends-up being stored in the cache. + as.addRetainedMsg(rm.Subject, seq, rm) + } +} + +// NOTE: This is maintained for backward compatibility reasons. Should be removed in 2.14/2.15? +func (as *mqttAccountSessionManager) processRetainedMsgDel(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + idHash := tokenAt(subject, 3) + if idHash == _EMPTY_ || idHash == as.jsa.id { + return + } + _, msg := c.msgParts(rmsg) + if len(msg) < LEN_CR_LF { + return + } + var drm mqttRetMsgDel + if err := json.Unmarshal(msg, &drm); err != nil { + return + } + as.removeRetainedMsg(drm.Subject, drm.Seq) +} + +// This will receive all JS API replies for a request to store a session record, +// including the reply for our own server, which we will ignore. +// This allows us to detect that some application somewhere else in the cluster +// is connecting with the same client ID, and therefore we need to close the +// connection that is currently using this client ID. +// +// Can run from various go routines (system send loop, etc..). +// No lock held on entry. +func (as *mqttAccountSessionManager) processSessionPersist(_ *subscription, pc *client, _ *Account, subject, _ string, rmsg []byte) { + // Ignore our own responses here (they are handled elsewhere) + if tokenAt(subject, mqttJSAIdTokenPos) == as.jsa.id { + return + } + cIDHash := tokenAt(subject, mqttJSAClientIDPos) + _, msg := pc.msgParts(rmsg) + if len(msg) < LEN_CR_LF { + return + } + var par = &JSPubAckResponse{} + if err := json.Unmarshal(msg, par); err != nil { + return + } + if err := par.Error; err != nil { + return + } + if par.PubAck == nil { + return + } + as.mu.RLock() + // Note that as.domainTk includes a terminal '.', so strip to compare to PubAck.Domain. + dl := len(as.domainTk) + if dl > 0 { + dl-- + } + ignore := par.Domain != as.domainTk[:dl] + as.mu.RUnlock() + if ignore { + return + } + + as.mu.Lock() + defer as.mu.Unlock() + sess, ok := as.sessByHash[cIDHash] + if !ok { + return + } + // If our current session's stream sequence is higher, it means that this + // update is stale, so we don't do anything here. + sess.mu.Lock() + ignore = par.Sequence < sess.seq + sess.mu.Unlock() + if ignore { + return + } + as.removeSession(sess, false) + sess.mu.Lock() + if ec := sess.c; ec != nil { + as.addSessToFlappers(sess.id) + ec.Warnf("Closing because a remote connection has started with the same client ID: %q", sess.id) + // Disassociate the client from the session so that on client close, + // nothing will be done with regards to cleaning up the session, + // such as deleting stream, etc.. + sess.c = nil + // Remove in separate go routine. + go ec.closeConnection(DuplicateClientID) + } + sess.mu.Unlock() +} + +// Adds this client ID to the flappers map, and if needed start the timer +// for map cleanup. +// +// Lock held on entry. +func (as *mqttAccountSessionManager) addSessToFlappers(clientID string) { + as.flappers[clientID] = time.Now() + if as.flapTimer == nil { + as.flapTimer = time.AfterFunc(mqttFlapCleanItvl, func() { + as.mu.Lock() + defer as.mu.Unlock() + // In case of shutdown, this will be nil + if as.flapTimer == nil { + return + } + now := time.Now() + for cID, tm := range as.flappers { + if now.Sub(tm) > mqttSessJailDur { + delete(as.flappers, cID) + } + } + as.flapTimer.Reset(mqttFlapCleanItvl) + }) + } +} + +// Remove this client ID from the flappers map. +// +// Lock held on entry. +func (as *mqttAccountSessionManager) removeSessFromFlappers(clientID string) { + delete(as.flappers, clientID) + // Do not stop/set timer to nil here. Better leave the timer run at its + // regular interval and detect that there is nothing to do. The timer + // will be stopped on shutdown. +} + +// Helper to create a subscription. It updates the sid and array of subscriptions. +func (as *mqttAccountSessionManager) createSubscription(subject string, cb msgHandler, sid *int64, subs *[]*subscription) error { + sub, err := as.jsa.c.processSub([]byte(subject), nil, []byte(strconv.FormatInt(*sid, 10)), cb, false) + if err != nil { + return err + } + *sid++ + *subs = append(*subs, sub) + return nil +} + +// A timer loop to cleanup up expired cached retained messages for a given MQTT account. +// The closeCh is used by the caller to be able to interrupt this routine +// if the rest of the initialization fails, since the quitCh is really +// only used when the server shutdown. +// +// No lock held on entry. +func (as *mqttAccountSessionManager) cleanupRetainedMessageCache(s *Server, closeCh chan struct{}) { + tt := time.NewTicker(mqttRetainedCacheTTL) + defer tt.Stop() + for { + select { + case <-tt.C: + // Set a limit to the number of retained messages to scan since we + // lock as for it. Since the map enumeration gives random order we + // should eventually clean up everything. + i, maxScan := 0, 10*1000 + now := time.Now() + as.rmsCache.Range(func(key, value any) bool { + rm := value.(*mqttRetainedMsg) + if now.After(rm.expiresFromCache) { + as.rmsCache.Delete(key) + } + i++ + return i < maxScan + }) + + case <-closeCh: + return + case <-s.quitCh: + return + } + } +} + +// Loop to send JS API requests for a given MQTT account. +// The closeCh is used by the caller to be able to interrupt this routine +// if the rest of the initialization fails, since the quitCh is really +// only used when the server shutdown. +// +// No lock held on entry. +func (as *mqttAccountSessionManager) sendJSAPIrequests(s *Server, c *client, accName string, closeCh chan struct{}) { + var cluster string + if s.JetStreamEnabled() && !as.jsa.domainSet { + // Only request the own cluster when it is clear that + cluster = s.cachedClusterName() + } + as.mu.RLock() + sendq := as.jsa.sendq + quitCh := as.jsa.quitCh + ci := ClientInfo{Account: accName, Cluster: cluster} + acc := c.acc + as.mu.RUnlock() + + // The account session manager does not have a suhtdown API per-se, instead, + // we will cleanup things when this go routine exits after detecting that the + // server is shutdown or the initialization of the account manager failed. + defer func() { + as.mu.Lock() + if as.flapTimer != nil { + as.flapTimer.Stop() + as.flapTimer = nil + } + as.mu.Unlock() + }() + + b, _ := json.Marshal(ci) + hdrStart := bytes.Buffer{} + hdrStart.WriteString(hdrLine) + http.Header{ClientInfoHdr: []string{string(b)}}.Write(&hdrStart) + hdrStart.WriteString(CR_LF) + hdrStart.WriteString(CR_LF) + hdrb := hdrStart.Bytes() + + for { + select { + case <-sendq.ch: + pmis := sendq.pop() + for _, r := range pmis { + var nsize int + + msg := r.msg + // If r.hdr is set to -1, it means that there is no need for any header. + if r.hdr != -1 { + bb := bytes.Buffer{} + if r.hdr > 0 { + // This means that the header has been set by the caller and is + // already part of `msg`, so simply set c.pa.hdr to the given value. + c.pa.hdr = r.hdr + nsize = len(msg) + msg = append(msg, _CRLF_...) + } else { + // We need the ClientInfo header, so add it here. + bb.Write(hdrb) + c.pa.hdr = bb.Len() + bb.Write(r.msg) + nsize = bb.Len() + bb.WriteString(_CRLF_) + msg = bb.Bytes() + } + c.pa.hdb = []byte(strconv.Itoa(c.pa.hdr)) + } else { + c.pa.hdr = -1 + c.pa.hdb = nil + nsize = len(msg) + msg = append(msg, _CRLF_...) + } + + c.pa.subject = []byte(r.subj) + c.pa.reply = []byte(r.reply) + c.pa.size = nsize + c.pa.szb = []byte(strconv.Itoa(nsize)) + c.pa.mapped = nil + + if acc.hasMappings() { + if changed := c.selectMappedSubject(); changed { + c.traceOutOp("MAPPINGS", fmt.Appendf(nil, "%s -> %s", c.pa.mapped, c.pa.subject)) + } + } + c.processInboundClientMsg(msg) + c.flushClients(0) + } + sendq.recycle(&pmis) + + case <-closeCh: + return + case <-quitCh: + return + } + } +} + +// Add/Replace this message from the retained messages map. +// If a message for this topic already existed, the existing record is updated +// with the provided information. +// Lock not held on entry. +func (as *mqttAccountSessionManager) addRetainedMsg(key string, sseq uint64, rm *mqttRetainedMsg) { + as.mu.Lock() + defer as.mu.Unlock() + if as.retmsgs == nil { + as.retmsgs = stree.NewSubjectTree[mqttRetainedMsgRef]() + } else { + // Check if we already had one retained message. If so, update the existing one. + if erf, exists := as.retmsgs.Find(stringToBytes(key)); exists { + // Update the stream sequence with the new value. + erf.sseq = sseq + // Update the in-memory retained message cache but only for messages + // that are already in the cache, i.e. have been (recently) used. + // If that is the case, we ask setCachedRetainedMsg() to make a copy + // of rm.Msg bytes slice. + as.setCachedRetainedMsg(key, rm, true, true) + return + } + } + as.retmsgs.Insert([]byte(key), mqttRetainedMsgRef{sseq: sseq}) +} + +// Remove the retained message stored with the `subject` key from the map/cache. +// When invoked from the retained message stream's consumer, this function will +// be called with `seq == 0`, this is because add/remove are serialized in this +// stream and so the request is to remove the current retained message. +// But in some conditions, we will invoke this function from some other places +// with `seq > 0` which means that the retained message will be removed only if +// its sequence is the same than the provided one. +// This function returns the sequence associated with the existing retained +// message that is being removed (used with `seq == 0`) and returns 0 if the +// retained message was not removed from the map (not found or sequence did not +// match). +func (as *mqttAccountSessionManager) removeRetainedMsg(subject string, seq uint64) uint64 { + as.mu.Lock() + defer as.mu.Unlock() + rm, ok := as.retmsgs.Find(stringToBytes(subject)) + if !ok || (seq > 0 && rm.sseq != seq) { + return 0 + } + rm, _ = as.retmsgs.Delete(stringToBytes(subject)) + as.rmsCache.Delete(subject) + return rm.sseq +} + +// First check if this session's client ID is already in the "locked" map, +// which if it is the case means that another client is now bound to this +// session and this should return an error. +// If not in the "locked" map, but the client is not bound with this session, +// then same error is returned. +// Finally, if all checks ok, then the session's ID is added to the "locked" map. +// +// No lock held on entry. +func (as *mqttAccountSessionManager) lockSession(sess *mqttSession, c *client) error { + as.mu.Lock() + defer as.mu.Unlock() + var fail bool + if _, fail = as.sessLocked[sess.id]; !fail { + sess.mu.Lock() + fail = sess.c != c + sess.mu.Unlock() + } + if fail { + return fmt.Errorf("another session is in use with client ID %q", sess.id) + } + as.sessLocked[sess.id] = struct{}{} + return nil +} + +// Remove the session from the "locked" map. +// +// No lock held on entry. +func (as *mqttAccountSessionManager) unlockSession(sess *mqttSession) { + as.mu.Lock() + delete(as.sessLocked, sess.id) + as.mu.Unlock() +} + +// Simply adds the session to the various sessions maps. +// The boolean `lock` indicates if this function should acquire the lock +// prior to adding to the maps. +// +// No lock held on entry. +func (as *mqttAccountSessionManager) addSession(sess *mqttSession, lock bool) { + if lock { + as.mu.Lock() + } + as.sessions[sess.id] = sess + as.sessByHash[sess.idHash] = sess + if lock { + as.mu.Unlock() + } +} + +// Simply removes the session from the various sessions maps. +// The boolean `lock` indicates if this function should acquire the lock +// prior to removing from the maps. +// +// No lock held on entry. +func (as *mqttAccountSessionManager) removeSession(sess *mqttSession, lock bool) { + if lock { + as.mu.Lock() + } + delete(as.sessions, sess.id) + delete(as.sessByHash, sess.idHash) + if lock { + as.mu.Unlock() + } +} + +// Helper to set the sub's mqtt fields and possibly serialize (pre-loaded) +// retained messages. +// +// Session lock held on entry. Acquires the subs lock and holds it for +// the duration. Non-MQTT messages coming into mqttDeliverMsgCbQoS0 will be +// waiting. +func (sess *mqttSession) processQOS12Sub( + c *client, // subscribing client. + subject, sid []byte, isReserved bool, qos byte, jsDurName string, h msgHandler, // subscription parameters. +) (*subscription, error) { + return sess.processSub(c, subject, sid, isReserved, qos, jsDurName, h, false, nil, false, nil) +} + +func (sess *mqttSession) processSub( + c *client, // subscribing client. + subject, sid []byte, isReserved bool, qos byte, jsDurName string, h msgHandler, // subscription parameters. + initShadow bool, // do we need to scan for shadow subscriptions? (not for QOS1+) + rms map[string]*mqttRetainedMsg, // preloaded rms (can be empty, or missing items if errors) + trace bool, // trace serialized retained messages in the log? + as *mqttAccountSessionManager, // needed only for rms serialization. +) (*subscription, error) { + start := time.Now() + defer func() { + elapsed := time.Since(start) + if elapsed > mqttProcessSubTooLong { + c.Warnf("Took too long to process subscription for %q: %v", subject, elapsed) + } + }() + + // Hold subsMu to prevent QOS0 messages callback from doing anything until + // the (MQTT) sub is initialized. + sess.subsMu.Lock() + defer sess.subsMu.Unlock() + + sub, err := c.processSub(subject, nil, sid, h, false) + if err != nil { + // c.processSub already called c.Errorf(), so no need here. + return nil, err + } + subs := []*subscription{sub} + if initShadow { + subs = append(subs, sub.shadow...) + } + for _, ss := range subs { + if ss.mqtt == nil { + // reserved is set only once and once the subscription has been + // created it can be considered immutable. + ss.mqtt = &mqttSub{ + reserved: isReserved, + } + } + // QOS and jsDurName can be changed on an existing subscription, so + // accessing it later requires a lock. + ss.mqtt.qos = qos + ss.mqtt.jsDur = jsDurName + // A (re)configured subscription is live; clear any prior teardown mark. + ss.mqtt.closed = false + } + + if len(rms) > 0 { + // Only deal with retained messages for the normal subscription, + // not the shadow one (which is for a different account and subject). + as.serializeRetainedMsgsForSub(rms, sess, c, sub, trace) + } + + return sub, nil +} + +// Process subscriptions for the given session/client. +// +// When `fromSubProto` is false, it means that this is invoked from the CONNECT +// protocol, when restoring subscriptions that were saved for this session. +// In that case, there is no need to update the session record. +// +// When `fromSubProto` is true, it means that this call is invoked from the +// processing of the SUBSCRIBE protocol, which means that the session needs to +// be updated. It also means that if a subscription on same subject with same +// QoS already exist, we should not be recreating the subscription/JS durable, +// since it was already done when processing the CONNECT protocol. +// +// Runs from the client's readLoop. +// Lock not held on entry, but session is in the locked map. +func (as *mqttAccountSessionManager) processSubs(sess *mqttSession, c *client, + filters []*mqttFilter, fromSubProto, trace bool) ([]*subscription, error) { + + // Helper to determine if we need to create a separate top-level + // subscription for a wildcard. + fwc := func(subject string) (bool, string, string) { + if !mqttNeedSubForLevelUp(subject) { + return false, _EMPTY_, _EMPTY_ + } + // Say subject is "foo.>", remove the ".>" so that it becomes "foo" + fwcsubject := subject[:len(subject)-2] + // Change the sid to "foo fwc" + fwcsid := fwcsubject + mqttMultiLevelSidSuffix + + return true, fwcsubject, fwcsid + } + + rmSubjects := map[string]uint64{} + // Preload retained messages for all requested subscriptions. Also, since + // it's the first iteration over the filter list, do some cleanup. + for _, f := range filters { + if f.qos > 2 { + f.qos = 2 + } + if c.mqtt.downgradeQoS2Sub && f.qos == 2 { + c.Warnf("Downgrading subscription QoS2 to QoS1 for %q, as configured", f.filter) + f.qos = 1 + } + + // Do not allow MQTT clients to subscribe directly to internal subjects. + // Otherwise, subjects such as "$MQTT.msgs.*" could be used to bypass + // MQTT subscription permissions. + if strings.HasPrefix(f.filter, mqttPrefix) { + f.qos = mqttSubAckFailure + continue + } + + if f.qos == 2 { + if err := sess.ensurePubRelConsumerSubscription(c); err != nil { + c.Errorf("failed to initialize PUBREL processing: %v", err) + f.qos = mqttSubAckFailure + continue + } + } + + // Find retained messages. + if fromSubProto { + as.addRetainedSubjectsForSubject(rmSubjects, f.filter) + if need, subject, _ := fwc(f.filter); need { + as.addRetainedSubjectsForSubject(rmSubjects, subject) + } + } + } + + var rms map[string]*mqttRetainedMsg + if len(rmSubjects) > 0 { + // Make the best effort to load retained messages. + rms = as.loadRetainedMessages(rmSubjects, c) + } + + // Small helper to add the consumer config to the session. + addJSConsToSess := func(sid string, cc *ConsumerConfig) { + if cc == nil { + return + } + if sess.cons == nil { + sess.cons = make(map[string]*ConsumerConfig) + } + sess.cons[sid] = cc + } + + var err error + subs := make([]*subscription, 0, len(filters)) + for _, f := range filters { + // Skip what's already been identified as a failure. + if f.qos == mqttSubAckFailure { + continue + } + subject := f.filter + bsubject := []byte(subject) + sid := subject + bsid := bsubject + isReserved := isMQTTReservedSubscription(subject) + + var jscons *ConsumerConfig + var jssub *subscription + + // Note that if a subscription already exists on this subject, the + // existing sub is returned. Need to update the qos. + var sub *subscription + var err error + + const processShadowSubs = true + + as.mu.Lock() + sess.mu.Lock() + sub, err = sess.processSub(c, + bsubject, bsid, isReserved, f.qos, // main subject + _EMPTY_, mqttDeliverMsgCbQoS0, // no jsDur for QOS0 + processShadowSubs, + rms, trace, as) + sess.mu.Unlock() + as.mu.Unlock() + + if err != nil { + f.qos = mqttSubAckFailure + sess.cleanupFailedSub(c, sub, jscons, jssub) + continue + } + + // This will create (if not already exist) a JS consumer for + // subscriptions of QoS >= 1. But if a JS consumer already exists and + // the subscription for same subject is now a QoS==0, then the JS + // consumer will be deleted. + jscons, jssub, err = sess.processJSConsumer(c, subject, sid, f.qos, fromSubProto) + if err != nil { + f.qos = mqttSubAckFailure + sess.cleanupFailedSub(c, sub, jscons, jssub) + continue + } + + // Process the wildcard subject if needed. + if need, fwcsubject, fwcsid := fwc(subject); need { + var fwjscons *ConsumerConfig + var fwjssub *subscription + var fwcsub *subscription + + // See note above about existing subscription. + as.mu.Lock() + sess.mu.Lock() + fwcsub, err = sess.processSub(c, + []byte(fwcsubject), []byte(fwcsid), isReserved, f.qos, // FWC (top-level wildcard) subject + _EMPTY_, mqttDeliverMsgCbQoS0, // no jsDur for QOS0 + processShadowSubs, + rms, trace, as) + sess.mu.Unlock() + as.mu.Unlock() + if err != nil { + // c.processSub already called c.Errorf(), so no need here. + f.qos = mqttSubAckFailure + sess.cleanupFailedSub(c, sub, jscons, jssub) + continue + } + + fwjscons, fwjssub, err = sess.processJSConsumer(c, fwcsubject, fwcsid, f.qos, fromSubProto) + if err != nil { + // c.processSub already called c.Errorf(), so no need here. + f.qos = mqttSubAckFailure + sess.cleanupFailedSub(c, sub, jscons, jssub) + sess.cleanupFailedSub(c, fwcsub, fwjscons, fwjssub) + continue + } + + subs = append(subs, fwcsub) + addJSConsToSess(fwcsid, fwjscons) + } + + subs = append(subs, sub) + addJSConsToSess(sid, jscons) + } + + if fromSubProto { + err = sess.update(filters, true) + } + + return subs, err +} + +// Retained publish messages matching this subscription are serialized in the +// subscription's `prm` mqtt writer. This buffer will be queued for outbound +// after the subscription is processed and SUBACK is sent or possibly when +// server processes an incoming published message matching the newly +// registered subscription. +// +// Runs from the client's readLoop. +// Account session manager lock held on entry. +// Session lock held on entry. +func (as *mqttAccountSessionManager) serializeRetainedMsgsForSub(rms map[string]*mqttRetainedMsg, sess *mqttSession, c *client, sub *subscription, trace bool) { + if as.retmsgs.Size() == 0 || len(rms) == 0 { + return + } + toTrace := []mqttPublish{} + as.retmsgs.Match(sub.subject, func(subj []byte, _ *mqttRetainedMsgRef) { + rm := rms[string(subj)] + if rm == nil { + // This should not happen since we pre-load messages into rms before + // calling serialize. + return + } + // A broad wildcard subscription can overlap a subscribe deny clause. + c.mu.Lock() + denied := c.mperms != nil && c.checkDenySub(string(subj), bytesToString(sub.queue)) + c.mu.Unlock() + if denied { + return + } + var pi uint16 + qos := min(mqttGetQoS(rm.Flags), sub.mqtt.qos) + if c.mqtt.rejectQoS2Pub && qos == 2 { + c.Warnf("Rejecting retained message with QoS2 for subscription %q, as configured", sub.subject) + return + } + if qos > 0 { + pi = sess.trackPublishRetained(string(sub.sid)) + + // If we failed to get a PI for this message, send it as a QoS0, the + // best we can do? + if pi == 0 { + qos = 0 + } + } + + // Need to use the subject for the retained message, not the `sub` subject. + // We can find the published retained message in rm.sub.subject. + // Set the RETAIN flag: [MQTT-3.3.1-8]. + flags, headerBytes := mqttMakePublishHeader(pi, qos, false, true, []byte(rm.Topic), len(rm.Msg)) + c.mu.Lock() + sub.mqtt.prm = append(sub.mqtt.prm, headerBytes, rm.Msg) + c.mu.Unlock() + if trace { + toTrace = append(toTrace, mqttPublish{ + topic: []byte(rm.Topic), + flags: flags, + pi: pi, + sz: len(rm.Msg), + }) + } + }) + for _, pp := range toTrace { + c.traceOutOp("PUBLISH", []byte(mqttPubTrace(&pp))) + } +} + +// Appends the stored message subjects for all retained message records that +// match the given subscription's `subject` (which could have wildcards). +// +// Account session manager NOT lock held on entry. +func (as *mqttAccountSessionManager) addRetainedSubjectsForSubject(list map[string]uint64, topSubject string) { + as.mu.RLock() + defer as.mu.RUnlock() + + if as.retmsgs.Size() == 0 { + return + } + + as.retmsgs.Match(stringToBytes(topSubject), func(subj []byte, ret *mqttRetainedMsgRef) { + subject := string(subj) + if _, ok := list[subject]; ok { + return + } + if seq := ret.sseq; seq > 0 { + list[subject] = seq + } + }) +} + +type warner interface { + Warnf(format string, v ...any) +} + +// Loads a list of retained messages given a list of stored message subjects. +func (as *mqttAccountSessionManager) loadRetainedMessages(subjects map[string]uint64, w warner) map[string]*mqttRetainedMsg { + rms := make(map[string]*mqttRetainedMsg, len(subjects)) + ss := []string{} + for s := range subjects { + if rm := as.getCachedRetainedMsg(s); rm != nil { + rms[s] = rm + } else { + ss = append(ss, mqttRetainedMsgsStreamSubject+s) + } + } + + if len(ss) == 0 { + return rms + } + + // Although we have the stream sequence for a given subject, we still use + // the load with "last for subject" because it will cover the cases where a + // new retained message has arrived since we collected the subject/seq pair. + // If we were doing a load "by seq" and the message is not found, we would + // incorrectly remove the retained message from our map. + results, err := as.jsa.loadLastMsgForMulti(mqttRetainedMsgsStreamName, ss) + // If an error occurred, warn, but then proceed with what we got. + if err != nil { + w.Warnf("error loading retained messages: %v", err) + } + for i, result := range results { + if result == nil { + continue // skip requests that timed out + } + if err := result.ToError(); err != nil { + // Skip the "$MQTT.rmsgs." prefix... + subj := ss[i][len(mqttRetainedMsgsStreamSubject):] + if IsNatsErr(err, JSNoMessageFoundErr) { + // If there is no message for that subject, delete from our map. + // The good thing here is that we handle the race where a retained + // message may just arrive and be replacing it in the map. The + // removeRetainedMsg() function below will not remove if the sequence + // does not match. + seq := subjects[subj] + as.removeRetainedMsg(subj, seq) + } + w.Warnf("failed to load retained message for subject %q: %v", subj, err) + continue + } + // Guard before dereferencing below. + if result.Message == nil { + continue + } + rm, err := mqttDecodeRetainedMessage(result.Message.Subject, result.Message.Header, result.Message.Data) + if err != nil { + // Unlikely that we can recover from that, so remove the message. + // (see comment above if failing to load the message). + subj := ss[i][len(mqttRetainedMsgsStreamSubject):] + seq := subjects[subj] + as.removeRetainedMsg(subj, seq) + w.Warnf("failed to decode retained message for subject %q: %v", subj, err) + continue + } + + // Add the loaded retained message to the cache, and to the results map. + // We don't need setCachedRetainedMsg() to clone the `rm.Msg` bytes slice + // since we own it. + as.setCachedRetainedMsg(rm.Subject, rm, false, false) + rms[rm.Subject] = rm + } + return rms +} + +// Composes a NATS message for a storeable mqttRetainedMsg. +// If the body is empty, the flags are encoded in a way that will cause older +// servers to fail to decode the message in processRetainedMsg callback and +// will simply ignore it, which is what we want. +func mqttEncodeRetainedMessage(rm *mqttRetainedMsg) (natsMsg []byte, headerLen int) { + delRM := len(rm.Msg) == 0 + + // No need to encode the subject, we can restore it from topic. + l := len(hdrLine) + l += len(mqttNatsRetainedMessageTopic) + 1 + len(rm.Topic) + 2 // 1 byte for ':', 2 bytes for CRLF + if rm.Origin != _EMPTY_ { + l += len(mqttNatsRetainedMessageOrigin) + 1 + len(rm.Origin) + 2 // 1 byte for ':', 2 bytes for CRLF + } + if rm.Source != _EMPTY_ { + l += len(mqttNatsRetainedMessageSource) + 1 + len(rm.Source) + 2 // 1 byte for ':', 2 bytes for CRLF + } + l += len(mqttNatsRetainedMessageFlags) + 1 + 2 + 2 // 1 byte for ':', 2 bytes for the flags, 2 bytes for CRLF + l += 2 // 2 bytes for the extra CRLF after the header + if delRM { + l++ // Will add the delete marker before the flag + } else { + l += len(rm.Msg) + } + + buf := bytes.NewBuffer(make([]byte, 0, l)) + + buf.WriteString(hdrLine) + + buf.WriteString(mqttNatsRetainedMessageTopic) + buf.WriteByte(':') + buf.WriteString(rm.Topic) + buf.WriteString(_CRLF_) + + buf.WriteString(mqttNatsRetainedMessageFlags) + buf.WriteByte(':') + if delRM { + buf.WriteByte(mqttRetainedFlagDelMarker) + } + buf.WriteString(strconv.FormatUint(uint64(rm.Flags), 16)) + buf.WriteString(_CRLF_) + + if rm.Origin != _EMPTY_ { + buf.WriteString(mqttNatsRetainedMessageOrigin) + buf.WriteByte(':') + buf.WriteString(rm.Origin) + buf.WriteString(_CRLF_) + } + if rm.Source != _EMPTY_ { + buf.WriteString(mqttNatsRetainedMessageSource) + buf.WriteByte(':') + buf.WriteString(rm.Source) + buf.WriteString(_CRLF_) + } + + // End of header, finalize + buf.WriteString(_CRLF_) + headerLen = buf.Len() + buf.Write(rm.Msg) + return buf.Bytes(), headerLen +} + +func mqttSliceHeaders(headers map[string][]byte, hdr []byte) { + // Skip the hdrLine + if !bytes.HasPrefix(hdr, stringToBytes(hdrLine)) { + return + } + crLFAsBytes := stringToBytes(CR_LF) + for i := len(hdrLine); i < len(hdr); { + // Search for key/val delimiter. + del := bytes.IndexByte(hdr[i:], ':') + // Not found or key is length 0, we stop. + if del < 0 || del == i { + break + } + keyStart := i + // Walk back to remove spaces between the key and ':' if applicable. + index := keyStart + del - 1 + for index > keyStart && hdr[index] == ' ' { + index-- + } + key := hdr[keyStart : index+1] + // If what we had is only spaces, we stop. + if len(key) == 0 { + break + } + i += del + 1 + valStart := i + // Search for `\r\n`. + nl := bytes.Index(hdr[valStart:], crLFAsBytes) + // If we don't find, we stop. + if nl < 0 { + break + } + // Look if the caller is interested in this key. + if _, ok := headers[bytesToString(key)]; ok { + index := valStart + // Remove possible spaces between the ':' and the value. + for index < valStart+nl && hdr[index] == ' ' { + index++ + } + // Create a slice and limit capacity to the value range. + val := hdr[index : valStart+nl : valStart+nl] + // Record in the caller's map the value for this key. + headers[bytesToString(key)] = val + } + // Reposition to past the `\r\n`. + i += nl + 2 + } +} + +// Decodes a retained message based on the content of the header `h`. +// The returned `*mqttRetainedMsg` object will hold a reference to `m`. +// If the buffer `m` is not owned by the caller, it is the caller +// responsibility to make a copy of the byte slice. +func mqttDecodeRetainedMessage(subject string, h, m []byte) (*mqttRetainedMsg, error) { + headers := map[string][]byte{ + mqttNatsRetainedMessageOrigin: nil, + mqttNatsRetainedMessageFlags: nil, + mqttNatsRetainedMessageSource: nil, + } + var rm *mqttRetainedMsg + // Retrieve the values for the above headers. + mqttSliceHeaders(headers, h) + // Get the flag header. + fHeader := headers[mqttNatsRetainedMessageFlags] + // If we don't, it could be that this is an old retained message that + // was JSON encoded. + if len(fHeader) > 0 { + if len(fHeader) > 1 && fHeader[0] == mqttRetainedFlagDelMarker { + fHeader = fHeader[1:] + } + flagsUint, err := strconv.ParseUint(bytesToString(fHeader), 16, 8) + if err != nil { + // Since the error is currently not reported in the server, we + // will simply replace with this one. + return nil, errMQTTInvalidRetainFlags + } + rm = &mqttRetainedMsg{ + Flags: byte(flagsUint), + Origin: string(headers[mqttNatsRetainedMessageOrigin]), + Source: string(headers[mqttNatsRetainedMessageSource]), + Msg: m, + } + } else { + if err := json.Unmarshal(m, &rm); err != nil { + return nil, err + } + if rm == nil { + return nil, errMQTTInvalidRetainedMessage + } + } + // Now check that the values are correct. + // + // For "Flags", anything at or above binary (1111) is too big. + if rm.Flags >= mqttPacketFlagMask { + return nil, errMQTTInvalidRetainFlags + } + if qos := mqttGetQoS(rm.Flags); qos > 2 { + return nil, errMQTTInvalidRetainFlags + } + // We store `Topic` in the retained message because we used to store + // all retained messages under the same subject `$MQTT_rmsgs` in + // the retained messages stream. That is no longer the case, and to + // cover setups where the retained message stream is sourced from another + // account and has some subject transforms, simply reconstruct the + // topic/subject based on the `subject` passed to this function. + rm.Subject = strings.TrimPrefix(subject, mqttRetainedMsgsStreamSubject) + rm.Topic = bytesToString(natsSubjectStrToMQTTTopic(rm.Subject)) + return rm, nil +} + +// Creates the session stream (limit msgs of 1) for this client ID if it does +// not already exist. If it exists, recover the single record to rebuild the +// state of the session. If there is a session record but this session is not +// registered in the runtime of this server, then a request is made to the +// owner to close the client associated with this session since specification +// [MQTT-3.1.4-2] specifies that if the ClientId represents a Client already +// connected to the Server then the Server MUST disconnect the existing client. +// +// Runs from the client's readLoop. +// Lock not held on entry, but session is in the locked map. +func (as *mqttAccountSessionManager) createOrRestoreSession(clientID string, opts *Options) (*mqttSession, bool, error) { + jsa := &as.jsa + + hash := getHash(clientID) + smsg, err := jsa.loadSessionMsg(as.domainTk, hash) + if err != nil { + if isErrorOtherThan(err, JSNoMessageFoundErr) { + return nil, false, fmt.Errorf("loading session record: %w", err) + } + // Message not found, so reate the session... + // Create a session and indicate that this session did not exist. + sess := mqttSessionCreate(jsa, clientID, hash, 0, opts) + sess.domainTk = as.domainTk + return sess, false, nil + } + // We need to recover the existing record now. + ps := &mqttPersistedSession{} + if err := json.Unmarshal(smsg.Data, ps); err != nil { + return nil, false, fmt.Errorf("unmarshal of session record at sequence %v: %w", smsg.Sequence, err) + } + if ps.ID != clientID { + return nil, false, errMQTTSessionCollision + } + for sid, cc := range ps.Cons { + if cc == nil { + delete(ps.Cons, sid) + } + } + + // Restore this session (even if we don't own it), the caller will do the right thing. + sess := mqttSessionCreate(jsa, clientID, hash, smsg.Sequence, opts) + sess.domainTk = as.domainTk + sess.clean = ps.Clean + sess.subs = ps.Subs + sess.cons = ps.Cons + sess.pubRelConsumer = ps.PubRel + as.addSession(sess, true) + return sess, true, nil +} + +// Sends a request to delete a message, but does not wait for the response. +// +// No lock held on entry. +func (as *mqttAccountSessionManager) deleteRetainedMsg(seq uint64) { + as.jsa.deleteMsg(mqttRetainedMsgsStreamName, seq, false) +} + +// Sends a message indicating that a retained message on a given subject and stream sequence +// is being removed. +// NOTE: This is maintained for backward compatibility reasons. Should be removed in 2.14/2.15? +func (as *mqttAccountSessionManager) notifyRetainedMsgDeleted(subject string, seq uint64) { + req := mqttRetMsgDel{ + Subject: subject, + Seq: seq, + } + b, _ := json.Marshal(&req) + jsa := &as.jsa + jsa.sendq.push(&mqttJSPubMsg{ + subj: jsa.rplyr + mqttJSARetainedMsgDel, + msg: b, + }) +} + +func (as *mqttAccountSessionManager) transferUniqueSessStreamsToMuxed(log *Server) { + // Set retry to true, will be set to false on success. + retry := true + defer func() { + if retry { + next := mqttDefaultTransferRetry + log.Warnf("Failed to transfer all MQTT session streams, will try again in %v", next) + time.AfterFunc(next, func() { as.transferUniqueSessStreamsToMuxed(log) }) + } + }() + + jsa := &as.jsa + sni, err := jsa.newRequestEx(mqttJSAStreamNames, JSApiStreams, _EMPTY_, 0, nil) + if err != nil { + log.Errorf("Unable to transfer MQTT session streams: %v", err) + return + } + snames := sni.(*JSApiStreamNamesResponse) + if snames.Error != nil { + log.Errorf("Unable to transfer MQTT session streams: %v", snames.ToError()) + return + } + var oldMQTTSessStreams []string + for _, sn := range snames.Streams { + if strings.HasPrefix(sn, mqttSessionsStreamNamePrefix) { + oldMQTTSessStreams = append(oldMQTTSessStreams, sn) + } + } + ns := len(oldMQTTSessStreams) + if ns == 0 { + // Nothing to do + retry = false + return + } + log.Noticef("Transferring %v MQTT session streams...", ns) + for _, sn := range oldMQTTSessStreams { + log.Noticef(" Transferring stream %q to %q", sn, mqttSessStreamName) + smsg, err := jsa.loadLastMsgFor(sn, sn) + if err != nil { + log.Errorf(" Unable to load session record: %v", err) + return + } + ps := &mqttPersistedSession{} + if err := json.Unmarshal(smsg.Data, ps); err != nil { + log.Warnf(" Unable to unmarshal the content of this stream, may not be a legitimate MQTT session stream, skipping") + continue + } + // Store record to MQTT session stream + if _, err := jsa.storeSessionMsg(as.domainTk, getHash(ps.ID), 0, smsg.Data); err != nil { + log.Errorf(" Unable to transfer the session record: %v", err) + return + } + jsa.deleteStream(sn) + } + log.Noticef("Transfer of %v MQTT session streams done!", ns) + retry = false +} + +func (as *mqttAccountSessionManager) transferRetainedToPerKeySubjectStream(log *Server) error { + jsa := &as.jsa + var processed int + var transferred int + + start := time.Now() + deadline := start.Add(mqttRetainedTransferTimeout) + for { + // Try and look up messages on the original undivided "$MQTT.rmsgs" subject. + // If nothing is returned here, we assume to have migrated all old messages. + smsg, err := jsa.loadNextMsgFor(mqttRetainedMsgsStreamName, "$MQTT.rmsgs") + if IsNatsErr(err, JSNoMessageFoundErr) { + // We've ran out of messages to transfer, done. + break + } + if err != nil { + log.Warnf(" Unable to transfer a retained message: failed to load from '$MQTT.rmsgs': %s", err) + return err + } + + // Unmarshal the message so that we can obtain the subject name. Do not + // use mqttDecodeRetainedMessage() here because these messages are from + // older versions, and contain the full JSON encoding in payload. + var rmsg mqttRetainedMsg + if err = json.Unmarshal(smsg.Data, &rmsg); err == nil { + // Store the message again, this time with the new per-key subject. + subject := mqttRetainedMsgsStreamSubject + rmsg.Subject + if _, err = jsa.storeMsg(subject, 0, smsg.Data); err != nil { + log.Errorf(" Unable to transfer the retained message with sequence %d: %v", smsg.Sequence, err) + } + transferred++ + } else { + log.Warnf(" Unable to unmarshal retained message with sequence %d, skipping", smsg.Sequence) + } + + // Delete the original message. + if err := jsa.deleteMsg(mqttRetainedMsgsStreamName, smsg.Sequence, true); err != nil { + log.Errorf(" Unable to clean up the retained message with sequence %d: %v", smsg.Sequence, err) + return err + } + processed++ + + now := time.Now() + if now.After(deadline) { + err := fmt.Errorf("timed out while transferring retained messages from '$MQTT.rmsgs' after %v, %d processed, %d successfully transferred", now.Sub(start), processed, transferred) + log.Noticef(err.Error()) + return err + } + } + if processed > 0 { + log.Noticef("Processed %d messages from '$MQTT.rmsgs', successfully transferred %d in %v", processed, transferred, time.Since(start)) + } else { + log.Debugf("No messages found to transfer from '$MQTT.rmsgs'") + } + return nil +} + +func (as *mqttAccountSessionManager) getCachedRetainedMsg(subject string) *mqttRetainedMsg { + v, ok := as.rmsCache.Load(subject) + if !ok { + return nil + } + rm := v.(*mqttRetainedMsg) + if rm.expiresFromCache.Before(time.Now()) { + as.rmsCache.Delete(subject) + return nil + } + return rm +} + +// If cache is enabled, the expiration for the `rm` is bumped by +// `mqttRetainedCacheTTL` seconds. +// If `onlyReplace` is true, then the `rm` object is stored in the cache using +// the `subject` key only if there was already an object stored under that key. +// If `copyMsgBytes` is true, then the `rm.Msg` bytes are copied (because it +// references some buffer that is not owned by the caller). +// +// Note: currently `onlyReplace` and `cloneMsgBytes` always have the same +// value (all `true` or all `false`) however we use different booleans to +// better express the intent. +func (as *mqttAccountSessionManager) setCachedRetainedMsg(subject string, rm *mqttRetainedMsg, onlyReplace, copyMsgBytes bool) { + if rm == nil { + return + } + rm.expiresFromCache = time.Now().Add(mqttRetainedCacheTTL) + if onlyReplace { + if _, ok := as.rmsCache.Load(subject); !ok { + return + } + } + if copyMsgBytes { + rm.Msg = copyBytes(rm.Msg) + } + as.rmsCache.Store(subject, rm) +} + +////////////////////////////////////////////////////////////////////////////// +// +// MQTT session related functions +// +////////////////////////////////////////////////////////////////////////////// + +// Returns a new mqttSession object with max ack pending set based on +// option or use mqttDefaultMaxAckPending if no option set. +func mqttSessionCreate(jsa *mqttJSA, id, idHash string, seq uint64, opts *Options) *mqttSession { + maxp := opts.MQTT.MaxAckPending + if maxp == 0 { + maxp = mqttDefaultMaxAckPending + } + + return &mqttSession{ + jsa: jsa, + id: id, + idHash: idHash, + seq: seq, + maxp: maxp, + pubRelSubject: mqttPubRelSubjectPrefix + idHash, + pubRelDeliverySubject: mqttPubRelDeliverySubjectPrefix + idHash, + pubRelDeliverySubjectB: []byte(mqttPubRelDeliverySubjectPrefix + idHash), + } +} + +// Persists a session. Note that if the session's current client does not match +// the given client, nothing is done. +// +// Lock not held on entry. +func (sess *mqttSession) save() error { + sess.mu.Lock() + ps := mqttPersistedSession{ + Origin: sess.jsa.id, + ID: sess.id, + Clean: sess.clean, + Subs: sess.subs, + Cons: sess.cons, + PubRel: sess.pubRelConsumer, + } + b, _ := json.Marshal(&ps) + + domainTk, cidHash := sess.domainTk, sess.idHash + seq := sess.seq + sess.mu.Unlock() + + var hdr int + if seq != 0 { + bb := bytes.Buffer{} + bb.WriteString(hdrLine) + bb.WriteString(JSExpectedLastSubjSeq) + bb.WriteString(":") + bb.WriteString(strconv.FormatInt(int64(seq), 10)) + bb.WriteString(CR_LF) + bb.WriteString(CR_LF) + hdr = bb.Len() + bb.Write(b) + b = bb.Bytes() + } + + resp, err := sess.jsa.storeSessionMsg(domainTk, cidHash, hdr, b) + if err != nil { + return fmt.Errorf("unable to persist session %q (seq=%v): %v", ps.ID, seq, err) + } + // Guard before dereferencing below. + if resp == nil || resp.PubAck == nil { + return fmt.Errorf("unable to persist session %q (seq=%v): invalid pub ack response", ps.ID, seq) + } + sess.mu.Lock() + sess.seq = resp.Sequence + sess.mu.Unlock() + return nil +} + +// Clear the session. +// +// Runs from the client's readLoop. +// Lock not held on entry, but session is in the locked map. +func (sess *mqttSession) clear(noWait bool) error { + var durs []string + var pubRelDur string + + sess.mu.Lock() + id := sess.id + seq := sess.seq + if l := len(sess.cons); l > 0 { + durs = make([]string, 0, l) + } + for sid, cc := range sess.cons { + delete(sess.cons, sid) + durs = append(durs, cc.Durable) + } + if sess.pubRelConsumer != nil { + pubRelDur = sess.pubRelConsumer.Durable + } + + sess.subs = nil + sess.pendingPublish = nil + sess.pendingPubRel = nil + sess.cpending = nil + sess.pubRelConsumer = nil + sess.seq = 0 + sess.tmaxack = 0 + // Discarded session: reset the PI counter too so a reused session object + // does not inherit the previous session's identifiers. Spec [MQTT-3.1.2-6]. + sess.last_pi = 0 + sess.mu.Unlock() + + for _, dur := range durs { + if _, err := sess.jsa.deleteConsumer(mqttStreamName, dur, noWait); isErrorOtherThan(err, JSConsumerNotFoundErr) { + return fmt.Errorf("unable to delete consumer %q for session %q: %v", dur, sess.id, err) + } + } + if pubRelDur != _EMPTY_ { + _, err := sess.jsa.deleteConsumer(mqttOutStreamName, pubRelDur, noWait) + if isErrorOtherThan(err, JSConsumerNotFoundErr) { + return fmt.Errorf("unable to delete consumer %q for session %q: %v", pubRelDur, sess.id, err) + } + } + + if seq > 0 { + err := sess.jsa.deleteMsg(mqttSessStreamName, seq, !noWait) + // Ignore the various errors indicating that the message (or sequence) + // is already deleted, can happen in a cluster. + if isErrorOtherThan(err, JSSequenceNotFoundErrF) { + if isErrorOtherThan(err, JSStreamMsgDeleteFailedF) || !strings.Contains(err.Error(), ErrStoreMsgNotFound.Error()) { + return fmt.Errorf("unable to delete session %q record at sequence %v: %v", id, seq, err) + } + } + } + return nil +} + +// This will update the session record for this client in the account's MQTT +// sessions stream if the session had any change in the subscriptions. +// +// Runs from the client's readLoop. +// Lock not held on entry, but session is in the locked map. +func (sess *mqttSession) update(filters []*mqttFilter, add bool) error { + // Evaluate if we need to persist anything. + var needUpdate bool + for _, f := range filters { + if add { + if f.qos == mqttSubAckFailure { + continue + } + if qos, ok := sess.subs[f.filter]; !ok || qos != f.qos { + if sess.subs == nil { + sess.subs = make(map[string]byte) + } + sess.subs[f.filter] = f.qos + needUpdate = true + } + } else { + if _, ok := sess.subs[f.filter]; ok { + delete(sess.subs, f.filter) + needUpdate = true + } + } + } + var err error + if needUpdate { + err = sess.save() + } + return err +} + +func (sess *mqttSession) bumpPI() uint16 { + var avail bool + next := sess.last_pi + for i := 0; i < 0xFFFF; i++ { + next++ + if next == 0 { + next = 1 + } + + _, usedInPublish := sess.pendingPublish[next] + _, usedInPubRel := sess.pendingPubRel[next] + if !usedInPublish && !usedInPubRel { + sess.last_pi = next + avail = true + break + } + } + if !avail { + return 0 + } + return sess.last_pi +} + +// mqttRetainedPendingDur returns the pseudo consumer-durable key under which a +// subscription's in-flight retained QoS deliveries are tracked in cpending. +// Retained deliveries have no JS consumer, but keying them per subscription +// lets unsubscribe/downgrade teardown purge them like consumer deliveries. +// Cannot collide with real durables (idHash+"_"+nuid, $MQTT_PUBREL_ prefix). +func mqttRetainedPendingDur(sid string) string { + return mqttRetainedMsgsStreamName + "/" + sid +} + +// trackPublishRetained is invoked when a retained (QoS) message is published. +// It needs a new PI to be allocated, so we add it to the pendingPublish map, +// and serialize it in cpending under the subscription's pseudo-durable key +// (with the PI as the sequence) so consumer teardown purges it; an ack removes +// both entries via untrackPublish. +// +// Lock held on entry +func (sess *mqttSession) trackPublishRetained(sid string) uint16 { + // Make sure we initialize the tracking maps. + if sess.pendingPublish == nil { + sess.pendingPublish = make(map[uint16]*mqttPending) + } + if sess.cpending == nil { + sess.cpending = make(map[string]map[uint64]uint16) + } + + pi := sess.bumpPI() + if pi == 0 { + return 0 + } + dur := mqttRetainedPendingDur(sid) + sseqToPi := sess.cpending[dur] + if sseqToPi == nil { + sseqToPi = make(map[uint64]uint16) + sess.cpending[dur] = sseqToPi + } + sseqToPi[uint64(pi)] = pi + sess.pendingPublish[pi] = &mqttPending{jsDur: dur, sseq: uint64(pi)} + + return pi +} + +// trackPublish is invoked when a (QoS) PUBLISH message is to be delivered. It +// detects an untracked (new) message based on its sequence extracted from its +// delivery-time jsAckSubject, and adds it to the tracking maps. Returns a PI to +// use for the message (new, or previously used), and whether this is a +// duplicate delivery attempt. +// +// Lock held on entry +func (sess *mqttSession) trackPublish(jsDur, jsAckSubject string) (uint16, bool) { + var dup bool + var pi uint16 + + if jsAckSubject == _EMPTY_ || jsDur == _EMPTY_ { + return 0, false + } + + // Make sure we initialize the tracking maps. + if sess.pendingPublish == nil { + sess.pendingPublish = make(map[uint16]*mqttPending) + } + if sess.cpending == nil { + sess.cpending = make(map[string]map[uint64]uint16) + } + + // Get the stream sequence and duplicate flag from the ack reply subject. + sseq, _, dcount, _, _ := ackReplyInfo(jsAckSubject) + if dcount > 1 { + dup = true + } + + var ack *mqttPending + sseqToPi, ok := sess.cpending[jsDur] + if !ok { + sseqToPi = make(map[uint64]uint16) + sess.cpending[jsDur] = sseqToPi + } else { + pi = sseqToPi[sseq] + } + + if pi != 0 { + // There is a possible race between a PUBLISH re-delivery calling us, + // and a PUBREC received already having submitting a PUBREL into JS . If + // so, indicate no need for (re-)delivery by returning a PI of 0. + _, usedForPubRel := sess.pendingPubRel[pi] + if /*dup && */ usedForPubRel { + return 0, false + } + + // We should have a pending JS ACK for this PI. + ack = sess.pendingPublish[pi] + } else { + // sess.maxp will always have a value > 0. + if len(sess.pendingPublish) >= int(sess.maxp) { + // Indicate that we did not assign a packet identifier. + // The caller will not send the message to the subscription + // and JS will redeliver later, based on consumer's AckWait. + return 0, false + } + + pi = sess.bumpPI() + if pi == 0 { + return 0, false + } + + sseqToPi[sseq] = pi + } + + if ack == nil { + sess.pendingPublish[pi] = &mqttPending{ + jsDur: jsDur, + sseq: sseq, + jsAckSubject: jsAckSubject, + } + } else { + ack.jsAckSubject = jsAckSubject + ack.sseq = sseq + ack.jsDur = jsDur + } + + return pi, dup +} + +// Stops a PI from being tracked as a PUBLISH. It can still be in use for a +// pending PUBREL. +// +// Lock held on entry +func (sess *mqttSession) untrackPublish(pi uint16) (jsAckSubject string) { + ack, ok := sess.pendingPublish[pi] + if !ok { + return _EMPTY_ + } + + delete(sess.pendingPublish, pi) + // Do NOT reset last_pi here: it is a monotonic rolling counter (see bumpPI) so + // a just-freed id is not reused within a delivery burst, which a client would + // read as a duplicate. Spec [MQTT-2.3.1-4]. + + if len(sess.cpending) != 0 && ack.jsDur != _EMPTY_ { + if sseqToPi := sess.cpending[ack.jsDur]; sseqToPi != nil { + delete(sseqToPi, ack.sseq) + } + } + + return ack.jsAckSubject +} + +// trackAsPubRel is invoked in 2 cases: (a) when we receive a PUBREC and we need +// to change from tracking the PI as a PUBLISH to a PUBREL; and (b) when we +// attempt to deliver the PUBREL to record the JS ack subject for it. +// +// Lock held on entry +func (sess *mqttSession) trackAsPubRel(pi uint16, jsAckSubject string) { + if sess.pubRelConsumer == nil { + // The cosumer MUST be set up already. + return + } + jsDur := sess.pubRelConsumer.Durable + + if sess.pendingPubRel == nil { + sess.pendingPubRel = make(map[uint16]*mqttPending) + } + + if jsAckSubject == _EMPTY_ { + sess.pendingPubRel[pi] = &mqttPending{ + jsDur: jsDur, + } + return + } + + sseq, _, _, _, _ := ackReplyInfo(jsAckSubject) + + if sess.cpending == nil { + sess.cpending = make(map[string]map[uint64]uint16) + } + sseqToPi := sess.cpending[jsDur] + if sseqToPi == nil { + sseqToPi = make(map[uint64]uint16) + sess.cpending[jsDur] = sseqToPi + } + sseqToPi[sseq] = pi + sess.pendingPubRel[pi] = &mqttPending{ + jsDur: sess.pubRelConsumer.Durable, + sseq: sseq, + jsAckSubject: jsAckSubject, + } +} + +// Stops a PI from being tracked as a PUBREL. +// +// Lock held on entry +func (sess *mqttSession) untrackPubRel(pi uint16) (jsAckSubject string) { + ack, ok := sess.pendingPubRel[pi] + if !ok { + return _EMPTY_ + } + + delete(sess.pendingPubRel, pi) + + if sess.pubRelConsumer != nil && len(sess.cpending) > 0 { + if sseqToPi := sess.cpending[ack.jsDur]; sseqToPi != nil { + delete(sseqToPi, ack.sseq) + } + } + + return ack.jsAckSubject +} + +// Sends a consumer delete request, but does not wait for response. +// +// Lock not held on entry. +func (sess *mqttSession) deleteConsumer(cc *ConsumerConfig) { + sess.mu.Lock() + sess.tmaxack -= cc.MaxAckPending + sess.jsa.deleteConsumer(mqttStreamName, cc.Durable, true) + sess.mu.Unlock() +} + +////////////////////////////////////////////////////////////////////////////// +// +// CONNECT protocol related functions +// +////////////////////////////////////////////////////////////////////////////// + +// Parse the MQTT connect protocol +func (c *client) mqttParseConnect(r *mqttReader, hasMappings bool) (byte, *mqttConnectProto, error) { + // Protocol name + proto, err := r.readBytes("protocol name", false) + if err != nil { + return 0, nil, err + } + + // Spec [MQTT-3.1.2-1] + if !bytes.Equal(proto, mqttProtoName) { + // Check proto name against v3.1 to report better error + if bytes.Equal(proto, mqttOldProtoName) { + return 0, nil, fmt.Errorf("older protocol %q not supported", proto) + } + return 0, nil, fmt.Errorf("expected connect packet with protocol name %q, got %q", mqttProtoName, proto) + } + + // Protocol level + level, err := r.readByte("protocol level") + if err != nil { + return 0, nil, err + } + // Spec [MQTT-3.1.2-2] + if level != mqttProtoLevel { + return mqttConnAckRCUnacceptableProtocolVersion, nil, fmt.Errorf("unacceptable protocol version of %v", level) + } + + cp := &mqttConnectProto{} + // Connect flags + cp.flags, err = r.readByte("flags") + if err != nil { + return 0, nil, err + } + + // Spec [MQTT-3.1.2-3] + if cp.flags&mqttConnFlagReserved != 0 { + return 0, nil, errMQTTConnFlagReserved + } + + var hasWill bool + wqos := (cp.flags & mqttConnFlagWillQoS) >> 3 + wretain := cp.flags&mqttConnFlagWillRetain != 0 + // Spec [MQTT-3.1.2-11] + if cp.flags&mqttConnFlagWillFlag == 0 { + // Spec [MQTT-3.1.2-13] + if wqos != 0 { + return 0, nil, fmt.Errorf("if Will flag is set to 0, Will QoS must be 0 too, got %v", wqos) + } + // Spec [MQTT-3.1.2-15] + if wretain { + return 0, nil, errMQTTWillAndRetainFlag + } + } else { + // Spec [MQTT-3.1.2-14] + if wqos == 3 { + return 0, nil, fmt.Errorf("if Will flag is set to 1, Will QoS can be 0, 1 or 2, got %v", wqos) + } + hasWill = true + } + + if c.mqtt.rejectQoS2Pub && hasWill && wqos == 2 { + return mqttConnAckRCQoS2WillRejected, nil, fmt.Errorf("server does not accept QoS2 for Will messages") + } + + // Spec [MQTT-3.1.2-19] + hasUser := cp.flags&mqttConnFlagUsernameFlag != 0 + // Spec [MQTT-3.1.2-21] + hasPassword := cp.flags&mqttConnFlagPasswordFlag != 0 + // Spec [MQTT-3.1.2-22] + if !hasUser && hasPassword { + return 0, nil, errMQTTPasswordFlagAndNoUser + } + + // Keep alive + var ka uint16 + ka, err = r.readUint16("keep alive") + if err != nil { + return 0, nil, err + } + // Spec [MQTT-3.1.2-24] + if ka > 0 { + cp.rd = time.Duration(float64(ka)*1.5) * time.Second + } + + // Payload starts here and order is mandated by: + // Spec [MQTT-3.1.3-1]: client ID, will topic, will message, username, password + + // Client ID + c.mqtt.cid, err = r.readString("client ID") + if err != nil { + return 0, nil, err + } + // Spec [MQTT-3.1.3-7] + if c.mqtt.cid == _EMPTY_ { + if cp.flags&mqttConnFlagCleanSession == 0 { + return mqttConnAckRCIdentifierRejected, nil, errMQTTCIDEmptyNeedsCleanFlag + } + // Spec [MQTT-3.1.3-6] + c.mqtt.cid = nuid.Next() + } + // Spec [MQTT-3.1.3-4] and [MQTT-3.1.3-9] + if err := mqttValidateString(c.mqtt.cid, "client ID"); err != nil { + return mqttConnAckRCIdentifierRejected, nil, err + } else if !isValidName(c.mqtt.cid) { + // Should not contain characters that make it an invalid name for NATS subjects, etc. + err = fmt.Errorf("invalid character in %s %q", "client ID", c.mqtt.cid) + return mqttConnAckRCIdentifierRejected, nil, err + } + + if hasWill { + cp.will = &mqttWill{ + qos: wqos, + retain: wretain, + } + var topic []byte + // Need to make a copy since we need to hold to this topic after the + // parsing of this protocol. + topic, err = r.readBytes("Will topic", true) + if err != nil { + return 0, nil, err + } + if len(topic) == 0 { + return 0, nil, errMQTTEmptyWillTopic + } + if err := mqttValidateTopic(topic, "Will topic"); err != nil { + return 0, nil, err + } + // Convert MQTT topic to NATS subject + cp.will.subject, err = mqttTopicToNATSPubSubject(topic) + if err != nil { + return 0, nil, err + } + // Check for subject mapping. + if hasMappings { + // For selectMappedSubject to work, we need to have c.pa.subject set. + // If there is a change, c.pa.mapped will be set after the call. + c.pa.subject = cp.will.subject + if changed := c.selectMappedSubject(); changed { + // We need to keep track of the NATS subject/mapped in the `cp` structure. + cp.will.subject = c.pa.subject + cp.will.mapped = c.pa.mapped + // We also now need to map the original MQTT topic to the new topic + // based on the new subject. + topic = natsSubjectToMQTTTopic(cp.will.subject) + } + // Reset those now. + c.pa.subject, c.pa.mapped = nil, nil + } + cp.will.topic = topic + // Now "will" message. + // Ask for a copy since we need to hold to this after parsing of this protocol. + cp.will.message, err = r.readBytes("Will message", true) + if err != nil { + return 0, nil, err + } + } + + if hasUser { + c.opts.Username, err = r.readString("user name") + if err != nil { + return 0, nil, err + } + if c.opts.Username == _EMPTY_ { + return mqttConnAckRCBadUserOrPassword, nil, errMQTTEmptyUsername + } + // Spec [MQTT-3.1.3-11] + if err := mqttValidateString(c.opts.Username, "user name"); err != nil { + return mqttConnAckRCBadUserOrPassword, nil, err + } + } + + if hasPassword { + c.opts.Password, err = r.readString("password") + if err != nil { + return 0, nil, err + } + c.opts.Token = c.opts.Password + } + return 0, cp, nil +} + +func (c *client) mqttConnectTrace(cp *mqttConnectProto) string { + trace := fmt.Sprintf("clientID=%s", c.mqtt.cid) + if cp.rd > 0 { + trace += fmt.Sprintf(" keepAlive=%v", cp.rd) + } + if cp.will != nil { + trace += fmt.Sprintf(" will=(topic=%s QoS=%v retain=%v)", + cp.will.topic, cp.will.qos, cp.will.retain) + } + if cp.flags&mqttConnFlagCleanSession != 0 { + trace += " clean" + } + if c.opts.Username != _EMPTY_ { + trace += fmt.Sprintf(" username=%s", c.opts.Username) + } + if c.opts.Password != _EMPTY_ { + trace += " password=****" + } + return trace +} + +// Process the CONNECT packet. +// +// For the first session on the account, an account session manager will be created, +// along with the JetStream streams/consumer necessary for the working of MQTT. +// +// The session, identified by a client ID, will be registered, or if already existing, +// will be resumed. If the session exists but is associated with an existing client, +// the old client is evicted, as per the specifications. +// +// Due to specific locking requirements around JS API requests, we cannot hold some +// locks for the entire duration of processing of some protocols, therefore, we use +// a map that registers the client ID in a "locked" state. If a different client tries +// to connect and the server detects that the client ID is in that map, it will try +// a little bit until it is not, or fail the new client, since we can't protect +// processing of protocols in the original client. This is not expected to happen often. +// +// Runs from the client's readLoop. +// No lock held on entry. +func (s *Server) mqttProcessConnect(c *client, cp *mqttConnectProto, trace bool) error { + sendConnAck := func(rc byte, sessp bool) { + c.mqttEnqueueConnAck(rc, sessp) + if trace { + c.traceOutOp("CONNACK", []byte(fmt.Sprintf("sp=%v rc=%v", sessp, rc))) + } + } + + c.mu.Lock() + cid := c.mqtt.cid + c.clearAuthTimer() + c.mu.Unlock() + if !s.isClientAuthorized(c) { + if trace { + c.traceOutOp("CONNACK", []byte(fmt.Sprintf("sp=%v rc=%v", false, mqttConnAckRCNotAuthorized))) + } + c.authViolation() + return ErrAuthentication + } + // Now that we are authenticated, we have the client bound to the account. + // Get the account's level MQTT sessions manager. If it does not exists yet, + // this will create it along with the streams where sessions and messages + // are stored. + asm, err := s.getOrCreateMQTTAccountSessionManager(c) + if err != nil { + return err + } + + // Most of the session state is altered only in the readLoop so does not + // need locking. For things that can be access in the readLoop and in + // callbacks, we will use explicit locking. + // To prevent other clients to connect with the same client ID, we will + // add the client ID to a "locked" map so that the connect somewhere else + // is put on hold. + // This keep track of how many times this client is detecting that its + // client ID is in the locked map. After a short amount, the server will + // fail this inbound client. + locked := 0 + +CHECK: + asm.mu.Lock() + // Check if different applications keep trying to connect with the same + // client ID at the same time. + if tm, ok := asm.flappers[cid]; ok { + // If the last time it tried to connect was more than 1 sec ago, + // then accept and remove from flappers map. + if time.Since(tm) > mqttSessJailDur { + asm.removeSessFromFlappers(cid) + } else { + // Will hold this client for a second and then close it. We + // do this so that if the client has a reconnect feature we + // don't end-up with very rapid flapping between apps. + // We need to wait in place and not schedule the connection + // close because if this is a misbehaved client that does + // not wait for the CONNACK and sends other protocols, the + // server would not have a fully setup client and may panic. + asm.mu.Unlock() + select { + case <-s.quitCh: + case <-time.After(mqttSessJailDur): + } + c.closeConnection(DuplicateClientID) + return ErrConnectionClosed + } + } + // If an existing session is in the process of processing some packet, we can't + // evict the old client just yet. So try again to see if the state clears, but + // if it does not, then we have no choice but to fail the new client instead of + // the old one. + if _, ok := asm.sessLocked[cid]; ok { + asm.mu.Unlock() + if locked++; locked == 10 { + return fmt.Errorf("other session with client ID %q is in the process of connecting", cid) + } + time.Sleep(100 * time.Millisecond) + goto CHECK + } + + // Register this client ID the "locked" map for the duration if this function. + asm.sessLocked[cid] = struct{}{} + // And remove it on exit, regardless of error or not. + defer func() { + asm.mu.Lock() + delete(asm.sessLocked, cid) + asm.mu.Unlock() + }() + + // Is the client requesting a clean session or not. + cleanSess := cp.flags&mqttConnFlagCleanSession != 0 + // Session present? Assume false, will be set to true only when applicable. + sessp := false + // Do we have an existing session for this client ID + es, exists := asm.sessions[cid] + asm.mu.Unlock() + formatError := func(err error) error { + return fmt.Errorf("%v for account %q, session %q", err, c.acc.GetName(), cid) + } + + // The session is not in the map, but may be on disk, so try to recover + // or create the stream if not. + if !exists { + es, exists, err = asm.createOrRestoreSession(cid, s.getOpts()) + if err != nil { + if err == errMQTTSessionCollision { + sendConnAck(mqttConnAckRCIdentifierRejected, false) + } + return formatError(err) + } + } + if exists { + // Clear the session if client wants a clean session. + // Also, Spec [MQTT-3.2.2-1]: don't report session present + if cleanSess || es.clean { + // Spec [MQTT-3.1.2-6]: If CleanSession is set to 1, the Client and + // Server MUST discard any previous Session and start a new one. + // This Session lasts as long as the Network Connection. State data + // associated with this Session MUST NOT be reused in any subsequent + // Session. + if err := es.clear(false); err != nil { + asm.removeSession(es, true) + return err + } + } else { + // Report to the client that the session was present + sessp = true + } + // Spec [MQTT-3.1.4-2]. If the ClientId represents a Client already + // connected to the Server then the Server MUST disconnect the existing + // client. + // Bind with the new client. This needs to be protected because can be + // accessed outside of the readLoop. + es.mu.Lock() + ec := es.c + es.c = c + es.clean = cleanSess + // Clear this flag so we resubscribe to PUBREL subject is needed. + es.pubRelSubscribed = false + es.mu.Unlock() + if ec != nil { + // Remove "will" of existing client before closing + ec.mu.Lock() + ec.mqtt.cp.will = nil + ec.mu.Unlock() + // Add to the map of the flappers + asm.mu.Lock() + asm.addSessToFlappers(cid) + asm.mu.Unlock() + c.Warnf("Replacing old client %q since both have the same client ID %q", ec, cid) + // Close old client in separate go routine + go ec.closeConnection(DuplicateClientID) + } + } else { + // Spec [MQTT-3.2.2-3]: if the Server does not have stored Session state, + // it MUST set Session Present to 0 in the CONNACK packet. + es.mu.Lock() + es.c, es.clean = c, cleanSess + es.mu.Unlock() + // Now add this new session into the account sessions + asm.addSession(es, true) + } + // We would need to save only if it did not exist previously, but we save + // always in case we are running in cluster mode. This will notify other + // running servers that this session is being used. + if err := es.save(); err != nil { + asm.removeSession(es, true) + return err + } + c.mu.Lock() + c.flags.set(connectReceived) + c.mqtt.cp = cp + c.mqtt.asm = asm + c.mqtt.sess = es + c.mu.Unlock() + + // Spec [MQTT-3.2.0-1]: CONNACK must be the first protocol sent to the session. + sendConnAck(mqttConnAckRCConnectionAccepted, sessp) + + // Process possible saved subscriptions. + if l := len(es.subs); l > 0 { + filters := make([]*mqttFilter, 0, l) + for subject, qos := range es.subs { + filters = append(filters, &mqttFilter{filter: subject, qos: qos}) + } + if _, err := asm.processSubs(es, c, filters, false, trace); err != nil { + return err + } + } + return nil +} + +func (c *client) mqttEnqueueConnAck(rc byte, sessionPresent bool) { + proto := [4]byte{mqttPacketConnectAck, 2, 0, rc} + c.mu.Lock() + // Spec [MQTT-3.2.2-4]. If return code is different from 0, then + // session present flag must be set to 0. + if rc == 0 { + if sessionPresent { + proto[2] = 1 + } + } + c.enqueueProto(proto[:]) + c.mu.Unlock() +} + +func (s *Server) mqttHandleWill(c *client) { + c.mu.Lock() + if c.mqtt.cp == nil { + c.mu.Unlock() + return + } + will := c.mqtt.cp.will + if will == nil { + c.mu.Unlock() + return + } + // Create a synthetic PUBLISH packet to be delivered to the session's + // subscriptions, regardless of what is currently in c.mqtt.pp. + pp := &mqttPublish{ + topic: will.topic, + subject: will.subject, + mapped: will.mapped, + msg: will.message, + sz: len(will.message), + flags: will.qos << 1, + } + if will.retain { + pp.flags |= mqttPubFlagRetain + } + c.mu.Unlock() + s.mqttInitiateMsgDelivery(c, pp) + c.flushClients(0) +} + +////////////////////////////////////////////////////////////////////////////// +// +// PUBLISH protocol related functions +// +////////////////////////////////////////////////////////////////////////////// + +func (c *client) mqttParsePub(r *mqttReader, pl int, pp *mqttPublish, hasMappings bool) error { + qos := mqttGetQoS(pp.flags) + if qos > 2 { + return fmt.Errorf("QoS=%v is invalid in MQTT", qos) + } + + if c.mqtt.rejectQoS2Pub && qos == 2 { + return fmt.Errorf("QoS=2 is disabled for PUBLISH messages") + } + + // Keep track of where we are when starting to read the variable header + start := r.pos + + var err error + pp.topic, err = r.readBytes("topic", false) + if err != nil { + return err + } + if len(pp.topic) == 0 { + return errMQTTTopicIsEmpty + } + if err := mqttValidateTopic(pp.topic, "topic"); err != nil { + return err + } + // Convert the topic to a NATS subject. This call will also check that + // there is no MQTT wildcards (Spec [MQTT-3.3.2-2] and [MQTT-4.7.1-1]) + // Note that this may not result in a copy if there is no conversion. + // It is good because after the message is processed we won't have a + // reference to the buffer and we save a copy. + pp.subject, err = mqttTopicToNATSPubSubject(pp.topic) + if err != nil { + return err + } + + // Check for subject mapping. + if hasMappings { + // For selectMappedSubject to work, we need to have c.pa.subject set. + // If there is a change, c.pa.mapped will be set after the call. + c.pa.subject = pp.subject + if changed := c.selectMappedSubject(); changed { + // We need to keep track of the NATS subject/mapped in the `pp` structure. + pp.subject = c.pa.subject + pp.mapped = c.pa.mapped + // We also now need to map the original MQTT topic to the new topic + // based on the new subject. + pp.topic = natsSubjectToMQTTTopic(pp.subject) + } + // Reset those now. + c.pa.subject, c.pa.mapped = nil, nil + } + + if qos > 0 { + pp.pi, err = r.readUint16("packet identifier") + if err != nil { + return err + } + if pp.pi == 0 { + return fmt.Errorf("with QoS=%v, packet identifier cannot be 0", qos) + } + } else { + pp.pi = 0 + } + + // The message payload will be the total packet length minus + // what we have consumed for the variable header + payloadSize := pl - (r.pos - start) + if payloadSize < 0 { + return fmt.Errorf("invalid remaining length %d for PUBLISH packet", pl) + } + pp.sz = payloadSize + if pp.sz > 0 { + start = r.pos + r.pos += pp.sz + pp.msg = r.buf[start:r.pos] + } else if pp.sz == 0 { + pp.msg = nil + } else { + return errMQTTInvalidPublishLength + } + return nil +} + +func mqttValidateTopic(topic []byte, field string) error { + if !utf8.Valid(topic) { + return fmt.Errorf("invalid utf8 for %s %q", field, topic) + } + if bytes.IndexByte(topic, 0) >= 0 { + return fmt.Errorf("invalid null character in %s %q", field, topic) + } + return nil +} + +func mqttValidateString(value string, field string) error { + if !utf8.ValidString(value) { + return fmt.Errorf("invalid utf8 for %s %q", field, value) + } + if strings.IndexByte(value, 0) >= 0 { + return fmt.Errorf("invalid null character in %s %q", field, value) + } + return nil +} + +func mqttPubTrace(pp *mqttPublish) string { + dup := pp.flags&mqttPubFlagDup != 0 + qos := mqttGetQoS(pp.flags) + retain := mqttIsRetained(pp.flags) + var piStr string + if pp.pi > 0 { + piStr = fmt.Sprintf(" pi=%v", pp.pi) + } + return fmt.Sprintf("%s dup=%v QoS=%v retain=%v size=%v%s", + pp.topic, dup, qos, retain, pp.sz, piStr) +} + +// mqttComputeNatsMsgSize computes the size the NATS message to be delivered +// based on a MQTT PUBLISH packet. +// encodePP: whether to encode complete MQTT PUBLISH packet header information +// - false: initial delivery (QoS 0/1) needs only base header +// - true: QoS2 storage needs to encode Nmqtt-Subject and Nmqtt-Mapped +func mqttComputeNatsMsgSize(pp *mqttPublish, encodePP bool) int { + size := len(hdrLine) + + len(mqttNatsHeader) + 2 + 2 + // 2 for ':', and 2 for CRLF + 2 + // end-of-header CRLF + pp.sz + if encodePP { + size++ // for the flags byte + size += len(mqttNatsHeaderSubject) + 1 + // +1 for ':' + len(pp.subject) + 2 // 2 for CRLF + + if len(pp.mapped) > 0 { + size += len(mqttNatsHeaderMapped) + 1 + // +1 for ':' + len(pp.mapped) + 2 // 2 for CRLF + } + } + return size +} + +// Composes a NATS message from a MQTT PUBLISH packet. The message includes an +// internal header containint the original packet's QoS, and for QoS2 packets +// the original subject. +// +// Example (QoS2, subject: "foo.bar"): +// +// NATS/1.0\r\n +// Nmqtt-Pub:2foo.bar\r\n +// \r\n +func mqttNewDeliverableMessage(pp *mqttPublish, encodePP bool) (natsMsg []byte, headerLen int) { + size := mqttComputeNatsMsgSize(pp, encodePP) + + buf := bytes.NewBuffer(make([]byte, 0, size)) + + qos := mqttGetQoS(pp.flags) + + buf.WriteString(hdrLine) + buf.WriteString(mqttNatsHeader) + buf.WriteByte(':') + buf.WriteByte(qos + '0') + if encodePP { + buf.WriteByte(mqttNatsHeaderEncodeFlags(pp.flags)) + } + buf.WriteString(_CRLF_) + + if encodePP { + buf.WriteString(mqttNatsHeaderSubject) + buf.WriteByte(':') + buf.Write(pp.subject) + buf.WriteString(_CRLF_) + + if len(pp.mapped) > 0 { + buf.WriteString(mqttNatsHeaderMapped) + buf.WriteByte(':') + buf.Write(pp.mapped) + buf.WriteString(_CRLF_) + } + } + + // End of header + buf.WriteString(_CRLF_) + + headerLen = buf.Len() + + buf.Write(pp.msg) + return buf.Bytes(), headerLen +} + +// Composes a NATS message for a pending PUBREL packet. The message includes an +// internal header containing the PI for PUBREL/PUBCOMP. +// +// Example (PI:123): +// +// NATS/1.0\r\n +// Nmqtt-PubRel:123\r\n +// \r\n +func mqttNewDeliverablePubRel(pi uint16) (natsMsg []byte, headerLen int) { + size := len(hdrLine) + + len(mqttNatsPubRelHeader) + 6 + 2 + // 6 for ':65535', and 2 for CRLF + 2 // end-of-header CRLF + buf := bytes.NewBuffer(make([]byte, 0, size)) + buf.WriteString(hdrLine) + buf.WriteString(mqttNatsPubRelHeader) + buf.WriteByte(':') + buf.WriteString(strconv.FormatInt(int64(pi), 10)) + buf.WriteString(_CRLF_) + buf.WriteString(_CRLF_) + return buf.Bytes(), buf.Len() +} + +// Process the PUBLISH packet. +// +// Runs from the client's readLoop. +// No lock held on entry. +func (s *Server) mqttProcessPub(c *client, pp *mqttPublish, trace bool) error { + qos := mqttGetQoS(pp.flags) + + // Enforce max_payload using existing client max payload logic (mpay) by + // checking the total NATS message size that would be processed. + if maxPayload := atomic.LoadInt32(&c.mpay); maxPayload != jwt.NoLimit { + if total := mqttComputeNatsMsgSize(pp, qos == 2); total > int(maxPayload) { + c.maxPayloadViolation(total, maxPayload) + return ErrMaxPayload + } + } + + switch qos { + case 0: + return s.mqttInitiateMsgDelivery(c, pp) + + case 1: + // [MQTT-4.3.2-2]. Initiate onward delivery of the Application Message, + // Send PUBACK. + // + // The receiver is not required to complete delivery of the Application + // Message before sending the PUBACK. When its original sender receives + // the PUBACK packet, ownership of the Application Message is + // transferred to the receiver. + // + // The PUBACK is emitted by the pipeline once JetStream acks the + // store, in the order the PUBLISH packets were received. + return s.mqttInitiateMsgDelivery(c, pp) + + case 2: + // [MQTT-4.3.3-2]. Method A, Store message, send PUBREC. + // + // The receiver is not required to complete delivery of the Application + // Message before sending the PUBREC or PUBCOMP. When its original + // sender receives the PUBREC packet, ownership of the Application + // Message is transferred to the receiver. + err := s.mqttStoreQoS2MsgOnce(c, pp) + if err == nil { + c.mqttEnqueuePubResponse(mqttPacketPubRec, pp.pi, trace) + } + return err + + default: + return fmt.Errorf("unreachable: invalid QoS in mqttProcessPub: %v", qos) + } +} + +////////////////////////////////////////////////////////////////////////////// +// +// QoS1 PUBACK pipeline +// +////////////////////////////////////////////////////////////////////////////// + +// Max pipelined inbound QoS1 PUBLISH messages per connection (the server's +// effective "Receive Maximum"). A full window blocks the readLoop, +// backpressuring the client. +const mqttMaxAcksInFlight = 1024 + +// A QoS>0 PUBLISH must carry a non-zero PI, Spec [MQTT-2.3.1-1]; +// mqttParsePub zeroes pi for QoS0 and synthesized messages (wills) carry +// none. So pi != 0 means a client awaits an acknowledgment. +func (pp *mqttPublish) hasClientPI() bool { + return pp.pi != 0 +} + +// An inbound QoS1 PUBLISH from a client must be acknowledged with a PUBACK +// even if the message itself is dropped (e.g. on a permission violation). +// QoS2 flows are acknowledged with PUBREC/PUBCOMP instead. +func (pp *mqttPublish) owesPubAck() bool { + return mqttGetQoS(pp.flags) == 1 && pp.hasClientPI() +} + +// mqttAckPipeline emits the PUBACKs of asynchronously submitted QoS1 +// stores in the order the PUBLISH packets were received. A failed or +// timed-out store closes the connection, like the synchronous path did. +// mqttPipeline* submit work here; mqttEnqueue* write a packet directly to +// the outbound buffer. +type mqttAckPipeline struct { + jsa *mqttJSA + q chan *mqttPipelinedAck + quitCh chan struct{} + quitOnce sync.Once +} + +type mqttPipelinedAck struct { + pi uint16 + reply string // JSA reply subject, to clean up if abandoned. + done chan error // Buffered(1), receives the result of the JS store. +} + +// Idempotent, callable from any goroutine; unblocks a readLoop waiting in +// mqttPipelinePush. Queued entries are left for shutdown(). +func (pipe *mqttAckPipeline) stop() { + pipe.quitOnce.Do(func() { close(pipe.quitCh) }) +} + +// stop(), then delete the queued entries' registrations from the +// account-scoped jsa.replies, which outlives the connection. Must run on +// the readLoop (the sole producer), after its last push. +func (pipe *mqttAckPipeline) shutdown() { + pipe.stop() + for { + select { + case ack := <-pipe.q: + pipe.jsa.replies.Delete(ack.reply) + default: + return + } + } +} + +// Emits the PUBACKs in publish order as the JS acks arrive; on a store +// failure or timeout, stops the pipeline and closes the connection. Runs +// in its own goroutine, one per connection with inbound QoS1 traffic. +func (s *Server) mqttAckLoop(c *client, pipe *mqttAckPipeline, jsa *mqttJSA) { + // Reset per entry; as of Go 1.23 Stop/Reset guarantee no stale receive, + // no draining needed. + t := time.NewTimer(time.Hour) + defer t.Stop() + + fail := func(ack *mqttPipelinedAck, err error) { + jsa.replies.Delete(ack.reply) + c.Errorf("unable to store QoS1 message in JetStream (pi=%v): %v; "+ + "closing the connection, the client will re-send unacknowledged PUBLISH packets on reconnect", + ack.pi, err) + c.closeConnection(ProtocolViolation) + } + + for { + select { + case ack := <-pipe.q: + t.Reset(jsa.timeout) + select { + case err := <-ack.done: + if err != nil { + fail(ack, err) + return + } + c.mu.Lock() + trace := c.trace + c.mu.Unlock() + c.mqttEnqueuePubResponse(mqttPacketPubAck, ack.pi, trace) + + case <-t.C: + fail(ack, fmt.Errorf("timeout after %v waiting for the JetStream ack", jsa.timeout)) + return + + case <-pipe.quitCh: + // pipe.shutdown only covers entries still queued; this one is + // ours to clean up. + jsa.replies.Delete(ack.reply) + return + + case <-s.quitCh: + // pipe.shutdown only covers entries still queued; this one is + // ours to clean up. + jsa.replies.Delete(ack.reply) + return + } + + case <-pipe.quitCh: + return + + case <-s.quitCh: + return + } + } +} + +// Submits the JS store; the pipeline sends PUBACK(pi) on its ack, in +// publish order. readLoop only. +func (s *Server) mqttPipelineStoreThenPubAck(c *client, pi uint16, subject string, hdrLen int, natsMsg []byte) error { + jsa := c.mqtt.sess.jsa + ack := &mqttPipelinedAck{pi: pi, done: make(chan error, 1)} + jsa.storeMsgAsync(subject, hdrLen, natsMsg, ack) + return s.mqttPipelinePush(c, jsa, ack) +} + +// Sends PUBACK(pi) without a store (e.g. the message was dropped on a +// permission violation), through the pipeline to preserve ordering. +// readLoop only. +func (s *Server) mqttPipelinePubAck(c *client, pi uint16) error { + ack := &mqttPipelinedAck{pi: pi, done: make(chan error, 1)} + ack.done <- nil + return s.mqttPipelinePush(c, c.mqtt.sess.jsa, ack) +} + +// Creates the pipeline and starts its mqttAckLoop goroutine. Returns nil +// if the goroutine cannot be started (server shutting down). readLoop only. +func (s *Server) mqttPipelineStart(c *client, jsa *mqttJSA) *mqttAckPipeline { + pipe := &mqttAckPipeline{ + jsa: jsa, + q: make(chan *mqttPipelinedAck, mqttMaxAcksInFlight), + quitCh: make(chan struct{}), + } + if !s.startGoRoutine(func() { + defer s.grWG.Done() + + s.mqttAckLoop(c, pipe, jsa) + // The loop was the sole consumer; release a readLoop possibly + // waiting for room in push, it observes only pipe.quitCh. + pipe.stop() + }) { + return nil + } + return pipe +} + +// Admits an entry, creating the pipeline on first use. readLoop only. +func (s *Server) mqttPipelinePush(c *client, jsa *mqttJSA, ack *mqttPipelinedAck) error { + // The field is readLoop-owned (we are the readLoop), no lock. + pipe := c.mqtt.acks + if pipe == nil { + if pipe = s.mqttPipelineStart(c, jsa); pipe == nil { + // Server is shutting down, complete synchronously. + t := time.NewTimer(jsa.timeout) + defer t.Stop() + select { + case err := <-ack.done: + if err != nil { + return err + } + c.mu.Lock() + trace := c.trace + c.mu.Unlock() + c.mqttEnqueuePubResponse(mqttPacketPubAck, ack.pi, trace) + return nil + case <-t.C: + jsa.replies.Delete(ack.reply) + return fmt.Errorf("JetStream did not acknowledge the QoS1 message within %v "+ + "(server is shutting down); failing the connection, "+ + "the client will re-send unacknowledged PUBLISH packets on reconnect", jsa.timeout) + case <-s.quitCh: + // The reply may never come; do not hold up the shutdown. + jsa.replies.Delete(ack.reply) + return ErrServerNotRunning + } + } + c.mqtt.acks = pipe + } + + return pipe.push(ack) +} + +// Admits an entry into the pipeline. Rejects it once the pipeline is +// stopped: an entry admitted after the connection-close drain +// (mqttHandleClosedClient -> pipe.shutdown) would strand its reply +// registration in the account-scoped jsa.replies. readLoop only. +func (pipe *mqttAckPipeline) push(ack *mqttPipelinedAck) error { + jsa := pipe.jsa + select { + case <-pipe.quitCh: + jsa.replies.Delete(ack.reply) + return errMQTTAckPipelineStopped + default: + } + + enqueued := false + select { + case pipe.q <- ack: + enqueued = true + default: + // Window full: wait for an available slot in the pipeline. + t := time.NewTimer(jsa.timeout) + defer t.Stop() + select { + case pipe.q <- ack: + enqueued = true + case <-pipe.quitCh: + case <-t.C: + jsa.replies.Delete(ack.reply) + return fmt.Errorf("QoS1 in-flight window is full (%d messages) and JetStream has not acknowledged "+ + "the oldest message within %v; failing the connection, "+ + "the client will re-send unacknowledged PUBLISH packets on reconnect", + mqttMaxAcksInFlight, jsa.timeout) + } + } + if !enqueued { + // Stopped while waiting for a slot, never admitted. + jsa.replies.Delete(ack.reply) + return errMQTTAckPipelineStopped + } + + // A stop may have raced the enqueue: the close-time drain could run + // before the entry landed and miss it. Drain again; both drains only + // dequeue and delete, so overlapping is harmless. + select { + case <-pipe.quitCh: + pipe.shutdown() + return errMQTTAckPipelineStopped + default: + } + return nil +} + +func (s *Server) mqttInitiateMsgDelivery(c *client, pp *mqttPublish) error { + natsMsg, headerLen := mqttNewDeliverableMessage(pp, false) + + // The delivered message becomes the client's current publish (it is not + // the last PARSED packet for a PUBREL- or will-initiated delivery), and + // c.pa carries its pubargs; one defer restores both. c.mqtt.pp is + // readLoop-owned, like c.pa. + prevPP := c.mqtt.pp + c.mqtt.pp = pp + + c.pa.subject = pp.subject + c.pa.mapped = pp.mapped + c.pa.reply = nil + c.pa.hdr = headerLen + c.pa.hdb = []byte(strconv.FormatInt(int64(c.pa.hdr), 10)) + c.pa.size = len(natsMsg) + c.pa.szb = []byte(strconv.FormatInt(int64(c.pa.size), 10)) + defer func() { + c.mqtt.pp = prevPP + c.pa.subject = nil + c.pa.mapped = nil + c.pa.reply = nil + c.pa.hdr = -1 + c.pa.hdb = nil + c.pa.size = 0 + c.pa.szb = nil + }() + + _, permIssue := c.processInboundClientMsg(natsMsg) + if permIssue { + // The message was dropped, but the client may still be owed a + // PUBACK (routed through the pipeline to preserve ordering). + if pp.owesPubAck() { + return s.mqttPipelinePubAck(c, pp.pi) + } + return nil + } + + // If QoS 0 messages don't need to be stored, other (1 and 2) do. Store them + // JetStream under "$MQTT.msgs." + if qos := mqttGetQoS(pp.flags); qos == 0 { + return nil + } + + // We need to call flushClients now since this we may have called c.addToPCD + // with destination clients (possibly a route). Without calling flushClients + // the following call may then be stuck waiting for a reply that may never + // come because the destination is not flushed (due to c.out.fsp > 0, + // see addToPCD and writeLoop for details). + c.flushClients(0) + + subject := mqttStreamSubjectPrefix + string(c.pa.subject) + + // QoS1 from the wire is pipelined; wills and QoS2 PUBREL-initiated + // deliveries owe no PUBACK and store synchronously. + if pp.owesPubAck() { + return s.mqttPipelineStoreThenPubAck(c, pp.pi, subject, headerLen, natsMsg) + } + + _, err := c.mqtt.sess.jsa.storeMsg(subject, headerLen, natsMsg) + + return err +} + +var mqttMaxMsgErrPattern = fmt.Sprintf("%s (%v)", ErrMaxMsgsPerSubject.Error(), JSStreamStoreFailedF) + +func (s *Server) mqttStoreQoS2MsgOnce(c *client, pp *mqttPublish) error { + // `true` means encode the MQTT PUBLISH packet in the NATS message header. + natsMsg, headerLen := mqttNewDeliverableMessage(pp, true) + + // Do not broadcast the message until it has been deduplicated and released + // by the sender. Instead store this QoS2 message as + // "$MQTT.qos2..". If the message is a duplicate, we get back + // a ErrMaxMsgsPerSubject, otherwise it does not change the flow, still need + // to send a PUBREC back to the client. The original subject (translated + // from MQTT topic) is included in the NATS header of the stored message to + // use for latter delivery. + _, err := c.mqtt.sess.jsa.storeMsg(c.mqttQoS2InternalSubject(pp.pi), headerLen, natsMsg) + + // TODO: would prefer a more robust and performant way of checking the + // error, but it comes back wrapped as an API result. + if err != nil && + (isErrorOtherThan(err, JSStreamStoreFailedF) || err.Error() != mqttMaxMsgErrPattern) { + return err + } + + return nil +} + +func (c *client) mqttQoS2InternalSubject(pi uint16) string { + return mqttQoS2IncomingMsgsStreamSubjectPrefix + c.mqtt.cid + "." + strconv.FormatUint(uint64(pi), 10) +} + +// Process a PUBREL packet (QoS2, acting as Receiver). +// +// Runs from the client's readLoop. +// No lock held on entry. +func (s *Server) mqttProcessPubRel(c *client, pi uint16, trace bool) error { + // Once done with the processing, send a PUBCOMP back to the client. + defer c.mqttEnqueuePubResponse(mqttPacketPubComp, pi, trace) + + // See if there is a message pending for this pi. All failures are treated + // as "not found". + asm := c.mqtt.asm + stored, _ := asm.jsa.loadLastMsgFor(mqttQoS2IncomingMsgsStreamName, c.mqttQoS2InternalSubject(pi)) + + if stored == nil { + // No message found, nothing to do. + return nil + } + // Best attempt to delete the message from the QoS2 stream. + asm.jsa.deleteMsg(mqttQoS2IncomingMsgsStreamName, stored.Sequence, true) + + // only MQTT QoS2 messages should be here, and they must have a subject. + h := mqttParsePublishNATSHeader(stored.Header) + if h == nil || h.qos != 2 || len(h.subject) == 0 { + return errors.New("invalid message in QoS2 PUBREL stream") + } + + flags := h.qos << 1 + if h.retained { + flags |= mqttPubFlagRetain + } + pp := &mqttPublish{ + topic: natsSubjectToMQTTTopic(h.subject), + subject: h.subject, + mapped: h.mapped, + msg: stored.Data, + sz: len(stored.Data), + pi: pi, + flags: flags, + } + + return s.mqttInitiateMsgDelivery(c, pp) +} + +// Invoked when processing an inbound client message. If the "retain" flag is +// set, the message is stored so it can be later resent to (re)starting +// subscriptions that match the subject. +// +// Invoked from the MQTT publisher's readLoop. No client lock is held on entry. +func (c *client) mqttHandlePubRetain() { + pp := c.mqtt.pp + retainMQTT := mqttIsRetained(pp.flags) + isBirth, _, isCertificate := sparkbParseBirthDeathTopic(pp.topic) + retainSparkbBirth := isBirth && !isCertificate + + // [tck-id-topics-nbirth-mqtt] NBIRTH messages MUST be published with MQTT + // QoS equal to 0 and retain equal to false. + // + // [tck-id-conformance-mqtt-aware-nbirth-mqtt-retain] A Sparkplug Aware MQTT + // Server MUST make NBIRTH messages available on the topic: + // $sparkplug/certificates/namespace/group_id/NBIRTH/edge_node_id with the + // MQTT retain flag set to true. + if retainMQTT == retainSparkbBirth { + // (retainSparkbBirth && retainMQTT) : not valid, so ignore altogether. + // (!retainSparkbBirth && !retainMQTT) : nothing to do. + return + } + + asm := c.mqtt.asm + key := string(pp.subject) + + // Always clear the retain flag to deliver a normal published message. + defer func() { + pp.flags &= ^mqttPubFlagRetain + }() + + // Spec [MQTT-3.3.1-11]. Payload of size 0 removes the retained message, but + // should still be delivered as a normal message. + // + // We used to delete the message here from our map, the stream, and notify + // the network about the delete. We no longer do that. Instead, we store + // the message with an empty body. When servers will get the empty body + // in processRetainedMsg, then will remove the message from their map. This + // effectively serializes all add/remove of retained messages without the + // need for "network" notifications about deletes (we still support that + // for backward compatibility but will be pulled in future releases). + + rm := &mqttRetainedMsg{ + Origin: asm.jsa.id, + Msg: pp.msg, // will copy these bytes later as we process rm. + Flags: pp.flags, + Source: c.opts.Username, + } + + if retainSparkbBirth { + // [tck-id-conformance-mqtt-aware-store] A Sparkplug Aware MQTT Server + // MUST store NBIRTH and DBIRTH messages as they pass through the MQTT + // Server. + // + // [tck-id-conformance-mqtt-aware-nbirth-mqtt-topic]. A Sparkplug Aware + // MQTT Server MUST make NBIRTH messages available on a topic of the + // form: $sparkplug/certificates/namespace/group_id/NBIRTH/edge_node_id + // + // [tck-id-conformance-mqtt-aware-dbirth-mqtt-topic] A Sparkplug Aware + // MQTT Server MUST make DBIRTH messages available on a topic of the + // form: + // $sparkplug/certificates/namespace/group_id/DBIRTH/edge_node_id/device_id + topic := append(sparkbCertificatesTopicPrefix, pp.topic...) + subject, _ := mqttTopicToNATSPubSubject(topic) + rm.Topic = string(topic) + rm.Subject = string(subject) + + // will use to save the retained message. + key = string(subject) + + // Store the retained message with the RETAIN flag set. + rm.Flags |= mqttPubFlagRetain + + if pp.sz > 0 { + // Copy the payload out of pp since we will be sending the message + // asynchronously. + msg := make([]byte, pp.sz) + copy(msg, pp.msg[:pp.sz]) + asm.jsa.sendMsg(key, msg) + } + + } else { // isRetained + // Spec [MQTT-3.3.1-5]. Store the retained message with its QoS. + // + // When coming from a publish protocol, `pp` is referencing a stack + // variable that itself possibly references the read buffer. + rm.Topic = string(pp.topic) + } + + // Set the key to the subject of the message for retained, or the composed + // $sparkplug subject for sparkB. + rm.Subject = key + rmBytes, hdr := mqttEncodeRetainedMessage(rm) // will copy the payload bytes + _, err := asm.jsa.storeMsg(mqttRetainedMsgsStreamSubject+key, hdr, rmBytes) + if err != nil { + c.mu.Lock() + acc := c.acc + c.mu.Unlock() + c.Errorf("unable to store retained message for account %q, subject %q: %v", + acc.GetName(), key, err) + } +} + +// After a config reload, it is possible that the source of a publish retained +// message is no longer allowed to publish on the given topic. If that is the +// case, the retained message is removed from the map and will no longer be +// sent to (re)starting subscriptions. +// +// Server lock MUST NOT be held on entry. +func (s *Server) mqttCheckPubRetainedPerms() { + sm := &s.mqtt.sessmgr + sm.mu.RLock() + done := len(sm.sessions) == 0 + sm.mu.RUnlock() + + if done { + return + } + + s.mu.Lock() + users := make(map[string]*User, len(s.users)) + for un, u := range s.users { + users[un] = u + } + s.mu.Unlock() + + // First get a list of all of the sessions. + sm.mu.RLock() + asms := make([]*mqttAccountSessionManager, 0, len(sm.sessions)) + for _, asm := range sm.sessions { + asms = append(asms, asm) + } + sm.mu.RUnlock() + + // For each session we will obtain a list of retained messages. + var _rms [128]uint64 + rms := _rms[:0] + for _, asm := range asms { + // Get all of the retained messages. Then we will sort them so + // that they are in sequence order, which should help the file + // store to not have to load out-of-order blocks so often. + asm.mu.RLock() + rms = rms[:0] // reuse slice + // Copy the sequence out of the tree. The tree entry itself can be + // updated concurrently by addRetainedMsg() after we release the lock, + // so keeping a pointer here would race with the later sort. + asm.retmsgs.IterOrdered(func(_ []byte, rm *mqttRetainedMsgRef) bool { + rms = append(rms, rm.sseq) + return true + }) + jsaID := asm.jsa.id + asm.mu.RUnlock() + slices.Sort(rms) + + perms := map[string]*mqttPerm{} + for _, rf := range rms { + jsm, err := asm.jsa.loadMsg(mqttRetainedMsgsStreamName, rf) + if err != nil || jsm == nil { + continue + } + rm, err := mqttDecodeRetainedMessage(jsm.Subject, jsm.Header, jsm.Data) + if err != nil { + continue + } + // We deal only with messages that have a source (the username that produced + // this message) and were produced on this server. + if rm.Source == _EMPTY_ || rm.Origin != jsaID { + continue + } + // Lookup source from global users. + u := users[rm.Source] + if u != nil { + p, ok := perms[rm.Source] + if !ok { + p = generatePubPerms(u.Permissions) + perms[rm.Source] = p // possibly nil + } + // If there is permission and no longer allowed to publish in + // the subject, remove the publish retained message from the map. + if p != nil && !pubAllowed(p, rm.Subject) { + u = nil + } + } + + // Not present or permissions have changed such that the source can't + // publish on that subject anymore: delete this retained message. + if u == nil { + // Set the payload to empty to notify that we are deleting this + // retained message. We will send this message async. + rm.Msg = nil + rmBytes, hdrLen := mqttEncodeRetainedMessage(rm) + asm.jsa.storeMsgNoWait(mqttRetainedMsgsStreamSubject+rm.Subject, hdrLen, rmBytes) + } + } + } +} + +// Helper to generate only pub permissions from a Permissions object +type mqttPerm struct { + allow *gsl.SimpleSublist + deny *gsl.SimpleSublist +} + +func generatePubPerms(perms *Permissions) *mqttPerm { + // If given permissions is `nil`, then it means that permissions block + // has been removed (so the user is now allowed to publish on everything) + // or was never there in the first place. Returning `nil` will let the + // caller know that there are no permissions to enforce. + if perms == nil { + return nil + } + var p *mqttPerm + if perms.Publish.Allow != nil { + p = &mqttPerm{} + p.allow = gsl.NewSimpleSublist() + for _, pubSubject := range perms.Publish.Allow { + _ = p.allow.Insert(pubSubject, struct{}{}) + } + } + if len(perms.Publish.Deny) > 0 { + if p == nil { + p = &mqttPerm{} + } + p.deny = gsl.NewSimpleSublist() + for _, pubSubject := range perms.Publish.Deny { + _ = p.deny.Insert(pubSubject, struct{}{}) + } + } + return p +} + +// Helper that checks if given `perms` allow to publish on the given `subject` +func pubAllowed(perms *mqttPerm, subject string) bool { + if perms == nil { + return true + } + allowed := true + if perms.allow != nil { + allowed = perms.allow.HasInterest(subject) + } + // If we have a deny list and are currently allowed, check that as well. + if allowed && perms.deny != nil { + allowed = !perms.deny.HasInterest(subject) + } + return allowed +} + +func (c *client) mqttEnqueuePubResponse(packetType byte, pi uint16, trace bool) { + proto := [4]byte{packetType, 0x2, 0, 0} + proto[2] = byte(pi >> 8) + proto[3] = byte(pi) + + // Bits 3,2,1 and 0 of the fixed header in the PUBREL Control Packet are + // reserved and MUST be set to 0,0,1 and 0 respectively. The Server MUST treat + // any other value as malformed and close the Network Connection [MQTT-3.6.1-1]. + if packetType == mqttPacketPubRel { + proto[0] |= 0x2 + } + + c.mu.Lock() + c.enqueueProto(proto[:4]) + c.mu.Unlock() + + if trace { + name := "(???)" + switch packetType { + case mqttPacketPubAck: + name = "PUBACK" + case mqttPacketPubRec: + name = "PUBREC" + case mqttPacketPubRel: + name = "PUBREL" + case mqttPacketPubComp: + name = "PUBCOMP" + } + c.traceOutOp(name, []byte(fmt.Sprintf("pi=%v", pi))) + } +} + +func mqttParsePIPacket(r *mqttReader) (uint16, error) { + pi, err := r.readUint16("packet identifier") + if err != nil { + return 0, err + } + if pi == 0 { + return 0, errMQTTPacketIdentifierIsZero + } + return pi, nil +} + +// Process a PUBACK (QoS1) or a PUBREC (QoS2) packet, acting as Sender. Set +// isPubRec to false to process as a PUBACK. +// +// Runs from the client's readLoop. No lock held on entry. +func (c *client) mqttProcessPublishReceived(pi uint16, isPubRec bool) (err error) { + sess := c.mqtt.sess + if sess == nil { + return errMQTTInvalidSession + } + + var jsAckSubject string + sess.mu.Lock() + // Must be the same client, and the session must have been setup for QoS2. + if sess.c != c { + sess.mu.Unlock() + return errMQTTInvalidSession + } + if isPubRec { + // The JS ACK subject for the PUBREL will be filled in at the delivery + // attempt. + sess.trackAsPubRel(pi, _EMPTY_) + } + jsAckSubject = sess.untrackPublish(pi) + sess.mu.Unlock() + + if isPubRec { + natsMsg, headerLen := mqttNewDeliverablePubRel(pi) + _, err = sess.jsa.storeMsg(sess.pubRelSubject, headerLen, natsMsg) + if err != nil { + // Failure to send out PUBREL will terminate the connection. + return err + } + } + + // Send the ack to JS to remove the pending message from the consumer. + sess.jsa.sendAck(jsAckSubject) + return nil +} + +func (c *client) mqttProcessPubAck(pi uint16) error { + return c.mqttProcessPublishReceived(pi, false) +} + +func (c *client) mqttProcessPubRec(pi uint16) error { + return c.mqttProcessPublishReceived(pi, true) +} + +// Runs from the client's readLoop. No lock held on entry. +func (c *client) mqttProcessPubComp(pi uint16) { + sess := c.mqtt.sess + if sess == nil { + return + } + + var jsAckSubject string + sess.mu.Lock() + if sess.c != c { + sess.mu.Unlock() + return + } + jsAckSubject = sess.untrackPubRel(pi) + sess.mu.Unlock() + + // Send the ack to JS to remove the pending message from the consumer. + sess.jsa.sendAck(jsAckSubject) +} + +// Return the QoS from the given PUBLISH protocol's flags +func mqttGetQoS(flags byte) byte { + return flags & mqttPubFlagQoS >> 1 +} + +func mqttIsRetained(flags byte) bool { + return flags&mqttPubFlagRetain != 0 +} + +func sparkbParseBirthDeathTopic(topic []byte) (isBirth, isDeath, isCertificate bool) { + if bytes.HasPrefix(topic, sparkbCertificatesTopicPrefix) { + isCertificate = true + topic = topic[len(sparkbCertificatesTopicPrefix):] + } + if !bytes.HasPrefix(topic, sparkbNamespaceTopicPrefix) { + return false, false, false + } + topic = topic[len(sparkbNamespaceTopicPrefix):] + + parts := bytes.Split(topic, []byte{'/'}) + if len(parts) < 3 || len(parts) > 4 { + return false, false, false + } + typ := bytesToString(parts[1]) + switch typ { + case sparkbNBIRTH, sparkbDBIRTH: + isBirth = true + case sparkbNDEATH, sparkbDDEATH: + isDeath = true + default: + return false, false, false + } + return isBirth, isDeath, isCertificate +} + +////////////////////////////////////////////////////////////////////////////// +// +// SUBSCRIBE related functions +// +////////////////////////////////////////////////////////////////////////////// + +func (c *client) mqttParseSubs(r *mqttReader, b byte, pl int) (uint16, []*mqttFilter, error) { + return c.mqttParseSubsOrUnsubs(r, b, pl, true) +} + +func (c *client) mqttParseSubsOrUnsubs(r *mqttReader, b byte, pl int, sub bool) (uint16, []*mqttFilter, error) { + var expectedFlag byte + var action string + if sub { + expectedFlag = mqttSubscribeFlags + } else { + expectedFlag = mqttUnsubscribeFlags + action = "un" + } + // Spec [MQTT-3.8.1-1], [MQTT-3.10.1-1] + if rf := b & 0xf; rf != expectedFlag { + return 0, nil, fmt.Errorf("wrong %ssubscribe reserved flags: %x", action, rf) + } + pi, err := mqttParsePIPacket(r) + if err != nil { + return 0, nil, err + } + end := r.pos + (pl - 2) + var filters []*mqttFilter + for r.pos < end { + // Don't make a copy now because, this will happen during conversion + // or when processing the sub. + topic, err := r.readBytes("topic filter", false) + if err != nil { + return 0, nil, err + } + if len(topic) == 0 { + return 0, nil, errMQTTTopicFilterCannotBeEmpty + } + // Spec [MQTT-3.8.3-1], [MQTT-3.10.3-1] + if err := mqttValidateTopic(topic, "topic filter"); err != nil { + return 0, nil, err + } + var qos byte + // We are going to report if we had an error during the conversion, + // but we don't fail the parsing. When processing the sub, we will + // have an error then, and the processing of subs code will send + // the proper mqttSubAckFailure flag for this given subscription. + filter, err := mqttFilterToNATSSubject(topic) + if err != nil { + c.Errorf("invalid topic %q: %v", topic, err) + } + if sub { + qos, err = r.readByte("QoS") + if err != nil { + return 0, nil, err + } + // Spec [MQTT-3-8.3-4]. + if qos > 2 { + return 0, nil, fmt.Errorf("subscribe QoS value must be 0, 1 or 2, got %v", qos) + } + } + f := &mqttFilter{ttopic: topic, filter: string(filter), qos: qos} + filters = append(filters, f) + } + // Spec [MQTT-3.8.3-3], [MQTT-3.10.3-2] + if len(filters) == 0 { + return 0, nil, fmt.Errorf("%ssubscribe protocol must contain at least 1 topic filter", action) + } + return pi, filters, nil +} + +func mqttSubscribeTrace(pi uint16, filters []*mqttFilter) string { + var sep string + sb := &strings.Builder{} + sb.WriteString("[") + for i, f := range filters { + sb.WriteString(sep) + sb.Write(f.ttopic) + sb.WriteString(" (") + sb.WriteString(f.filter) + sb.WriteString(") QoS=") + sb.WriteString(fmt.Sprintf("%v", f.qos)) + if i == 0 { + sep = ", " + } + } + sb.WriteString(fmt.Sprintf("] pi=%v", pi)) + return sb.String() +} + +// For a MQTT QoS0 subscription, we create a single NATS subscription on the +// actual subject, for instance "foo.bar". +// +// For a MQTT QoS1+ subscription, we create 2 subscriptions, one on "foo.bar" +// (as for QoS0, but sub.mqtt.qos will be 1 or 2), and one on the subject +// "$MQTT.sub." which is the delivery subject of the JS durable consumer +// with the filter subject "$MQTT.msgs.foo.bar". +// +// This callback delivers messages to the client as QoS0 messages, either +// because: (a) they have been produced as MQTT QoS0 messages (and therefore +// only this callback can receive them); (b) they are MQTT QoS1+ published +// messages but this callback is for a subscription that is QoS0; or (c) the +// published messages come from (other) NATS publishers on the subject. +// +// This callback must reject a message if it is known to be a QoS1+ published +// message and this is the callback for a QoS1+ subscription because in that +// case, it will be handled by the other callback. This avoid getting duplicate +// deliveries. +func mqttDeliverMsgCbQoS0(sub *subscription, pc *client, _ *Account, subject, reply string, rmsg []byte) { + if pc.kind == JETSTREAM && len(reply) > 0 && strings.HasPrefix(reply, jsAckPre) { + return + } + + // This is the client associated with the subscription. + cc := sub.client + + // This is immutable + sess := cc.mqtt.sess + + // Lock here, otherwise we may be called with sub.mqtt == nil. Ignore + // wildcard subscriptions if this subject starts with '$', per Spec + // [MQTT-4.7.2-1]. + sess.subsMu.RLock() + subQoS := sub.mqtt.qos + ignore := mqttMustIgnoreForReservedSub(sub, subject) + sess.subsMu.RUnlock() + + if ignore { + return + } + + hdr, msg := pc.msgParts(rmsg) + var topic []byte + if pc.isMqtt() { + // This is an MQTT publisher directly connected to this server. + + // Check the subscription's QoS. If the message was published with a + // QoS>0 and the sub has the QoS>0 then the message will be delivered by + // mqttDeliverMsgCbQoS12. + msgQoS := mqttGetQoS(pc.mqtt.pp.flags) + if subQoS > 0 && msgQoS > 0 { + return + } + topic = pc.mqtt.pp.topic + // If the subject is different than the one in pp.subject, then some + // mapping/transform occurred and we need to recreate the topic. + if subject != bytesToString(pc.mqtt.pp.subject) { + topic = natsSubjectStrToMQTTTopic(subject) + } + + } else { + // Non MQTT client, could be NATS publisher, or ROUTER, etc.. + h := mqttParsePublishNATSHeader(hdr) + + // Check the subscription's QoS. If the message was published with a + // QoS>0 (in the header) and the sub has the QoS>0 then the message will + // be delivered by mqttDeliverMsgCbQoS12. + if subQoS > 0 && h != nil && h.qos > 0 { + return + } + + // If size is more than what a MQTT client can handle, we should probably reject, + // for now just truncate. + if len(msg) > mqttMaxPayloadSize { + msg = msg[:mqttMaxPayloadSize] + } + topic = natsSubjectStrToMQTTTopic(subject) + } + + // Message never has a packet identifier nor is marked as duplicate. + pc.mqttEnqueuePublishMsgTo(cc, sub, 0, 0, false, topic, msg) +} + +// This is the callback attached to a JS durable subscription for a MQTT QoS 1+ +// sub. Only JETSTREAM should be sending a message to this subject (the delivery +// subject associated with the JS durable consumer), but in cluster mode, this +// can be coming from a route, gw, etc... We make sure that if this is the case, +// the message contains a NATS/MQTT header that indicates that this is a +// published QoS1+ message. +func mqttDeliverMsgCbQoS12(sub *subscription, pc *client, _ *Account, subject, reply string, rmsg []byte) { + // Message on foo.bar is stored under $MQTT.msgs.foo.bar, so the subject has to be + // at least as long as the stream subject prefix "$MQTT.msgs.", and after removing + // the prefix, has to be at least 1 character long. + if len(subject) < len(mqttStreamSubjectPrefix)+1 { + return + } + + hdr, msg := pc.msgParts(rmsg) + h := mqttParsePublishNATSHeader(hdr) + if pc.kind != JETSTREAM && (h == nil || h.qos == 0) { + // MQTT QoS 0 messages must be ignored, they will be delivered by the + // other callback, the direct NATS subscription. All JETSTREAM messages + // will have the header. + return + } + + // This is the client associated with the subscription. + cc := sub.client + + // This is immutable + sess := cc.mqtt.sess + + // We lock to check some of the subscription's fields and if we need to keep + // track of pending acks, etc. There is no need to acquire the subsMu RLock + // since sess.Lock is overarching for modifying subscriptions. + sess.mu.Lock() + if sess.c != cc || sub.mqtt == nil || sub.mqtt.closed { + sess.mu.Unlock() + return + } + + // In this callback we handle only QoS-published messages to QoS + // subscriptions. Ignore if either is 0, will be delivered by the other + // callback, mqttDeliverMsgCbQos1. + var qos byte + if h != nil { + qos = h.qos + } + if qos > sub.mqtt.qos { + qos = sub.mqtt.qos + } + if qos == 0 { + sess.mu.Unlock() + return + } + + // Check for reserved subject violation. If so, we will send the ack to + // remove the message, and do nothing else. + strippedSubj := subject[len(mqttStreamSubjectPrefix):] + if mqttMustIgnoreForReservedSub(sub, strippedSubj) { + sess.mu.Unlock() + sess.jsa.sendAck(reply) + return + } + + // A broad wildcard subscription can overlap a subscribe deny clause. + cc.mu.Lock() + denied := cc.mperms != nil && cc.checkDenySub(strippedSubj, bytesToString(sub.queue)) + cc.mu.Unlock() + if denied { + sess.mu.Unlock() + sess.jsa.sendAck(reply) + return + } + + pi, dup := sess.trackPublish(sub.mqtt.jsDur, reply) + sess.mu.Unlock() + + if pi == 0 { + // We have reached max pending, don't send the message now. + // JS will cause a redelivery and if by then the number of pending + // messages has fallen below threshold, the message will be resent. + return + } + + originalTopic := natsSubjectStrToMQTTTopic(strippedSubj) + pc.mqttEnqueuePublishMsgTo(cc, sub, pi, qos, dup, originalTopic, msg) +} + +func mqttDeliverPubRelCb(sub *subscription, pc *client, _ *Account, subject, reply string, rmsg []byte) { + if sub.client.mqtt == nil || sub.client.mqtt.sess == nil || reply == _EMPTY_ { + return + } + + hdr, _ := pc.msgParts(rmsg) + pi := mqttParsePubRelNATSHeader(hdr) + if pi == 0 { + return + } + + // This is the client associated with the subscription. + cc := sub.client + + // This is immutable + sess := cc.mqtt.sess + + sess.mu.Lock() + if sess.c != cc || sess.pubRelConsumer == nil { + sess.mu.Unlock() + return + } + sess.trackAsPubRel(pi, reply) + trace := cc.trace + sess.mu.Unlock() + + cc.mqttEnqueuePubResponse(mqttPacketPubRel, pi, trace) +} + +// The MQTT Server MUST NOT match Topic Filters starting with a wildcard +// character (# or +) with Topic Names beginning with a $ character, Spec +// [MQTT-4.7.2-1]. We will return true if there is a violation. +// +// Session or subMu lock must be held on entry to protect access to sub.mqtt. +func mqttMustIgnoreForReservedSub(sub *subscription, subject string) bool { + // If the subject does not start with $ nothing to do here. + if !sub.mqtt.reserved || len(subject) == 0 || subject[0] != mqttReservedPre { + return false + } + return true +} + +// Check if a sub is a reserved wildcard. E.g. '#', '*', or '*/" prefix. +func isMQTTReservedSubscription(subject string) bool { + if len(subject) == 1 && (subject[0] == fwc || subject[0] == pwc) { + return true + } + // Match "*.<>" + if len(subject) > 1 && (subject[0] == pwc && subject[1] == btsep) { + return true + } + return false +} + +func sparkbReplaceDeathTimestamp(msg []byte) []byte { + const VARINT = 0 + const TIMESTAMP = 1 + + orig := msg + buf := bytes.NewBuffer(make([]byte, 0, len(msg)+16)) // 16 bytes should be enough if we need to add a timestamp + writeDeathTimestamp := func() { + // [tck-id-conformance-mqtt-aware-ndeath-timestamp] A Sparkplug Aware + // MQTT Server MAY replace the timestamp of NDEATH messages. If it does, + // it MUST set the timestamp to the UTC time at which it attempts to + // deliver the NDEATH to subscribed clients + // + // sparkB spec: 6.4.1. Google Protocol Buffer Schema + // optional uint64 timestamp = 1; // Timestamp at message sending time + // + // SparkplugB timestamps are milliseconds since epoch, represented as + // uint64 in go, transmitted as protobuf varint. + ts := uint64(time.Now().UnixMilli()) + buf.Write(protoEncodeVarint(TIMESTAMP<<3 | VARINT)) + buf.Write(protoEncodeVarint(ts)) + } + + for len(msg) > 0 { + fieldNumericID, fieldType, size, err := protoScanField(msg) + if err != nil { + return orig + } + if fieldType != VARINT || fieldNumericID != TIMESTAMP { + // Add the field as is + buf.Write(msg[:size]) + msg = msg[size:] + continue + } + + writeDeathTimestamp() + + // Add the rest of the message as is, we are done + buf.Write(msg[size:]) + return buf.Bytes() + } + + // Add timestamp if we did not find one. + writeDeathTimestamp() + + return buf.Bytes() +} + +// Common function to mqtt delivery callbacks to serialize and send the message +// to the `cc` client. +func (c *client) mqttEnqueuePublishMsgTo(cc *client, sub *subscription, pi uint16, qos byte, dup bool, topic, msg []byte) { + // [tck-id-conformance-mqtt-aware-nbirth-mqtt-retain] A Sparkplug Aware + // MQTT Server MUST make NBIRTH messages available on the topic: + // $sparkplug/certificates/namespace/group_id/NBIRTH/edge_node_id with + // the MQTT retain flag set to true + // + // [tck-id-conformance-mqtt-aware-dbirth-mqtt-retain] A Sparkplug Aware + // MQTT Server MUST make DBIRTH messages available on the topic: + // $sparkplug/certificates/namespace/group_id/DBIRTH/edge_node_id/device_id + // with the MQTT retain flag set to true + // + // $sparkplug/certificates messages are sent as NATS messages, so we + // need to add the retain flag when sending them to MQTT clients. + + retain := false + isBirth, isDeath, isCertificate := sparkbParseBirthDeathTopic(topic) + if isBirth && qos == 0 { + retain = isCertificate + } else if isDeath && !isCertificate { + msg = sparkbReplaceDeathTimestamp(msg) + } + + flags, headerBytes := mqttMakePublishHeader(pi, qos, dup, retain, topic, len(msg)) + + cc.mu.Lock() + if sub.mqtt.prm != nil { + for _, data := range sub.mqtt.prm { + cc.queueOutbound(data) + } + sub.mqtt.prm = nil + } + cc.queueOutbound(headerBytes) + cc.queueOutbound(msg) + c.addToPCD(cc) + trace := cc.trace + cc.mu.Unlock() + + if trace { + pp := mqttPublish{ + topic: topic, + flags: flags, + pi: pi, + sz: len(msg), + } + cc.traceOutOp("PUBLISH", []byte(mqttPubTrace(&pp))) + } +} + +// Serializes to the given writer the message for the given subject. +func (w *mqttWriter) WritePublishHeader(pi uint16, qos byte, dup, retained bool, topic []byte, msgLen int) byte { + // Compute len (will have to add packet id if message is sent as QoS>=1) + pkLen := 2 + len(topic) + msgLen + var flags byte + + // Set flags for dup/retained/qos1 + if dup { + flags |= mqttPubFlagDup + } + if retained { + flags |= mqttPubFlagRetain + } + if qos > 0 { + pkLen += 2 + flags |= qos << 1 + } + + w.WriteByte(mqttPacketPub | flags) + w.WriteVarInt(pkLen) + w.WriteBytes(topic) + if qos > 0 { + w.WriteUint16(pi) + } + + return flags +} + +// Serializes to the given writer the message for the given subject. +func mqttMakePublishHeader(pi uint16, qos byte, dup, retained bool, topic []byte, msgLen int) (byte, []byte) { + headerBuf := newMQTTWriter(mqttInitialPubHeader + len(topic)) + flags := headerBuf.WritePublishHeader(pi, qos, dup, retained, topic, msgLen) + return flags, headerBuf.Bytes() +} + +// Process the SUBSCRIBE packet. +// +// Process the list of subscriptions and update the given filter +// with the QoS that has been accepted (or failure). +// +// Spec [MQTT-3.8.4-3] says that if an exact same subscription is +// found, it needs to be replaced with the new one (possibly updating +// the qos) and that the flow of publications must not be interrupted, +// which I read as the replacement cannot be a "remove then add" if there +// is a chance that in between the 2 actions, published messages +// would be "lost" because there would not be any matching subscription. +// +// Run from client's readLoop. +// No lock held on entry. +func (c *client) mqttProcessSubs(filters []*mqttFilter) ([]*subscription, error) { + // Those things are immutable, but since processing subs is not + // really in the fast path, let's get them under the client lock. + c.mu.Lock() + asm := c.mqtt.asm + sess := c.mqtt.sess + trace := c.trace + c.mu.Unlock() + + if err := asm.lockSession(sess, c); err != nil { + return nil, err + } + defer asm.unlockSession(sess) + return asm.processSubs(sess, c, filters, true, trace) +} + +// Cleanup that is performed in processSubs if there was an error. +// +// Runs from client's readLoop. +// Lock not held on entry, but session is in the locked map. +func (sess *mqttSession) cleanupFailedSub(c *client, sub *subscription, cc *ConsumerConfig, jssub *subscription) { + if sub != nil { + c.processUnsub(sub.sid) + } + if jssub != nil { + c.processUnsub(jssub.sid) + } + if cc != nil { + sess.deleteConsumer(cc) + } +} + +// Make sure we are set up to deliver PUBREL messages to this QoS2-subscribed +// session. +func (sess *mqttSession) ensurePubRelConsumerSubscription(c *client) error { + + sess.mu.Lock() + pubRelSubscribed := sess.pubRelSubscribed + pubRelDeliverySubjectB := sess.pubRelDeliverySubjectB + pubRelDeliverySubject := sess.pubRelDeliverySubject + pubRelConsumer := sess.pubRelConsumer + sess.mu.Unlock() + + // Subscribe before the consumer is created so we don't loose any messages. + if !pubRelSubscribed { + _, err := c.processSub(pubRelDeliverySubjectB, nil, pubRelDeliverySubjectB, mqttDeliverPubRelCb, false) + if err != nil { + c.Errorf("Unable to create subscription for JetStream consumer on %q: %v", pubRelDeliverySubject, err) + return err + } + sess.mu.Lock() + sess.pubRelSubscribed = true + sess.mu.Unlock() + } + + // If the JS consumer already exists, we are done. + if pubRelConsumer != nil { + return nil + } + + opts := c.srv.getOpts() + ackWait := opts.MQTT.AckWait + if ackWait == 0 { + ackWait = mqttDefaultAckWait + } + maxAckPending := int(opts.MQTT.MaxAckPending) + if maxAckPending == 0 { + maxAckPending = mqttDefaultMaxAckPending + } + + sess.mu.Lock() + pubRelSubject := sess.pubRelSubject + tmaxack := sess.tmaxack + idHash := sess.idHash + id := sess.id + sess.mu.Unlock() + + // Check that the limit of subs' maxAckPending are not going over the limit + if after := tmaxack + maxAckPending; after > mqttMaxAckTotalLimit { + return fmt.Errorf("max_ack_pending for all consumers would be %v which exceeds the limit of %v", + after, mqttMaxAckTotalLimit) + } + + ccr := &CreateConsumerRequest{ + Stream: mqttOutStreamName, + Config: ConsumerConfig{ + DeliverSubject: pubRelDeliverySubject, + Durable: mqttPubRelConsumerDurablePrefix + idHash, + AckPolicy: AckExplicit, + DeliverPolicy: DeliverNew, + FilterSubject: pubRelSubject, + AckWait: ackWait, + MaxAckPending: maxAckPending, + MemoryStorage: opts.MQTT.ConsumerMemoryStorage, + }, + } + if opts.MQTT.ConsumerInactiveThreshold > 0 { + ccr.Config.InactiveThreshold = opts.MQTT.ConsumerInactiveThreshold + } + if _, err := sess.jsa.createDurableConsumer(ccr); err != nil { + c.Errorf("Unable to add JetStream consumer for PUBREL for client %q: err=%v", id, err) + return err + } + pubRelConsumer = &ccr.Config + tmaxack += maxAckPending + + sess.mu.Lock() + sess.pubRelConsumer = pubRelConsumer + sess.tmaxack = tmaxack + sess.mu.Unlock() + + return nil +} + +// When invoked with a QoS of 0, looks for an existing JS durable consumer for +// the given sid and if one is found, delete the JS durable consumer and unsub +// the NATS subscription on the delivery subject. +// +// With a QoS > 0, creates or update the existing JS durable consumer along with +// its NATS subscription on a delivery subject. +// +// Session lock is acquired and released as needed. Session is in the locked +// map. +func (sess *mqttSession) processJSConsumer(c *client, subject, sid string, + qos byte, fromSubProto bool) (*ConsumerConfig, *subscription, error) { + + sess.mu.Lock() + cc, exists := sess.cons[sid] + tmaxack := sess.tmaxack + idHash := sess.idHash + sess.mu.Unlock() + + // Check if we are already a JS consumer for this SID. + if exists { + // If current QoS is 0, it means that we need to delete the existing + // one (that was QoS > 0) + if qos == 0 { + // The JS durable consumer's delivery subject is on a NUID of + // the form: mqttSubPrefix + . It is also used as the sid + // for the NATS subscription, so use that for the lookup. + c.mu.Lock() + sub := c.subs[cc.DeliverSubject] + c.mu.Unlock() + + // Delete the consumer entry, mark its delivery subscription closed, + // and purge its pending QoS 1/2 deliveries — all under the session + // lock. Otherwise those packet identifiers leak and count against the + // in-flight cap for the life of the session (as mqttProcessUnsubs + // purges on unsubscribe). deleteConsumer is asynchronous, so an + // in-flight delivery callback could re-populate the maps after the + // purge; marking sub.mqtt.closed (guarded by sess.mu and sess.subsMu, + // same as delivery) makes mqttDeliverMsgCbQoS12 skip instead. The flag + // leaves sub.mqtt valid so an already-committed enqueue does not panic. + sess.mu.Lock() + delete(sess.cons, sid) + if sub != nil && sub.mqtt != nil { + sess.subsMu.Lock() + sub.mqtt.closed = true + sess.subsMu.Unlock() + } + // Also purge this sid's in-flight retained deliveries, tracked + // under a pseudo-durable key (no JS consumer, no redelivery). + for _, dur := range [...]string{cc.Durable, mqttRetainedPendingDur(sid)} { + if seqPis, ok := sess.cpending[dur]; ok { + delete(sess.cpending, dur) + for _, pi := range seqPis { + delete(sess.pendingPublish, pi) + } + } + } + sess.mu.Unlock() + + sess.deleteConsumer(cc) + if sub != nil { + c.processUnsub(sub.sid) + } + return nil, nil, nil + } + // If this is called when processing SUBSCRIBE protocol, then if + // the JS consumer already exists, we are done (it was created + // during the processing of CONNECT). + if fromSubProto { + return nil, nil, nil + } + } + // Here it means we don't have a JS consumer and if we are QoS 0, + // we have nothing to do. + if qos == 0 { + return nil, nil, nil + } + var err error + var inbox string + if exists { + inbox = cc.DeliverSubject + } else { + inbox = mqttSubPrefix + nuid.Next() + opts := c.srv.getOpts() + ackWait := opts.MQTT.AckWait + if ackWait == 0 { + ackWait = mqttDefaultAckWait + } + maxAckPending := int(opts.MQTT.MaxAckPending) + if maxAckPending == 0 { + maxAckPending = mqttDefaultMaxAckPending + } + + // Check that the limit of subs' maxAckPending are not going over the limit + if after := tmaxack + maxAckPending; after > mqttMaxAckTotalLimit { + return nil, nil, fmt.Errorf("max_ack_pending for all consumers would be %v which exceeds the limit of %v", + after, mqttMaxAckTotalLimit) + } + + durName := idHash + "_" + nuid.Next() + ccr := &CreateConsumerRequest{ + Stream: mqttStreamName, + Config: ConsumerConfig{ + DeliverSubject: inbox, + Durable: durName, + AckPolicy: AckExplicit, + DeliverPolicy: DeliverNew, + FilterSubject: mqttStreamSubjectPrefix + subject, + AckWait: ackWait, + MaxAckPending: maxAckPending, + MemoryStorage: opts.MQTT.ConsumerMemoryStorage, + }, + } + if opts.MQTT.ConsumerInactiveThreshold > 0 { + ccr.Config.InactiveThreshold = opts.MQTT.ConsumerInactiveThreshold + } + if _, err := sess.jsa.createDurableConsumer(ccr); err != nil { + c.Errorf("Unable to add JetStream consumer for subscription on %q: err=%v", subject, err) + return nil, nil, err + } + cc = &ccr.Config + tmaxack += maxAckPending + } + + // This is an internal subscription on subject like "$MQTT.sub." that is setup + // for the JS durable's deliver subject. + sess.mu.Lock() + sess.tmaxack = tmaxack + sub, err := sess.processQOS12Sub(c, []byte(inbox), []byte(inbox), + isMQTTReservedSubscription(subject), qos, cc.Durable, mqttDeliverMsgCbQoS12) + sess.mu.Unlock() + + if err != nil { + sess.deleteConsumer(cc) + c.Errorf("Unable to create subscription for JetStream consumer on %q: %v", subject, err) + return nil, nil, err + } + return cc, sub, nil +} + +// Queues the published retained messages for each subscription and signals +// the writeLoop. +func (c *client) mqttSendRetainedMsgsToNewSubs(subs []*subscription) { + c.mu.Lock() + for _, sub := range subs { + if sub.mqtt != nil && sub.mqtt.prm != nil { + for _, data := range sub.mqtt.prm { + c.queueOutbound(data) + } + sub.mqtt.prm = nil + } + } + c.flushSignal() + c.mu.Unlock() +} + +func (c *client) mqttEnqueueSubAck(pi uint16, filters []*mqttFilter) { + w := newMQTTWriter(7 + len(filters)) + w.WriteByte(mqttPacketSubAck) + // packet length is 2 (for packet identifier) and 1 byte per filter. + w.WriteVarInt(2 + len(filters)) + w.WriteUint16(pi) + for _, f := range filters { + w.WriteByte(f.qos) + } + c.mu.Lock() + c.enqueueProto(w.Bytes()) + c.mu.Unlock() +} + +////////////////////////////////////////////////////////////////////////////// +// +// UNSUBSCRIBE related functions +// +////////////////////////////////////////////////////////////////////////////// + +func (c *client) mqttParseUnsubs(r *mqttReader, b byte, pl int) (uint16, []*mqttFilter, error) { + return c.mqttParseSubsOrUnsubs(r, b, pl, false) +} + +// Process the UNSUBSCRIBE packet. +// +// Given the list of topics, this is going to unsubscribe the low level NATS subscriptions +// and delete the JS durable consumers when applicable. +// +// Runs from the client's readLoop. +// No lock held on entry. +func (c *client) mqttProcessUnsubs(filters []*mqttFilter) error { + // Those things are immutable, but since processing unsubs is not + // really in the fast path, let's get them under the client lock. + c.mu.Lock() + asm := c.mqtt.asm + sess := c.mqtt.sess + c.mu.Unlock() + + if err := asm.lockSession(sess, c); err != nil { + return err + } + defer asm.unlockSession(sess) + + removeJSCons := func(sid string) { + cc, ok := sess.cons[sid] + if ok { + delete(sess.cons, sid) + sess.deleteConsumer(cc) + + c.mu.Lock() + sub := c.subs[cc.DeliverSubject] + c.mu.Unlock() + + // Need lock here since these are accessed by callbacks + sess.mu.Lock() + // Mark the delivery sub closed so an in-flight QoS 1/2 callback stops + // tracking new messages after the purge (deleteConsumer is async); + // same barrier as the QoS 0 downgrade path in processJSConsumer. + if sub != nil && sub.mqtt != nil { + sess.subsMu.Lock() + sub.mqtt.closed = true + sess.subsMu.Unlock() + } + // Purge both the consumer's deliveries and this sid's in-flight + // retained deliveries (tracked under a pseudo-durable key). + for _, dur := range [...]string{cc.Durable, mqttRetainedPendingDur(sid)} { + if seqPis, ok := sess.cpending[dur]; ok { + delete(sess.cpending, dur) + for _, pi := range seqPis { + delete(sess.pendingPublish, pi) + } + // last_pi stays monotonic (see untrackPublish); do not reset here. + } + } + sess.mu.Unlock() + } + } + for _, f := range filters { + sid := f.filter + // Remove JS Consumer if one exists for this sid + removeJSCons(sid) + if err := c.processUnsub([]byte(sid)); err != nil { + c.Errorf("error unsubscribing from %q: %v", sid, err) + } + if mqttNeedSubForLevelUp(sid) { + subject := sid[:len(sid)-2] + sid = subject + mqttMultiLevelSidSuffix + removeJSCons(sid) + if err := c.processUnsub([]byte(sid)); err != nil { + c.Errorf("error unsubscribing from %q: %v", subject, err) + } + } + } + return sess.update(filters, false) +} + +func (c *client) mqttEnqueueUnsubAck(pi uint16) { + w := newMQTTWriter(4) + w.WriteByte(mqttPacketUnsubAck) + w.WriteVarInt(2) + w.WriteUint16(pi) + c.mu.Lock() + c.enqueueProto(w.Bytes()) + c.mu.Unlock() +} + +func mqttUnsubscribeTrace(pi uint16, filters []*mqttFilter) string { + var sep string + sb := strings.Builder{} + sb.WriteString("[") + for i, f := range filters { + sb.WriteString(sep) + sb.Write(f.ttopic) + sb.WriteString(" (") + sb.WriteString(f.filter) + sb.WriteString(")") + if i == 0 { + sep = ", " + } + } + sb.WriteString(fmt.Sprintf("] pi=%v", pi)) + return sb.String() +} + +////////////////////////////////////////////////////////////////////////////// +// +// PINGREQ/PINGRESP related functions +// +////////////////////////////////////////////////////////////////////////////// + +func (c *client) mqttEnqueuePingResp() { + c.mu.Lock() + c.enqueueProto(mqttPingResponse) + c.mu.Unlock() +} + +////////////////////////////////////////////////////////////////////////////// +// +// Trace functions +// +////////////////////////////////////////////////////////////////////////////// + +func errOrTrace(err error, trace string) []byte { + if err != nil { + return []byte(err.Error()) + } + return []byte(trace) +} + +////////////////////////////////////////////////////////////////////////////// +// +// Subject/Topic conversion functions +// +////////////////////////////////////////////////////////////////////////////// + +// Converts an MQTT Topic Name to a NATS Subject (used by PUBLISH) +// See mqttToNATSSubjectConversion() for details. +func mqttTopicToNATSPubSubject(mt []byte) ([]byte, error) { + return mqttToNATSSubjectConversion(mt, false) +} + +// Converts an MQTT Topic Filter to a NATS Subject (used by SUBSCRIBE) +// See mqttToNATSSubjectConversion() for details. +func mqttFilterToNATSSubject(filter []byte) ([]byte, error) { + return mqttToNATSSubjectConversion(filter, true) +} + +// Converts an MQTT Topic Name or Filter to a NATS Subject. +// In MQTT: +// - a Topic Name does not have wildcard (PUBLISH uses only topic names). +// - a Topic Filter can include wildcards (SUBSCRIBE uses those). +// - '+' and '#' are wildcard characters (single and multiple levels respectively) +// - '/' is the topic level separator. +// +// Conversion that occurs: +// - '/' is replaced with '/.' if it is the first character in mt +// - '/' is replaced with './' if the last or next character in mt is '/' +// For instance, foo//bar would become foo./.bar +// - '/' is replaced with '.' for all other conditions (foo/bar -> foo.bar) +// - '.' is replaced with '//'. +// - ' ' cause an error to be returned. +// +// If there is no need to convert anything (say "foo" remains "foo"), then +// the no memory is allocated and the returned slice is the original `mt`. +func mqttToNATSSubjectConversion(mt []byte, wcOk bool) ([]byte, error) { + var cp bool + var j int + res := mt + + makeCopy := func(i int) { + cp = true + res = make([]byte, 0, len(mt)+10) + if i > 0 { + res = append(res, mt[:i]...) + } + } + + end := len(mt) - 1 + for i := 0; i < len(mt); i++ { + switch mt[i] { + case mqttTopicLevelSep: + if i == 0 || res[j-1] == btsep { + if !cp { + makeCopy(0) + } + res = append(res, mqttTopicLevelSep, btsep) + j++ + } else if i == end || mt[i+1] == mqttTopicLevelSep { + if !cp { + makeCopy(i) + } + res = append(res, btsep, mqttTopicLevelSep) + j++ + } else { + if !cp { + makeCopy(i) + } + res = append(res, btsep) + } + case ' ', '\t', '\n', '\r', '\f': + // We cannot support whitespace in the MQTT topic/filter — these + // characters would also corrupt the NATS wire protocol when the + // subject is forwarded to other connection types (e.g. leaf + // nodes) where the resulting control line could be split. + return nil, errMQTTUnsupportedCharacters + case 0x7f: + // SubjectTree uses DEL as an internal pivot marker, so retained + // subjects containing it cannot be indexed safely, including + // legacy retained messages recovered from the retained-message + // stream. + return nil, errMQTTUnsupportedCharacters + case btsep: + if !cp { + makeCopy(i) + } + res = append(res, mqttTopicLevelSep, mqttTopicLevelSep) + j++ + case mqttSingleLevelWC, mqttMultiLevelWC: + if !wcOk { + // Spec [MQTT-3.3.2-2] and [MQTT-4.7.1-1] + // The wildcard characters can be used in Topic Filters, but MUST NOT be used within a Topic Name + return nil, fmt.Errorf("wildcards not allowed in publish's topic: %q", mt) + } + if !cp { + makeCopy(i) + } + if mt[i] == mqttSingleLevelWC { + res = append(res, pwc) + } else { + res = append(res, fwc) + } + default: + if cp { + res = append(res, mt[i]) + } + } + j++ + } + if cp && res[j-1] == btsep { + res = append(res, mqttTopicLevelSep) + j++ + } + return res[:j], nil +} + +// Converts a NATS subject to MQTT topic. This is for publish +// messages only, so there is no checking for wildcards. +// Rules are reversed of mqttToNATSSubjectConversion. +func natsSubjectStrToMQTTTopic(subject string) []byte { + return natsSubjectToMQTTTopic(stringToBytes(subject)) +} + +func natsSubjectToMQTTTopic(subject []byte) []byte { + topic := make([]byte, len(subject)) + end := len(subject) - 1 + var j int + for i := 0; i < len(subject); i++ { + switch subject[i] { + case mqttTopicLevelSep: + if i < end { + switch c := subject[i+1]; c { + case btsep, mqttTopicLevelSep: + if c == btsep { + topic[j] = mqttTopicLevelSep + } else { + topic[j] = btsep + } + j++ + i++ + default: + } + } + case btsep: + topic[j] = mqttTopicLevelSep + j++ + default: + topic[j] = subject[i] + j++ + } + } + return topic[:j] +} + +// Returns true if the subject has more than 1 token and ends with ".>" +func mqttNeedSubForLevelUp(subject string) bool { + if len(subject) < 3 { + return false + } + end := len(subject) + if subject[end-2] == '.' && subject[end-1] == fwc { + return true + } + return false +} + +////////////////////////////////////////////////////////////////////////////// +// +// MQTT Reader functions +// +////////////////////////////////////////////////////////////////////////////// + +func (r *mqttReader) reset(buf []byte) { + if l := len(r.pbuf); l > 0 { + tmp := make([]byte, l+len(buf)) + copy(tmp, r.pbuf) + copy(tmp[l:], buf) + buf = tmp + r.pbuf = nil + } + r.buf = buf + r.pos = 0 + r.pstart = 0 +} + +func (r *mqttReader) hasMore() bool { + return r.pos != len(r.buf) +} + +func (r *mqttReader) readByte(field string) (byte, error) { + if r.pos == len(r.buf) { + return 0, fmt.Errorf("error reading %s: %v", field, io.EOF) + } + b := r.buf[r.pos] + r.pos++ + return b, nil +} + +func (r *mqttReader) readPacketLen(maxLen int32) (int, bool, error) { + v, complete, err := r.readVarInt() + if err != nil { + return 0, false, err + } + if complete { + packetEnd := r.pos + v + packetLen := packetEnd - r.pstart + if maxLen != jwt.NoLimit && int64(packetLen) > int64(maxLen) { + return packetLen, false, ErrMaxPayload + } + if packetEnd <= len(r.buf) { + return v, true, nil + } + } + r.pbuf = make([]byte, len(r.buf)-r.pstart) + copy(r.pbuf, r.buf[r.pstart:]) + return 0, false, nil +} + +func (r *mqttReader) readVarInt() (int, bool, error) { + m := 1 + v := 0 + for { + var b byte + if r.pos != len(r.buf) { + b = r.buf[r.pos] + r.pos++ + } else { + break + } + v += int(b&0x7f) * m + if (b & 0x80) == 0 { + return v, true, nil + } + m *= 0x80 + if m > 0x200000 { + return 0, false, errMQTTMalformedVarInt + } + } + return 0, false, nil +} + +func (r *mqttReader) readString(field string) (string, error) { + var s string + bs, err := r.readBytes(field, false) + if err == nil { + s = string(bs) + } + return s, err +} + +func (r *mqttReader) readBytes(field string, cp bool) ([]byte, error) { + luint, err := r.readUint16(field) + if err != nil { + return nil, err + } + l := int(luint) + if l == 0 { + return nil, nil + } + start := r.pos + if start+l > len(r.buf) { + return nil, fmt.Errorf("error reading %s: %v", field, io.ErrUnexpectedEOF) + } + r.pos += l + b := r.buf[start:r.pos] + if cp { + b = copyBytes(b) + } + return b, nil +} + +func (r *mqttReader) readUint16(field string) (uint16, error) { + if len(r.buf)-r.pos < 2 { + return 0, fmt.Errorf("error reading %s: %v", field, io.ErrUnexpectedEOF) + } + start := r.pos + r.pos += 2 + return binary.BigEndian.Uint16(r.buf[start:r.pos]), nil +} + +////////////////////////////////////////////////////////////////////////////// +// +// MQTT Writer functions +// +////////////////////////////////////////////////////////////////////////////// + +func (w *mqttWriter) WriteUint16(i uint16) { + w.WriteByte(byte(i >> 8)) + w.WriteByte(byte(i)) +} + +func (w *mqttWriter) WriteString(s string) { + w.WriteBytes([]byte(s)) +} + +func (w *mqttWriter) WriteBytes(bs []byte) { + w.WriteUint16(uint16(len(bs))) + w.Write(bs) +} + +func (w *mqttWriter) WriteVarInt(value int) { + for { + b := byte(value & 0x7f) + value >>= 7 + if value > 0 { + b |= 0x80 + } + w.WriteByte(b) + if value == 0 { + break + } + } +} + +func newMQTTWriter(cap int) *mqttWriter { + w := &mqttWriter{} + w.Grow(cap) + return w +} diff --git a/server/opts.go b/server/opts.go new file mode 100644 index 0000000..fbfc88e --- /dev/null +++ b/server/opts.go @@ -0,0 +1,6691 @@ +// Copyright 2012-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 ( + "context" + "crypto/fips140" + "crypto/tls" + "crypto/x509" + "errors" + "flag" + "fmt" + "math" + "net" + "net/url" + "os" + "path" + "path/filepath" + "reflect" + "regexp" + "runtime" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/nats-io/jwt/v2" + "github.com/nats-io/nats-server/v2/conf" + "github.com/nats-io/nats-server/v2/server/certidp" + "github.com/nats-io/nats-server/v2/server/certstore" + "github.com/nats-io/nkeys" +) + +var allowUnknownTopLevelField = int32(0) + +// NoErrOnUnknownFields can be used to change the behavior the processing +// of a configuration file. By default, an error is reported if unknown +// fields are found. If `noError` is set to true, no error will be reported +// if top-level unknown fields are found. +func NoErrOnUnknownFields(noError bool) { + var val int32 + if noError { + val = int32(1) + } + atomic.StoreInt32(&allowUnknownTopLevelField, val) +} + +// PinnedCertSet is a set of lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo +type PinnedCertSet map[string]struct{} + +// ClusterOpts are options for clusters. +// NOTE: This structure is no longer used for monitoring endpoints +// and json tags are deprecated and may be removed in the future. +type ClusterOpts struct { + Name string `json:"-"` + Host string `json:"addr,omitempty"` + Port int `json:"cluster_port,omitempty"` + Username string `json:"-"` + Password string `json:"-"` + AuthTimeout float64 `json:"auth_timeout,omitempty"` + Permissions *RoutePermissions `json:"-"` + TLSTimeout float64 `json:"-"` + TLSConfig *tls.Config `json:"-"` + TLSMap bool `json:"-"` + TLSCheckKnownURLs bool `json:"-"` + TLSPinnedCerts PinnedCertSet `json:"-"` + ListenStr string `json:"-"` + Advertise string `json:"-"` + NoAdvertise bool `json:"-"` + ConnectRetries int `json:"-"` + ConnectBackoff bool `json:"-"` + PoolSize int `json:"-"` + PinnedAccounts []string `json:"-"` + Compression CompressionOpts `json:"-"` + PingInterval time.Duration `json:"-"` + MaxPingsOut int `json:"-"` + WriteDeadline time.Duration `json:"-"` + WriteTimeout WriteTimeoutPolicy `json:"-"` + + // Not exported (used in tests) + resolver netResolver + // Snapshot of configured TLS options. + tlsConfigOpts *TLSConfigOpts +} + +// CompressionOpts defines the compression mode and optional configuration. +type CompressionOpts struct { + Mode string + // If `Mode` is set to CompressionS2Auto, RTTThresholds provides the + // thresholds at which the compression level will go from + // CompressionS2Uncompressed to CompressionS2Fast, CompressionS2Better + // or CompressionS2Best. If a given level is not desired, specify 0 + // for this slot. For instance, the slice []{0, 10ms, 20ms} means that + // for any RTT up to 10ms included the compression level will be + // CompressionS2Fast, then from ]10ms..20ms], the level will be selected + // as CompressionS2Better. Anything above 20ms will result in picking + // the CompressionS2Best compression level. + RTTThresholds []time.Duration +} + +func (c1 *CompressionOpts) equals(c2 *CompressionOpts) bool { + if c1 == c2 { + return true + } + if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) { + return false + } + if c1.Mode != c2.Mode { + return false + } + // For s2_auto, if one has an empty RTTThresholds, it is equivalent + // to the defaultCompressionS2AutoRTTThresholds array, so compare with that. + if c1.Mode == CompressionS2Auto { + rtts1 := c1.RTTThresholds + if len(rtts1) == 0 { + rtts1 = defaultCompressionS2AutoRTTThresholds + } + rtts2 := c2.RTTThresholds + if len(rtts2) == 0 { + rtts2 = defaultCompressionS2AutoRTTThresholds + } + if !reflect.DeepEqual(rtts1, rtts2) { + return false + } + } + return true +} + +// GatewayOpts are options for gateways. +// NOTE: This structure is no longer used for monitoring endpoints +// and json tags are deprecated and may be removed in the future. +type GatewayOpts struct { + Name string `json:"name"` + Host string `json:"addr,omitempty"` + Port int `json:"port,omitempty"` + Username string `json:"-"` + Password string `json:"-"` + AuthTimeout float64 `json:"auth_timeout,omitempty"` + TLSConfig *tls.Config `json:"-"` + TLSTimeout float64 `json:"tls_timeout,omitempty"` + TLSMap bool `json:"-"` + TLSCheckKnownURLs bool `json:"-"` + TLSPinnedCerts PinnedCertSet `json:"-"` + Advertise string `json:"advertise,omitempty"` + ConnectRetries int `json:"connect_retries,omitempty"` + ConnectBackoff bool `json:"connect_backoff,omitempty"` + Gateways []*RemoteGatewayOpts `json:"gateways,omitempty"` + RejectUnknown bool `json:"reject_unknown,omitempty"` // config got renamed to reject_unknown_cluster + WriteDeadline time.Duration `json:"-"` + WriteTimeout WriteTimeoutPolicy `json:"-"` + + // Not exported, for tests. + resolver netResolver + sendQSubsBufSize int + + // Snapshot of configured TLS options. + tlsConfigOpts *TLSConfigOpts +} + +// RemoteGatewayOpts are options for connecting to a remote gateway +// NOTE: This structure is no longer used for monitoring endpoints +// and json tags are deprecated and may be removed in the future. +type RemoteGatewayOpts struct { + Name string `json:"name"` + TLSConfig *tls.Config `json:"-"` + TLSTimeout float64 `json:"tls_timeout,omitempty"` + URLs []*url.URL `json:"urls,omitempty"` + tlsConfigOpts *TLSConfigOpts +} + +// LeafNodeOpts are options for a given server to accept leaf node connections and/or connect to a remote cluster. +type LeafNodeOpts struct { + Host string `json:"addr,omitempty"` + Port int `json:"port,omitempty"` + Username string `json:"-"` + Password string `json:"-"` + ProxyRequired bool `json:"-"` + Nkey string `json:"-"` + Account string `json:"-"` + Users []*User `json:"-"` + AuthTimeout float64 `json:"auth_timeout,omitempty"` + TLSConfig *tls.Config `json:"-"` + TLSTimeout float64 `json:"tls_timeout,omitempty"` + TLSMap bool `json:"-"` + TLSPinnedCerts PinnedCertSet `json:"-"` + // When set to true, the server will perform the TLS handshake before + // sending the INFO protocol. For remote leafnodes that are not configured + // with a similar option, their connection will fail with some sort + // of timeout or EOF error since they are expecting to receive an + // INFO protocol first. + TLSHandshakeFirst bool `json:"-"` + // If TLSHandshakeFirst is true and this value is strictly positive, + // the server will wait for that amount of time for the TLS handshake + // to start before falling back to previous behavior of sending the + // INFO protocol first. It allows for a mix of newer remote leafnodes + // that can require a TLS handshake first, and older that can't. + TLSHandshakeFirstFallback time.Duration `json:"-"` + Advertise string `json:"-"` + NoAdvertise bool `json:"-"` + ReconnectInterval time.Duration `json:"-"` + WriteDeadline time.Duration `json:"-"` + WriteTimeout WriteTimeoutPolicy `json:"-"` + + // Compression options + Compression CompressionOpts `json:"-"` + + // For solicited connections to other clusters/superclusters. + Remotes []*RemoteLeafOpts `json:"remotes,omitempty"` + + // This is the minimum version that is accepted for remote connections. + // Note that since the server version in the CONNECT protocol was added + // only starting at v2.8.0, any version below that will be rejected + // (since empty version string in CONNECT would fail the "version at + // least" test). + MinVersion string + + // Isolate subject interest from other leafnode connections, preventing + // east-west propagation. + IsolateLeafnodeInterest bool `json:"-"` + + // DialTimeout is the amount of time the server will wait for the TCP + // connection to a remote server to be established. This is useful on high + // latency links where the default (DEFAULT_ROUTE_DIAL, 1 second) is not + // enough for the handshake to complete. It applies to all remotes, but can + // be overridden on a per-remote basis with RemoteLeafOpts.DialTimeout. + // If not set (or <= 0), DEFAULT_ROUTE_DIAL is used. + DialTimeout time.Duration `json:"-"` + + // Not exported, for tests. + resolver netResolver + connDelay time.Duration + dialer leafNodeDialer + + // Snapshot of configured TLS options. + tlsConfigOpts *TLSConfigOpts +} + +// SignatureHandler is used to sign a nonce from the server while +// authenticating with Nkeys. The callback should sign the nonce and +// return the JWT and the raw signature. +type SignatureHandler func([]byte) (string, []byte, error) + +// RemoteLeafOpts are options for connecting to a remote server as a leaf node. +type RemoteLeafOpts struct { + LocalAccount string `json:"local_account,omitempty"` + NoRandomize bool `json:"-"` + URLs []*url.URL `json:"urls,omitempty"` + Credentials string `json:"-"` + Nkey string `json:"-"` + SignatureCB SignatureHandler `json:"-"` + TLS bool `json:"-"` + TLSConfig *tls.Config `json:"-"` + TLSTimeout float64 `json:"tls_timeout,omitempty"` + TLSHandshakeFirst bool `json:"-"` + Hub bool `json:"hub,omitempty"` + DenyImports []string `json:"-"` + DenyExports []string `json:"-"` + + // FirstInfoTimeout is the amount of time the server will wait for the + // initial INFO protocol from the remote server before closing the + // connection. + FirstInfoTimeout time.Duration `json:"-"` + + // DialTimeout is the amount of time the server will wait for the TCP + // connection to this remote server to be established. This is useful on + // high latency links where the default is not enough for the handshake to + // complete. If not set (or <= 0), the server-wide LeafNodeOpts.DialTimeout + // is used, which itself defaults to DEFAULT_ROUTE_DIAL (1 second). + DialTimeout time.Duration `json:"-"` + + // Compression options for this remote. Each remote could have a different + // setting and also be different from the LeafNode options. + Compression CompressionOpts `json:"-"` + + // When an URL has the "ws" (or "wss") scheme, then the server will initiate the + // connection as a websocket connection. By default, the websocket frames will be + // masked (as if this server was a websocket client to the remote server). The + // NoMasking option will change this behavior and will send umasked frames. + Websocket struct { + Compression bool `json:"-"` + NoMasking bool `json:"-"` + } + + // HTTP Proxy configuration for WebSocket connections + Proxy struct { + // URL of the HTTP proxy server (e.g., "http://proxy.example.com:8080") + URL string `json:"-"` + // Username for proxy authentication + Username string `json:"-"` + // Password for proxy authentication + Password string `json:"-"` + // Timeout for proxy connection + Timeout time.Duration `json:"-"` + } + + tlsConfigOpts *TLSConfigOpts + + // If we are clustered and our local account has JetStream, if apps are accessing + // a stream or consumer leader through this LN and it gets dropped, the apps will + // not be able to work. This tells the system to migrate the leaders away from this server. + // This only changes leader for R>1 assets. + JetStreamClusterMigrate bool `json:"jetstream_cluster_migrate,omitempty"` + + // If JetStreamClusterMigrate is set to true, this is the time after which the leader + // will be migrated away from this server if still disconnected. + JetStreamClusterMigrateDelay time.Duration `json:"jetstream_cluster_migrate_delay,omitempty"` + + // LocalIsolation isolates this remote from east-west subject interest originating locally. + LocalIsolation bool `json:"local_isolation,omitempty"` + + // RequestIsolation asks the remote side to isolate us from their east-west subject interest. + RequestIsolation bool `json:"request_isolation,omitempty"` + + // If this is set to true, the connection to this remote will not be solicited. + // During a configuration reload, if this is changed from `false` to `true`, the + // existing connection will be closed and not solicited again (until it is changed + // to `false` again. + Disabled bool `json:"-"` + + // If this is set to true, this remote will ignore any server leafnode URLs + // returned by the hub, allowing the user to fully manage the servers this + // remote can connect to. + IgnoreDiscoveredServers bool `json:"-"` +} + +// Returns a string representation of this `RemoteLeafOpts` object, containing +// the URLs (unredacted), the account (or "$G" if none is specified) and, if present, +// the credentials filename. +func (r *RemoteLeafOpts) name() string { + return generateRemoteLeafOptsName(r, false) +} + +// Same than RemoteLeafOpts.name() but uses redacted URLs. This is to be used for logging. +func (r *RemoteLeafOpts) safeName() string { + return generateRemoteLeafOptsName(r, true) +} + +func generateRemoteLeafOptsName(r *RemoteLeafOpts, redacted bool) string { + acc := r.LocalAccount + if acc == _EMPTY_ { + acc = globalAccountName + } + var optional string + // There could be Credentials or NKey, not both (would be caught as a misconfig) + if c := r.Credentials; c != _EMPTY_ { + optional = fmt.Sprintf(", credentials=%q", c) + } else if nk := r.Nkey; nk != _EMPTY_ { + if redacted { + optional = ", nkey=\"[REDACTED]\"" + } else { + optional = fmt.Sprintf(", nkey=%q", nk) + } + } + var urls []*url.URL + if redacted { + urls = redactURLList(r.URLs) + } else { + urls = r.URLs + } + return fmt.Sprintf("urls=%q, account=%q%s", urls, acc, optional) +} + +// JSLimitOpts are active limits for the meta cluster +type JSLimitOpts struct { + MaxRequestBatch int `json:"max_request_batch,omitempty"` // MaxRequestBatch is the maximum amount of updates that can be sent in a batch + MaxAckPending int `json:"max_ack_pending,omitempty"` // MaxAckPending is the server limit for maximum amount of outstanding Acks + MaxHAAssets int `json:"max_ha_assets,omitempty"` // MaxHAAssets is the maximum of Streams and Consumers that may have more than 1 replica + Duplicates time.Duration `json:"max_duplicate_window,omitempty"` // Duplicates is the maximum value for duplicate tracking on Streams + MaxBatchInflightPerStream int `json:"max_batch_inflight_per_stream,omitempty"` // MaxBatchInflightPerStream is the maximum amount of open batches per stream + MaxBatchInflightTotal int `json:"max_batch_inflight_total,omitempty"` // MaxBatchInflightTotal is the maximum amount of total open batches per server + MaxBatchSize int `json:"max_batch_size,omitempty"` // MaxBatchSize is the maximum amount of messages allowed in a batch publish to a Stream + MaxBatchTimeout time.Duration `json:"max_batch_timeout,omitempty"` // MaxBatchTimeout is the maximum time to receive the commit message after receiving the first message of a batch + + // Max asset limits + DefaultMaxConsumers int `json:"default_max_consumers,omitempty"` // DefaultMaxConsumers is the maximum number of consumers per stream (unless overwritten on the stream or the account) (-1=unlimited) +} + +type JSTpmOpts struct { + KeysFile string + KeyPassword string + SrkPassword string + Pcr int +} + +// AuthCallout option used to map external AuthN to NATS based AuthZ. +type AuthCallout struct { + // Must be a public account Nkey. + Issuer string + // Account to be used for sending requests. + Account string + // Users that will bypass auth_callout and be used for the auth service itself. + AuthUsers []string + // XKey is a public xkey for the authorization service. + // This will enable encryption for server requests and the authorization service responses. + XKey string + // AllowedAccounts that will be delegated to the auth service. + // If empty then all accounts will be delegated. + AllowedAccounts []string +} + +// Options block for nats-server. +// NOTE: This structure is no longer used for monitoring endpoints +// and json tags are deprecated and may be removed in the future. +type Options struct { + ConfigFile string `json:"-"` + ServerName string `json:"server_name"` + Host string `json:"addr"` + Port int `json:"port"` + DontListen bool `json:"dont_listen"` + ClientAdvertise string `json:"-"` + Trace bool `json:"-"` + Debug bool `json:"-"` + TraceVerbose bool `json:"-"` + + // TraceHeaders if true will only trace message headers, not the payload. + TraceHeaders bool `json:"-"` + NoLog bool `json:"-"` + NoSigs bool `json:"-"` + NoSublistCache bool `json:"-"` + NoHeaderSupport bool `json:"-"` + DisableShortFirstPing bool `json:"-"` + Logtime bool `json:"-"` + LogtimeUTC bool `json:"-"` + MaxConn int `json:"max_connections"` + MaxSubs int `json:"max_subscriptions,omitempty"` + MaxSubTokens uint8 `json:"-"` + Nkeys []*NkeyUser `json:"-"` + Users []*User `json:"-"` + Accounts []*Account `json:"-"` + NoAuthUser string `json:"-"` + DefaultSentinel string `json:"-"` + SystemAccount string `json:"-"` + NoSystemAccount bool `json:"-"` + Username string `json:"-"` + Password string `json:"-"` + ProxyRequired bool `json:"-"` + ProxyProtocol bool `json:"-"` + Authorization string `json:"-"` + AuthCallout *AuthCallout `json:"-"` + PingInterval time.Duration `json:"ping_interval"` + MaxPingsOut int `json:"ping_max"` + HTTPHost string `json:"http_host"` + HTTPPort int `json:"http_port"` + HTTPBasePath string `json:"http_base_path"` + HTTPSPort int `json:"https_port"` + AuthTimeout float64 `json:"auth_timeout"` + MaxControlLine int32 `json:"max_control_line"` + MaxPayload int32 `json:"max_payload"` + MaxPending int64 `json:"max_pending"` + NoFastProducerStall bool `json:"-"` + Cluster ClusterOpts `json:"cluster,omitempty"` + Gateway GatewayOpts `json:"gateway,omitempty"` + LeafNode LeafNodeOpts `json:"leaf,omitempty"` + JetStream bool `json:"jetstream"` + NoJetStreamStrict bool `json:"-"` // Strict by default. + JetStreamMaxMemory int64 `json:"-"` + JetStreamMaxStore int64 `json:"-"` + JetStreamDomain string `json:"-"` + JetStreamExtHint string `json:"-"` + JetStreamKey string `json:"-"` + JetStreamOldKey string `json:"-"` + JetStreamCipher StoreCipher `json:"-"` + JetStreamUniqueTag string + JetStreamLimits JSLimitOpts + JetStreamTpm JSTpmOpts + JetStreamMaxCatchup int64 + JetStreamRequestQueueLimit int64 + JetStreamInfoQueueLimit int64 + JetStreamMetaCompact uint64 + JetStreamMetaCompactSize uint64 + JetStreamMetaCompactSync bool + JetStreamConcurrentIOs int + StreamMaxBufferedMsgs int `json:"-"` + StreamMaxBufferedSize int64 `json:"-"` + StoreDir string `json:"-"` + SyncInterval time.Duration `json:"-"` + SyncAlways bool `json:"-"` + JsAccDefaultDomain map[string]string `json:"-"` // account to domain name mapping + Websocket WebsocketOpts `json:"-"` + MQTT MQTTOpts `json:"-"` + ProfPort int `json:"-"` + ProfBlockRate int `json:"-"` + PidFile string `json:"-"` + PortsFileDir string `json:"-"` + LogFile string `json:"-"` + LogSizeLimit int64 `json:"-"` + LogMaxFiles int64 `json:"-"` + Syslog bool `json:"-"` + RemoteSyslog string `json:"-"` + Routes []*url.URL `json:"-"` + RoutesStr string `json:"-"` + TLSTimeout float64 `json:"tls_timeout"` + TLS bool `json:"-"` + TLSVerify bool `json:"-"` + TLSMap bool `json:"-"` + TLSCert string `json:"-"` + TLSKey string `json:"-"` + TLSCaCert string `json:"-"` + TLSConfig *tls.Config `json:"-"` + TLSPinnedCerts PinnedCertSet `json:"-"` + TLSRateLimit int64 `json:"-"` + // When set to true, the server will perform the TLS handshake before + // sending the INFO protocol. For clients that are not configured + // with a similar option, their connection will fail with some sort + // of timeout or EOF error since they are expecting to receive an + // INFO protocol first. + TLSHandshakeFirst bool `json:"-"` + // If TLSHandshakeFirst is true and this value is strictly positive, + // the server will wait for that amount of time for the TLS handshake + // to start before falling back to previous behavior of sending the + // INFO protocol first. It allows for a mix of newer clients that can + // require a TLS handshake first, and older clients that can't. + TLSHandshakeFirstFallback time.Duration `json:"-"` + AllowNonTLS bool `json:"-"` + WriteDeadline time.Duration `json:"-"` + WriteTimeout WriteTimeoutPolicy `json:"-"` + MaxClosedClients int `json:"-"` + LameDuckDuration time.Duration `json:"-"` + LameDuckGracePeriod time.Duration `json:"-"` + + // MaxTracedMsgLen is the maximum printable length for traced messages. + MaxTracedMsgLen int `json:"-"` + + // Operating a trusted NATS server + TrustedKeys []string `json:"-"` + TrustedOperators []*jwt.OperatorClaims `json:"-"` + AccountResolver AccountResolver `json:"-"` + AccountResolverTLSConfig *tls.Config `json:"-"` + + // AlwaysEnableNonce will always present a nonce to new connections + // typically used by custom Authentication implementations who embeds + // the server and so not presented as a configuration option + AlwaysEnableNonce bool + + CustomClientAuthentication Authentication `json:"-"` + CustomRouterAuthentication Authentication `json:"-"` + + // CheckConfig configuration file syntax test was successful and exit. + CheckConfig bool `json:"-"` + + // DisableJetStreamBanner will not print the ascii art on startup for JetStream enabled servers + DisableJetStreamBanner bool `json:"-"` + + // ConnectErrorReports specifies the number of failed attempts + // at which point server should report the failure of an initial + // connection to a route, gateway or leaf node. + // See DEFAULT_CONNECT_ERROR_REPORTS for default value. + ConnectErrorReports int + + // ReconnectErrorReports is similar to ConnectErrorReports except + // that this applies to reconnect events. + ReconnectErrorReports int + + // Tags describing the server. They will be included in varz + // and used as a filter criteria for some system requests. + Tags jwt.TagList `json:"-"` + + // Metadata describing the server. They will be included in 'Z' responses. + Metadata map[string]string `json:"-"` + + // FeatureFlags the server opts-in to (or opts-out of). They will be included in 'Z' responses. + FeatureFlags map[string]bool `json:"-"` + + // OCSPConfig enables OCSP Stapling in the server. + OCSPConfig *OCSPConfig + tlsConfigOpts *TLSConfigOpts + + // Proxies configuration. + Proxies *ProxiesConfig + + // private fields, used to know if bool options are explicitly + // defined in config and/or command line params. + inConfig map[string]bool + inCmdLine map[string]bool + + // private fields for operator mode + operatorJWT []string + resolverPreloads map[string]string + resolverPinnedAccounts map[string]struct{} + + // private fields, used for testing + gatewaysSolicitDelay time.Duration + overrideProto int + + // JetStream + maxMemSet bool + maxStoreSet bool + syncSet bool + + // OCSP Cache config enables next-gen cache for OCSP features + OCSPCacheConfig *OCSPResponseCacheConfig + + // Used to mark that we had a top level authorization block. + authBlockDefined bool + + // configDigest represents the state of configuration. + configDigest string +} + +// WebsocketOpts are options for websocket +type WebsocketOpts struct { + // The server will accept websocket client connections on this hostname/IP. + Host string + // The server will accept websocket client connections on this port. + Port int + // The host:port to advertise to websocket clients in the cluster. + Advertise string + + // If no user name is provided when a client connects, will default to the + // matching user from the global list of users in `Options.Users`. + NoAuthUser string + + // Name of the cookie, which if present in WebSocket upgrade headers, + // will be treated as JWT during CONNECT phase as long as + // "jwt" specified in the CONNECT options is missing or empty. + JWTCookie string + + // Name of the cookie, which if present in WebSocket upgrade headers, + // will be treated as Username during CONNECT phase as long as + // "user" specified in the CONNECT options is missing or empty. + UsernameCookie string + + // Name of the cookie, which if present in WebSocket upgrade headers, + // will be treated as Password during CONNECT phase as long as + // "pass" specified in the CONNECT options is missing or empty. + PasswordCookie string + + // Name of the cookie, which if present in WebSocket upgrade headers, + // will be treated as Token during CONNECT phase as long as + // "auth_token" specified in the CONNECT options is missing or empty. + // Note that when this is useful for passing a JWT to an cuth callout + // when the server uses delegated authentication ("operator mode") or + // when using delegated authentication, but the auth callout validates some + // other JWT or string. Note that this does map to an actual server-wide + // "auth_token", note that using it for that purpose is greatly discouraged. + TokenCookie string + + // Authentication section. If anything is configured in this section, + // it will override the authorization configuration of regular clients. + Username string + Password string + Token string + + // Timeout for the authentication process. + AuthTimeout float64 + + // By default the server will enforce the use of TLS. If no TLS configuration + // is provided, you need to explicitly set NoTLS to true to allow the server + // to start without TLS configuration. Note that if a TLS configuration is + // present, this boolean is ignored and the server will run the Websocket + // server with that TLS configuration. + // Running without TLS is less secure since Websocket clients that use bearer + // tokens will send them in clear. So this should not be used in production. + NoTLS bool + + // TLS configuration is required. + TLSConfig *tls.Config + // If true, map certificate values for authentication purposes. + TLSMap bool + + // When present, accepted client certificates (verify/verify_and_map) must be in this list + TLSPinnedCerts PinnedCertSet + + // If true, the Origin header must match the request's host. + SameOrigin bool + + // Only origins in this list will be accepted. If empty and + // SameOrigin is false, any origin is accepted. + AllowedOrigins []string + + // If set to true, the server will negotiate with clients + // if compression can be used. If this is false, no compression + // will be used (both in server and clients) since it has to + // be negotiated between both endpoints + Compression bool + + // Total time allowed for the server to read the client request + // and write the response back to the client. This include the + // time needed for the TLS Handshake. + HandshakeTimeout time.Duration + + // How often to send pings to WebSocket clients. When set to a non-zero + // duration, this overrides the default PingInterval for WebSocket connections. + // If not set or zero, the server's default PingInterval will be used. + PingInterval time.Duration + + // Headers to be added to the upgrade response. + // Useful for adding custom headers like Strict-Transport-Security. + Headers map[string]string + + // Snapshot of configured TLS options. + tlsConfigOpts *TLSConfigOpts +} + +// MQTTOpts are options for MQTT +type MQTTOpts struct { + // The server will accept MQTT client connections on this hostname/IP. + Host string + // The server will accept MQTT client connections on this port. + Port int + + // If no user name is provided when a client connects, will default to the + // matching user from the global list of users in `Options.Users`. + NoAuthUser string + + // Authentication section. If anything is configured in this section, + // it will override the authorization configuration of regular clients. + Username string + Password string + Token string + + // JetStream domain mqtt is supposed to pick up + JsDomain string + + // Number of replicas for MQTT streams. + // Negative or 0 value means that the server(s) will pick a replica + // number based on the known size of the cluster (but capped at 3). + // Note that if an account was already connected, the stream's replica + // count is not modified. Use the NATS CLI to update the count if desired. + StreamReplicas int + + // Number of replicas for MQTT consumers. + // Negative or 0 value means that there is no override and the consumer + // will have the same replica factor that the stream it belongs to. + // If a value is specified, it will require to be lower than the stream + // replicas count (lower than StreamReplicas if specified, but also lower + // than the automatic value determined by cluster size). + // Note that existing consumers are not modified. + // + // UPDATE: This is no longer used while messages stream has interest policy retention + // which requires consumer replica count to match the parent stream. + ConsumerReplicas int + + // Indicate if the consumers should be created with memory storage. + // Note that existing consumers are not modified. + ConsumerMemoryStorage bool + + // If specified will have the system auto-cleanup the consumers after being + // inactive for the specified amount of time. + ConsumerInactiveThreshold time.Duration + + // Timeout for the authentication process. + AuthTimeout float64 + + // TLS configuration is required. + TLSConfig *tls.Config + // If true, map certificate values for authentication purposes. + TLSMap bool + // Timeout for the TLS handshake + TLSTimeout float64 + // Set of allowable certificates + TLSPinnedCerts PinnedCertSet + + // AckWait is the amount of time after which a QoS 1 or 2 message sent to a + // client is redelivered as a DUPLICATE if the server has not received the + // PUBACK on the original Packet Identifier. The same value applies to + // PubRel redelivery. The value has to be positive. Zero will cause the + // server to use the default value (30 seconds). Note that changes to this + // option is applied only to new MQTT subscriptions (or sessions for + // PubRels). + AckWait time.Duration + + // JSAPITimeout defines timeout for JetStream api calls (default is 5 seconds) + JSAPITimeout time.Duration + + // MaxAckPending is the amount of QoS 1 and 2 messages (combined) the server + // can send to a subscription without receiving any PUBACK for those + // messages. The valid range is [0..65535]. + // + // The total of subscriptions' MaxAckPending on a given session cannot + // exceed 65535. Attempting to create a subscription that would bring the + // total above the limit would result in the server returning 0x80 in the + // SUBACK for this subscription. + // + // Due to how the NATS Server handles the MQTT "#" wildcard, each + // subscription ending with "#" will use 2 times the MaxAckPending value. + // Note that changes to this option is applied only to new subscriptions. + MaxAckPending uint16 + + // Snapshot of configured TLS options. + tlsConfigOpts *TLSConfigOpts + + // rejectQoS2Pub tells the MQTT client to not accept QoS2 PUBLISH, instead + // error and terminate the connection. + rejectQoS2Pub bool + + // downgradeQOS2Sub tells the MQTT client to downgrade QoS2 SUBSCRIBE + // requests to QoS1. + downgradeQoS2Sub bool +} + +type netResolver interface { + LookupHost(ctx context.Context, host string) ([]string, error) +} + +// Clone performs a deep copy of the Options struct, returning a new clone +// with all values copied. +func (o *Options) Clone() *Options { + if o == nil { + return nil + } + clone := &Options{} + *clone = *o + if o.Users != nil { + clone.Users = make([]*User, len(o.Users)) + for i, user := range o.Users { + clone.Users[i] = user.clone() + } + } + if o.Nkeys != nil { + clone.Nkeys = make([]*NkeyUser, len(o.Nkeys)) + for i, nkey := range o.Nkeys { + clone.Nkeys[i] = nkey.clone() + } + } + + if o.Routes != nil { + clone.Routes = deepCopyURLs(o.Routes) + } + if o.TLSConfig != nil { + clone.TLSConfig = o.TLSConfig.Clone() + } + if o.Cluster.TLSConfig != nil { + clone.Cluster.TLSConfig = o.Cluster.TLSConfig.Clone() + } + if o.Gateway.TLSConfig != nil { + clone.Gateway.TLSConfig = o.Gateway.TLSConfig.Clone() + } + if len(o.Gateway.Gateways) > 0 { + clone.Gateway.Gateways = make([]*RemoteGatewayOpts, len(o.Gateway.Gateways)) + for i, g := range o.Gateway.Gateways { + clone.Gateway.Gateways[i] = g.clone() + } + } + // FIXME(dlc) - clone leaf node stuff. + return clone +} + +func deepCopyURLs(urls []*url.URL) []*url.URL { + if urls == nil { + return nil + } + curls := make([]*url.URL, len(urls)) + for i, u := range urls { + cu := &url.URL{} + *cu = *u + curls[i] = cu + } + return curls +} + +// Configuration file authorization section. +type authorization struct { + // Singles + user string + pass string + token string + nkey string + acc string + // If connection must come through proxy + proxyRequired bool + // Multiple Nkeys/Users + nkeys []*NkeyUser + users []*User + timeout float64 + defaultPermissions *Permissions + // Auth Callouts + callout *AuthCallout +} + +// TLSConfigOpts holds the parsed tls config information, +// used with flag parsing +type TLSConfigOpts struct { + CertFile string + KeyFile string + CaFile string + Verify bool + Insecure bool + Map bool + TLSCheckKnownURLs bool + HandshakeFirst bool // Indicate that the TLS handshake should occur first, before sending the INFO protocol. + FallbackDelay time.Duration // Where supported, indicates how long to wait for the handshake before falling back to sending the INFO protocol first. + Timeout float64 + RateLimit int64 + AllowInsecureCiphers bool + Ciphers []uint16 + CurvePreferences []tls.CurveID + PinnedCerts PinnedCertSet + CertStore certstore.StoreType + CertMatchBy certstore.MatchByType + CertMatch string + CertMatchSkipInvalid bool + CaCertsMatch []string + OCSPPeerConfig *certidp.OCSPPeerConfig + Certificates []*TLSCertPairOpt + MinVersion uint16 +} + +// TLSCertPairOpt are the paths to a certificate and private key. +type TLSCertPairOpt struct { + CertFile string + KeyFile string +} + +// OCSPConfig represents the options of OCSP stapling options. +type OCSPConfig struct { + // Mode defines the policy for OCSP stapling. + Mode OCSPMode + + // OverrideURLs is the http URL endpoint used to get OCSP staples. + OverrideURLs []string +} + +// ProxiesConfig represents the options of Proxies. +type ProxiesConfig struct { + Trusted []*ProxyConfig +} + +// ProxyConfig represents the options of Proxy. +type ProxyConfig struct { + // Public key. + Key string +} + +var tlsUsage = ` +TLS configuration is specified in the tls section of a configuration file: + +e.g. + + tls { + cert_file: "./certs/server-cert.pem" + key_file: "./certs/server-key.pem" + ca_file: "./certs/ca.pem" + verify: true + verify_and_map: true + + cipher_suites: [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + curve_preferences: [ + "CurveP256", + "CurveP384", + "CurveP521" + ] + } + +Available cipher suites include: +` + +// ProcessConfigFile processes a configuration file. +// FIXME(dlc): A bit hacky +func ProcessConfigFile(configFile string) (*Options, error) { + opts := &Options{} + if err := opts.ProcessConfigFile(configFile); err != nil { + // If only warnings then continue and return the options. + if cerr, ok := err.(*processConfigErr); ok && len(cerr.Errors()) == 0 { + return opts, nil + } + + return nil, err + } + return opts, nil +} + +// token is an item parsed from the configuration. +type token interface { + Value() any + Line() int + IsUsedVariable() bool + SourceFile() string + Position() int +} + +// unwrapValue can be used to get the token and value from an item +// to be able to report the line number in case of an incorrect +// configuration. +// also stores the token in lastToken for use in convertPanicToError +func unwrapValue(v any, lastToken *token) (token, any) { + switch tk := v.(type) { + case token: + if lastToken != nil { + *lastToken = tk + } + return tk, tk.Value() + default: + return nil, v + } +} + +// use in defer to recover from panic and turn it into an error associated with last token +func convertPanicToErrorList(lastToken *token, errors *[]error) { + // only recover if an error can be stored + if errors == nil { + return + } else if err := recover(); err == nil { + return + } else if lastToken != nil && *lastToken != nil { + *errors = append(*errors, &configErr{*lastToken, fmt.Sprint(err)}) + } else { + *errors = append(*errors, fmt.Errorf("encountered panic without a token %v", err)) + } +} + +// use in defer to recover from panic and turn it into an error associated with last token +func convertPanicToError(lastToken *token, e *error) { + // only recover if an error can be stored + if e == nil || *e != nil { + return + } else if err := recover(); err == nil { + return + } else if lastToken != nil && *lastToken != nil { + *e = &configErr{*lastToken, fmt.Sprint(err)} + } else { + *e = fmt.Errorf("%v", err) + } +} + +// configureSystemAccount configures a system account +// if present in the configuration. +func configureSystemAccount(o *Options, m map[string]any) (retErr error) { + var lt token + defer convertPanicToError(<, &retErr) + configure := func(v any) error { + tk, v := unwrapValue(v, <) + sa, ok := v.(string) + if !ok { + return &configErr{tk, "system account name must be a string"} + } + o.SystemAccount = sa + return nil + } + + if v, ok := m["system_account"]; ok { + return configure(v) + } else if v, ok := m["system"]; ok { + return configure(v) + } + + return nil +} + +// ProcessConfigFile updates the Options structure with options +// present in the given configuration file. +// This version is convenient if one wants to set some default +// options and then override them with what is in the config file. +// For instance, this version allows you to do something such as: +// +// opts := &Options{Debug: true} +// opts.ProcessConfigFile(myConfigFile) +// +// If the config file contains "debug: false", after this call, +// opts.Debug would really be false. It would be impossible to +// achieve that with the non receiver ProcessConfigFile() version, +// since one would not know after the call if "debug" was not present +// or was present but set to false. +func (o *Options) ProcessConfigFile(configFile string) error { + o.ConfigFile = configFile + if configFile == _EMPTY_ { + return nil + } + m, digest, err := conf.ParseFileWithChecksDigest(configFile) + if err != nil { + return err + } + o.configDigest = digest + + return o.processConfigFile(configFile, m) +} + +// ProcessConfigString is the same as ProcessConfigFile, but expects the +// contents of the config file to be passed in rather than the file name. +func (o *Options) ProcessConfigString(data string) error { + m, err := conf.ParseWithChecks(data) + if err != nil { + return err + } + + return o.processConfigFile(_EMPTY_, m) +} + +// ConfigDigest returns the digest representing the configuration. +func (o *Options) ConfigDigest() string { + return o.configDigest +} + +func (o *Options) processConfigFile(configFile string, m map[string]any) error { + // Collect all errors and warnings and report them all together. + errors := make([]error, 0) + warnings := make([]error, 0) + if len(m) == 0 { + warnings = append(warnings, fmt.Errorf("%s: config has no values or is empty", configFile)) + } + + // First check whether a system account has been defined, + // as that is a condition for other features to be enabled. + if err := configureSystemAccount(o, m); err != nil { + errors = append(errors, err) + } + + for k, v := range m { + o.processConfigFileLine(k, v, &errors, &warnings) + } + + // Post-process: check auth callout allowed accounts against configured accounts. + if o.AuthCallout != nil { + accounts := make(map[string]struct{}) + for _, acc := range o.Accounts { + accounts[acc.Name] = struct{}{} + } + + for _, acc := range o.AuthCallout.AllowedAccounts { + if _, ok := accounts[acc]; !ok { + err := &configErr{nil, fmt.Sprintf("auth_callout allowed account %q not found in configured accounts", acc)} + errors = append(errors, err) + } + } + } + + if len(errors) > 0 || len(warnings) > 0 { + return &processConfigErr{ + errors: errors, + warnings: warnings, + } + } + + return nil +} + +func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnings *[]error) { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + switch strings.ToLower(k) { + case "listen": + hp, err := parseListen(v) + if err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + return + } + o.Host = hp.host + o.Port = hp.port + case "client_advertise": + o.ClientAdvertise = v.(string) + case "port": + o.Port = int(v.(int64)) + case "server_name": + sn := v.(string) + if strings.Contains(sn, " ") { + err := &configErr{tk, ErrServerNameHasSpaces.Error()} + *errors = append(*errors, err) + return + } + o.ServerName = sn + case "host", "net": + o.Host = v.(string) + case "debug": + o.Debug = v.(bool) + trackExplicitVal(&o.inConfig, "Debug", o.Debug) + case "trace": + o.Trace = v.(bool) + trackExplicitVal(&o.inConfig, "Trace", o.Trace) + case "trace_verbose": + o.TraceVerbose = v.(bool) + o.Trace = v.(bool) + trackExplicitVal(&o.inConfig, "TraceVerbose", o.TraceVerbose) + trackExplicitVal(&o.inConfig, "Trace", o.Trace) + case "trace_headers": + o.TraceHeaders = v.(bool) + o.Trace = v.(bool) + trackExplicitVal(&o.inConfig, "TraceHeaders", o.TraceHeaders) + trackExplicitVal(&o.inConfig, "Trace", o.Trace) + case "logtime": + o.Logtime = v.(bool) + trackExplicitVal(&o.inConfig, "Logtime", o.Logtime) + case "logtime_utc": + o.LogtimeUTC = v.(bool) + trackExplicitVal(&o.inConfig, "LogtimeUTC", o.LogtimeUTC) + case "mappings", "maps": + gacc := NewAccount(globalAccountName) + o.Accounts = append(o.Accounts, gacc) + err := parseAccountMappings(tk, gacc, errors) + if err != nil { + *errors = append(*errors, err) + return + } + case "disable_sublist_cache", "no_sublist_cache": + o.NoSublistCache = v.(bool) + case "accounts": + err := parseAccounts(tk, o, errors, warnings) + if err != nil { + *errors = append(*errors, err) + return + } + case "default_sentinel": + o.DefaultSentinel = v.(string) + case "authorization": + auth, err := parseAuthorization(tk, errors, warnings) + if err != nil { + *errors = append(*errors, err) + return + } + o.authBlockDefined = true + o.Username = auth.user + o.Password = auth.pass + o.ProxyRequired = auth.proxyRequired + o.Authorization = auth.token + o.AuthTimeout = auth.timeout + o.AuthCallout = auth.callout + + if (auth.user != _EMPTY_ || auth.pass != _EMPTY_) && auth.token != _EMPTY_ { + err := &configErr{tk, "Cannot have a user/pass and token"} + *errors = append(*errors, err) + return + } + // In case parseAccounts() was done first, we need to check for duplicates. + unames := setupUsersAndNKeysDuplicateCheckMap(o) + // Check for multiple users defined. + // Note: auth.users will be != nil as long as `users: []` is present + // in the authorization block, even if empty, and will also account for + // nkey users. We also check for users/nkeys that may have been already + // added in parseAccounts() (which means they will be in unames) + if auth.users != nil || len(unames) > 0 { + if auth.user != _EMPTY_ { + err := &configErr{tk, "Can not have a single user/pass and a users array"} + *errors = append(*errors, err) + return + } + if auth.token != _EMPTY_ { + err := &configErr{tk, "Can not have a token and a users array"} + *errors = append(*errors, err) + return + } + // Now check that if we have users, there is no duplicate, including + // users that may have been configured in parseAccounts(). + if len(auth.users) > 0 { + for _, u := range auth.users { + if _, ok := unames[u.Username]; ok { + err := &configErr{tk, fmt.Sprintf("Duplicate user %q detected", u.Username)} + *errors = append(*errors, err) + return + } + unames[u.Username] = struct{}{} + } + // Users may have been added from Accounts parsing, so do an append here + o.Users = append(o.Users, auth.users...) + } + } + // Check for nkeys + if len(auth.nkeys) > 0 { + for _, u := range auth.nkeys { + if _, ok := unames[u.Nkey]; ok { + err := &configErr{tk, fmt.Sprintf("Duplicate nkey %q detected", u.Nkey)} + *errors = append(*errors, err) + return + } + unames[u.Nkey] = struct{}{} + } + // NKeys may have been added from Accounts parsing, so do an append here + o.Nkeys = append(o.Nkeys, auth.nkeys...) + } + case "http": + hp, err := parseListen(v) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + return + } + o.HTTPHost = hp.host + o.HTTPPort = hp.port + case "https": + hp, err := parseListen(v) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + return + } + o.HTTPHost = hp.host + o.HTTPSPort = hp.port + case "http_port", "monitor_port": + o.HTTPPort = int(v.(int64)) + case "https_port": + o.HTTPSPort = int(v.(int64)) + case "http_base_path": + o.HTTPBasePath = v.(string) + case "cluster": + err := parseCluster(tk, o, errors, warnings) + if err != nil { + *errors = append(*errors, err) + return + } + case "gateway": + if err := parseGateway(tk, o, errors, warnings); err != nil { + *errors = append(*errors, err) + return + } + case "leaf", "leafnodes": + err := parseLeafNodes(tk, o, errors, warnings) + if err != nil { + *errors = append(*errors, err) + return + } + case "store_dir", "storedir": + // Check if JetStream configuration is also setting the storage directory. + if o.StoreDir != _EMPTY_ { + *errors = append(*errors, &configErr{tk, "Duplicate 'store_dir' configuration"}) + return + } + o.StoreDir = v.(string) + case "jetstream": + err := parseJetStream(tk, o, errors, warnings) + if err != nil { + *errors = append(*errors, err) + return + } + case "logfile", "log_file": + o.LogFile = v.(string) + case "logfile_size_limit", "log_size_limit": + o.LogSizeLimit = v.(int64) + case "logfile_max_num", "log_max_num": + o.LogMaxFiles = v.(int64) + case "syslog": + o.Syslog = v.(bool) + trackExplicitVal(&o.inConfig, "Syslog", o.Syslog) + case "remote_syslog": + o.RemoteSyslog = v.(string) + case "pidfile", "pid_file": + o.PidFile = v.(string) + case "ports_file_dir": + o.PortsFileDir = v.(string) + case "prof_port": + o.ProfPort = int(v.(int64)) + case "prof_block_rate": + o.ProfBlockRate = int(v.(int64)) + case "max_control_line": + if v.(int64) > 1<<31-1 { + err := &configErr{tk, fmt.Sprintf("%s value is too big", k)} + *errors = append(*errors, err) + return + } + o.MaxControlLine = int32(v.(int64)) + case "max_payload": + if v.(int64) > 1<<31-1 { + err := &configErr{tk, fmt.Sprintf("%s value is too big", k)} + *errors = append(*errors, err) + return + } + o.MaxPayload = int32(v.(int64)) + case "max_pending": + o.MaxPending = v.(int64) + case "proxy_protocol": + o.ProxyProtocol = v.(bool) + case "max_connections", "max_conn": + if o.MaxConn = int(v.(int64)); o.MaxConn == 0 { + o.MaxConn = -1 + } + case "max_traced_msg_len": + o.MaxTracedMsgLen = int(v.(int64)) + case "max_subscriptions", "max_subs": + o.MaxSubs = int(v.(int64)) + case "max_sub_tokens", "max_subscription_tokens": + if n := v.(int64); n > math.MaxUint8 { + err := &configErr{tk, fmt.Sprintf("%s value is too big", k)} + *errors = append(*errors, err) + return + } else if n <= 0 { + err := &configErr{tk, fmt.Sprintf("%s value can not be negative", k)} + *errors = append(*errors, err) + return + } else { + o.MaxSubTokens = uint8(n) + } + case "ping_interval": + o.PingInterval = parseDuration("ping_interval", tk, v, errors, warnings) + case "ping_max": + o.MaxPingsOut = int(v.(int64)) + case "tls": + tc, err := parseTLS(tk, true) + if err != nil { + *errors = append(*errors, err) + return + } + if o.TLSConfig, err = GenTLSConfig(tc); err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + return + } + o.TLSTimeout = tc.Timeout + o.TLSMap = tc.Map + o.TLSPinnedCerts = tc.PinnedCerts + o.TLSRateLimit = tc.RateLimit + o.TLSHandshakeFirst = tc.HandshakeFirst + o.TLSHandshakeFirstFallback = tc.FallbackDelay + + // Need to keep track of path of the original TLS config + // and certs path for OCSP Stapling monitoring. + o.tlsConfigOpts = tc + case "ocsp": + switch vv := v.(type) { + case bool: + if vv { + // Default is Auto which honors Must Staple status request + // but does not shutdown the server in case it is revoked, + // letting the client choose whether to trust or not the server. + o.OCSPConfig = &OCSPConfig{Mode: OCSPModeAuto} + } else { + o.OCSPConfig = &OCSPConfig{Mode: OCSPModeNever} + } + case map[string]any: + ocsp := &OCSPConfig{Mode: OCSPModeAuto} + + for kk, kv := range vv { + _, v = unwrapValue(kv, &tk) + switch kk { + case "mode": + mode := v.(string) + switch { + case strings.EqualFold(mode, "always"): + ocsp.Mode = OCSPModeAlways + case strings.EqualFold(mode, "must"): + ocsp.Mode = OCSPModeMust + case strings.EqualFold(mode, "never"): + ocsp.Mode = OCSPModeNever + case strings.EqualFold(mode, "auto"): + ocsp.Mode = OCSPModeAuto + default: + *errors = append(*errors, &configErr{tk, fmt.Sprintf("error parsing ocsp config: unsupported ocsp mode %T", mode)}) + } + case "urls": + urls := v.([]string) + ocsp.OverrideURLs = urls + case "url": + url := v.(string) + ocsp.OverrideURLs = []string{url} + default: + *errors = append(*errors, &configErr{tk, fmt.Sprintf("error parsing ocsp config: unsupported field %T", kk)}) + return + } + } + o.OCSPConfig = ocsp + default: + *errors = append(*errors, &configErr{tk, fmt.Sprintf("error parsing ocsp config: unsupported type %T", v)}) + return + } + case "allow_non_tls": + o.AllowNonTLS = v.(bool) + case "write_deadline": + o.WriteDeadline = parseDuration("write_deadline", tk, v, errors, warnings) + case "write_timeout": + o.WriteTimeout = parseWriteDeadlinePolicy(tk, v.(string), errors) + case "lame_duck_duration": + dur, err := time.ParseDuration(v.(string)) + if err != nil { + err := &configErr{tk, fmt.Sprintf("error parsing lame_duck_duration: %v", err)} + *errors = append(*errors, err) + return + } + if dur < 30*time.Second { + err := &configErr{tk, fmt.Sprintf("invalid lame_duck_duration of %v, minimum is 30 seconds", dur)} + *errors = append(*errors, err) + return + } + o.LameDuckDuration = dur + case "lame_duck_grace_period": + dur, err := time.ParseDuration(v.(string)) + if err != nil { + err := &configErr{tk, fmt.Sprintf("error parsing lame_duck_grace_period: %v", err)} + *errors = append(*errors, err) + return + } + if dur < 0 { + err := &configErr{tk, "invalid lame_duck_grace_period, needs to be positive"} + *errors = append(*errors, err) + return + } + o.LameDuckGracePeriod = dur + case "operator", "operators", "roots", "root", "root_operators", "root_operator": + opFiles := []string{} + switch v := v.(type) { + case string: + opFiles = append(opFiles, v) + case []string: + opFiles = append(opFiles, v...) + case []any: + for _, t := range v { + if token, ok := t.(token); ok { + if v, ok := token.Value().(string); ok { + opFiles = append(opFiles, v) + } else { + err := &configErr{tk, fmt.Sprintf("error parsing operators: unsupported type %T where string is expected", token)} + *errors = append(*errors, err) + break + } + } else { + err := &configErr{tk, fmt.Sprintf("error parsing operators: unsupported type %T", t)} + *errors = append(*errors, err) + break + } + } + default: + err := &configErr{tk, fmt.Sprintf("error parsing operators: unsupported type %T", v)} + *errors = append(*errors, err) + } + // Assume for now these are file names, but they can also be the JWT itself inline. + o.TrustedOperators = make([]*jwt.OperatorClaims, 0, len(opFiles)) + for _, fname := range opFiles { + theJWT, opc, err := readOperatorJWT(fname) + if err != nil { + err := &configErr{tk, fmt.Sprintf("error parsing operator JWT: %v", err)} + *errors = append(*errors, err) + continue + } + o.operatorJWT = append(o.operatorJWT, theJWT) + o.TrustedOperators = append(o.TrustedOperators, opc) + } + if len(o.TrustedOperators) == 1 { + // In case "resolver" is defined as well, it takes precedence + if o.AccountResolver == nil { + if accUrl, err := parseURL(o.TrustedOperators[0].AccountServerURL, "account resolver"); err == nil { + // nsc automatically appends "/accounts" during nsc push + o.AccountResolver, _ = NewURLAccResolver(accUrl.String() + "/accounts") + } + } + // In case "system_account" is defined as well, it takes precedence + if o.SystemAccount == _EMPTY_ { + o.SystemAccount = o.TrustedOperators[0].SystemAccount + } + } + case "resolver", "account_resolver", "accounts_resolver": + switch v := v.(type) { + case string: + // "resolver" takes precedence over value obtained from "operator". + // Clear so that parsing errors are not silently ignored. + o.AccountResolver = nil + memResolverRe := regexp.MustCompile(`(?i)(MEM|MEMORY)\s*`) + resolverRe := regexp.MustCompile(`(?i)(?:URL){1}(?:\({1}\s*"?([^\s"]*)"?\s*\){1})?\s*`) + if memResolverRe.MatchString(v) { + o.AccountResolver = &MemAccResolver{} + } else if items := resolverRe.FindStringSubmatch(v); len(items) == 2 { + url := items[1] + _, err := parseURL(url, "account resolver") + if err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + return + } + if ur, err := NewURLAccResolver(url); err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + return + } else { + o.AccountResolver = ur + } + } + case map[string]any: + del := false + hdel := false + hdel_set := false + dir := _EMPTY_ + dirType := _EMPTY_ + limit := int64(0) + ttl := time.Duration(0) + sync := time.Duration(0) + opts := []DirResOption{} + var err error + if v, ok := v["dir"]; ok { + _, v := unwrapValue(v, <) + dir = v.(string) + } + if v, ok := v["type"]; ok { + _, v := unwrapValue(v, <) + dirType = v.(string) + } + if v, ok := v["allow_delete"]; ok { + _, v := unwrapValue(v, <) + del = v.(bool) + } + if v, ok := v["hard_delete"]; ok { + _, v := unwrapValue(v, <) + hdel_set = true + hdel = v.(bool) + } + if v, ok := v["limit"]; ok { + _, v := unwrapValue(v, <) + limit = v.(int64) + } + if v, ok := v["ttl"]; ok { + _, v := unwrapValue(v, <) + ttl, err = time.ParseDuration(v.(string)) + } + if v, ok := v["interval"]; err == nil && ok { + _, v := unwrapValue(v, <) + sync, err = time.ParseDuration(v.(string)) + } + if v, ok := v["timeout"]; err == nil && ok { + _, v := unwrapValue(v, <) + var to time.Duration + if to, err = time.ParseDuration(v.(string)); err == nil { + opts = append(opts, FetchTimeout(to)) + } + } + if err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + return + } + + checkDir := func() { + if dir == _EMPTY_ { + *errors = append(*errors, &configErr{tk, "dir has no value and needs to point to a directory"}) + return + } + if info, _ := os.Stat(dir); info != nil && (!info.IsDir() || info.Mode().Perm()&(1<<(uint(7))) == 0) { + *errors = append(*errors, &configErr{tk, "dir needs to point to an accessible directory"}) + return + } + } + + var res AccountResolver + switch strings.ToUpper(dirType) { + case "CACHE": + checkDir() + if sync != 0 { + *errors = append(*errors, &configErr{tk, "CACHE does not accept sync"}) + } + if del { + *errors = append(*errors, &configErr{tk, "CACHE does not accept allow_delete"}) + } + if hdel_set { + *errors = append(*errors, &configErr{tk, "CACHE does not accept hard_delete"}) + } + res, err = NewCacheDirAccResolver(dir, limit, ttl, opts...) + case "FULL": + checkDir() + if ttl != 0 { + *errors = append(*errors, &configErr{tk, "FULL does not accept ttl"}) + } + if hdel_set && !del { + *errors = append(*errors, &configErr{tk, "hard_delete has no effect without delete"}) + } + delete := NoDelete + if del { + if hdel { + delete = HardDelete + } else { + delete = RenameDeleted + } + } + res, err = NewDirAccResolver(dir, limit, sync, delete, opts...) + case "MEM", "MEMORY": + res = &MemAccResolver{} + } + if err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + return + } + o.AccountResolver = res + default: + err := &configErr{tk, fmt.Sprintf("error parsing operator resolver, wrong type %T", v)} + *errors = append(*errors, err) + return + } + if o.AccountResolver == nil { + err := &configErr{tk, "error parsing account resolver, should be MEM or " + + " URL(\"url\") or a map containing dir and type state=[FULL|CACHE])"} + *errors = append(*errors, err) + } + case "resolver_tls": + tc, err := parseTLS(tk, true) + if err != nil { + *errors = append(*errors, err) + return + } + tlsConfig, err := GenTLSConfig(tc) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + return + } + o.AccountResolverTLSConfig = tlsConfig + // GenTLSConfig loads the CA file into ClientCAs, but since this will + // be used as a client connection, we need to set RootCAs. + o.AccountResolverTLSConfig.RootCAs = tlsConfig.ClientCAs + case "resolver_preload": + mp, ok := v.(map[string]any) + if !ok { + err := &configErr{tk, "preload should be a map of account_public_key:account_jwt"} + *errors = append(*errors, err) + return + } + o.resolverPreloads = make(map[string]string) + for key, val := range mp { + tk, val = unwrapValue(val, <) + if jwtstr, ok := val.(string); !ok { + *errors = append(*errors, &configErr{tk, "preload map value should be a string JWT"}) + continue + } else { + // Make sure this is a valid account JWT, that is a config error. + // We will warn of expirations, etc later. + if _, err := jwt.DecodeAccountClaims(jwtstr); err != nil { + err := &configErr{tk, "invalid account JWT"} + *errors = append(*errors, err) + continue + } + o.resolverPreloads[key] = jwtstr + } + } + case "resolver_pinned_accounts": + switch v := v.(type) { + case string: + o.resolverPinnedAccounts = map[string]struct{}{v: {}} + case []string: + o.resolverPinnedAccounts = make(map[string]struct{}) + for _, mv := range v { + o.resolverPinnedAccounts[mv] = struct{}{} + } + case []any: + o.resolverPinnedAccounts = make(map[string]struct{}) + for _, mv := range v { + tk, mv = unwrapValue(mv, <) + if key, ok := mv.(string); ok { + o.resolverPinnedAccounts[key] = struct{}{} + } else { + err := &configErr{tk, + fmt.Sprintf("error parsing resolver_pinned_accounts: unsupported type in array %T", mv)} + *errors = append(*errors, err) + continue + } + } + default: + err := &configErr{tk, fmt.Sprintf("error parsing resolver_pinned_accounts: unsupported type %T", v)} + *errors = append(*errors, err) + return + } + case "no_auth_user": + o.NoAuthUser = v.(string) + case "system_account", "system": + // Already processed at the beginning so we just skip them + // to not treat them as unknown values. + return + case "no_system_account", "no_system", "no_sys_acc": + o.NoSystemAccount = v.(bool) + case "no_header_support": + o.NoHeaderSupport = v.(bool) + case "trusted", "trusted_keys": + switch v := v.(type) { + case string: + o.TrustedKeys = []string{v} + case []string: + o.TrustedKeys = v + case []any: + keys := make([]string, 0, len(v)) + for _, mv := range v { + tk, mv = unwrapValue(mv, <) + if key, ok := mv.(string); ok { + keys = append(keys, key) + } else { + err := &configErr{tk, fmt.Sprintf("error parsing trusted: unsupported type in array %T", mv)} + *errors = append(*errors, err) + continue + } + } + o.TrustedKeys = keys + default: + err := &configErr{tk, fmt.Sprintf("error parsing trusted: unsupported type %T", v)} + *errors = append(*errors, err) + } + // Do a quick sanity check on keys + for _, key := range o.TrustedKeys { + if !nkeys.IsValidPublicOperatorKey(key) { + err := &configErr{tk, fmt.Sprintf("trust key %q required to be a valid public operator nkey", key)} + *errors = append(*errors, err) + } + } + case "connect_error_reports": + o.ConnectErrorReports = int(v.(int64)) + case "reconnect_error_reports": + o.ReconnectErrorReports = int(v.(int64)) + case "websocket", "ws": + if err := parseWebsocket(tk, o, errors, warnings); err != nil { + *errors = append(*errors, err) + return + } + case "mqtt": + if err := parseMQTT(tk, o, errors, warnings); err != nil { + *errors = append(*errors, err) + return + } + case "server_tags": + var err error + switch v := v.(type) { + case string: + o.Tags.Add(v) + case []string: + o.Tags.Add(v...) + case []any: + for _, t := range v { + if token, ok := t.(token); ok { + if ts, ok := token.Value().(string); ok { + o.Tags.Add(ts) + continue + } else { + err = &configErr{tk, fmt.Sprintf("error parsing tags: unsupported type %T where string is expected", token)} + } + } else { + err = &configErr{tk, fmt.Sprintf("error parsing tags: unsupported type %T", t)} + } + break + } + default: + err = &configErr{tk, fmt.Sprintf("error parsing tags: unsupported type %T", v)} + } + if err != nil { + *errors = append(*errors, err) + return + } + case "server_metadata": + var err error + switch v := v.(type) { + case map[string]any: + for mk, mv := range v { + tk, mv = unwrapValue(mv, <) + if o.Metadata == nil { + o.Metadata = make(map[string]string) + } + o.Metadata[mk] = mv.(string) + } + default: + err = &configErr{tk, fmt.Sprintf("error parsing metadata: unsupported type %T", v)} + } + if err != nil { + *errors = append(*errors, err) + return + } + case "feature_flags": + var err error + switch v := v.(type) { + case map[string]any: + for mk, mv := range v { + tk, mv = unwrapValue(mv, <) + b, ok := mv.(bool) + if !ok { + err = &configErr{tk, fmt.Sprintf("error parsing feature flag %q: expected bool, got %T", mk, mv)} + break + } + if o.FeatureFlags == nil { + o.FeatureFlags = make(map[string]bool) + } + o.FeatureFlags[mk] = b + } + default: + err = &configErr{tk, fmt.Sprintf("error parsing feature flags: unsupported type %T", v)} + } + if err != nil { + *errors = append(*errors, err) + return + } + case "default_js_domain": + vv, ok := v.(map[string]any) + if !ok { + *errors = append(*errors, &configErr{tk, fmt.Sprintf("error default_js_domain config: unsupported type %T", v)}) + return + } + m := make(map[string]string) + for kk, kv := range vv { + _, v = unwrapValue(kv, &tk) + m[kk] = v.(string) + } + o.JsAccDefaultDomain = m + case "ocsp_cache": + var err error + switch vv := v.(type) { + case bool: + pc := NewOCSPResponseCacheConfig() + if vv { + // Set enabled + pc.Type = LOCAL + o.OCSPCacheConfig = pc + } else { + // Set disabled (none cache) + pc.Type = NONE + o.OCSPCacheConfig = pc + } + case map[string]any: + pc, err := parseOCSPResponseCache(v) + if err != nil { + *errors = append(*errors, err) + return + } + o.OCSPCacheConfig = pc + default: + err = &configErr{tk, fmt.Sprintf("error parsing tags: unsupported type %T", v)} + } + if err != nil { + *errors = append(*errors, err) + return + } + case "no_fast_producer_stall": + o.NoFastProducerStall = v.(bool) + case "max_closed_clients": + o.MaxClosedClients = int(v.(int64)) + case "proxies": + proxies, err := parseProxies(tk, errors) + if err != nil { + *errors = append(*errors, err) + return + } + o.Proxies = proxies + default: + if au := atomic.LoadInt32(&allowUnknownTopLevelField); au == 0 && !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + } +} + +func setupUsersAndNKeysDuplicateCheckMap(o *Options) map[string]struct{} { + unames := make(map[string]struct{}, len(o.Users)+len(o.Nkeys)) + for _, u := range o.Users { + unames[u.Username] = struct{}{} + } + for _, u := range o.Nkeys { + unames[u.Nkey] = struct{}{} + } + return unames +} + +func parseDuration(field string, tk token, v any, errors *[]error, warnings *[]error) time.Duration { + if wd, ok := v.(string); ok { + if dur, err := time.ParseDuration(wd); err != nil { + err := &configErr{tk, fmt.Sprintf("error parsing %s: %v", field, err)} + *errors = append(*errors, err) + return 0 + } else { + return dur + } + } else { + // Backward compatible with old type, assume this is the + // number of seconds. + err := &configWarningErr{ + field: field, + configErr: configErr{ + token: tk, + reason: field + " should be converted to a duration", + }, + } + *warnings = append(*warnings, err) + return time.Duration(v.(int64)) * time.Second + } +} + +func parseWriteDeadlinePolicy(tk token, v string, errors *[]error) WriteTimeoutPolicy { + switch v { + case "default": + return WriteTimeoutPolicyDefault + case "close": + return WriteTimeoutPolicyClose + case "retry": + return WriteTimeoutPolicyRetry + default: + err := &configErr{tk, "write_timeout must be 'default', 'close' or 'retry'"} + *errors = append(*errors, err) + return WriteTimeoutPolicyDefault + } +} + +func trackExplicitVal(pm *map[string]bool, name string, val bool) { + m := *pm + if m == nil { + m = make(map[string]bool) + *pm = m + } + m[name] = val +} + +// hostPort is simple struct to hold parsed listen/addr strings. +type hostPort struct { + host string + port int +} + +// parseListen will parse listen option which is replacing host/net and port +func parseListen(v any) (*hostPort, error) { + hp := &hostPort{} + switch vv := v.(type) { + // Only a port + case int64: + hp.port = int(vv) + case string: + host, port, err := net.SplitHostPort(vv) + if err != nil { + return nil, fmt.Errorf("could not parse address string %q", vv) + } + hp.port, err = strconv.Atoi(port) + if err != nil { + return nil, fmt.Errorf("could not parse port %q", port) + } + hp.host = host + default: + return nil, fmt.Errorf("expected port or host:port, got %T", vv) + } + return hp, nil +} + +// parseCluster will parse the cluster config. +func parseCluster(v any, opts *Options, errors *[]error, warnings *[]error) error { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + cm, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected map to define cluster, got %T", v)} + } + + for mk, mv := range cm { + // Again, unwrap token value if line check is required. + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "name": + cn := mv.(string) + if strings.Contains(cn, " ") { + err := &configErr{tk, ErrClusterNameHasSpaces.Error()} + *errors = append(*errors, err) + continue + } + if cn == leafNoOriginCluster { + err := &configErr{tk, ErrClusterNameReserved.Error()} + *errors = append(*errors, err) + continue + } + opts.Cluster.Name = cn + case "listen": + hp, err := parseListen(mv) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + opts.Cluster.Host = hp.host + opts.Cluster.Port = hp.port + case "port": + opts.Cluster.Port = int(mv.(int64)) + case "host", "net": + opts.Cluster.Host = mv.(string) + case "authorization": + auth, err := parseAuthorization(tk, errors, warnings) + if err != nil { + *errors = append(*errors, err) + continue + } + if auth.users != nil { + err := &configErr{tk, "Cluster authorization does not allow multiple users"} + *errors = append(*errors, err) + continue + } + if auth.token != _EMPTY_ { + err := &configErr{tk, "Cluster authorization does not support tokens"} + *errors = append(*errors, err) + continue + } + if auth.callout != nil { + err := &configErr{tk, "Cluster authorization does not support callouts"} + *errors = append(*errors, err) + continue + } + + opts.Cluster.Username = auth.user + opts.Cluster.Password = auth.pass + opts.Cluster.AuthTimeout = auth.timeout + + if auth.defaultPermissions != nil { + if err := checkClusterPermissionSubjects(auth.defaultPermissions); err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + continue + } + err := &configWarningErr{ + field: mk, + configErr: configErr{ + token: tk, + reason: `setting "permissions" within cluster authorization block is deprecated`, + }, + } + *warnings = append(*warnings, err) + + // Do not set permissions if they were specified in top-level cluster block. + if opts.Cluster.Permissions == nil { + setClusterPermissions(&opts.Cluster, auth.defaultPermissions) + } + } + case "routes": + ra := mv.([]any) + routes, errs := parseURLs(ra, "route", warnings) + if errs != nil { + *errors = append(*errors, errs...) + continue + } + opts.Routes = routes + case "tls": + config, tlsopts, err := getTLSConfig(tk) + if err != nil { + *errors = append(*errors, err) + continue + } + opts.Cluster.TLSConfig = config + opts.Cluster.TLSTimeout = tlsopts.Timeout + opts.Cluster.TLSMap = tlsopts.Map + opts.Cluster.TLSPinnedCerts = tlsopts.PinnedCerts + opts.Cluster.TLSCheckKnownURLs = tlsopts.TLSCheckKnownURLs + opts.Cluster.tlsConfigOpts = tlsopts + case "cluster_advertise", "advertise": + opts.Cluster.Advertise = mv.(string) + case "no_advertise": + opts.Cluster.NoAdvertise = mv.(bool) + trackExplicitVal(&opts.inConfig, "Cluster.NoAdvertise", opts.Cluster.NoAdvertise) + case "connect_retries": + opts.Cluster.ConnectRetries = int(mv.(int64)) + case "connect_backoff": + opts.Cluster.ConnectBackoff = mv.(bool) + case "permissions": + perms, err := parseUserPermissions(mv, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + // Dynamic response permissions do not make sense here. + if perms.Response != nil { + err := &configErr{tk, "Cluster permissions do not support dynamic responses"} + *errors = append(*errors, err) + continue + } + if err := checkClusterPermissionSubjects(perms); err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + continue + } + // This will possibly override permissions that were define in auth block + setClusterPermissions(&opts.Cluster, perms) + case "pool_size": + opts.Cluster.PoolSize = int(mv.(int64)) + case "accounts": + opts.Cluster.PinnedAccounts, _ = parseStringArray("accounts", tk, <, mv, errors) + case "compression": + if err := parseCompression(&opts.Cluster.Compression, CompressionS2Fast, tk, mk, mv); err != nil { + *errors = append(*errors, err) + continue + } + case "ping_interval": + opts.Cluster.PingInterval = parseDuration("ping_interval", tk, mv, errors, warnings) + if opts.Cluster.PingInterval > routeMaxPingInterval { + *warnings = append(*warnings, &configErr{tk, fmt.Sprintf("Cluster 'ping_interval' will reset to %v which is the max for routes", routeMaxPingInterval)}) + } + case "ping_max": + opts.Cluster.MaxPingsOut = int(mv.(int64)) + case "write_deadline": + opts.Cluster.WriteDeadline = parseDuration("write_deadline", tk, mv, errors, warnings) + case "write_timeout": + opts.Cluster.WriteTimeout = parseWriteDeadlinePolicy(tk, mv.(string), errors) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +// The parameter `chosenModeForOn` indicates which compression mode to use +// when the user selects "on" (or enabled, true, etc..). This is because +// we may have different defaults depending on where the compression is used. +func parseCompression(c *CompressionOpts, chosenModeForOn string, tk token, mk string, mv any) (retErr error) { + var lt token + defer convertPanicToError(<, &retErr) + + switch mv := mv.(type) { + case string: + // Do not validate here, it will be done in NewServer. + c.Mode = mv + case bool: + if mv { + c.Mode = chosenModeForOn + } else { + c.Mode = CompressionOff + } + case map[string]any: + for mk, mv := range mv { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "mode": + c.Mode = mv.(string) + case "rtt_thresholds", "thresholds", "rtts", "rtt": + for _, iv := range mv.([]any) { + _, mv := unwrapValue(iv, <) + dur, err := time.ParseDuration(mv.(string)) + if err != nil { + return &configErr{tk, err.Error()} + } + c.RTTThresholds = append(c.RTTThresholds, dur) + } + default: + if !tk.IsUsedVariable() { + return &configErr{tk, fmt.Sprintf("unknown field %q", mk)} + } + } + } + default: + return &configErr{tk, fmt.Sprintf("field %q should be a boolean or a structure, got %T", mk, mv)} + } + return nil +} + +func parseURLs(a []any, typ string, warnings *[]error) (urls []*url.URL, errors []error) { + urls = make([]*url.URL, 0, len(a)) + var lt token + defer convertPanicToErrorList(<, &errors) + + dd := make(map[string]bool) + + for _, u := range a { + tk, u := unwrapValue(u, <) + sURL := u.(string) + if dd[sURL] { + err := &configWarningErr{ + field: sURL, + configErr: configErr{ + token: tk, + reason: fmt.Sprintf("Duplicate %s entry detected", typ), + }, + } + *warnings = append(*warnings, err) + continue + } + dd[sURL] = true + url, err := parseURL(sURL, typ) + if err != nil { + err := &configErr{tk, err.Error()} + errors = append(errors, err) + continue + } + urls = append(urls, url) + } + return urls, errors +} + +func parseURL(u string, typ string) (*url.URL, error) { + urlStr := strings.TrimSpace(u) + url, err := url.Parse(urlStr) + if err != nil { + // Security note: if it's not well-formed but still reached us, then we're going to log as-is which might include password information here. + // If the URL parses, we don't log the credentials ever, but if it doesn't even parse we don't have a sane way to redact. + return nil, fmt.Errorf("error parsing %s url [%q]", typ, urlStr) + } + return url, nil +} + +func parseGateway(v any, o *Options, errors *[]error, warnings *[]error) error { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + gm, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected gateway to be a map, got %T", v)} + } + for mk, mv := range gm { + // Again, unwrap token value if line check is required. + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "name": + gn := mv.(string) + if strings.Contains(gn, " ") { + err := &configErr{tk, ErrGatewayNameHasSpaces.Error()} + *errors = append(*errors, err) + continue + } + if gn == leafNoOriginCluster { + err := &configErr{tk, ErrClusterNameReserved.Error()} + *errors = append(*errors, err) + continue + } + o.Gateway.Name = gn + case "listen": + hp, err := parseListen(mv) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + o.Gateway.Host = hp.host + o.Gateway.Port = hp.port + case "port": + o.Gateway.Port = int(mv.(int64)) + case "host", "net": + o.Gateway.Host = mv.(string) + case "authorization": + auth, err := parseAuthorization(tk, errors, warnings) + if err != nil { + *errors = append(*errors, err) + continue + } + if auth.users != nil { + *errors = append(*errors, &configErr{tk, "Gateway authorization does not allow multiple users"}) + continue + } + if auth.token != _EMPTY_ { + err := &configErr{tk, "Gateway authorization does not support tokens"} + *errors = append(*errors, err) + continue + } + if auth.callout != nil { + err := &configErr{tk, "Gateway authorization does not support callouts"} + *errors = append(*errors, err) + continue + } + + o.Gateway.Username = auth.user + o.Gateway.Password = auth.pass + o.Gateway.AuthTimeout = auth.timeout + case "tls": + config, tlsopts, err := getTLSConfig(tk) + if err != nil { + *errors = append(*errors, err) + continue + } + o.Gateway.TLSConfig = config + o.Gateway.TLSTimeout = tlsopts.Timeout + o.Gateway.TLSMap = tlsopts.Map + o.Gateway.TLSCheckKnownURLs = tlsopts.TLSCheckKnownURLs + o.Gateway.TLSPinnedCerts = tlsopts.PinnedCerts + o.Gateway.tlsConfigOpts = tlsopts + case "advertise": + o.Gateway.Advertise = mv.(string) + case "connect_retries": + o.Gateway.ConnectRetries = int(mv.(int64)) + case "connect_backoff": + o.Gateway.ConnectBackoff = mv.(bool) + case "gateways": + gateways, err := parseGateways(mv, errors, warnings) + if err != nil { + return err + } + o.Gateway.Gateways = gateways + case "reject_unknown", "reject_unknown_cluster": + o.Gateway.RejectUnknown = mv.(bool) + case "write_deadline": + o.Gateway.WriteDeadline = parseDuration("write_deadline", tk, mv, errors, warnings) + case "write_timeout": + o.Gateway.WriteTimeout = parseWriteDeadlinePolicy(tk, mv.(string), errors) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +var dynamicJSAccountLimits = JetStreamAccountLimits{-1, -1, -1, -1, -1, -1, -1, false} +var defaultJSAccountTiers = map[string]JetStreamAccountLimits{_EMPTY_: dynamicJSAccountLimits} + +// Parses jetstream account limits for an account. Simple setup with boolen is allowed, and we will +// use dynamic account limits. +func parseJetStreamForAccount(v any, acc *Account, errors *[]error) error { + var lt token + + tk, v := unwrapValue(v, <) + + // Value here can be bool, or string "enabled" or a map. + switch vv := v.(type) { + case bool: + if vv { + acc.jsLimits = defaultJSAccountTiers + } + case string: + switch strings.ToLower(vv) { + case "enabled", "enable": + acc.jsLimits = defaultJSAccountTiers + case "disabled", "disable": + acc.jsLimits = nil + default: + return &configErr{tk, fmt.Sprintf("Expected 'enabled' or 'disabled' for string value, got '%s'", vv)} + } + case map[string]any: + jsLimits := JetStreamAccountLimits{-1, -1, -1, -1, -1, -1, -1, false} + for mk, mv := range vv { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "max_memory", "max_mem", "mem", "memory": + vv, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + jsLimits.MaxMemory = vv + case "max_store", "max_file", "max_disk", "store", "disk": + vv, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + jsLimits.MaxStore = vv + case "max_streams", "streams": + vv, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + jsLimits.MaxStreams = int(vv) + case "max_consumers", "consumers": + vv, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + jsLimits.MaxConsumers = int(vv) + case "max_bytes_required", "max_stream_bytes", "max_bytes": + vv, ok := mv.(bool) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable bool for %q, got %v", mk, mv)} + } + jsLimits.MaxBytesRequired = vv + case "mem_max_stream_bytes", "memory_max_stream_bytes": + vv, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + jsLimits.MemoryMaxStreamBytes = vv + case "disk_max_stream_bytes", "store_max_stream_bytes": + vv, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + jsLimits.StoreMaxStreamBytes = vv + case "max_ack_pending": + vv, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + jsLimits.MaxAckPending = int(vv) + case "cluster_traffic": + vv, ok := mv.(string) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected either 'system' or 'owner' string value for %q, got %v", mk, mv)} + } + switch vv { + case "system", _EMPTY_: + acc.nrgAccount = _EMPTY_ + case "owner": + acc.nrgAccount = acc.Name + default: + return &configErr{tk, fmt.Sprintf("Expected 'system' or 'owner' string value for %q, got %v", mk, mv)} + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + acc.jsLimits = map[string]JetStreamAccountLimits{_EMPTY_: jsLimits} + default: + return &configErr{tk, fmt.Sprintf("Expected map, bool or string to define JetStream, got %T", v)} + } + return nil +} + +// takes in a storage size as either an int or a string and returns an int64 value based on the input. +func getStorageSize(v any) (int64, error) { + _, ok := v.(int64) + if ok { + return v.(int64), nil + } + + s, ok := v.(string) + if !ok { + return 0, fmt.Errorf("must be int64 or string") + } + + if s == _EMPTY_ { + return 0, nil + } + + suffix := s[len(s)-1:] + prefix := s[:len(s)-1] + num, err := strconv.ParseInt(prefix, 10, 64) + if err != nil { + return 0, err + } + + suffixMap := map[string]int64{"K": 10, "M": 20, "G": 30, "T": 40} + + mult, ok := suffixMap[suffix] + if !ok { + return 0, fmt.Errorf("sizes defined as strings must end in K, M, G, T") + } + num *= 1 << mult + + return num, nil +} + +// Parse enablement of jetstream for a server. +func parseJetStreamLimits(v any, opts *Options, errors *[]error) error { + var lt token + tk, v := unwrapValue(v, <) + + opts.JetStreamLimits = JSLimitOpts{} + + vv, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a map to define JetStreamLimits, got %T", v)} + } + for mk, mv := range vv { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "max_ack_pending": + opts.JetStreamLimits.MaxAckPending = int(mv.(int64)) + case "max_ha_assets": + opts.JetStreamLimits.MaxHAAssets = int(mv.(int64)) + case "max_request_batch": + opts.JetStreamLimits.MaxRequestBatch = int(mv.(int64)) + case "default_max_consumers": + opts.JetStreamLimits.DefaultMaxConsumers = int(mv.(int64)) + case "duplicate_window": + var err error + opts.JetStreamLimits.Duplicates, err = time.ParseDuration(mv.(string)) + if err != nil { + *errors = append(*errors, err) + } + case "batch": + if err := parseJetStreamLimitsBatch(tk, opts, errors); err != nil { + return err + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +func parseJetStreamLimitsBatch(v any, opts *Options, errors *[]error) error { + var lt token + tk, v := unwrapValue(v, <) + + vv, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a map to define batch limits, got %T", v)} + } + for mk, mv := range vv { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "max_inflight_per_stream": + opts.JetStreamLimits.MaxBatchInflightPerStream = int(mv.(int64)) + case "max_inflight_total": + opts.JetStreamLimits.MaxBatchInflightTotal = int(mv.(int64)) + case "max_msgs": + opts.JetStreamLimits.MaxBatchSize = int(mv.(int64)) + case "timeout": + var err error + opts.JetStreamLimits.MaxBatchTimeout, err = time.ParseDuration(mv.(string)) + if err != nil { + *errors = append(*errors, err) + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +// Parse the JetStream TPM options. +func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error { + var lt token + tk, v := unwrapValue(v, <) + + opts.JetStreamTpm = JSTpmOpts{} + + vv, ok := v.(map[string]interface{}) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a map to define JetStreamLimits, got %T", v)} + } + for mk, mv := range vv { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "keys_file": + opts.JetStreamTpm.KeysFile = mv.(string) + case "encryption_password": + opts.JetStreamTpm.KeyPassword = mv.(string) + case "srk_password": + opts.JetStreamTpm.SrkPassword = mv.(string) + case "pcr": + opts.JetStreamTpm.Pcr = int(mv.(int64)) + case "cipher": + if err := setJetStreamEkCipher(opts, mv, tk); err != nil { + return err + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +func setJetStreamEkCipher(opts *Options, mv interface{}, tk token) error { + switch strings.ToLower(mv.(string)) { + case "chacha", "chachapoly": + if fips140.Enabled() { + return &configErr{tk, fmt.Sprintf("Cipher type %q cannot be used in FIPS-140 mode", mv)} + } + opts.JetStreamCipher = ChaCha + case "aes": + opts.JetStreamCipher = AES + default: + return &configErr{tk, fmt.Sprintf("Unknown cipher type: %q", mv)} + } + return nil +} + +// Parse enablement of jetstream for a server. +func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) error { + var lt token + + tk, v := unwrapValue(v, <) + + // Value here can be bool, or string "enabled" or a map. + switch vv := v.(type) { + case bool: + opts.JetStream = v.(bool) + case string: + switch strings.ToLower(vv) { + case "enabled", "enable": + opts.JetStream = true + case "disabled", "disable": + opts.JetStream = false + default: + return &configErr{tk, fmt.Sprintf("Expected 'enabled' or 'disabled' for string value, got '%s'", vv)} + } + case map[string]any: + doEnable := true + for mk, mv := range vv { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "strict": + if v, ok := mv.(bool); ok { + opts.NoJetStreamStrict = !v + } else { + return &configErr{tk, fmt.Sprintf("Expected 'true' or 'false' for bool value, got '%s'", mv)} + } + case "store", "store_dir", "storedir": + // StoreDir can be set at the top level as well so have to prevent ambiguous declarations. + if opts.StoreDir != _EMPTY_ { + return &configErr{tk, "Duplicate 'store_dir' configuration"} + } + opts.StoreDir = mv.(string) + case "sync", "sync_interval": + if v, ok := mv.(string); ok && strings.ToLower(v) == "always" { + opts.SyncInterval = defaultSyncInterval + opts.SyncAlways = true + } else { + opts.SyncInterval = parseDuration(mk, tk, mv, errors, warnings) + } + opts.syncSet = true + case "max_memory_store", "max_mem_store", "max_mem": + s, err := getStorageSize(mv) + if err != nil { + return &configErr{tk, fmt.Sprintf("max_mem_store %s", err)} + } + opts.JetStreamMaxMemory = s + opts.maxMemSet = true + case "max_file_store", "max_file": + s, err := getStorageSize(mv) + if err != nil { + return &configErr{tk, fmt.Sprintf("max_file_store %s", err)} + } + opts.JetStreamMaxStore = s + opts.maxStoreSet = true + case "domain": + opts.JetStreamDomain = mv.(string) + case "enable", "enabled": + doEnable = mv.(bool) + case "key", "ek", "encryption_key": + opts.JetStreamKey = mv.(string) + case "prev_key", "prev_ek", "prev_encryption_key": + opts.JetStreamOldKey = mv.(string) + case "cipher": + if err := setJetStreamEkCipher(opts, mv, tk); err != nil { + return err + } + case "extension_hint": + opts.JetStreamExtHint = mv.(string) + case "limits": + if err := parseJetStreamLimits(tk, opts, errors); err != nil { + return err + } + case "tpm": + if err := parseJetStreamTPM(tk, opts, errors); err != nil { + return err + } + case "unique_tag": + opts.JetStreamUniqueTag = strings.ToLower(strings.TrimSpace(mv.(string))) + case "max_outstanding_catchup": + s, err := getStorageSize(mv) + if err != nil { + return &configErr{tk, fmt.Sprintf("%s %s", strings.ToLower(mk), err)} + } + opts.JetStreamMaxCatchup = s + case "max_buffered_size": + s, err := getStorageSize(mv) + if err != nil { + return &configErr{tk, fmt.Sprintf("%s %s", strings.ToLower(mk), err)} + } + opts.StreamMaxBufferedSize = s + case "max_buffered_msgs": + mlen, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + opts.StreamMaxBufferedMsgs = int(mlen) + case "request_queue_limit": + lim, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + opts.JetStreamRequestQueueLimit = lim + case "info_queue_limit": + lim, ok := mv.(int64) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)} + } + opts.JetStreamInfoQueueLimit = lim + case "meta_compact": + thres, ok := mv.(int64) + if !ok || thres < 0 { + return &configErr{tk, fmt.Sprintf("Expected an absolute size for %q, got %v", mk, mv)} + } + opts.JetStreamMetaCompact = uint64(thres) + case "meta_compact_size": + s, err := getStorageSize(mv) + if err != nil { + return &configErr{tk, fmt.Sprintf("%s %s", strings.ToLower(mk), err)} + } + if s < 0 { + return &configErr{tk, fmt.Sprintf("Expected an absolute size for %q, got %v", mk, mv)} + } + opts.JetStreamMetaCompactSize = uint64(s) + case "meta_compact_sync": + opts.JetStreamMetaCompactSync = mv.(bool) + case "max_concurrent_io": + dios, ok := mv.(int64) + if !ok || dios < minConcurrentIOs || dios > maxConcurrentIOs { + return &configErr{tk, fmt.Sprintf("Expected an absolute size for %q between 4 and 8192, got %v", mk, mv)} + } + opts.JetStreamConcurrentIOs = int(dios) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + opts.JetStream = doEnable + default: + return &configErr{tk, fmt.Sprintf("Expected map, bool or string to define JetStream, got %T", v)} + } + + return nil +} + +// parseLeafNodes will parse the leaf node config. +func parseLeafNodes(v any, opts *Options, errors *[]error, warnings *[]error) error { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + cm, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected map to define a leafnode, got %T", v)} + } + + for mk, mv := range cm { + // Again, unwrap token value if line check is required. + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "listen": + hp, err := parseListen(mv) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + opts.LeafNode.Host = hp.host + opts.LeafNode.Port = hp.port + case "port": + opts.LeafNode.Port = int(mv.(int64)) + case "host", "net": + opts.LeafNode.Host = mv.(string) + case "authorization": + auth, err := parseLeafAuthorization(tk, errors, warnings) + if err != nil { + *errors = append(*errors, err) + continue + } + opts.LeafNode.Username = auth.user + opts.LeafNode.Password = auth.pass + opts.LeafNode.ProxyRequired = auth.proxyRequired + opts.LeafNode.AuthTimeout = auth.timeout + opts.LeafNode.Account = auth.acc + opts.LeafNode.Users = auth.users + opts.LeafNode.Nkey = auth.nkey + // Validate user info config for leafnode authorization + if err := validateLeafNodeAuthOptions(opts); err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + continue + } + case "remotes": + // Parse the remote options here. + remotes, err := parseRemoteLeafNodes(tk, errors, warnings) + if err != nil { + *errors = append(*errors, err) + continue + } + opts.LeafNode.Remotes = remotes + case "reconnect", "reconnect_delay", "reconnect_interval": + opts.LeafNode.ReconnectInterval = parseDuration("reconnect", tk, mv, errors, warnings) + case "dial_timeout": + opts.LeafNode.DialTimeout = parseDuration("dial_timeout", tk, mv, errors, warnings) + case "tls": + tc, err := parseTLS(tk, true) + if err != nil { + *errors = append(*errors, err) + continue + } + if opts.LeafNode.TLSConfig, err = GenTLSConfig(tc); err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + opts.LeafNode.TLSTimeout = tc.Timeout + opts.LeafNode.TLSMap = tc.Map + opts.LeafNode.TLSPinnedCerts = tc.PinnedCerts + opts.LeafNode.TLSHandshakeFirst = tc.HandshakeFirst + opts.LeafNode.TLSHandshakeFirstFallback = tc.FallbackDelay + opts.LeafNode.tlsConfigOpts = tc + case "leafnode_advertise", "advertise": + opts.LeafNode.Advertise = mv.(string) + case "no_advertise": + opts.LeafNode.NoAdvertise = mv.(bool) + trackExplicitVal(&opts.inConfig, "LeafNode.NoAdvertise", opts.LeafNode.NoAdvertise) + case "min_version", "minimum_version": + version := mv.(string) + if err := checkLeafMinVersionConfig(version); err != nil { + err = &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + opts.LeafNode.MinVersion = version + case "compression": + if err := parseCompression(&opts.LeafNode.Compression, CompressionS2Auto, tk, mk, mv); err != nil { + *errors = append(*errors, err) + continue + } + case "isolate_leafnode_interest", "isolate": + opts.LeafNode.IsolateLeafnodeInterest = mv.(bool) + case "write_deadline": + opts.LeafNode.WriteDeadline = parseDuration("write_deadline", tk, mv, errors, warnings) + case "write_timeout": + opts.LeafNode.WriteTimeout = parseWriteDeadlinePolicy(tk, mv.(string), errors) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +// This is the authorization parser adapter for the leafnode's +// authorization config. +func parseLeafAuthorization(v any, errors, warnings *[]error) (*authorization, error) { + var ( + am map[string]any + tk token + lt token + auth = &authorization{} + ) + defer convertPanicToErrorList(<, errors) + + _, v = unwrapValue(v, <) + am = v.(map[string]any) + for mk, mv := range am { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "user", "username": + auth.user = mv.(string) + case "pass", "password": + auth.pass = mv.(string) + case "nkey": + nk := mv.(string) + if !nkeys.IsValidPublicUserKey(nk) { + *errors = append(*errors, &configErr{tk, "Not a valid public nkey for leafnode authorization"}) + } + auth.nkey = nk + case "timeout": + at := float64(0) + switch mv := mv.(type) { + case int64: + at = float64(mv) + case float64: + at = mv + case string: + d, err := time.ParseDuration(mv) + if err != nil { + return nil, &configErr{tk, fmt.Sprintf("error parsing leafnode authorization config, 'timeout' %s", err)} + } + at = d.Seconds() + default: + return nil, &configErr{tk, "error parsing leafnode authorization config, 'timeout' wrong type"} + } + if at > (60 * time.Second).Seconds() { + reason := fmt.Sprintf("timeout of %v (%f seconds) is high, consider keeping it under 60 seconds. possibly caused by unquoted duration; use '1m' instead of 1m, for example", mv, at) + *warnings = append(*warnings, &configWarningErr{field: mk, configErr: configErr{token: tk, reason: reason}}) + } + auth.timeout = at + case "users": + users, err := parseLeafUsers(tk, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + auth.users = users + case "account": + auth.acc = mv.(string) + case "proxy_required": + auth.proxyRequired = mv.(bool) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + continue + } + } + return auth, nil +} + +// This is a trimmed down version of parseUsers that is adapted +// for the users possibly defined in the authorization{} section +// of leafnodes {}. +func parseLeafUsers(mv any, errors *[]error) ([]*User, error) { + var ( + tk token + lt token + users = []*User{} + ) + defer convertPanicToErrorList(<, errors) + + tk, mv = unwrapValue(mv, <) + // Make sure we have an array + uv, ok := mv.([]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("Expected users field to be an array, got %v", mv)} + } + for _, u := range uv { + tk, u = unwrapValue(u, <) + // Check its a map/struct + um, ok := u.(map[string]any) + if !ok { + err := &configErr{tk, fmt.Sprintf("Expected user entry to be a map/struct, got %v", u)} + *errors = append(*errors, err) + continue + } + user := &User{} + for k, v := range um { + tk, v = unwrapValue(v, <) + switch strings.ToLower(k) { + case "user", "username": + user.Username = v.(string) + case "pass", "password": + user.Password = v.(string) + case "account": + // We really want to save just the account name here, but + // the User object is *Account. So we create an account object + // but it won't be registered anywhere. The server will just + // use opts.LeafNode.Users[].Account.Name. Alternatively + // we need to create internal objects to store u/p and account + // name and have a server structure to hold that. + user.Account = NewAccount(v.(string)) + case "proxy_required": + user.ProxyRequired = v.(bool) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + users = append(users, user) + } + return users, nil +} + +func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteLeafOpts, error) { + var lt token + defer convertPanicToErrorList(<, errors) + tk, v := unwrapValue(v, <) + ra, ok := v.([]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("Expected remotes field to be an array, got %T", v)} + } + names := make(map[string]struct{}) + remotes := make([]*RemoteLeafOpts, 0, len(ra)) + for _, r := range ra { + tk, r = unwrapValue(r, <) + // Check its a map/struct + rm, ok := r.(map[string]any) + if !ok { + *errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected remote leafnode entry to be a map/struct, got %v", r)}) + continue + } + remote := &RemoteLeafOpts{} + var proxyToken token + for k, v := range rm { + tk, v = unwrapValue(v, <) + switch strings.ToLower(k) { + case "no_randomize", "dont_randomize": + remote.NoRandomize = v.(bool) + case "url", "urls": + switch v := v.(type) { + case []any, []string: + urls, errs := parseURLs(v.([]any), "leafnode", warnings) + if errs != nil { + *errors = append(*errors, errs...) + continue + } + remote.URLs = urls + case string: + url, err := parseURL(v, "leafnode") + if err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + continue + } + remote.URLs = append(remote.URLs, url) + default: + *errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected remote leafnode url to be an array or string, got %v", v)}) + continue + } + case "account", "local": + remote.LocalAccount = v.(string) + case "creds", "credentials": + p, err := expandPath(v.(string)) + if err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + continue + } + // Can't have both creds and nkey + if remote.Nkey != _EMPTY_ { + *errors = append(*errors, &configErr{tk, "Remote leafnode can not have both creds and nkey defined"}) + continue + } + remote.Credentials = p + case "nkey", "seed": + nk := v.(string) + if pb, _, err := nkeys.DecodeSeed([]byte(nk)); err != nil || pb != nkeys.PrefixByteUser { + err := &configErr{tk, fmt.Sprintf("Remote leafnode nkey is not a valid seed: %q", v)} + *errors = append(*errors, err) + continue + } + if remote.Credentials != _EMPTY_ { + *errors = append(*errors, &configErr{tk, "Remote leafnode can not have both creds and nkey defined"}) + continue + } + remote.Nkey = nk + case "tls": + tc, err := parseTLS(tk, true) + if err != nil { + *errors = append(*errors, err) + continue + } + if remote.TLSConfig, err = GenTLSConfig(tc); err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + continue + } + // If ca_file is defined, GenTLSConfig() sets TLSConfig.ClientCAs. + // Set RootCAs since this tls.Config is used when soliciting + // a connection (therefore behaves as a client). + remote.TLSConfig.RootCAs = remote.TLSConfig.ClientCAs + if tc.Timeout > 0 { + remote.TLSTimeout = tc.Timeout + } else { + remote.TLSTimeout = float64(DEFAULT_LEAF_TLS_TIMEOUT) / float64(time.Second) + } + remote.TLSHandshakeFirst = tc.HandshakeFirst + remote.tlsConfigOpts = tc + case "hub": + remote.Hub = v.(bool) + case "deny_imports", "deny_import": + subjects, err := parsePermSubjects(tk, errors, false) + if err != nil { + *errors = append(*errors, err) + continue + } + remote.DenyImports = subjects + case "deny_exports", "deny_export": + subjects, err := parsePermSubjects(tk, errors, false) + if err != nil { + *errors = append(*errors, err) + continue + } + remote.DenyExports = subjects + case "ws_compress", "ws_compression", "websocket_compress", "websocket_compression": + remote.Websocket.Compression = v.(bool) + case "ws_no_masking", "websocket_no_masking": + remote.Websocket.NoMasking = v.(bool) + case "jetstream_cluster_migrate", "js_cluster_migrate": + var lt token + + tk, v := unwrapValue(v, <) + switch vv := v.(type) { + case bool: + remote.JetStreamClusterMigrate = vv + case map[string]any: + remote.JetStreamClusterMigrate = true + migrateConfig, ok := v.(map[string]any) + if !ok { + continue + } + val, ok := migrateConfig["leader_migrate_delay"] + tk, delay := unwrapValue(val, &tk) + if ok { + remote.JetStreamClusterMigrateDelay = parseDuration("leader_migrate_delay", tk, delay, errors, warnings) + } + default: + *errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected boolean or map for jetstream_cluster_migrate, got %T", v)}) + } + case "isolate_leafnode_interest", "isolate": + remote.LocalIsolation = v.(bool) + case "request_isolation": + remote.RequestIsolation = v.(bool) + case "compression": + if err := parseCompression(&remote.Compression, CompressionS2Auto, tk, k, v); err != nil { + *errors = append(*errors, err) + continue + } + case "first_info_timeout": + remote.FirstInfoTimeout = parseDuration(k, tk, v, errors, warnings) + case "dial_timeout": + remote.DialTimeout = parseDuration(k, tk, v, errors, warnings) + case "disabled": + remote.Disabled = v.(bool) + case "proxy": + proxyMap, ok := v.(map[string]any) + if !ok { + *errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected proxy to be a map, got %T", v)}) + continue + } + // Capture the token for the "proxy" field itself, before the map iteration + proxyToken = tk + for pk, pv := range proxyMap { + tk, pv = unwrapValue(pv, <) + switch strings.ToLower(pk) { + case "url": + remote.Proxy.URL = pv.(string) + case "username": + remote.Proxy.Username = pv.(string) + case "password": + remote.Proxy.Password = pv.(string) + case "timeout": + remote.Proxy.Timeout = parseDuration("proxy timeout", tk, pv, errors, warnings) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: pk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + case "ignore_discovered_servers": + remote.IgnoreDiscoveredServers = v.(bool) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + // Use the saved proxy token for validation errors, not the last field token + if warns, err := validateLeafNodeProxyOptions(remote); err != nil { + *errors = append(*errors, &configErr{proxyToken, err.Error()}) + continue + } else { + // Add any warnings about proxy configuration + for _, warn := range warns { + *warnings = append(*warnings, &configErr{proxyToken, warn}) + } + } + rn := remote.name() + if _, dup := names[rn]; dup { + *errors = append(*errors, &configErr{tk, fmt.Sprintf("duplicate remote %s", remote.safeName())}) + continue + } + names[rn] = struct{}{} + remotes = append(remotes, remote) + } + return remotes, nil +} + +// Parse TLS and returns a TLSConfig and TLSTimeout. +// Used by cluster and gateway parsing. +func getTLSConfig(tk token) (*tls.Config, *TLSConfigOpts, error) { + tc, err := parseTLS(tk, false) + if err != nil { + return nil, nil, err + } + config, err := GenTLSConfig(tc) + if err != nil { + err := &configErr{tk, err.Error()} + return nil, nil, err + } + // For clusters/gateways, we will force strict verification. We also act + // as both client and server, so will mirror the rootCA to the + // clientCA pool. + config.ClientAuth = tls.RequireAndVerifyClientCert + config.RootCAs = config.ClientCAs + return config, tc, nil +} + +func parseGateways(v any, errors *[]error, warnings *[]error) ([]*RemoteGatewayOpts, error) { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + // Make sure we have an array + ga, ok := v.([]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("Expected gateways field to be an array, got %T", v)} + } + gateways := []*RemoteGatewayOpts{} + for _, g := range ga { + tk, g = unwrapValue(g, <) + // Check its a map/struct + gm, ok := g.(map[string]any) + if !ok { + *errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected gateway entry to be a map/struct, got %v", g)}) + continue + } + gateway := &RemoteGatewayOpts{} + for k, v := range gm { + tk, v = unwrapValue(v, <) + switch strings.ToLower(k) { + case "name": + gateway.Name = v.(string) + case "tls": + tls, tlsopts, err := getTLSConfig(tk) + if err != nil { + *errors = append(*errors, err) + continue + } + gateway.TLSConfig = tls + gateway.TLSTimeout = tlsopts.Timeout + gateway.tlsConfigOpts = tlsopts + case "url": + url, err := parseURL(v.(string), "gateway") + if err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + continue + } + gateway.URLs = append(gateway.URLs, url) + case "urls": + urls, errs := parseURLs(v.([]any), "gateway", warnings) + if errs != nil { + *errors = append(*errors, errs...) + continue + } + gateway.URLs = urls + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + gateways = append(gateways, gateway) + } + return gateways, nil +} + +// Sets cluster's permissions based on given pub/sub permissions, +// doing the appropriate translation. +func setClusterPermissions(opts *ClusterOpts, perms *Permissions) { + // Import is whether or not we will send a SUB for interest to the other side. + // Export is whether or not we will accept a SUB from the remote for a given subject. + // Both only effect interest registration. + // The parsing sets Import into Publish and Export into Subscribe, convert + // accordingly. + opts.Permissions = &RoutePermissions{ + Import: perms.Publish, + Export: perms.Subscribe, + } +} + +func checkClusterPermissionSubjects(perms *Permissions) error { + if perms == nil { + return nil + } + if perms.Publish != nil { + if err := checkPermSubjectArray(perms.Publish.Allow, false); err != nil { + return fmt.Errorf("cluster import allow: %w", err) + } + if err := checkPermSubjectArray(perms.Publish.Deny, false); err != nil { + return fmt.Errorf("cluster import deny: %w", err) + } + } + if perms.Subscribe != nil { + if err := checkPermSubjectArray(perms.Subscribe.Allow, false); err != nil { + return fmt.Errorf("cluster export allow: %w", err) + } + if err := checkPermSubjectArray(perms.Subscribe.Deny, false); err != nil { + return fmt.Errorf("cluster export deny: %w", err) + } + } + return nil +} + +// Temp structures to hold account import and export defintions since they need +// to be processed after being parsed. +type export struct { + acc *Account + sub string + accs []string + rt ServiceRespType + lat *serviceLatency + rthr time.Duration + tPos uint + atrc bool // allow_trace +} + +type importStream struct { + acc *Account + an string + sub string + to string + pre string + atrc bool // allow_trace +} + +type importService struct { + acc *Account + an string + sub string + to string + share bool +} + +// Checks if an account name is reserved. +func isReservedAccount(name string) bool { + return name == globalAccountName +} + +func parseAccountMapDest(v any, tk token, errors *[]error) (*MapDest, *configErr) { + // These should be maps. + mv, ok := v.(map[string]any) + if !ok { + err := &configErr{tk, "Expected an entry for the mapping destination"} + *errors = append(*errors, err) + return nil, err + } + + mdest := &MapDest{} + var lt token + var sw bool + + for k, v := range mv { + tk, dmv := unwrapValue(v, <) + switch strings.ToLower(k) { + case "dest", "destination": + mdest.Subject = dmv.(string) + case "weight": + switch vv := dmv.(type) { + case string: + ws := vv + ws = strings.TrimSuffix(ws, "%") + weight, err := strconv.Atoi(ws) + if err != nil { + err := &configErr{tk, fmt.Sprintf("Invalid weight %q for mapping destination", ws)} + *errors = append(*errors, err) + return nil, err + } + if weight > 100 || weight < 0 { + err := &configErr{tk, fmt.Sprintf("Invalid weight %d for mapping destination", weight)} + *errors = append(*errors, err) + return nil, err + } + mdest.Weight = uint8(weight) + sw = true + case int64: + weight := vv + if weight > 100 || weight < 0 { + err := &configErr{tk, fmt.Sprintf("Invalid weight %d for mapping destination", weight)} + *errors = append(*errors, err) + return nil, err + } + mdest.Weight = uint8(weight) + sw = true + default: + err := &configErr{tk, fmt.Sprintf("Unknown entry type for weight of %v\n", vv)} + *errors = append(*errors, err) + return nil, err + } + case "cluster": + mdest.Cluster = dmv.(string) + default: + err := &configErr{tk, fmt.Sprintf("Unknown field %q for mapping destination", k)} + *errors = append(*errors, err) + return nil, err + } + } + + if !sw { + err := &configErr{tk, fmt.Sprintf("Missing weight for mapping destination %q", mdest.Subject)} + *errors = append(*errors, err) + return nil, err + } + + return mdest, nil +} + +// parseAccountMappings is called to parse account mappings. +func parseAccountMappings(v any, acc *Account, errors *[]error) error { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + am := v.(map[string]any) + for subj, mv := range am { + if !IsValidSubject(subj) { + err := &configErr{tk, fmt.Sprintf("Subject %q is not a valid subject", subj)} + *errors = append(*errors, err) + continue + } + tk, v := unwrapValue(mv, <) + + switch vv := v.(type) { + case string: + if err := acc.AddMapping(subj, v.(string)); err != nil { + err := &configErr{tk, fmt.Sprintf("Error adding mapping for %q to %q : %v", subj, v.(string), err)} + *errors = append(*errors, err) + continue + } + case []any: + var mappings []*MapDest + for _, mv := range v.([]any) { + tk, amv := unwrapValue(mv, <) + mdest, err := parseAccountMapDest(amv, tk, errors) + if err != nil { + continue + } + mappings = append(mappings, mdest) + } + + // Now add them in.. + if err := acc.AddWeightedMappings(subj, mappings...); err != nil { + err := &configErr{tk, fmt.Sprintf("Error adding mapping for %q : %v", subj, err)} + *errors = append(*errors, err) + continue + } + case any: + tk, amv := unwrapValue(mv, <) + mdest, err := parseAccountMapDest(amv, tk, errors) + if err != nil { + continue + } + // Now add it in.. + if err := acc.AddWeightedMappings(subj, mdest); err != nil { + err := &configErr{tk, fmt.Sprintf("Error adding mapping for %q : %v", subj, err)} + *errors = append(*errors, err) + continue + } + default: + err := &configErr{tk, fmt.Sprintf("Unknown type %T for mapping destination", vv)} + *errors = append(*errors, err) + continue + } + } + + return nil +} + +// parseAccountLimits is called to parse account limits in a server config. +func parseAccountLimits(mv any, acc *Account, errors *[]error) error { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(mv, <) + am, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected account limits to be a map/struct, got %+v", v)} + } + + for k, v := range am { + tk, mv = unwrapValue(v, <) + switch strings.ToLower(k) { + case "max_connections", "max_conn": + acc.mconns = int32(mv.(int64)) + case "max_subscriptions", "max_subs": + acc.msubs = int32(mv.(int64)) + case "max_payload", "max_pay": + acc.mpay = int32(mv.(int64)) + case "max_leafnodes", "max_leafs": + acc.mleafs = int32(mv.(int64)) + default: + if !tk.IsUsedVariable() { + err := &configErr{tk, fmt.Sprintf("Unknown field %q parsing account limits", k)} + *errors = append(*errors, err) + } + } + } + + return nil +} + +func parseAccountMsgTrace(mv any, topKey string, acc *Account) error { + processDest := func(tk token, k string, v any) error { + td, ok := v.(string) + if !ok { + return &configErr{tk, fmt.Sprintf("Field %q should be a string, got %T", k, v)} + } + if !IsValidPublishSubject(td) { + return &configErr{tk, fmt.Sprintf("Trace destination %q is not valid", td)} + } + acc.traceDest = td + return nil + } + processSampling := func(tk token, n int) error { + if n <= 0 || n > 100 { + return &configErr{tk, fmt.Sprintf("Ttrace destination sampling value %d is invalid, needs to be [1..100]", n)} + } + acc.traceDestSampling = n + return nil + } + + var lt token + tk, v := unwrapValue(mv, <) + switch vv := v.(type) { + case string: + return processDest(tk, topKey, v) + case map[string]any: + for k, v := range vv { + tk, v := unwrapValue(v, <) + switch strings.ToLower(k) { + case "dest": + if err := processDest(tk, k, v); err != nil { + return err + } + case "sampling": + switch vv := v.(type) { + case int64: + if err := processSampling(tk, int(vv)); err != nil { + return err + } + case string: + s := strings.TrimSuffix(vv, "%") + n, err := strconv.Atoi(s) + if err != nil { + return &configErr{tk, fmt.Sprintf("Invalid trace destination sampling value %q", vv)} + } + if err := processSampling(tk, n); err != nil { + return err + } + default: + return &configErr{tk, fmt.Sprintf("Trace destination sampling field %q should be an integer or a percentage, got %T", k, v)} + } + default: + if !tk.IsUsedVariable() { + return &configErr{tk, fmt.Sprintf("Unknown field %q parsing account message trace map/struct %q", k, topKey)} + } + } + } + default: + return &configErr{tk, fmt.Sprintf("Expected account message trace %q to be a string or a map/struct, got %T", topKey, v)} + } + return nil +} + +// parseAccounts will parse the different accounts syntax. +func parseAccounts(v any, opts *Options, errors *[]error, warnings *[]error) error { + var ( + importStreams []*importStream + importServices []*importService + exportStreams []*export + exportServices []*export + lt token + ) + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + switch vv := v.(type) { + // Simple array of account names. + case []any, []string: + m := make(map[string]struct{}, len(v.([]any))) + for _, n := range v.([]any) { + tk, name := unwrapValue(n, <) + ns := name.(string) + // Check for reserved names. + if isReservedAccount(ns) { + err := &configErr{tk, fmt.Sprintf("%q is a Reserved Account", ns)} + *errors = append(*errors, err) + continue + } + if _, ok := m[ns]; ok { + err := &configErr{tk, fmt.Sprintf("Duplicate Account Entry: %s", ns)} + *errors = append(*errors, err) + continue + } + opts.Accounts = append(opts.Accounts, NewAccount(ns)) + m[ns] = struct{}{} + } + // More common map entry + case map[string]any: + // Track users across accounts, must be unique across + // accounts and nkeys vs users. + // We also want to check for users that may have been added in + // parseAuthorization{} if that happened first. + uorn := setupUsersAndNKeysDuplicateCheckMap(opts) + + for aname, mv := range vv { + tk, amv := unwrapValue(mv, <) + + // Skip referenced config vars within the account block. + if tk.IsUsedVariable() { + continue + } + + // These should be maps. + mv, ok := amv.(map[string]any) + if !ok { + err := &configErr{tk, "Expected map entries for accounts"} + *errors = append(*errors, err) + continue + } + if isReservedAccount(aname) { + err := &configErr{tk, fmt.Sprintf("%q is a Reserved Account", aname)} + *errors = append(*errors, err) + continue + } + var ( + users []*User + nkeyUsr []*NkeyUser + usersTk token + ) + acc := NewAccount(aname) + opts.Accounts = append(opts.Accounts, acc) + + for k, v := range mv { + tk, mv := unwrapValue(v, <) + switch strings.ToLower(k) { + case "nkey": + nk, ok := mv.(string) + if !ok || !nkeys.IsValidPublicAccountKey(nk) { + err := &configErr{tk, fmt.Sprintf("Not a valid public nkey for an account: %q", mv)} + *errors = append(*errors, err) + continue + } + acc.Nkey = nk + case "imports": + streams, services, err := parseAccountImports(tk, acc, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + importStreams = append(importStreams, streams...) + importServices = append(importServices, services...) + case "exports": + streams, services, err := parseAccountExports(tk, acc, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + exportStreams = append(exportStreams, streams...) + exportServices = append(exportServices, services...) + case "jetstream": + err := parseJetStreamForAccount(mv, acc, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + case "users": + var err error + usersTk = tk + nkeyUsr, users, err = parseUsers(mv, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + case "default_permissions": + permissions, err := parseUserPermissions(tk, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + acc.defaultPerms = permissions + case "mappings", "maps": + err := parseAccountMappings(tk, acc, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + case "limits": + err := parseAccountLimits(tk, acc, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + case "msg_trace", "trace_dest": + if err := parseAccountMsgTrace(tk, k, acc); err != nil { + *errors = append(*errors, err) + continue + } + // If trace destination is set but no sampling, set it to 100%. + if acc.traceDest != _EMPTY_ && acc.traceDestSampling == 0 { + acc.traceDestSampling = 100 + } else if acc.traceDestSampling > 0 && acc.traceDest == _EMPTY_ { + // If no trace destination is provided, no trace would be + // triggered, so if the user set a sampling value expecting + // something to happen, want and set the value to 0 for good + // measure. + *warnings = append(*warnings, + &configErr{tk, "Trace destination sampling ignored since no destination was set"}) + acc.traceDestSampling = 0 + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + } + } + // Report error if there is an authorization{} block + // with u/p or token and any user defined in accounts{} + if len(nkeyUsr) > 0 || len(users) > 0 { + if opts.Username != _EMPTY_ { + err := &configErr{usersTk, "Can not have a single user/pass and accounts"} + *errors = append(*errors, err) + continue + } + if opts.Authorization != _EMPTY_ { + err := &configErr{usersTk, "Can not have a token and accounts"} + *errors = append(*errors, err) + continue + } + } + applyDefaultPermissions(users, nkeyUsr, acc.defaultPerms) + for _, u := range nkeyUsr { + if _, ok := uorn[u.Nkey]; ok { + err := &configErr{usersTk, fmt.Sprintf("Duplicate nkey %q detected", u.Nkey)} + *errors = append(*errors, err) + continue + } + uorn[u.Nkey] = struct{}{} + u.Account = acc + } + opts.Nkeys = append(opts.Nkeys, nkeyUsr...) + for _, u := range users { + if _, ok := uorn[u.Username]; ok { + err := &configErr{usersTk, fmt.Sprintf("Duplicate user %q detected", u.Username)} + *errors = append(*errors, err) + continue + } + uorn[u.Username] = struct{}{} + u.Account = acc + } + opts.Users = append(opts.Users, users...) + } + } + lt = tk + // Bail already if there are previous errors. + if len(*errors) > 0 { + return nil + } + + // Parse Imports and Exports here after all accounts defined. + // Do exports first since they need to be defined for imports to succeed + // since we do permissions checks. + + // Create a lookup map for accounts lookups. + am := make(map[string]*Account, len(opts.Accounts)) + for _, a := range opts.Accounts { + am[a.Name] = a + } + // Do stream exports + for _, stream := range exportStreams { + // Make array of accounts if applicable. + var accounts []*Account + for _, an := range stream.accs { + ta := am[an] + if ta == nil { + msg := fmt.Sprintf("%q account not defined for stream export", an) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + accounts = append(accounts, ta) + } + if err := stream.acc.addStreamExportWithAccountPos(stream.sub, accounts, stream.tPos); err != nil { + msg := fmt.Sprintf("Error adding stream export %q: %v", stream.sub, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + } + for _, service := range exportServices { + // Make array of accounts if applicable. + var accounts []*Account + for _, an := range service.accs { + ta := am[an] + if ta == nil { + msg := fmt.Sprintf("%q account not defined for service export", an) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + accounts = append(accounts, ta) + } + if err := service.acc.addServiceExportWithResponseAndAccountPos(service.sub, service.rt, accounts, service.tPos); err != nil { + msg := fmt.Sprintf("Error adding service export %q: %v", service.sub, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + + if service.rthr != 0 { + // Response threshold was set in options. + if err := service.acc.SetServiceExportResponseThreshold(service.sub, service.rthr); err != nil { + msg := fmt.Sprintf("Error adding service export response threshold for %q: %v", service.sub, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + } + + if service.lat != nil { + // System accounts are on be default so just make sure we have not opted out.. + if opts.NoSystemAccount { + msg := fmt.Sprintf("Error adding service latency sampling for %q: %v", service.sub, ErrNoSysAccount.Error()) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + + if err := service.acc.TrackServiceExportWithSampling(service.sub, service.lat.subject, int(service.lat.sampling)); err != nil { + msg := fmt.Sprintf("Error adding service latency sampling for %q on subject %q: %v", service.sub, service.lat.subject, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + } + + if service.atrc { + if err := service.acc.SetServiceExportAllowTrace(service.sub, true); err != nil { + msg := fmt.Sprintf("Error adding allow_trace for %q: %v", service.sub, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + } + } + for _, stream := range importStreams { + ta := am[stream.an] + if ta == nil { + msg := fmt.Sprintf("%q account not defined for stream import", stream.an) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + if stream.pre != _EMPTY_ { + if err := stream.acc.addStreamImportWithClaim(ta, stream.sub, stream.pre, stream.atrc, nil); err != nil { + msg := fmt.Sprintf("Error adding stream import %q: %v", stream.sub, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + } else { + if err := stream.acc.addMappedStreamImportWithClaim(ta, stream.sub, stream.to, stream.atrc, nil); err != nil { + msg := fmt.Sprintf("Error adding stream import %q: %v", stream.sub, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + } + } + for _, service := range importServices { + ta := am[service.an] + if ta == nil { + msg := fmt.Sprintf("%q account not defined for service import", service.an) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + if service.to == _EMPTY_ { + service.to = service.sub + } + if err := service.acc.AddServiceImport(ta, service.to, service.sub); err != nil { + msg := fmt.Sprintf("Error adding service import %q: %v", service.sub, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + if err := service.acc.SetServiceImportSharing(ta, service.sub, service.share); err != nil { + msg := fmt.Sprintf("Error setting service import sharing %q: %v", service.sub, err) + *errors = append(*errors, &configErr{tk, msg}) + continue + } + } + + return nil +} + +// Parse the account exports +func parseAccountExports(v any, acc *Account, errors *[]error) ([]*export, []*export, error) { + var lt token + defer convertPanicToErrorList(<, errors) + + // This should be an array of objects/maps. + tk, v := unwrapValue(v, <) + ims, ok := v.([]any) + if !ok { + return nil, nil, &configErr{tk, fmt.Sprintf("Exports should be an array, got %T", v)} + } + + var services []*export + var streams []*export + + for _, v := range ims { + // Should have stream or service + stream, service, err := parseExportStreamOrService(v, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + if service != nil { + service.acc = acc + services = append(services, service) + } + if stream != nil { + stream.acc = acc + streams = append(streams, stream) + } + } + return streams, services, nil +} + +// Parse the account imports +func parseAccountImports(v any, acc *Account, errors *[]error) ([]*importStream, []*importService, error) { + var lt token + defer convertPanicToErrorList(<, errors) + + // This should be an array of objects/maps. + tk, v := unwrapValue(v, <) + ims, ok := v.([]any) + if !ok { + return nil, nil, &configErr{tk, fmt.Sprintf("Imports should be an array, got %T", v)} + } + + var services []*importService + var streams []*importStream + svcSubjects := map[string][]*importService{} + +IMS_LOOP: + for _, v := range ims { + // Should have stream or service + stream, service, err := parseImportStreamOrService(v, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + if service != nil { + sisPerSubj := svcSubjects[service.to] + for _, dup := range sisPerSubj { + if dup.an == service.an { + tk, _ := unwrapValue(v, <) + err := &configErr{tk, + fmt.Sprintf("Duplicate service import subject %q, previously used in import for account %q, subject %q", + service.to, dup.an, dup.sub)} + *errors = append(*errors, err) + continue IMS_LOOP + } + } + service.acc = acc + sisPerSubj = append(sisPerSubj, service) + svcSubjects[service.to] = sisPerSubj + services = append(services, service) + } + if stream != nil { + stream.acc = acc + streams = append(streams, stream) + } + } + return streams, services, nil +} + +// Helper to parse an embedded account description for imported services or streams. +func parseAccount(v map[string]any, errors *[]error) (string, string, error) { + var lt token + defer convertPanicToErrorList(<, errors) + + var accountName, subject string + for mk, mv := range v { + tk, mv := unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "account": + accountName = mv.(string) + case "subject": + subject = mv.(string) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + } + } + return accountName, subject, nil +} + +// Parse an export stream or service. +// e.g. +// {stream: "public.>"} # No accounts means public. +// {stream: "synadia.private.>", accounts: [cncf, natsio]} +// {service: "pub.request"} # No accounts means public. +// {service: "pub.special.request", accounts: [nats.io]} +func parseExportStreamOrService(v any, errors *[]error) (*export, *export, error) { + var ( + curStream *export + curService *export + accounts []string + rt ServiceRespType + rtSeen bool + rtToken token + lat *serviceLatency + threshSeen bool + thresh time.Duration + latToken token + lt token + accTokPos uint + atrc bool + atrcSeen bool + atrcToken token + ) + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + vv, ok := v.(map[string]any) + if !ok { + return nil, nil, &configErr{tk, fmt.Sprintf("Export Items should be a map with type entry, got %T", v)} + } + for mk, mv := range vv { + tk, mv := unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "stream": + if curService != nil { + err := &configErr{tk, fmt.Sprintf("Detected stream %q but already saw a service", mv)} + *errors = append(*errors, err) + continue + } + if rtToken != nil { + err := &configErr{rtToken, "Detected response directive on non-service"} + *errors = append(*errors, err) + continue + } + if latToken != nil { + err := &configErr{latToken, "Detected latency directive on non-service"} + *errors = append(*errors, err) + continue + } + if atrcToken != nil { + err := &configErr{atrcToken, "Detected allow_trace directive on non-service"} + *errors = append(*errors, err) + continue + } + mvs, ok := mv.(string) + if !ok { + err := &configErr{tk, fmt.Sprintf("Expected stream name to be string, got %T", mv)} + *errors = append(*errors, err) + continue + } + curStream = &export{sub: mvs} + if accounts != nil { + curStream.accs = accounts + } + case "service": + if curStream != nil { + err := &configErr{tk, fmt.Sprintf("Detected service %q but already saw a stream", mv)} + *errors = append(*errors, err) + continue + } + mvs, ok := mv.(string) + if !ok { + err := &configErr{tk, fmt.Sprintf("Expected service name to be string, got %T", mv)} + *errors = append(*errors, err) + continue + } + curService = &export{sub: mvs} + if accounts != nil { + curService.accs = accounts + } + if rtSeen { + curService.rt = rt + } + if lat != nil { + curService.lat = lat + } + if threshSeen { + curService.rthr = thresh + } + if atrcSeen { + curService.atrc = atrc + } + case "response", "response_type": + if rtSeen { + err := &configErr{tk, "Duplicate response type definition"} + *errors = append(*errors, err) + continue + } + rtSeen = true + rtToken = tk + mvs, ok := mv.(string) + if !ok { + err := &configErr{tk, fmt.Sprintf("Expected response type to be string, got %T", mv)} + *errors = append(*errors, err) + continue + } + switch strings.ToLower(mvs) { + case "single", "singleton": + rt = Singleton + case "stream": + rt = Streamed + case "chunk", "chunked": + rt = Chunked + default: + err := &configErr{tk, fmt.Sprintf("Unknown response type: %q", mvs)} + *errors = append(*errors, err) + continue + } + if curService != nil { + curService.rt = rt + } + if curStream != nil { + err := &configErr{tk, "Detected response directive on non-service"} + *errors = append(*errors, err) + } + case "threshold", "response_threshold", "response_max_time", "response_time": + if threshSeen { + err := &configErr{tk, "Duplicate response threshold detected"} + *errors = append(*errors, err) + continue + } + threshSeen = true + mvs, ok := mv.(string) + if !ok { + err := &configErr{tk, fmt.Sprintf("Expected response threshold to be a parseable time duration, got %T", mv)} + *errors = append(*errors, err) + continue + } + var err error + thresh, err = time.ParseDuration(mvs) + if err != nil { + err := &configErr{tk, fmt.Sprintf("Expected response threshold to be a parseable time duration, got %q", mvs)} + *errors = append(*errors, err) + continue + } + if curService != nil { + curService.rthr = thresh + } + if curStream != nil { + err := &configErr{tk, "Detected response directive on non-service"} + *errors = append(*errors, err) + } + case "accounts": + for _, iv := range mv.([]any) { + _, mv := unwrapValue(iv, <) + accounts = append(accounts, mv.(string)) + } + if curStream != nil { + curStream.accs = accounts + } else if curService != nil { + curService.accs = accounts + } + case "latency": + latToken = tk + var err error + lat, err = parseServiceLatency(tk, mv) + if err != nil { + *errors = append(*errors, err) + continue + } + if curStream != nil { + err = &configErr{tk, "Detected latency directive on non-service"} + *errors = append(*errors, err) + continue + } + if curService != nil { + curService.lat = lat + } + case "account_token_position": + accTokPos = uint(mv.(int64)) + case "allow_trace": + atrcSeen = true + atrcToken = tk + atrc = mv.(bool) + if curStream != nil { + *errors = append(*errors, + &configErr{tk, "Detected allow_trace directive on non-service"}) + continue + } + if curService != nil { + curService.atrc = atrc + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + } + } + if curStream != nil { + curStream.tPos = accTokPos + } + if curService != nil { + curService.tPos = accTokPos + } + return curStream, curService, nil +} + +// parseServiceLatency returns a latency config block. +func parseServiceLatency(root token, v any) (l *serviceLatency, retErr error) { + var lt token + defer convertPanicToError(<, &retErr) + + if subject, ok := v.(string); ok { + return &serviceLatency{ + subject: subject, + sampling: DEFAULT_SERVICE_LATENCY_SAMPLING, + }, nil + } + + latency, ok := v.(map[string]any) + if !ok { + return nil, &configErr{token: root, + reason: fmt.Sprintf("Expected latency entry to be a map/struct or string, got %T", v)} + } + + sl := serviceLatency{ + sampling: DEFAULT_SERVICE_LATENCY_SAMPLING, + } + + // Read sampling value. + if v, ok := latency["sampling"]; ok { + tk, v := unwrapValue(v, <) + header := false + var sample int64 + switch vv := v.(type) { + case int64: + // Sample is an int, like 50. + sample = vv + case string: + // Sample is a string, like "50%". + if strings.ToLower(strings.TrimSpace(vv)) == "headers" { + header = true + sample = 0 + break + } + s := strings.TrimSuffix(vv, "%") + n, err := strconv.Atoi(s) + if err != nil { + return nil, &configErr{token: tk, + reason: fmt.Sprintf("Failed to parse latency sample: %v", err)} + } + sample = int64(n) + default: + return nil, &configErr{token: tk, + reason: fmt.Sprintf("Expected latency sample to be a string or map/struct, got %T", v)} + } + if !header { + if sample < 1 || sample > 100 { + return nil, &configErr{token: tk, + reason: ErrBadSampling.Error()} + } + } + + sl.sampling = int8(sample) + } + + // Read subject value. + v, ok = latency["subject"] + if !ok { + return nil, &configErr{token: root, + reason: "Latency subject required, but missing"} + } + + tk, v := unwrapValue(v, <) + subject, ok := v.(string) + if !ok { + return nil, &configErr{token: tk, + reason: fmt.Sprintf("Expected latency subject to be a string, got %T", subject)} + } + sl.subject = subject + + return &sl, nil +} + +// Parse an import stream or service. +// e.g. +// {stream: {account: "synadia", subject:"public.synadia"}, prefix: "imports.synadia"} +// {stream: {account: "synadia", subject:"synadia.private.*"}} +// {service: {account: "synadia", subject: "pub.special.request"}, to: "synadia.request"} +func parseImportStreamOrService(v any, errors *[]error) (*importStream, *importService, error) { + var ( + curStream *importStream + curService *importService + pre, to string + share bool + lt token + atrc bool + atrcSeen bool + atrcToken token + ) + defer convertPanicToErrorList(<, errors) + + tk, mv := unwrapValue(v, <) + vv, ok := mv.(map[string]any) + if !ok { + return nil, nil, &configErr{tk, fmt.Sprintf("Import Items should be a map with type entry, got %T", mv)} + } + for mk, mv := range vv { + tk, mv := unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "stream": + if curService != nil { + err := &configErr{tk, "Detected stream but already saw a service"} + *errors = append(*errors, err) + continue + } + ac, ok := mv.(map[string]any) + if !ok { + err := &configErr{tk, fmt.Sprintf("Stream entry should be an account map, got %T", mv)} + *errors = append(*errors, err) + continue + } + // Make sure this is a map with account and subject + accountName, subject, err := parseAccount(ac, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + if accountName == _EMPTY_ || subject == _EMPTY_ { + err := &configErr{tk, "Expect an account name and a subject"} + *errors = append(*errors, err) + continue + } + curStream = &importStream{an: accountName, sub: subject} + if to != _EMPTY_ { + curStream.to = to + } + if pre != _EMPTY_ { + curStream.pre = pre + } + if atrcSeen { + curStream.atrc = atrc + } + case "service": + if curStream != nil { + err := &configErr{tk, "Detected service but already saw a stream"} + *errors = append(*errors, err) + continue + } + if atrcToken != nil { + err := &configErr{atrcToken, "Detected allow_trace directive on a non-stream"} + *errors = append(*errors, err) + continue + } + ac, ok := mv.(map[string]any) + if !ok { + err := &configErr{tk, fmt.Sprintf("Service entry should be an account map, got %T", mv)} + *errors = append(*errors, err) + continue + } + // Make sure this is a map with account and subject + accountName, subject, err := parseAccount(ac, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + if accountName == _EMPTY_ || subject == _EMPTY_ { + err := &configErr{tk, "Expect an account name and a subject"} + *errors = append(*errors, err) + continue + } + curService = &importService{an: accountName, sub: subject} + if to != _EMPTY_ { + curService.to = to + } else { + curService.to = subject + } + curService.share = share + case "prefix": + pre = mv.(string) + if curStream != nil { + curStream.pre = pre + } + case "to": + to = mv.(string) + if curService != nil { + curService.to = to + } + if curStream != nil { + curStream.to = to + if curStream.pre != _EMPTY_ { + err := &configErr{tk, "Stream import can not have a 'prefix' and a 'to' property"} + *errors = append(*errors, err) + continue + } + } + case "share": + share = mv.(bool) + if curService != nil { + curService.share = share + } + case "allow_trace": + if curService != nil { + err := &configErr{tk, "Detected allow_trace directive on a non-stream"} + *errors = append(*errors, err) + continue + } + atrcSeen = true + atrc = mv.(bool) + atrcToken = tk + if curStream != nil { + curStream.atrc = atrc + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + } + + } + return curStream, curService, nil +} + +// Apply permission defaults to users/nkeyuser that don't have their own. +func applyDefaultPermissions(users []*User, nkeys []*NkeyUser, defaultP *Permissions) { + if defaultP == nil { + return + } + for _, user := range users { + if user.Permissions == nil { + user.Permissions = defaultP + } + } + for _, user := range nkeys { + if user.Permissions == nil { + user.Permissions = defaultP + } + } +} + +// Helper function to parse Authorization configs. +func parseAuthorization(v any, errors, warnings *[]error) (*authorization, error) { + var ( + am map[string]any + tk token + lt token + auth = &authorization{} + ) + defer convertPanicToErrorList(<, errors) + + _, v = unwrapValue(v, <) + am = v.(map[string]any) + for mk, mv := range am { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "user", "username": + auth.user = mv.(string) + case "pass", "password": + auth.pass = mv.(string) + case "token": + auth.token = mv.(string) + case "timeout": + at := float64(0) + switch mv := mv.(type) { + case int64: + at = float64(mv) + case float64: + at = mv + case string: + d, err := time.ParseDuration(mv) + if err != nil { + return nil, &configErr{tk, fmt.Sprintf("error parsing authorization config, 'timeout' %s", err)} + } + at = d.Seconds() + default: + return nil, &configErr{tk, "error parsing authorization config, 'timeout' wrong type"} + } + if at > (60 * time.Second).Seconds() { + reason := fmt.Sprintf("timeout of %v (%f seconds) is high, consider keeping it under 60 seconds. possibly caused by unquoted duration; use '1m' instead of 1m, for example", mv, at) + *warnings = append(*warnings, &configWarningErr{field: mk, configErr: configErr{token: tk, reason: reason}}) + } + auth.timeout = at + case "users": + nkeys, users, err := parseUsers(tk, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + auth.users = users + auth.nkeys = nkeys + case "default_permission", "default_permissions", "permissions": + permissions, err := parseUserPermissions(tk, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + auth.defaultPermissions = permissions + case "auth_callout", "auth_hook": + if fips140.Enabled() { + *errors = append(*errors, fmt.Errorf("'auth_callout' cannot be configured in FIPS-140 mode")) + continue + } + ac, err := parseAuthCallout(tk, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + auth.callout = ac + case "proxy_required": + auth.proxyRequired = mv.(bool) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + continue + } + + applyDefaultPermissions(auth.users, auth.nkeys, auth.defaultPermissions) + } + return auth, nil +} + +// Helper function to parse multiple users array with optional permissions. +func parseUsers(mv any, errors *[]error) ([]*NkeyUser, []*User, error) { + var ( + tk token + lt token + keys []*NkeyUser + users = []*User{} + ) + defer convertPanicToErrorList(<, errors) + tk, mv = unwrapValue(mv, <) + + // Make sure we have an array + uv, ok := mv.([]any) + if !ok { + return nil, nil, &configErr{tk, fmt.Sprintf("Expected users field to be an array, got %v", mv)} + } + for _, u := range uv { + tk, u = unwrapValue(u, <) + + // Check its a map/struct + um, ok := u.(map[string]any) + if !ok { + err := &configErr{tk, fmt.Sprintf("Expected user entry to be a map/struct, got %v", u)} + *errors = append(*errors, err) + continue + } + + var ( + user = &User{} + nkey = &NkeyUser{} + perms *Permissions + err error + ) + for k, v := range um { + // Also needs to unwrap first + tk, v = unwrapValue(v, <) + + switch strings.ToLower(k) { + case "nkey": + nkey.Nkey = v.(string) + case "user", "username": + user.Username = v.(string) + case "pass", "password": + user.Password = v.(string) + case "permission", "permissions", "authorization": + perms, err = parseUserPermissions(tk, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + case "allowed_connection_types", "connection_types", "clients": + cts := parseAllowedConnectionTypes(tk, <, v, errors) + nkey.AllowedConnectionTypes = cts + user.AllowedConnectionTypes = cts + case "proxy_required": + nkey.ProxyRequired = v.(bool) + user.ProxyRequired = v.(bool) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + // Place perms if we have them. + if perms != nil { + // nkey takes precedent. + if nkey.Nkey != _EMPTY_ { + nkey.Permissions = perms + } else { + user.Permissions = perms + } + } + + // Check to make sure we have at least an nkey or username defined. + if nkey.Nkey == _EMPTY_ && user.Username == _EMPTY_ { + return nil, nil, &configErr{tk, "User entry requires a user"} + } else if nkey.Nkey != _EMPTY_ { + // Make sure the nkey a proper public nkey for a user.. + if !nkeys.IsValidPublicUserKey(nkey.Nkey) { + return nil, nil, &configErr{tk, "Not a valid public nkey for a user"} + } + // If we have user or password defined here that is an error. + if user.Username != _EMPTY_ || user.Password != _EMPTY_ { + return nil, nil, &configErr{tk, "Nkey users do not take usernames or passwords"} + } + keys = append(keys, nkey) + } else { + users = append(users, user) + } + } + return keys, users, nil +} + +func parseAllowedConnectionTypes(tk token, lt *token, mv any, errors *[]error) map[string]struct{} { + cts, err := parseStringArray("allowed connection types", tk, lt, mv, errors) + // If error, it has already been added to the `errors` array, simply return + if err != nil { + return nil + } + m, err := convertAllowedConnectionTypes(cts) + if err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + } + return m +} + +// Helper function to parse auth callouts. +func parseAuthCallout(mv any, errors *[]error) (*AuthCallout, error) { + var ( + tk token + lt token + ac = &AuthCallout{} + ) + defer convertPanicToErrorList(<, errors) + + tk, mv = unwrapValue(mv, <) + pm, ok := mv.(map[string]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("Expected authorization callout to be a map/struct, got %+v", mv)} + } + for k, v := range pm { + tk, mv = unwrapValue(v, <) + + switch strings.ToLower(k) { + case "issuer": + ac.Issuer = mv.(string) + if !nkeys.IsValidPublicAccountKey(ac.Issuer) { + return nil, &configErr{tk, fmt.Sprintf("Expected callout user to be a valid public account nkey, got %q", ac.Issuer)} + } + case "account", "acc": + ac.Account = mv.(string) + case "auth_users", "users": + aua, ok := mv.([]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("Expected auth_users field to be an array, got %T", v)} + } + for _, uv := range aua { + _, uv = unwrapValue(uv, <) + ac.AuthUsers = append(ac.AuthUsers, uv.(string)) + } + case "xkey", "key": + ac.XKey = mv.(string) + if !nkeys.IsValidPublicCurveKey(ac.XKey) { + return nil, &configErr{tk, fmt.Sprintf("Expected callout xkey to be a valid public xkey, got %q", ac.XKey)} + } + case "allowed_accounts": + aua, ok := mv.([]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("Expected allowed accounts field to be an array, got %T", v)} + } + for _, uv := range aua { + _, uv = unwrapValue(uv, <) + ac.AllowedAccounts = append(ac.AllowedAccounts, uv.(string)) + } + default: + if !tk.IsUsedVariable() { + err := &configErr{tk, fmt.Sprintf("Unknown field %q parsing authorization callout", k)} + *errors = append(*errors, err) + } + } + } + // Make sure we have all defined. All fields are required. + // If no account specified, selet $G. + if ac.Account == _EMPTY_ { + ac.Account = globalAccountName + } + if ac.Issuer == _EMPTY_ { + return nil, &configErr{tk, "Authorization callouts require an issuer to be specified"} + } + if len(ac.AuthUsers) == 0 { + return nil, &configErr{tk, "Authorization callouts require authorized users to be specified"} + } + return ac, nil +} + +// Helper function to parse user/account permissions +func parseUserPermissions(mv any, errors *[]error) (*Permissions, error) { + var ( + tk token + lt token + p = &Permissions{} + ) + defer convertPanicToErrorList(<, errors) + + tk, mv = unwrapValue(mv, <) + pm, ok := mv.(map[string]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("Expected permissions to be a map/struct, got %+v", mv)} + } + for k, v := range pm { + tk, mv = unwrapValue(v, <) + + switch strings.ToLower(k) { + // For routes: + // Import is Publish + // Export is Subscribe + case "pub", "publish", "import": + perms, err := parseVariablePermissions(mv, errors, false) + if err != nil { + *errors = append(*errors, err) + continue + } + p.Publish = perms + case "sub", "subscribe", "export": + perms, err := parseVariablePermissions(mv, errors, true) + if err != nil { + *errors = append(*errors, err) + continue + } + p.Subscribe = perms + case "publish_allow_responses", "allow_responses": + rp := &ResponsePermission{ + MaxMsgs: DEFAULT_ALLOW_RESPONSE_MAX_MSGS, + Expires: DEFAULT_ALLOW_RESPONSE_EXPIRATION, + } + // Try boolean first + responses, ok := mv.(bool) + if ok { + if responses { + p.Response = rp + } + } else { + p.Response = parseAllowResponses(v, errors) + } + if p.Response != nil { + if p.Publish == nil { + p.Publish = &SubjectPermission{} + } + if p.Publish.Allow == nil { + // We turn off the blanket allow statement. + p.Publish.Allow = []string{} + } + } + default: + if !tk.IsUsedVariable() { + err := &configErr{tk, fmt.Sprintf("Unknown field %q parsing permissions", k)} + *errors = append(*errors, err) + } + } + } + return p, nil +} + +// Top level parser for authorization configurations. +func parseVariablePermissions(v any, errors *[]error, allowQueue bool) (*SubjectPermission, error) { + switch vv := v.(type) { + case map[string]any: + // New style with allow and/or deny properties. + return parseSubjectPermission(vv, errors, allowQueue) + default: + // Old style + return parseOldPermissionStyle(v, errors, allowQueue) + } +} + +// Helper function to parse subject singletons and/or arrays +func parsePermSubjects(v any, errors *[]error, allowQueue bool) ([]string, error) { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + + var subjects []string + switch vv := v.(type) { + case string: + subjects = append(subjects, vv) + case []string: + subjects = vv + case []any: + for _, i := range vv { + tk, i := unwrapValue(i, <) + + subject, ok := i.(string) + if !ok { + return nil, &configErr{tk, "Subject in permissions array cannot be cast to string"} + } + subjects = append(subjects, subject) + } + default: + return nil, &configErr{tk, fmt.Sprintf("Expected subject permissions to be a subject, or array of subjects, got %T", v)} + } + if err := checkPermSubjectArray(subjects, allowQueue); err != nil { + return nil, &configErr{tk, err.Error()} + } + return subjects, nil +} + +// Helper function to parse a ResponsePermission. +func parseAllowResponses(v any, errors *[]error) *ResponsePermission { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + // Check if this is a map. + pm, ok := v.(map[string]any) + if !ok { + err := &configErr{tk, "error parsing response permissions, expected a boolean or a map"} + *errors = append(*errors, err) + return nil + } + + rp := &ResponsePermission{ + MaxMsgs: DEFAULT_ALLOW_RESPONSE_MAX_MSGS, + Expires: DEFAULT_ALLOW_RESPONSE_EXPIRATION, + } + + for k, v := range pm { + tk, v = unwrapValue(v, <) + switch strings.ToLower(k) { + case "max", "max_msgs", "max_messages", "max_responses": + max := int(v.(int64)) + // Negative values are accepted (mean infinite), and 0 + // means default value (set above). + if max != 0 { + rp.MaxMsgs = max + } + case "expires", "expiration", "ttl": + wd, ok := v.(string) + if ok { + ttl, err := time.ParseDuration(wd) + if err != nil { + err := &configErr{tk, fmt.Sprintf("error parsing expires: %v", err)} + *errors = append(*errors, err) + return nil + } + // Negative values are accepted (mean infinite), and 0 + // means default value (set above). + if ttl != 0 { + rp.Expires = ttl + } + } else { + err := &configErr{tk, "error parsing expires, not a duration string"} + *errors = append(*errors, err) + return nil + } + default: + if !tk.IsUsedVariable() { + err := &configErr{tk, fmt.Sprintf("Unknown field %q parsing permissions", k)} + *errors = append(*errors, err) + } + } + } + return rp +} + +// Helper function to parse old style authorization configs. +func parseOldPermissionStyle(v any, errors *[]error, allowQueue bool) (*SubjectPermission, error) { + subjects, err := parsePermSubjects(v, errors, allowQueue) + if err != nil { + return nil, err + } + return &SubjectPermission{Allow: subjects}, nil +} + +// Helper function to parse new style authorization into a SubjectPermission with Allow and Deny. +func parseSubjectPermission(v any, errors *[]error, allowQueue bool) (*SubjectPermission, error) { + var lt token + defer convertPanicToErrorList(<, errors) + + m := v.(map[string]any) + if len(m) == 0 { + return nil, nil + } + p := &SubjectPermission{} + for k, v := range m { + tk, _ := unwrapValue(v, <) + switch strings.ToLower(k) { + case "allow": + subjects, err := parsePermSubjects(tk, errors, allowQueue) + if err != nil { + *errors = append(*errors, err) + continue + } + p.Allow = subjects + case "deny": + subjects, err := parsePermSubjects(tk, errors, allowQueue) + if err != nil { + *errors = append(*errors, err) + continue + } + p.Deny = subjects + default: + if !tk.IsUsedVariable() { + err := &configErr{tk, fmt.Sprintf("Unknown field name %q parsing subject permissions, only 'allow' or 'deny' are permitted", k)} + *errors = append(*errors, err) + } + } + } + return p, nil +} + +// Helper function to validate permissions subjects. +func checkPermSubjectArray(sa []string, allowQueue bool) error { + for _, s := range sa { + if !IsValidSubject(s) { + if !allowQueue { + return fmt.Errorf("subject %q is not a valid subject", s) + } + // Check here if this is a queue group qualified subject. + elements := strings.Fields(s) + if len(elements) != 2 { + return fmt.Errorf("subject %q is not a valid subject", s) + } else if !IsValidSubject(elements[0]) { + return fmt.Errorf("subject %q is not a valid subject", elements[0]) + } else if !IsValidSubject(elements[1]) { + return fmt.Errorf("queue %q is not a valid queue", elements[1]) + } + } + } + return nil +} + +// PrintTLSHelpAndDie prints TLS usage and exits. +func PrintTLSHelpAndDie() { + fmt.Printf("%s", tlsUsage) + for k := range cipherMap { + fmt.Printf(" %s\n", k) + } + fmt.Printf("\nAvailable curve preferences include:\n") + for k := range curvePreferenceMap { + fmt.Printf(" %s\n", k) + } + if runtime.GOOS == "windows" { + fmt.Printf("%s\n", certstore.Usage) + } + fmt.Printf("%s", certidp.OCSPPeerUsage) + fmt.Printf("%s", OCSPResponseCacheUsage) + os.Exit(0) +} + +func parseCipher(cipherName string) (*tls.CipherSuite, error) { + cipher, exists := cipherMap[cipherName] + if !exists { + return nil, fmt.Errorf("unrecognized cipher %s", cipherName) + } + return cipher, nil +} + +func parseCurvePreferences(curveName string) (tls.CurveID, error) { + curve, exists := curvePreferenceMap[curveName] + if !exists { + return 0, fmt.Errorf("unrecognized curve preference %s", curveName) + } + return curve, nil +} + +func parseTLSVersion(v any) (uint16, error) { + var tlsVersionNumber uint16 + switch v := v.(type) { + case string: + n, err := tlsVersionFromString(v) + if err != nil { + return 0, err + } + tlsVersionNumber = n + default: + return 0, fmt.Errorf("'min_version' wrong type: %v", v) + } + if tlsVersionNumber < tls.VersionTLS12 { + return 0, fmt.Errorf("unsupported TLS version: %s", tls.VersionName(tlsVersionNumber)) + } + return tlsVersionNumber, nil +} + +// Helper function to parse TLS configs. +func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) { + var ( + tlsm map[string]any + tc = TLSConfigOpts{} + lt token + ics []*tls.CipherSuite // Insecure ciphers found + ) + defer convertPanicToError(<, &retErr) + + tk, v := unwrapValue(v, <) + tlsm = v.(map[string]any) + for mk, mv := range tlsm { + tk, mv := unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "cert_file": + certFile, ok := mv.(string) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'cert_file' to be filename"} + } + tc.CertFile = certFile + case "key_file": + keyFile, ok := mv.(string) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'key_file' to be filename"} + } + tc.KeyFile = keyFile + case "ca_file": + caFile, ok := mv.(string) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'ca_file' to be filename"} + } + tc.CaFile = caFile + case "insecure": + insecure, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'insecure' to be a boolean"} + } + tc.Insecure = insecure + case "verify": + verify, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'verify' to be a boolean"} + } + tc.Verify = verify + case "verify_and_map": + verify, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'verify_and_map' to be a boolean"} + } + if verify { + tc.Verify = verify + } + tc.Map = verify + case "verify_cert_and_check_known_urls": + verify, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'verify_cert_and_check_known_urls' to be a boolean"} + } + if verify && isClientCtx { + return nil, &configErr{tk, "verify_cert_and_check_known_urls not supported in this context"} + } + if verify { + tc.Verify = verify + } + tc.TLSCheckKnownURLs = verify + case "allow_insecure_cipher_suites": + allow, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'allow_insecure_cipher_suites' to be a boolean"} + } + tc.AllowInsecureCiphers = allow + case "cipher_suites": + ra := mv.([]any) + if len(ra) == 0 { + return nil, &configErr{tk, "error parsing tls config, 'cipher_suites' cannot be empty"} + } + tc.Ciphers = make([]uint16, 0, len(ra)) + for _, r := range ra { + tk, r := unwrapValue(r, <) + cipher, err := parseCipher(r.(string)) + if err != nil { + return nil, &configErr{tk, err.Error()} + } + tc.Ciphers = append(tc.Ciphers, cipher.ID) + if cipher.Insecure { + ics = append(ics, cipher) + } + } + case "curve_preferences": + ra := mv.([]any) + if len(ra) == 0 { + return nil, &configErr{tk, "error parsing tls config, 'curve_preferences' cannot be empty"} + } + tc.CurvePreferences = make([]tls.CurveID, 0, len(ra)) + for _, r := range ra { + tk, r := unwrapValue(r, <) + cps, err := parseCurvePreferences(r.(string)) + if err != nil { + return nil, &configErr{tk, err.Error()} + } + tc.CurvePreferences = append(tc.CurvePreferences, cps) + } + case "timeout": + at := float64(0) + switch mv := mv.(type) { + case int64: + at = float64(mv) + case float64: + at = mv + case string: + d, err := time.ParseDuration(mv) + if err != nil { + return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, 'timeout' %s", err)} + } + at = d.Seconds() + default: + return nil, &configErr{tk, "error parsing tls config, 'timeout' wrong type"} + } + tc.Timeout = at + case "connection_rate_limit": + at := int64(0) + switch mv := mv.(type) { + case int64: + at = mv + default: + return nil, &configErr{tk, "error parsing tls config, 'connection_rate_limit' wrong type"} + } + tc.RateLimit = at + case "pinned_certs": + ra, ok := mv.([]any) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'pinned_certs' to be a list of hex-encoded sha256 of DER encoded SubjectPublicKeyInfo"} + } + if len(ra) != 0 { + wl := PinnedCertSet{} + re := regexp.MustCompile("^[A-Fa-f0-9]{64}$") + for _, r := range ra { + tk, r := unwrapValue(r, <) + entry := strings.ToLower(r.(string)) + if !re.MatchString(entry) { + return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, 'pinned_certs' key %s does not look like hex-encoded sha256 of DER encoded SubjectPublicKeyInfo", entry)} + } + wl[entry] = struct{}{} + } + tc.PinnedCerts = wl + } + case "cert_store": + certStore, ok := mv.(string) + if !ok || certStore == _EMPTY_ { + return nil, &configErr{tk, certstore.ErrBadCertStoreField.Error()} + } + certStoreType, err := certstore.ParseCertStore(certStore) + if err != nil { + return nil, &configErr{tk, err.Error()} + } + tc.CertStore = certStoreType + case "cert_match_by": + certMatchBy, ok := mv.(string) + if !ok || certMatchBy == _EMPTY_ { + return nil, &configErr{tk, certstore.ErrBadCertMatchByField.Error()} + } + certMatchByType, err := certstore.ParseCertMatchBy(certMatchBy) + if err != nil { + return nil, &configErr{tk, err.Error()} + } + tc.CertMatchBy = certMatchByType + case "cert_match": + certMatch, ok := mv.(string) + if !ok || certMatch == _EMPTY_ { + return nil, &configErr{tk, certstore.ErrBadCertMatchField.Error()} + } + tc.CertMatch = certMatch + case "ca_certs_match": + rv := []string{} + switch mv := mv.(type) { + case string: + rv = append(rv, mv) + case []string: + rv = append(rv, mv...) + case []any: + for _, t := range mv { + if token, ok := t.(token); ok { + if ts, ok := token.Value().(string); ok { + rv = append(rv, ts) + continue + } else { + return nil, &configErr{tk, fmt.Sprintf("error parsing ca_cert_match: unsupported type %T where string is expected", token)} + } + } else { + return nil, &configErr{tk, fmt.Sprintf("error parsing ca_cert_match: unsupported type %T", t)} + } + } + } + tc.CaCertsMatch = rv + case "handshake_first", "first", "immediate": + switch mv := mv.(type) { + case bool: + tc.HandshakeFirst = mv + case string: + switch strings.ToLower(mv) { + case "true", "on": + tc.HandshakeFirst = true + case "false", "off": + tc.HandshakeFirst = false + case "auto", "auto_fallback": + tc.HandshakeFirst = true + tc.FallbackDelay = DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY + default: + // Check to see if this is a duration. + if dur, err := time.ParseDuration(mv); err == nil { + tc.HandshakeFirst = true + tc.FallbackDelay = dur + break + } + return nil, &configErr{tk, fmt.Sprintf("field %q's value %q is invalid", mk, mv)} + } + default: + return nil, &configErr{tk, fmt.Sprintf("field %q should be a boolean or a string, got %T", mk, mv)} + } + case "cert_match_skip_invalid": + certMatchSkipInvalid, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, certstore.ErrBadCertMatchSkipInvalidField.Error()} + } + tc.CertMatchSkipInvalid = certMatchSkipInvalid + case "ocsp_peer": + switch vv := mv.(type) { + case bool: + pc := certidp.NewOCSPPeerConfig() + if vv { + // Set enabled + pc.Verify = true + tc.OCSPPeerConfig = pc + } else { + // Set disabled + pc.Verify = false + tc.OCSPPeerConfig = pc + } + case map[string]any: + pc, err := parseOCSPPeer(mv) + if err != nil { + return nil, &configErr{tk, err.Error()} + } + tc.OCSPPeerConfig = pc + default: + return nil, &configErr{tk, fmt.Sprintf("error parsing ocsp peer config: unsupported type %T", v)} + } + case "certs", "certificates": + certs, ok := mv.([]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("error parsing certificates config: unsupported type %T", v)} + } + tc.Certificates = make([]*TLSCertPairOpt, len(certs)) + for i, v := range certs { + tk, vv := unwrapValue(v, <) + pair, ok := vv.(map[string]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("error parsing certificates config: unsupported type %T", vv)} + } + certPair := &TLSCertPairOpt{} + for k, v := range pair { + tk, vv = unwrapValue(v, <) + file, ok := vv.(string) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("error parsing certificates config: unsupported type %T", vv)} + } + switch k { + case "cert_file": + certPair.CertFile = file + case "key_file": + certPair.KeyFile = file + default: + return nil, &configErr{tk, fmt.Sprintf("error parsing tls certs config, unknown field %q", k)} + } + } + if certPair.CertFile == _EMPTY_ || certPair.KeyFile == _EMPTY_ { + return nil, &configErr{tk, "error parsing certificates config: both 'cert_file' and 'cert_key' options are required"} + } + tc.Certificates[i] = certPair + } + case "min_version": + minVersion, err := parseTLSVersion(mv) + if err != nil { + return nil, &configErr{tk, fmt.Sprintf("error parsing tls config: %v", err)} + } + tc.MinVersion = minVersion + default: + return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, unknown field %q", mk)} + } + } + if len(tc.Certificates) > 0 && tc.CertFile != _EMPTY_ { + return nil, &configErr{tk, "error parsing tls config, cannot combine 'cert_file' option with 'certs' option"} + } + + // If cipher suites were not specified then use the defaults + if tc.Ciphers == nil { + tc.Ciphers = defaultCipherSuites() + } + + // If curve preferences were not specified, then use the defaults + if tc.CurvePreferences == nil { + tc.CurvePreferences = defaultCurvePreferences() + } + + // If we don't allow insecure ciphers, and yet some were configured, then we + // should error. + if !tc.AllowInsecureCiphers && len(ics) > 0 { + names := make([]string, 0, len(ics)) + for _, ic := range ics { + names = append(names, ic.Name) + } + return nil, &configErr{tk, fmt.Sprintf("insecure cipher suites configured without 'allow_insecure_cipher_suites' option set: %s", strings.Join(names, ", "))} + } + + return &tc, nil +} + +func parseSimpleAuth(v any, errors *[]error) *authorization { + var ( + am map[string]any + tk token + lt token + auth = &authorization{} + ) + defer convertPanicToErrorList(<, errors) + + _, v = unwrapValue(v, <) + am = v.(map[string]any) + for mk, mv := range am { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "user", "username": + auth.user = mv.(string) + case "pass", "password": + auth.pass = mv.(string) + case "token": + auth.token = mv.(string) + case "timeout": + at := float64(1) + switch mv := mv.(type) { + case int64: + at = float64(mv) + case float64: + at = mv + } + auth.timeout = at + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + continue + } + } + return auth +} + +func parseStringArray(fieldName string, tk token, lt *token, mv any, errors *[]error) ([]string, error) { + switch mv := mv.(type) { + case string: + return []string{mv}, nil + case []any: + strs := make([]string, 0, len(mv)) + for _, val := range mv { + tk, val = unwrapValue(val, lt) + if str, ok := val.(string); ok { + strs = append(strs, str) + } else { + err := &configErr{tk, fmt.Sprintf("error parsing %s: unsupported type in array %T", fieldName, val)} + *errors = append(*errors, err) + continue + } + } + return strs, nil + default: + err := &configErr{tk, fmt.Sprintf("error parsing %s: unsupported type %T", fieldName, mv)} + *errors = append(*errors, err) + return nil, err + } +} + +func parseWebsocket(v any, o *Options, errors *[]error, warnings *[]error) error { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + gm, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected websocket to be a map, got %T", v)} + } + for mk, mv := range gm { + // Again, unwrap token value if line check is required. + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "listen": + hp, err := parseListen(mv) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + o.Websocket.Host = hp.host + o.Websocket.Port = hp.port + case "port": + o.Websocket.Port = int(mv.(int64)) + case "host", "net": + o.Websocket.Host = mv.(string) + case "advertise": + o.Websocket.Advertise = mv.(string) + case "no_tls": + o.Websocket.NoTLS = mv.(bool) + case "tls": + tc, err := parseTLS(tk, true) + if err != nil { + *errors = append(*errors, err) + continue + } + if o.Websocket.TLSConfig, err = GenTLSConfig(tc); err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + o.Websocket.TLSMap = tc.Map + o.Websocket.TLSPinnedCerts = tc.PinnedCerts + o.Websocket.tlsConfigOpts = tc + case "same_origin": + o.Websocket.SameOrigin = mv.(bool) + case "allowed_origins", "allowed_origin", "allow_origins", "allow_origin", "origins", "origin": + o.Websocket.AllowedOrigins, _ = parseStringArray("allowed origins", tk, <, mv, errors) + case "handshake_timeout": + ht := time.Duration(0) + switch mv := mv.(type) { + case int64: + ht = time.Duration(mv) * time.Second + case string: + var err error + ht, err = time.ParseDuration(mv) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + default: + err := &configErr{tk, fmt.Sprintf("error parsing handshake timeout: unsupported type %T", mv)} + *errors = append(*errors, err) + } + o.Websocket.HandshakeTimeout = ht + case "compress", "compression": + o.Websocket.Compression = mv.(bool) + case "authorization", "authentication": + auth := parseSimpleAuth(tk, errors) + o.Websocket.Username = auth.user + o.Websocket.Password = auth.pass + o.Websocket.Token = auth.token + o.Websocket.AuthTimeout = auth.timeout + case "jwt_cookie": + o.Websocket.JWTCookie = mv.(string) + case "user_cookie": + o.Websocket.UsernameCookie = mv.(string) + case "pass_cookie": + o.Websocket.PasswordCookie = mv.(string) + case "token_cookie": + o.Websocket.TokenCookie = mv.(string) + case "no_auth_user": + o.Websocket.NoAuthUser = mv.(string) + case "headers": + m, ok := mv.(map[string]any) + if !ok { + err := &configErr{tk, fmt.Sprintf("error parsing headers: unsupported type %T", mv)} + *errors = append(*errors, err) + continue + } + o.Websocket.Headers = make(map[string]string) + for key, val := range m { + tk, val = unwrapValue(val, <) + if headerValue, ok := val.(string); !ok { + *errors = append(*errors, &configErr{tk, fmt.Sprintf("error parsing header key %s: unsupported type %T", key, val)}) + continue + } else { + o.Websocket.Headers[key] = headerValue + } + } + case "ping_interval": + o.Websocket.PingInterval = parseDuration("ping_interval", tk, mv, errors, warnings) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +func parseMQTT(v any, o *Options, errors *[]error, warnings *[]error) error { + var lt token + defer convertPanicToErrorList(<, errors) + + tk, v := unwrapValue(v, <) + gm, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected mqtt to be a map, got %T", v)} + } + for mk, mv := range gm { + // Again, unwrap token value if line check is required. + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "listen": + hp, err := parseListen(mv) + if err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + o.MQTT.Host = hp.host + o.MQTT.Port = hp.port + case "port": + o.MQTT.Port = int(mv.(int64)) + case "host", "net": + o.MQTT.Host = mv.(string) + case "tls": + tc, err := parseTLS(tk, true) + if err != nil { + *errors = append(*errors, err) + continue + } + if o.MQTT.TLSConfig, err = GenTLSConfig(tc); err != nil { + err := &configErr{tk, err.Error()} + *errors = append(*errors, err) + continue + } + o.MQTT.TLSTimeout = tc.Timeout + o.MQTT.TLSMap = tc.Map + o.MQTT.TLSPinnedCerts = tc.PinnedCerts + o.MQTT.tlsConfigOpts = tc + case "authorization", "authentication": + auth := parseSimpleAuth(tk, errors) + o.MQTT.Username = auth.user + o.MQTT.Password = auth.pass + o.MQTT.Token = auth.token + o.MQTT.AuthTimeout = auth.timeout + case "no_auth_user": + o.MQTT.NoAuthUser = mv.(string) + case "ack_wait", "ackwait": + o.MQTT.AckWait = parseDuration("ack_wait", tk, mv, errors, warnings) + case "js_api_timeout", "api_timeout": + o.MQTT.JSAPITimeout = parseDuration("js_api_timeout", tk, mv, errors, warnings) + case "max_ack_pending", "max_pending", "max_inflight": + tmp := int(mv.(int64)) + if tmp < 0 || tmp > 0xFFFF { + err := &configErr{tk, fmt.Sprintf("invalid value %v, should in [0..%d] range", tmp, 0xFFFF)} + *errors = append(*errors, err) + } else { + o.MQTT.MaxAckPending = uint16(tmp) + } + case "js_domain": + o.MQTT.JsDomain = mv.(string) + case "stream_replicas": + o.MQTT.StreamReplicas = int(mv.(int64)) + case "consumer_replicas": + err := &configWarningErr{ + field: mk, + configErr: configErr{ + token: tk, + reason: `consumer replicas setting ignored in this server version`, + }, + } + *warnings = append(*warnings, err) + case "consumer_memory_storage": + o.MQTT.ConsumerMemoryStorage = mv.(bool) + case "consumer_inactive_threshold", "consumer_auto_cleanup": + o.MQTT.ConsumerInactiveThreshold = parseDuration("consumer_inactive_threshold", tk, mv, errors, warnings) + + case "reject_qos2_publish": + o.MQTT.rejectQoS2Pub = mv.(bool) + case "downgrade_qos2_subscribe": + o.MQTT.downgradeQoS2Sub = mv.(bool) + + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +func parseProxies(mv any, errors *[]error) (*ProxiesConfig, error) { + var ( + tk token + lt token + proxies = &ProxiesConfig{} + ) + defer convertPanicToErrorList(<, errors) + + tk, mv = unwrapValue(mv, <) + pm, ok := mv.(map[string]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("expected proxies to be a map/struct, got %T", mv)} + } + for mk, mv := range pm { + tk, _ = unwrapValue(mv, <) + + switch strings.ToLower(mk) { + case "trusted": + trusted, err := parseProxiesTrusted(tk, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + proxies.Trusted = trusted + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + } + } + return proxies, nil +} + +func parseProxiesTrusted(mv any, errors *[]error) ([]*ProxyConfig, error) { + var ( + tk token + lt token + trusted []*ProxyConfig + ) + defer convertPanicToErrorList(<, errors) + + tk, mv = unwrapValue(mv, <) + ta, ok := mv.([]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("expected proxies' trusted field to be an array, got %T", mv)} + } + for _, t := range ta { + tk, t = unwrapValue(t, <) + // Check its a map/struct + tm, ok := t.(map[string]any) + if !ok { + err := &configErr{tk, fmt.Sprintf("expected proxies' trusted entry to be a map/struct, got %T", t)} + *errors = append(*errors, err) + continue + } + proxy := &ProxyConfig{} + for k, v := range tm { + tk, v = unwrapValue(v, <) + switch strings.ToLower(k) { + case "key", "public_key": + proxy.Key = v.(string) + if !nkeys.IsValidPublicKey(proxy.Key) { + *errors = append(*errors, &configErr{tk, fmt.Sprintf("invalid proxy key %q", proxy.Key)}) + continue + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + trusted = append(trusted, proxy) + } + return trusted, nil +} + +// GenTLSConfig loads TLS related configuration parameters. +func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) { + // Create the tls.Config from our options before including the certs. + // It will determine the cipher suites that we prefer. + // FIXME(dlc) change if ARM based. + config := tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: tc.Ciphers, + CurvePreferences: tc.CurvePreferences, + InsecureSkipVerify: tc.Insecure, + } + + switch { + case tc.CertFile != _EMPTY_ && tc.CertStore != certstore.STOREEMPTY: + return nil, certstore.ErrConflictCertFileAndStore + case tc.CertFile != _EMPTY_ && tc.KeyFile == _EMPTY_: + return nil, fmt.Errorf("missing 'key_file' in TLS configuration") + case tc.CertFile == _EMPTY_ && tc.KeyFile != _EMPTY_: + return nil, fmt.Errorf("missing 'cert_file' in TLS configuration") + case tc.CertFile != _EMPTY_ && tc.KeyFile != _EMPTY_: + // Now load in cert and private key + cert, err := tls.LoadX509KeyPair(tc.CertFile, tc.KeyFile) + if err != nil { + return nil, fmt.Errorf("error parsing X509 certificate/key pair: %v", err) + } + cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return nil, fmt.Errorf("error parsing certificate: %v", err) + } + config.Certificates = []tls.Certificate{cert} + case tc.CertStore != certstore.STOREEMPTY: + err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, tc.CaCertsMatch, tc.CertMatchSkipInvalid, &config) + if err != nil { + return nil, err + } + case tc.Certificates != nil: + // Multiple certificate support. + config.Certificates = make([]tls.Certificate, len(tc.Certificates)) + for i, certPair := range tc.Certificates { + cert, err := tls.LoadX509KeyPair(certPair.CertFile, certPair.KeyFile) + if err != nil { + return nil, fmt.Errorf("error parsing X509 certificate/key pair %d/%d: %v", i+1, len(tc.Certificates), err) + } + cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return nil, fmt.Errorf("error parsing certificate %d/%d: %v", i+1, len(tc.Certificates), err) + } + config.Certificates[i] = cert + } + } + + // Require client certificates as needed + if tc.Verify { + config.ClientAuth = tls.RequireAndVerifyClientCert + } + // Add in CAs if applicable. + if tc.CaFile != _EMPTY_ { + rootPEM, err := os.ReadFile(tc.CaFile) + if err != nil || rootPEM == nil { + return nil, err + } + pool := x509.NewCertPool() + ok := pool.AppendCertsFromPEM(rootPEM) + if !ok { + return nil, fmt.Errorf("failed to parse root ca certificate") + } + config.ClientCAs = pool + } + // Allow setting TLS minimum version. + if tc.MinVersion > 0 { + if tc.MinVersion < tls.VersionTLS12 { + return nil, fmt.Errorf("unsupported minimum TLS version: %s", tls.VersionName(tc.MinVersion)) + } + config.MinVersion = tc.MinVersion + } + + return &config, nil +} + +// MergeOptions will merge two options giving preference to the flagOpts +// if the item is present. +func MergeOptions(fileOpts, flagOpts *Options) *Options { + if fileOpts == nil { + return flagOpts + } + if flagOpts == nil { + return fileOpts + } + // Merge the two, flagOpts override + opts := *fileOpts + + if flagOpts.Port != 0 { + opts.Port = flagOpts.Port + } + if flagOpts.Host != _EMPTY_ { + opts.Host = flagOpts.Host + } + if flagOpts.DontListen { + opts.DontListen = flagOpts.DontListen + } + if flagOpts.ClientAdvertise != _EMPTY_ { + opts.ClientAdvertise = flagOpts.ClientAdvertise + } + if flagOpts.Username != _EMPTY_ { + opts.Username = flagOpts.Username + } + if flagOpts.Password != _EMPTY_ { + opts.Password = flagOpts.Password + } + if flagOpts.Authorization != _EMPTY_ { + opts.Authorization = flagOpts.Authorization + } + if flagOpts.HTTPPort != 0 { + opts.HTTPPort = flagOpts.HTTPPort + } + if flagOpts.HTTPBasePath != _EMPTY_ { + opts.HTTPBasePath = flagOpts.HTTPBasePath + } + if flagOpts.Debug { + opts.Debug = true + } + if flagOpts.Trace { + opts.Trace = true + } + if flagOpts.Logtime { + opts.Logtime = true + } + if flagOpts.LogFile != _EMPTY_ { + opts.LogFile = flagOpts.LogFile + } + if flagOpts.PidFile != _EMPTY_ { + opts.PidFile = flagOpts.PidFile + } + if flagOpts.PortsFileDir != _EMPTY_ { + opts.PortsFileDir = flagOpts.PortsFileDir + } + if flagOpts.ProfPort != 0 { + opts.ProfPort = flagOpts.ProfPort + } + if flagOpts.Cluster.ListenStr != _EMPTY_ { + opts.Cluster.ListenStr = flagOpts.Cluster.ListenStr + } + if flagOpts.Cluster.NoAdvertise { + opts.Cluster.NoAdvertise = true + } + if flagOpts.Cluster.ConnectRetries != 0 { + opts.Cluster.ConnectRetries = flagOpts.Cluster.ConnectRetries + } + if flagOpts.Cluster.Advertise != _EMPTY_ { + opts.Cluster.Advertise = flagOpts.Cluster.Advertise + } + if flagOpts.RoutesStr != _EMPTY_ { + mergeRoutes(&opts, flagOpts) + } + if flagOpts.JetStream { + opts.JetStream = flagOpts.JetStream + } + if flagOpts.StoreDir != _EMPTY_ { + opts.StoreDir = flagOpts.StoreDir + } + return &opts +} + +// RoutesFromStr parses route URLs from a string +func RoutesFromStr(routesStr string) []*url.URL { + routes := strings.Split(routesStr, ",") + if len(routes) == 0 { + return nil + } + routeUrls := []*url.URL{} + for _, r := range routes { + r = strings.TrimSpace(r) + u, _ := url.Parse(r) + routeUrls = append(routeUrls, u) + } + return routeUrls +} + +// This will merge the flag routes and override anything that was present. +func mergeRoutes(opts, flagOpts *Options) { + routeUrls := RoutesFromStr(flagOpts.RoutesStr) + if routeUrls == nil { + return + } + opts.Routes = routeUrls + opts.RoutesStr = flagOpts.RoutesStr +} + +func setBaselineOptions(opts *Options) { + // Setup non-standard Go defaults + if opts.Host == _EMPTY_ { + opts.Host = DEFAULT_HOST + } + if opts.HTTPHost == _EMPTY_ { + // Default to same bind from server if left undefined + opts.HTTPHost = opts.Host + } + if opts.Port == 0 { + opts.Port = DEFAULT_PORT + } else if opts.Port == RANDOM_PORT { + // Choose randomly inside of net.Listen + opts.Port = 0 + } + if opts.MaxConn == 0 { + opts.MaxConn = DEFAULT_MAX_CONNECTIONS + } + if opts.PingInterval == 0 { + opts.PingInterval = DEFAULT_PING_INTERVAL + } + if opts.MaxPingsOut == 0 { + opts.MaxPingsOut = DEFAULT_PING_MAX_OUT + } + if opts.TLSTimeout == 0 { + opts.TLSTimeout = float64(TLS_TIMEOUT) / float64(time.Second) + } + if opts.AuthTimeout == 0 { + opts.AuthTimeout = getDefaultAuthTimeout(opts.TLSConfig, opts.TLSTimeout) + } + if opts.Cluster.Port != 0 || opts.Cluster.ListenStr != _EMPTY_ { + if opts.Cluster.Host == _EMPTY_ { + opts.Cluster.Host = DEFAULT_HOST + } + if opts.Cluster.TLSTimeout == 0 { + opts.Cluster.TLSTimeout = float64(TLS_TIMEOUT) / float64(time.Second) + } + if opts.Cluster.AuthTimeout == 0 { + opts.Cluster.AuthTimeout = getDefaultAuthTimeout(opts.Cluster.TLSConfig, opts.Cluster.TLSTimeout) + } + if opts.Cluster.PoolSize == 0 { + opts.Cluster.PoolSize = DEFAULT_ROUTE_POOL_SIZE + } + // Unless pooling/accounts are disabled (by PoolSize being set to -1), + // check for Cluster.Accounts. Add the system account if not present and + // unless we have a configuration that disabled it. + if opts.Cluster.PoolSize > 0 { + sysAccName := opts.SystemAccount + if sysAccName == _EMPTY_ && !opts.NoSystemAccount { + sysAccName = DEFAULT_SYSTEM_ACCOUNT + } + if sysAccName != _EMPTY_ { + var found bool + for _, acc := range opts.Cluster.PinnedAccounts { + if acc == sysAccName { + found = true + break + } + } + if !found { + opts.Cluster.PinnedAccounts = append(opts.Cluster.PinnedAccounts, sysAccName) + } + } + } + // Default to compression "accept", which means that compression is not + // initiated, but if the remote selects compression, this server will + // use the same. + if c := &opts.Cluster.Compression; c.Mode == _EMPTY_ { + if testDefaultClusterCompression != _EMPTY_ { + c.Mode = testDefaultClusterCompression + } else { + c.Mode = CompressionAccept + } + } + } + if opts.LeafNode.Port != 0 { + if opts.LeafNode.Host == _EMPTY_ { + opts.LeafNode.Host = DEFAULT_HOST + } + if opts.LeafNode.TLSTimeout == 0 { + opts.LeafNode.TLSTimeout = float64(TLS_TIMEOUT) / float64(time.Second) + } + if opts.LeafNode.AuthTimeout == 0 { + opts.LeafNode.AuthTimeout = getDefaultAuthTimeout(opts.LeafNode.TLSConfig, opts.LeafNode.TLSTimeout) + } + // Default to compression "s2_auto". + if c := &opts.LeafNode.Compression; c.Mode == _EMPTY_ { + if testDefaultLeafNodeCompression != _EMPTY_ { + c.Mode = testDefaultLeafNodeCompression + } else { + c.Mode = CompressionS2Auto + } + } + } + // Set baseline connect port for remotes. + for _, r := range opts.LeafNode.Remotes { + if r != nil { + for _, u := range r.URLs { + if u.Port() == _EMPTY_ { + u.Host = net.JoinHostPort(u.Host, strconv.Itoa(DEFAULT_LEAFNODE_PORT)) + } + } + // Default to compression "s2_auto". + if c := &r.Compression; c.Mode == _EMPTY_ { + if testDefaultLeafNodeCompression != _EMPTY_ { + c.Mode = testDefaultLeafNodeCompression + } else { + c.Mode = CompressionS2Auto + } + } + // Set default first info timeout value if not set. + if r.FirstInfoTimeout <= 0 { + r.FirstInfoTimeout = DEFAULT_LEAFNODE_INFO_WAIT + } + } + } + + // Set this regardless of opts.LeafNode.Port + if opts.LeafNode.ReconnectInterval == 0 { + opts.LeafNode.ReconnectInterval = DEFAULT_LEAF_NODE_RECONNECT + } + + if opts.MaxControlLine == 0 { + opts.MaxControlLine = MAX_CONTROL_LINE_SIZE + } + if opts.MaxPayload == 0 { + opts.MaxPayload = MAX_PAYLOAD_SIZE + } + if opts.MaxPending == 0 { + opts.MaxPending = MAX_PENDING_SIZE + } + if opts.WriteDeadline == time.Duration(0) { + opts.WriteDeadline = DEFAULT_FLUSH_DEADLINE + } + if opts.MaxClosedClients == 0 { + opts.MaxClosedClients = DEFAULT_MAX_CLOSED_CLIENTS + } + if opts.LameDuckDuration == 0 { + opts.LameDuckDuration = DEFAULT_LAME_DUCK_DURATION + } + if opts.LameDuckGracePeriod == 0 { + opts.LameDuckGracePeriod = DEFAULT_LAME_DUCK_GRACE_PERIOD + } + if opts.Gateway.Port != 0 { + if opts.Gateway.Host == _EMPTY_ { + opts.Gateway.Host = DEFAULT_HOST + } + if opts.Gateway.TLSTimeout == 0 { + opts.Gateway.TLSTimeout = float64(TLS_TIMEOUT) / float64(time.Second) + } + if opts.Gateway.AuthTimeout == 0 { + opts.Gateway.AuthTimeout = getDefaultAuthTimeout(opts.Gateway.TLSConfig, opts.Gateway.TLSTimeout) + } + } + if opts.ConnectErrorReports == 0 { + opts.ConnectErrorReports = DEFAULT_CONNECT_ERROR_REPORTS + } + if opts.ReconnectErrorReports == 0 { + opts.ReconnectErrorReports = DEFAULT_RECONNECT_ERROR_REPORTS + } + if opts.Websocket.Port != 0 { + if opts.Websocket.Host == _EMPTY_ { + opts.Websocket.Host = DEFAULT_HOST + } + } + if opts.MQTT.Port != 0 { + if opts.MQTT.Host == _EMPTY_ { + opts.MQTT.Host = DEFAULT_HOST + } + if opts.MQTT.TLSTimeout == 0 { + opts.MQTT.TLSTimeout = float64(TLS_TIMEOUT) / float64(time.Second) + } + } + // JetStream + if opts.JetStreamMaxMemory == 0 && !opts.maxMemSet { + opts.JetStreamMaxMemory = -1 + } + if opts.JetStreamMaxStore == 0 && !opts.maxStoreSet { + opts.JetStreamMaxStore = -1 + } + if opts.SyncInterval == 0 && !opts.syncSet { + opts.SyncInterval = defaultSyncInterval + } + if opts.JetStreamRequestQueueLimit <= 0 { + opts.JetStreamRequestQueueLimit = JSDefaultRequestQueueLimit + } + if opts.JetStreamInfoQueueLimit <= 0 { + opts.JetStreamInfoQueueLimit = opts.JetStreamRequestQueueLimit + } + if opts.JetStreamConcurrentIOs <= 0 { + opts.JetStreamConcurrentIOs = defaultConcurrentIOs + } + if opts.JetStreamLimits.DefaultMaxConsumers == 0 { + opts.JetStreamLimits.DefaultMaxConsumers = JSDefaultMaxConsumersPerStream + } +} + +func getDefaultAuthTimeout(tls *tls.Config, tlsTimeout float64) float64 { + var authTimeout float64 + if tls != nil { + authTimeout = tlsTimeout + 1.0 + } else { + authTimeout = float64(AUTH_TIMEOUT / time.Second) + } + return authTimeout +} + +// ConfigureOptions accepts a flag set and augments it with NATS Server +// specific flags. On success, an options structure is returned configured +// based on the selected flags and/or configuration file. +// The command line options take precedence to the ones in the configuration file. +func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp, printTLSHelp func()) (*Options, error) { + opts := &Options{} + var ( + showVersion bool + showHelp bool + showTLSHelp bool + signal string + configFile string + dbgAndTrace bool + trcAndVerboseTrc bool + dbgAndTrcAndVerboseTrc bool + err error + ) + + fs.BoolVar(&showHelp, "h", false, "Show this message.") + fs.BoolVar(&showHelp, "help", false, "Show this message.") + fs.IntVar(&opts.Port, "port", 0, "Port to listen on.") + fs.IntVar(&opts.Port, "p", 0, "Port to listen on.") + fs.StringVar(&opts.ServerName, "n", _EMPTY_, "Server name.") + fs.StringVar(&opts.ServerName, "name", _EMPTY_, "Server name.") + fs.StringVar(&opts.ServerName, "server_name", _EMPTY_, "Server name.") + fs.StringVar(&opts.Host, "addr", _EMPTY_, "Network host to listen on.") + fs.StringVar(&opts.Host, "a", _EMPTY_, "Network host to listen on.") + fs.StringVar(&opts.Host, "net", _EMPTY_, "Network host to listen on.") + fs.StringVar(&opts.ClientAdvertise, "client_advertise", _EMPTY_, "Client URL to advertise to other servers.") + fs.BoolVar(&opts.Debug, "D", false, "Enable Debug logging.") + fs.BoolVar(&opts.Debug, "debug", false, "Enable Debug logging.") + fs.BoolVar(&opts.Trace, "V", false, "Enable Trace logging.") + fs.BoolVar(&trcAndVerboseTrc, "VV", false, "Enable Verbose Trace logging. (Traces system account as well)") + fs.BoolVar(&opts.Trace, "trace", false, "Enable Trace logging.") + fs.BoolVar(&dbgAndTrace, "DV", false, "Enable Debug and Trace logging.") + fs.BoolVar(&dbgAndTrcAndVerboseTrc, "DVV", false, "Enable Debug and Verbose Trace logging. (Traces system account as well)") + fs.BoolVar(&opts.Logtime, "T", true, "Timestamp log entries.") + fs.BoolVar(&opts.Logtime, "logtime", true, "Timestamp log entries.") + fs.BoolVar(&opts.LogtimeUTC, "logtime_utc", false, "Timestamps in UTC instead of local timezone.") + fs.StringVar(&opts.Username, "user", _EMPTY_, "Username required for connection.") + fs.StringVar(&opts.Password, "pass", _EMPTY_, "Password required for connection.") + fs.StringVar(&opts.Authorization, "auth", _EMPTY_, "Authorization token required for connection.") + fs.IntVar(&opts.HTTPPort, "m", 0, "HTTP Port for /varz, /connz endpoints.") + fs.IntVar(&opts.HTTPPort, "http_port", 0, "HTTP Port for /varz, /connz endpoints.") + fs.IntVar(&opts.HTTPSPort, "ms", 0, "HTTPS Port for /varz, /connz endpoints.") + fs.IntVar(&opts.HTTPSPort, "https_port", 0, "HTTPS Port for /varz, /connz endpoints.") + fs.StringVar(&configFile, "c", _EMPTY_, "Configuration file.") + fs.StringVar(&configFile, "config", _EMPTY_, "Configuration file.") + fs.BoolVar(&opts.CheckConfig, "t", false, "Check configuration and exit.") + fs.StringVar(&signal, "sl", "", "Send signal to nats-server process (ldm, stop, quit, term, reopen, reload).") + fs.StringVar(&signal, "signal", "", "Send signal to nats-server process (ldm, stop, quit, term, reopen, reload).") + fs.StringVar(&opts.PidFile, "P", "", "File to store process pid.") + fs.StringVar(&opts.PidFile, "pid", "", "File to store process pid.") + fs.StringVar(&opts.PortsFileDir, "ports_file_dir", "", "Creates a ports file in the specified directory (_.ports).") + fs.StringVar(&opts.LogFile, "l", "", "File to store logging output.") + fs.StringVar(&opts.LogFile, "log", "", "File to store logging output.") + fs.Int64Var(&opts.LogSizeLimit, "log_size_limit", 0, "Logfile size limit being auto-rotated") + fs.BoolVar(&opts.Syslog, "s", false, "Enable syslog as log method.") + fs.BoolVar(&opts.Syslog, "syslog", false, "Enable syslog as log method.") + fs.StringVar(&opts.RemoteSyslog, "r", _EMPTY_, "Syslog server addr (udp://127.0.0.1:514).") + fs.StringVar(&opts.RemoteSyslog, "remote_syslog", _EMPTY_, "Syslog server addr (udp://127.0.0.1:514).") + fs.BoolVar(&showVersion, "version", false, "Print version information.") + fs.BoolVar(&showVersion, "v", false, "Print version information.") + fs.IntVar(&opts.ProfPort, "profile", 0, "Profiling HTTP port.") + fs.StringVar(&opts.RoutesStr, "routes", _EMPTY_, "Routes to actively solicit a connection.") + fs.StringVar(&opts.Cluster.ListenStr, "cluster", _EMPTY_, "Cluster url from which members can solicit routes.") + fs.StringVar(&opts.Cluster.ListenStr, "cluster_listen", _EMPTY_, "Cluster url from which members can solicit routes.") + fs.StringVar(&opts.Cluster.Advertise, "cluster_advertise", _EMPTY_, "Cluster URL to advertise to other servers.") + fs.BoolVar(&opts.Cluster.NoAdvertise, "no_advertise", false, "Do not advertise known cluster IPs to clients.") + fs.IntVar(&opts.Cluster.ConnectRetries, "connect_retries", 0, "For implicit routes, number of connect retries.") + fs.StringVar(&opts.Cluster.Name, "cluster_name", _EMPTY_, "Cluster Name, if not set one will be dynamically generated.") + fs.BoolVar(&showTLSHelp, "help_tls", false, "TLS help.") + fs.BoolVar(&opts.TLS, "tls", false, "Enable TLS.") + fs.BoolVar(&opts.TLSVerify, "tlsverify", false, "Enable TLS with client verification.") + fs.StringVar(&opts.TLSCert, "tlscert", _EMPTY_, "Server certificate file.") + fs.StringVar(&opts.TLSKey, "tlskey", _EMPTY_, "Private key for server certificate.") + fs.StringVar(&opts.TLSCaCert, "tlscacert", _EMPTY_, "Client certificate CA for verification.") + fs.IntVar(&opts.MaxTracedMsgLen, "max_traced_msg_len", 0, "Maximum printable length for traced messages. 0 for unlimited.") + fs.BoolVar(&opts.JetStream, "js", false, "Enable JetStream.") + fs.BoolVar(&opts.JetStream, "jetstream", false, "Enable JetStream.") + fs.StringVar(&opts.StoreDir, "sd", _EMPTY_, "Storage directory.") + fs.StringVar(&opts.StoreDir, "store_dir", _EMPTY_, "Storage directory.") + + // The flags definition above set "default" values to some of the options. + // Calling Parse() here will override the default options with any value + // specified from the command line. This is ok. We will then update the + // options with the content of the configuration file (if present), and then, + // call Parse() again to override the default+config with command line values. + // Calling Parse() before processing config file is necessary since configFile + // itself is a command line argument, and also Parse() is required in order + // to know if user wants simply to show "help" or "version", etc... + if err := fs.Parse(args); err != nil { + return nil, err + } + + if showVersion { + printVersion() + return nil, nil + } + + if showHelp { + printHelp() + return nil, nil + } + + if showTLSHelp { + printTLSHelp() + return nil, nil + } + + // Process args looking for non-flag options, + // 'version' and 'help' only for now + showVersion, showHelp, err = ProcessCommandLineArgs(fs) + if err != nil { + return nil, err + } else if showVersion { + printVersion() + return nil, nil + } else if showHelp { + printHelp() + return nil, nil + } + + // Snapshot flag options. + FlagSnapshot = opts.Clone() + + // Keep track of the boolean flags that were explicitly set with their value. + fs.Visit(func(f *flag.Flag) { + switch f.Name { + case "DVV": + trackExplicitVal(&FlagSnapshot.inCmdLine, "Debug", dbgAndTrcAndVerboseTrc) + trackExplicitVal(&FlagSnapshot.inCmdLine, "Trace", dbgAndTrcAndVerboseTrc) + trackExplicitVal(&FlagSnapshot.inCmdLine, "TraceVerbose", dbgAndTrcAndVerboseTrc) + case "DV": + trackExplicitVal(&FlagSnapshot.inCmdLine, "Debug", dbgAndTrace) + trackExplicitVal(&FlagSnapshot.inCmdLine, "Trace", dbgAndTrace) + case "D": + fallthrough + case "debug": + trackExplicitVal(&FlagSnapshot.inCmdLine, "Debug", FlagSnapshot.Debug) + case "VV": + trackExplicitVal(&FlagSnapshot.inCmdLine, "Trace", trcAndVerboseTrc) + trackExplicitVal(&FlagSnapshot.inCmdLine, "TraceVerbose", trcAndVerboseTrc) + case "V": + fallthrough + case "trace": + trackExplicitVal(&FlagSnapshot.inCmdLine, "Trace", FlagSnapshot.Trace) + case "T": + fallthrough + case "logtime": + trackExplicitVal(&FlagSnapshot.inCmdLine, "Logtime", FlagSnapshot.Logtime) + case "s": + fallthrough + case "syslog": + trackExplicitVal(&FlagSnapshot.inCmdLine, "Syslog", FlagSnapshot.Syslog) + case "no_advertise": + trackExplicitVal(&FlagSnapshot.inCmdLine, "Cluster.NoAdvertise", FlagSnapshot.Cluster.NoAdvertise) + case "js": + trackExplicitVal(&FlagSnapshot.inCmdLine, "JetStream", FlagSnapshot.JetStream) + } + }) + + // Process signal control. + if signal != _EMPTY_ { + // processSignal always either returns an error or calls os.Exit(0). + return nil, processSignal(signal) + } + + // Parse config if given + if configFile != _EMPTY_ { + // This will update the options with values from the config file. + err := opts.ProcessConfigFile(configFile) + if err != nil { + if opts.CheckConfig { + return nil, err + } + if cerr, ok := err.(*processConfigErr); !ok || len(cerr.Errors()) != 0 { + return nil, err + } + // If we get here we only have warnings and can still continue + fmt.Fprint(os.Stderr, err) + } else if opts.CheckConfig { + // Report configuration file syntax test was successful and exit. + return opts, nil + } + + // Call this again to override config file options with options from command line. + // Note: We don't need to check error here since if there was an error, it would + // have been caught the first time this function was called (after setting up the + // flags). + fs.Parse(args) + } else if opts.CheckConfig { + return nil, fmt.Errorf("must specify [-c, --config] option to check configuration file syntax") + } + + // Special handling of some flags + var ( + flagErr error + tlsDisabled bool + tlsOverride bool + ) + fs.Visit(func(f *flag.Flag) { + // short-circuit if an error was encountered + if flagErr != nil { + return + } + if strings.HasPrefix(f.Name, "tls") { + if f.Name == "tls" { + if !opts.TLS { + // User has specified "-tls=false", we need to disable TLS + opts.TLSConfig = nil + tlsDisabled = true + tlsOverride = false + return + } + tlsOverride = true + } else if !tlsDisabled { + tlsOverride = true + } + } else { + switch f.Name { + case "VV": + opts.Trace, opts.TraceVerbose = trcAndVerboseTrc, trcAndVerboseTrc + case "DVV": + opts.Trace, opts.Debug, opts.TraceVerbose = dbgAndTrcAndVerboseTrc, dbgAndTrcAndVerboseTrc, dbgAndTrcAndVerboseTrc + case "DV": + // Check value to support -DV=false + opts.Trace, opts.Debug = dbgAndTrace, dbgAndTrace + case "cluster", "cluster_listen": + // Override cluster config if explicitly set via flags. + flagErr = overrideCluster(opts) + case "routes": + // Keep in mind that the flag has updated opts.RoutesStr at this point. + if opts.RoutesStr == _EMPTY_ { + // Set routes array to nil since routes string is empty + opts.Routes = nil + return + } + routeUrls := RoutesFromStr(opts.RoutesStr) + opts.Routes = routeUrls + } + } + }) + if flagErr != nil { + return nil, flagErr + } + + // This will be true if some of the `-tls` params have been set and + // `-tls=false` has not been set. + if tlsOverride { + if err := overrideTLS(opts); err != nil { + return nil, err + } + } + + // If we don't have cluster defined in the configuration + // file and no cluster listen string override, but we do + // have a routes override, we need to report misconfiguration. + if opts.RoutesStr != _EMPTY_ && opts.Cluster.ListenStr == _EMPTY_ && opts.Cluster.Host == _EMPTY_ && opts.Cluster.Port == 0 { + return nil, errors.New("solicited routes require cluster capabilities, e.g. --cluster") + } + + return opts, nil +} + +func normalizeBasePath(p string) string { + if len(p) == 0 { + return "/" + } + // add leading slash + if p[0] != '/' { + p = "/" + p + } + return path.Clean(p) +} + +// overrideTLS is called when at least "-tls=true" has been set. +func overrideTLS(opts *Options) error { + if opts.TLSCert == _EMPTY_ { + return errors.New("TLS Server certificate must be present and valid") + } + if opts.TLSKey == _EMPTY_ { + return errors.New("TLS Server private key must be present and valid") + } + + tc := TLSConfigOpts{} + tc.CertFile = opts.TLSCert + tc.KeyFile = opts.TLSKey + tc.CaFile = opts.TLSCaCert + tc.Verify = opts.TLSVerify + tc.Ciphers = defaultCipherSuites() + + var err error + opts.TLSConfig, err = GenTLSConfig(&tc) + return err +} + +// overrideCluster updates Options.Cluster if that flag "cluster" (or "cluster_listen") +// has explicitly be set in the command line. If it is set to empty string, it will +// clear the Cluster options. +func overrideCluster(opts *Options) error { + if opts.Cluster.ListenStr == _EMPTY_ { + // This one is enough to disable clustering. + opts.Cluster.Port = 0 + return nil + } + // -1 will fail url.Parse, so if we have -1, change it to + // 0, and then after parse, replace the port with -1 so we get + // automatic port allocation + wantsRandom := false + if strings.HasSuffix(opts.Cluster.ListenStr, ":-1") { + wantsRandom = true + cls := fmt.Sprintf("%s:0", opts.Cluster.ListenStr[0:len(opts.Cluster.ListenStr)-3]) + opts.Cluster.ListenStr = cls + } + clusterURL, err := url.Parse(opts.Cluster.ListenStr) + if err != nil { + return err + } + h, p, err := net.SplitHostPort(clusterURL.Host) + if err != nil { + return err + } + if wantsRandom { + p = "-1" + } + opts.Cluster.Host = h + _, err = fmt.Sscan(p, &opts.Cluster.Port) + if err != nil { + return err + } + + if clusterURL.User != nil { + pass, hasPassword := clusterURL.User.Password() + if !hasPassword { + return errors.New("expected cluster password to be set") + } + opts.Cluster.Password = pass + + user := clusterURL.User.Username() + opts.Cluster.Username = user + } else { + // Since we override from flag and there is no user/pwd, make + // sure we clear what we may have gotten from config file. + opts.Cluster.Username = _EMPTY_ + opts.Cluster.Password = _EMPTY_ + } + + return nil +} + +func processSignal(signal string) error { + var ( + pid string + commandAndPid = strings.Split(signal, "=") + ) + if l := len(commandAndPid); l == 2 { + pid = maybeReadPidFile(commandAndPid[1]) + } else if l > 2 { + return fmt.Errorf("invalid signal parameters: %v", commandAndPid[2:]) + } + if err := ProcessSignal(Command(commandAndPid[0]), pid); err != nil { + return err + } + os.Exit(0) + return nil +} + +// maybeReadPidFile returns a PID or Windows service name obtained via the following method: +// 1. Try to open a file with path "pidStr" (absolute or relative). +// 2. If such a file exists and can be read, return its contents. +// 3. Otherwise, return the original "pidStr" string. +func maybeReadPidFile(pidStr string) string { + if b, err := os.ReadFile(pidStr); err == nil { + return string(b) + } + return pidStr +} + +func homeDir() (string, error) { + if runtime.GOOS == "windows" { + homeDrive, homePath := os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH") + userProfile := os.Getenv("USERPROFILE") + + home := filepath.Join(homeDrive, homePath) + if homeDrive == _EMPTY_ || homePath == _EMPTY_ { + if userProfile == _EMPTY_ { + return _EMPTY_, errors.New("nats: failed to get home dir, require %HOMEDRIVE% and %HOMEPATH% or %USERPROFILE%") + } + home = userProfile + } + + return home, nil + } + + home := os.Getenv("HOME") + if home == _EMPTY_ { + return _EMPTY_, errors.New("failed to get home dir, require $HOME") + } + return home, nil +} + +func expandPath(p string) (string, error) { + p = os.ExpandEnv(p) + + if !strings.HasPrefix(p, "~") { + return p, nil + } + + home, err := homeDir() + if err != nil { + return _EMPTY_, err + } + + return filepath.Join(home, p[1:]), nil +} + +// RedactArgs redacts sensitive arguments from the command line. +// For example, turns '--pass=secret' into '--pass=[REDACTED]'. +func RedactArgs(args []string) { + secretArg := regexp.MustCompile("^-{1,2}(user|pass|auth)(=.*)?$") + routeURLArg := regexp.MustCompile("^-{1,2}(routes)(=.*)?$") + singleURLArg := regexp.MustCompile("^-{1,2}(cluster|cluster_listen)(=.*)?$") + for i, arg := range args { + switch { + case secretArg.MatchString(arg): + redactArgValue(args, i, func(_ string) string { return "[REDACTED]" }) + case routeURLArg.MatchString(arg): + redactArgValue(args, i, redactURLListUser) + case singleURLArg.MatchString(arg): + redactArgValue(args, i, redactURLUser) + } + } +} + +func redactArgValue(args []string, i int, redact func(string) string) { + if flag, value, ok := strings.Cut(args[i], "="); ok { + args[i] = flag + "=" + redact(value) + } else if i+1 < len(args) { + args[i+1] = redact(args[i+1]) + } +} + +func redactURLUser(raw string) string { + if !strings.Contains(raw, "@") { + return raw + } + parseValue := strings.TrimSpace(raw) + restoreRandom := false + if prefix, ok := strings.CutSuffix(parseValue, ":-1"); ok { + parseValue = prefix + ":0" + restoreRandom = true + } + u, err := url.Parse(parseValue) + if err != nil || u.User == nil { + return raw + } + // url.String escapes brackets in userinfo, so use + // a placeholder here and rewrite it afterward. + u.User = url.User("_REDACTED_") + if restoreRandom { + u.Host = strings.TrimSuffix(u.Host, ":0") + ":-1" + } + return strings.Replace(u.String(), "_REDACTED_@", "[REDACTED]@", 1) +} + +func redactURLListUser(raw string) string { + parts := strings.Split(raw, ",") + for i, part := range parts { + parts[i] = redactURLUser(part) + } + return strings.Join(parts, ",") +} diff --git a/server/raft.go b/server/raft.go new file mode 100644 index 0000000..cc3ad78 --- /dev/null +++ b/server/raft.go @@ -0,0 +1,5960 @@ +// Copyright 2020-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 ( + "bytes" + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "iter" + "math" + "math/rand/v2" + "os" + "path/filepath" + "runtime" + "slices" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/antithesishq/antithesis-sdk-go/assert" + + "github.com/minio/highwayhash" +) + +type RaftNode interface { + Propose(term uint64, entry []byte) error + ProposeMulti(term uint64, entries []*Entry) error + ForwardProposal(entry []byte) error + LoadLastSnapshot() (uint64, []byte, error) + InstallSnapshot(snap []byte, force bool) error + CreateSnapshotCheckpoint(force bool) (RaftNodeCheckpoint, error) + SendSnapshot(snap []byte) error + NeedSnapshot() bool + SendHeartbeat() + Applied(index uint64) (entries uint64, bytes uint64) + Processed(index uint64, applied uint64) (entries uint64, bytes uint64) + State() RaftState + Size() (entries, bytes uint64) + Progress() (index, commit, applied uint64) + Leader() bool + LeaderSince() *time.Time + Quorum() bool + Current() bool + Healthy() bool + Term() uint64 + Leaderless() bool + GroupLeader() string + HadPreviousLeader() bool + StepDown(preferred ...string) error + SetObserver(isObserver bool) + IsObserver() bool + Campaign() error + CampaignImmediately() error + ID() string + Group() string + Peers() []*Peer + PeerNames() []string + VotingPeerNames() []string + LastHeardFromPeer(peer string) time.Time + ProposeAddPeer(peer string) error + ProposeRemovePeer(peer string) error + EvictPeers(peers []string) ([]string, error) + MembershipChangeInProgress() bool + AdjustClusterSize(csz int) error + AdjustBootClusterSize(csz int) error + RescueQuorum(qn int) (prev, cur int, err error) + InRescue() bool + ClusterSize() int + QuorumNeeded() int + ApplyQ() *ipQueue[*CommittedEntry] + PauseApply() error + ResumeApply() + DrainAndReplaySnapshot() bool + LeadChangeC() <-chan leadChange + QuitC() <-chan struct{} + Created() time.Time + Stop() + WaitForStop() + Delete() + IsDeleted() bool + Reset() + RecreateInternalSubs() error + IsSystemAccount() bool + GetTrafficAccountName() string + GetWriteErr() error +} + +// RaftNodeCheckpoint is used as an alternative to a direct InstallSnapshot. +// A checkpoint is created from CreateSnapshotCheckpoint and allows installing snapshots asynchronously, +// as well as loading the last snapshot or entries between the last snapshot and the one we're about to create. +// Abort can be called to cancel the snapshot installation at any time, or InstallSnapshot to install it. +type RaftNodeCheckpoint interface { + LoadLastSnapshot() (snap []byte, err error) + AppendEntriesSeq() iter.Seq2[*appendEntry, error] + Abort() + InstallSnapshot(data []byte) (uint64, error) +} + +type WAL interface { + Type() StorageType + StoreMsg(subj string, hdr, msg []byte, ttl int64) (uint64, int64, error) + LoadMsg(index uint64, sm *StoreMsg) (*StoreMsg, error) + RemoveMsg(index uint64) (bool, error) + Compact(index uint64) (uint64, error) + Purge() (uint64, error) + PurgeEx(subject string, seq, keep uint64) (uint64, error) + Truncate(seq uint64) error + State() StreamState + FastState(*StreamState) + Stop() error + Delete(inline bool) error +} + +type Peer struct { + ID string + Current bool + Last time.Time + Lag uint64 +} + +type RaftState uint8 + +// Allowable states for a NATS Consensus Group. +const ( + Follower RaftState = iota + Leader + Candidate + Closed +) + +func (state RaftState) String() string { + switch state { + case Follower: + return "FOLLOWER" + case Candidate: + return "CANDIDATE" + case Leader: + return "LEADER" + case Closed: + return "CLOSED" + } + return "UNKNOWN" +} + +type raft struct { + sync.RWMutex + // Serializes writing snapshot files, since async snapshots are written with the Raft lock released. + // MUST be acquired while already holding the Raft lock. But the Raft lock may then be released. + snapLock sync.Mutex + + created time.Time // Time that the group was created + accName string // Account name of the asset this raft group is for + group string // Raft group + sd string // Store directory + id string // Node ID + wg sync.WaitGroup // Wait for running goroutines to exit on shutdown + + dios *diskIOSemaphore + wal WAL // WAL store (filestore or memstore) + wtype StorageType // WAL type, e.g. FileStorage or MemoryStorage + bytes uint64 // Total amount of bytes stored in the WAL. (Saves us from needing to call wal.FastState very often) + werr error // Last write error + + state atomic.Int32 // RaftState + leaderState atomic.Bool // Is in (complete) leader state. + leaderSince atomic.Pointer[time.Time] // How long since becoming leader. + hh *highwayhash.Digest64 // Highwayhash, used for snapshots + snapfile string // Snapshot filename + + csz int // Cluster size + qn int // Number of nodes needed to establish quorum + peers map[string]*lps // Other peers in the Raft group + + removed map[string]time.Time // Peers that were removed from the group + observed map[string]time.Time // Peers not in our peer set that we've heard from, only for managed groups + acks map[uint64]map[string]struct{} // Append entry responses/acks, map of entry index -> peer ID + pae map[uint64]*appendEntry // Pending append entries + paeBytes uint64 // Total byte size of the pending append entries in pae + + rescue *time.Timer // Non-nil while an unsafe quorum rescue is active (see RescueQuorum) + elect *time.Timer // Election timer, normally accessed via electTimer + etlr time.Time // Election timer last reset time, for unit tests only + active time.Time // Last activity time, i.e. for heartbeats + llqrt time.Time // Last quorum lost time + lsut time.Time // Last scale-up time + + term uint64 // The current vote term + pterm uint64 // Previous term from the last snapshot + pindex uint64 // Previous index from the last snapshot + commit uint64 // Index of the most recent commit + processed uint64 // Index of the most recently processed commit + applied uint64 // Index of the most recently applied commit + papplied uint64 // First sequence of our log, matches when we last installed a snapshot. + + membChange *membChange // Uncommitted membership change entry at a specific log index. + + aflr uint64 // Index when to signal initial messages have been applied after becoming leader. 0 means signaling is disabled. + + leader string // The ID of the leader + vote string // Our current vote state + + s *Server // Reference to top-level server + js *jetStream // JetStream, if running, to see if we are out of resources + + hasleader atomic.Bool // Is there a group leader right now? + pleader atomic.Bool // Has the group ever had a leader? + isSysAcc atomic.Bool // Are we utilizing the system account? + + extSt extensionState // Extension state + + track bool // Whether out of resources checking is enabled. + dflag bool // Debug flag + managed bool // Peers managed by reconciliation loop, not automatic peer addition. + + psubj string // Proposals subject + rpsubj string // Remove peers subject + vsubj string // Vote requests subject + vreply string // Vote responses subject + asubj string // Append entries subject + areply string // Append entries responses subject + + t raftTransport // Transport that handles Raft messaging + aesub *subscription // Subscription for handleAppendEntry callbacks + + wtv []byte // Term and vote to be written + wps []byte // Peer state to be written + + catchup *catchupState // For when we need to catch up as a follower. + progress map[string]*ipQueue[uint64] // For leader or server catching up a follower. + + hcommit uint64 // The commit at the time that applies were paused + + prop *ipQueue[*proposedEntry] // Proposals + entry *ipQueue[*appendEntry] // Append entries + resp *ipQueue[*appendEntryResponse] // Append entries responses + apply *ipQueue[*CommittedEntry] // Apply queue (committed entries to be passed to upper layer) + reqs *ipQueue[*voteRequest] // Vote requests + votes *ipQueue[*voteResponse] // Vote responses + leadc chan leadChange // Leader changes + quit chan struct{} // Raft group shutdown + + lxfer bool // Are we doing a leadership transfer? + hcbehind bool // Were we falling behind at the last health check? (see: isCurrent) + maybeLeader bool // The group had a preferred leader. And is maybe already acting as leader prior to scale up. + paused bool // Whether or not applies are paused + observer bool // The node is observing, i.e. not able to become leader + initializing bool // The node is new, and "empty log" checks can be temporarily relaxed. + scaleUp bool // The node is part of a scale up, puts us in observer mode until the log contains data. + deleted bool // If the node was deleted. + snapshotting bool // Snapshot is in progress. + quorumPaused bool // Pause replication and quorum participation to prevent log growth during slow applies. + + overrunCount uint64 // Counter of how many times we were overrun, either as follower or as leader. + + fastPathTerm atomic.Uint64 // Term accepting fast path proposals, zero when closed. +} + +type proposedEntry struct { + *Entry + reply string // Optional, to respond once proposal handled + term uint64 // Raft term in which it was accepted. +} + +func (pe *proposedEntry) size() uint64 { + return uint64(len(pe.Data)) + 1 +} + +// catchupState structure that holds our subscription, and catchup term and index +// as well as starting term and index and how many updates we have seen. +type catchupState struct { + sub *subscription // Subscription that catchup messages will arrive on + cterm uint64 // Catchup term + cindex uint64 // Catchup index + pterm uint64 // Starting term + pindex uint64 // Starting index + active time.Time // Last time we received a message for this catchup + signal bool // Whether the EntryCatchup signal was sent. +} + +// lps holds peer state of last time and last index replicated. +type lps struct { + ts time.Time // Last timestamp + li uint64 // Last index replicated +} + +type membChange struct { + index uint64 // Which entry in the log is the membership change. + peer string // The peer whose membership is being changed. + prev *lps // The previous membership state if removing, unset if adding. +} + +const ( + minElectionTimeoutDefault = 4 * time.Second + maxElectionTimeoutDefault = 9 * time.Second + minCampaignTimeoutDefault = 100 * time.Millisecond + maxCampaignTimeoutDefault = 8 * minCampaignTimeoutDefault + hbIntervalDefault = 1 * time.Second + lostQuorumIntervalDefault = hbIntervalDefault * 10 // 10 seconds + lostQuorumCheckIntervalDefault = hbIntervalDefault * 10 // 10 seconds + lostQuorumSignalIntervalDefault = 20 * time.Second + observerModeIntervalDefault = 48 * time.Hour + peerRemoveTimeoutDefault = 5 * time.Minute + rescueQuorumTimeoutDefault = 5 * time.Minute +) + +var ( + minElectionTimeout = minElectionTimeoutDefault + maxElectionTimeout = maxElectionTimeoutDefault + minCampaignTimeout = minCampaignTimeoutDefault + maxCampaignTimeout = maxCampaignTimeoutDefault + hbInterval = hbIntervalDefault + lostQuorumInterval = lostQuorumIntervalDefault + lostQuorumCheck = lostQuorumCheckIntervalDefault + lostQuorumSignal = lostQuorumSignalIntervalDefault + observerModeInterval = observerModeIntervalDefault + peerRemoveTimeout = peerRemoveTimeoutDefault + rescueQuorumTimeout = rescueQuorumTimeoutDefault +) + +type RaftConfig struct { + Name string + Store string + Log WAL + Track bool + Managed bool + Observer bool + + // Recovering must be set for a Raft group that's recovering after a restart, or if it's + // first seen after a catchup from another server. If a server recovers with an empty log, + // we know to protect against data loss. + Recovering bool + + // ScaleUp identifies the Raft peer set is being scaled up. + // We need to protect against losing state due to the new peers starting with an empty log. + // Therefore, these empty servers can't try to become leader until they at least have _some_ state. + ScaleUp bool + + // NewTransport creates the transport used for Raft node communication. + // This is mainly for tests to inject a custom transport. + // If nil, the default transport is used. + NewTransport newTransportFunc +} + +var ( + errNotLeader = errors.New("raft: not leader") + errAlreadyLeader = errors.New("raft: already leader") + errNilCfg = errors.New("raft: no config given") + errCorruptPeers = errors.New("raft: corrupt peer state") + errEntryLoadFailed = errors.New("raft: could not load entry from WAL") + errEntryStoreFailed = errors.New("raft: could not store entry to WAL") + errNodeClosed = errors.New("raft: node is closed") + errNodeRemoved = errors.New("raft: peer was removed") + errBadSnapName = errors.New("raft: snapshot name could not be parsed") + errNoSnapAvailable = errors.New("raft: no snapshot available") + errSnapInProgress = errors.New("raft: snapshot is already in progress") + errSnapAborted = errors.New("raft: snapshot was aborted") + errCatchupsRunning = errors.New("raft: snapshot can not be installed while catchups running") + errRescueBadQuorum = errors.New("raft: rescue quorum must be between 1 and current quorum") + errRescueLeaderKnown = errors.New("raft: leader is known") + errRescueNotVoting = errors.New("raft: not a voting member") + errRescueEmptyLog = errors.New("raft: log is empty") + errSnapshotCorrupt = errors.New("raft: snapshot corrupt") + errTooManyPrefs = errors.New("raft: stepdown requires at most one preferred new leader") + errNoPeerState = errors.New("raft: no peerstate") + errAdjustBootCluster = errors.New("raft: can not adjust boot peer size on established group") + errLeaderLen = fmt.Errorf("raft: leader should be exactly %d bytes", idLen) + errTooManyEntries = errors.New("raft: append entry can contain a max of 64k entries") + errBadAppendEntry = errors.New("raft: append entry corrupt") + errNoInternalClient = errors.New("raft: no internal client") + errMembershipChange = errors.New("raft: membership change in progress") + errRemoveLastNode = errors.New("raft: cannot remove the last peer") + errPeerNotFound = errors.New("raft: peer not found") + errNotManaged = errors.New("raft: group membership is not managed") + errNotLeaderless = errors.New("raft: group is not leaderless") + errQuorumPossible = errors.New("raft: remaining peers could still reach quorum") +) + +// This will bootstrap a raftNode by writing its config into the store directory. +func (s *Server) bootstrapRaftNode(cfg *RaftConfig, knownPeers []string, allPeersKnown bool) error { + if cfg == nil { + return errNilCfg + } + // Check validity of peers if presented. + for _, p := range knownPeers { + if len(p) != idLen { + return fmt.Errorf("raft: illegal peer: %q", p) + } + } + expected := len(knownPeers) + // We need to adjust this is all peers are not known. + if !allPeersKnown { + s.Debugf("Determining expected peer size for JetStream meta group") + if expected < 2 { + expected = 2 + } + opts := s.getOpts() + nrs := len(opts.Routes) + + cn := s.ClusterName() + ngwps := 0 + for _, gw := range opts.Gateway.Gateways { + // Ignore our own cluster if specified. + if gw.Name == cn { + continue + } + // Each configured gateway URL represents one remote endpoint, so + // count it as a single peer. We must not resolve the host and add + // one per returned address: a hostname on a dual-stack host (e.g. + // "localhost" -> 127.0.0.1 + ::1) would then count the same server + // multiple times, inflating the expected meta-group size above the + // real node count and preventing meta leader election. + ngwps += len(gw.URLs) + } + + if expected < nrs+ngwps { + expected = nrs + ngwps + s.Debugf("Adjusting expected peer set size to %d with %d known", expected, len(knownPeers)) + } + } + + // Check the store directory. If we have a memory based WAL we need to make sure the directory is setup. + if stat, err := os.Stat(cfg.Store); os.IsNotExist(err) { + if err := os.MkdirAll(cfg.Store, defaultDirPerms); err != nil { + return fmt.Errorf("raft: could not create storage directory - %v", err) + } + } else if stat == nil || !stat.IsDir() { + return fmt.Errorf("raft: storage directory is not a directory") + } + tmpfile, err := os.CreateTemp(cfg.Store, "_test_") + if err != nil { + return fmt.Errorf("raft: storage directory is not writable") + } + tmpfile.Close() + os.Remove(tmpfile.Name()) + + return writePeerState(s.diskIOSemaphore(), cfg.Store, &peerState{knownPeers, expected, extUndetermined}) +} + +// initRaftNode will initialize the raft node, to be used by startRaftNode or when testing to not run the Go routine. +func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabels) (*raft, error) { + restorePeerState := func(n *raft) error { + ps, err := readPeerState(s.diskIOSemaphore(), cfg.Store) + if err != nil { + return err + } + if ps == nil { + return errNoPeerState + } + n.processPeerState(ps) + n.extSt = ps.domainExt + return nil + } + + if cfg == nil { + return nil, errNilCfg + } + s.mu.RLock() + if s.sys == nil { + s.mu.RUnlock() + return nil, ErrNoSysAccount + } + hash := s.sys.shash + s.mu.RUnlock() + + qpfx := fmt.Sprintf("[ACC:%s] RAFT '%s' ", accName, cfg.Name) + n := &raft{ + created: time.Now(), + id: hash[:idLen], + group: cfg.Name, + sd: cfg.Store, + wal: cfg.Log, + wtype: cfg.Log.Type(), + dios: s.diskIOSemaphore(), + track: cfg.Track, + managed: cfg.Managed, + peers: make(map[string]*lps), + acks: make(map[uint64]map[string]struct{}), + pae: make(map[uint64]*appendEntry), + s: s, + js: s.getJetStream(), + quit: make(chan struct{}), + reqs: newIPQueue[*voteRequest](s, qpfx+"vreq"), + votes: newIPQueue[*voteResponse](s, qpfx+"vresp"), + prop: newIPQueue[*proposedEntry](s, qpfx+"entry", ipqSizeCalculation((*proposedEntry).size)), + entry: newIPQueue[*appendEntry](s, qpfx+"appendEntry"), + resp: newIPQueue[*appendEntryResponse](s, qpfx+"appendEntryResponse"), + apply: newIPQueue[*CommittedEntry](s, qpfx+"committedEntry"), + accName: accName, + leadc: make(chan leadChange, 1), + observer: cfg.Observer, + } + + if cfg.NewTransport != nil { + n.t = cfg.NewTransport(s, n) + } else { + n.t = defaultRaftTransport(s, n) + } + + // Setup our internal subscriptions for proposals, votes and append entries. + // If we fail to do this for some reason then this is fatal — we cannot + // continue setting up or the Raft node may be partially/totally isolated. + if err := n.RecreateInternalSubs(); err != nil { + n.shutdown() + return nil, err + } + + if atomic.LoadInt32(&s.logging.debug) > 0 { + n.dflag = true + } + + // Set up the highwayhash for the snapshots. + key := sha256.Sum256([]byte(n.group)) + n.hh, _ = highwayhash.NewDigest64(key[:]) + + // If we have a term and vote file (tav.idx on the filesystem) then read in + // what we think the term and vote was. It's possible these are out of date + // so a catch-up may be required. + if term, vote, err := n.readTermVote(); err == nil && term > 0 { + n.term = term + n.vote = vote + } + + // Can't recover snapshots if memory based since wal will be reset. + // We will inherit from the current leader. + n.papplied = 0 + if _, ok := n.wal.(*memStore); ok { + _ = os.RemoveAll(filepath.Join(n.sd, snapshotsDir)) + } else if err := n.setupLastSnapshot(); err != nil && err != errNoSnapAvailable { + // If we failed to recover from the snapshot, then we should surface + // the error upwards, otherwise we can complete recovery but have only + // a partial view of the world. + n.shutdown() + return nil, err + } + + // We may have restored the peer state from the + // snapshot above. If not, we restore peers from + // the peer state file. + if len(n.peers) == 0 { + if err := restorePeerState(n); err != nil { + return nil, err + } + } + + // Make sure that the snapshots directory exists. + if err := os.MkdirAll(filepath.Join(n.sd, snapshotsDir), defaultDirPerms); err != nil { + n.shutdown() + return nil, fmt.Errorf("could not create snapshots directory - %v", err) + } + + truncateAndErr := func(index uint64) { + if err := n.wal.Truncate(index); err != nil { + n.setWriteErr(err) + } + } + + // Retrieve the stream state from the WAL. If there are pending append + // entries that were committed but not applied before we last shut down, + // we will try to replay them and process them here. + var state StreamState + n.wal.FastState(&state) + n.bytes = state.Bytes + + if state.Msgs > 0 { + n.debug("Replaying state of %d entries", state.Msgs) + + // This process will queue up entries on our applied queue but prior to the upper + // state machine running. So we will monitor how much we have queued and if we + // reach a limit will pause the apply queue and resume inside of run() go routine. + const maxQsz = 32 * 1024 * 1024 // 32MB max + + // It looks like there are entries we have committed but not applied + // yet. Replay them. + for index, qsz := state.FirstSeq, 0; index <= state.LastSeq; index++ { + ae, err := n.loadEntry(index) + // The first entry in our WAL initializes state but must align with our snapshot if we had one. + // Importantly, check this first, as we might need to truncate the WAL further than the index. + if index == state.FirstSeq { + // If the entry is missing, corrupt, or doesn't align with the snapshot, truncate the WAL. + if err != nil || ae == nil || ae.pindex != index-1 || n.pindex != ae.pindex { + if err != nil { + n.warn("Could not load %d from WAL [%+v]: %v", index, state, err) + } else { + n.warn("Misaligned WAL, will truncate") + } + // Truncate to the snapshot or beginning if there is none. + truncateAndErr(n.pindex) + break + } + n.pterm, n.pindex = ae.pterm, ae.pindex + if ae.commit > 0 && ae.commit > n.commit { + n.commit = ae.commit + } + } + if err != nil { + n.warn("Could not load %d from WAL [%+v]: %v", index, state, err) + // Truncate to the previous correct entry. + truncateAndErr(index - 1) + break + } + if ae.pindex != index-1 { + n.warn("Corrupt WAL, will truncate") + // Truncate to the previous correct entry. + truncateAndErr(index - 1) + break + } + n.processAppendEntry(ae, nil) + // Check how much we have queued up so far to determine if we should pause. + for _, e := range ae.entries { + qsz += len(e.Data) + if qsz > maxQsz && !n.paused { + n.PauseApply() + } + } + } + } + + n.debug("Started (cluster size %d, quorum %d)", n.csz, n.qn) + + // Check if we need to start in observer mode due to lame duck status. + // This will stop us from taking on the leader role when we're about to + // shutdown anyway. + if s.isLameDuckMode() { + n.debug("Will start in observer mode due to lame duck status") + n.SetObserver(true) + } + + // Set the election timer and lost quorum timers to now, so that we + // won't accidentally trigger either state without knowing the real state + // of the other nodes. + n.Lock() + n.resetElectionTimeout() + n.llqrt = time.Now() + + // If our log is empty, and we're initializing, relax the "empty log" checks temporarily. + if !cfg.Recovering && n.pindex == 0 { + n.initializing = true + // If we're scaling up and our log is empty, must put ourselves into observer + // and wait for data from the leader. + if !cfg.Observer && cfg.ScaleUp { + n.scaleUp = true + n.setObserverLocked(true, extUndetermined) + } + } + n.Unlock() + + // Register the Raft group. + labels["group"] = n.group + s.registerRaftNode(n.group, n) + + return n, nil +} + +// startRaftNode will start the raft node. +func (s *Server) startRaftNode(accName string, cfg *RaftConfig, labels pprofLabels) (RaftNode, error) { + n, err := s.initRaftNode(accName, cfg, labels) + if err != nil { + return nil, err + } + + // Start the run goroutine for the Raft state machine. + n.wg.Add(1) + s.startGoRoutine(n.run, labels) + + return n, nil +} + +// Returns whether peers within this group claim to support +// moving NRG traffic into the asset account. +// Lock must be held. +func (n *raft) checkAccountNRGStatus() bool { + if !n.s.accountNRGAllowed.Load() { + return false + } + enabled := true + for pn := range n.peers { + if si, ok := n.s.nodeToInfo.Load(pn); ok && si != nil { + enabled = enabled && si.(nodeInfo).accountNRG + } + } + return enabled +} + +// Whether we are using the system account or not. +func (n *raft) IsSystemAccount() bool { + return n.isSysAcc.Load() +} + +// GetTrafficAccountName returns the account name of the account used for replication traffic. +func (n *raft) GetTrafficAccountName() string { + n.RLock() + defer n.RUnlock() + if n.t == nil { + return (*Account)(nil).GetName() + } + return n.t.Account().GetName() +} + +func (n *raft) RecreateInternalSubs() error { + n.Lock() + defer n.Unlock() + return n.recreateInternalSubsLocked() +} + +func (n *raft) recreateInternalSubsLocked() error { + // Sanity check for system account, as it can disappear when + // the system is shutting down. + if n.s == nil { + return fmt.Errorf("server not found") + } + n.s.mu.RLock() + sys := n.s.sys + n.s.mu.RUnlock() + if sys == nil { + return fmt.Errorf("system account not found") + } + + // Default is the system account. + nrgAcc := sys.account + n.isSysAcc.Store(true) + + // Is account NRG enabled in this account and do all group + // peers claim to also support account NRG? + if n.checkAccountNRGStatus() { + // Check whether the account that the asset belongs to + // has volunteered a different NRG account. + target := nrgAcc.Name + if a, _ := n.s.lookupAccount(n.accName); a != nil { + a.mu.RLock() + if a.js != nil { + target = a.nrgAccount + } + a.mu.RUnlock() + } + + // If the target account exists, then we'll use that. + if target != _EMPTY_ { + if a, _ := n.s.lookupAccount(target); a != nil { + nrgAcc = a + if a != sys.account { + n.isSysAcc.Store(false) + } + } + } + } + if n.aesub != nil && n.t.Account() == nrgAcc { + // Subscriptions already exist and the account NRG state + // hasn't changed. + return nil + } + + // Need to cancel any in-progress catch-ups, otherwise the + // inboxes are about to be pulled out from underneath it in + // the next step... + n.cancelCatchup() + + if n.t.Account() != nrgAcc { + n.debug("Subscribing in '%s'", nrgAcc.GetName()) + } + + n.t.Reset(nrgAcc) + + // Recreate any internal subscriptions for voting, append + // entries etc in the new account. + return n.createInternalSubs() +} + +// outOfResources checks to see if we are out of resources. +func (n *raft) outOfResources() bool { + js := n.js + if !n.track || js == nil { + return false + } + return js.limitsExceeded(n.wtype) +} + +// Maps node names back to server names. +func (s *Server) serverNameForNode(node string) string { + if si, ok := s.nodeToInfo.Load(node); ok && si != nil { + return si.(nodeInfo).name + } + return _EMPTY_ +} + +// Maps node names back to cluster names. +func (s *Server) clusterNameForNode(node string) string { + if si, ok := s.nodeToInfo.Load(node); ok && si != nil { + return si.(nodeInfo).cluster + } + return _EMPTY_ +} + +// Registers the Raft node with the server, as it will track all of the Raft +// nodes. +func (s *Server) registerRaftNode(group string, n RaftNode) { + s.rnMu.Lock() + defer s.rnMu.Unlock() + if s.raftNodes == nil { + s.raftNodes = make(map[string]RaftNode) + } + s.raftNodes[group] = n +} + +// Unregisters the Raft node from the server, i.e. at shutdown. +func (s *Server) unregisterRaftNode(group string) { + s.rnMu.Lock() + defer s.rnMu.Unlock() + if s.raftNodes != nil { + delete(s.raftNodes, group) + } +} + +// Returns how many Raft nodes are running in this server instance. +func (s *Server) numRaftNodes() int { + s.rnMu.RLock() + defer s.rnMu.RUnlock() + return len(s.raftNodes) +} + +// Finds the Raft node for a given Raft group, if any. If there is no Raft node +// running for this group then it can return nil. +func (s *Server) lookupRaftNode(group string) RaftNode { + s.rnMu.RLock() + defer s.rnMu.RUnlock() + var n RaftNode + if s.raftNodes != nil { + n = s.raftNodes[group] + } + return n +} + +// Reloads the debug state for all running Raft nodes. This is necessary when +// the configuration has been reloaded and the debug log level has changed. +func (s *Server) reloadDebugRaftNodes(debug bool) { + if s == nil { + return + } + s.rnMu.RLock() + for _, ni := range s.raftNodes { + n := ni.(*raft) + n.Lock() + n.dflag = debug + n.Unlock() + } + s.rnMu.RUnlock() +} + +// Requests that all Raft nodes on this server step down and place them into +// observer mode. This is called when the server is shutting down. +func (s *Server) stepdownRaftNodes() { + if s == nil { + return + } + s.rnMu.RLock() + if len(s.raftNodes) == 0 { + s.rnMu.RUnlock() + return + } + s.Debugf("Stepping down all leader raft nodes") + nodes := make([]RaftNode, 0, len(s.raftNodes)) + for _, n := range s.raftNodes { + nodes = append(nodes, n) + } + s.rnMu.RUnlock() + + for _, node := range nodes { + node.StepDown() + node.SetObserver(true) + } +} + +// Shuts down all Raft nodes on this server. This is called either when the +// server is either entering lame duck mode, shutting down or when JetStream +// has been disabled. +func (s *Server) shutdownRaftNodes() { + if s == nil { + return + } + s.rnMu.RLock() + if len(s.raftNodes) == 0 { + s.rnMu.RUnlock() + return + } + nodes := make([]RaftNode, 0, len(s.raftNodes)) + s.Debugf("Shutting down all raft nodes") + for _, n := range s.raftNodes { + nodes = append(nodes, n) + } + s.rnMu.RUnlock() + + for _, node := range nodes { + node.Stop() + } +} + +// Used in lameduck mode to move off the leaders. +// We also put all nodes in observer mode so new leaders +// can not be placed on this server. +func (s *Server) transferRaftLeaders() bool { + if s == nil { + return false + } + s.rnMu.RLock() + if len(s.raftNodes) == 0 { + s.rnMu.RUnlock() + return false + } + nodes := make([]RaftNode, 0, len(s.raftNodes)) + for _, n := range s.raftNodes { + nodes = append(nodes, n) + } + s.rnMu.RUnlock() + + var didTransfer bool + for _, node := range nodes { + if err := node.StepDown(); err == nil { + didTransfer = true + } + node.SetObserver(true) + } + return didTransfer +} + +// checkFastPathLimits reports whether the proposal queue exceeds a fixed +// entry count or byte limit. When the limit is exceeded, the proposal +// fast path is closed for the given term so proposals fall back to the +// locking path. +func (n *raft) checkFastPathLimits(term uint64) bool { + const ( + maxBatches = 8 + maxBytes = maxBatches * maxBatchBytes + maxEntries = maxBatches * maxBatchEntries + ) + if n.prop.len() > maxEntries || n.prop.size() > maxBytes { + n.fastPathTerm.CompareAndSwap(term, 0) + return true + } + return false +} + +// tryFastPathPropose enqueues the given data buffer to the +// proposal queue, provided that the fast path is open, and that +// the given term matches the current raft term. +// Returns true if it was successfully enqueued. False if the +// caller should fall back to the locking proposal path. +func (n *raft) tryFastPathPropose(term uint64, data []byte) bool { + if term == 0 || term != n.fastPathTerm.Load() || n.State() != Leader || n.checkFastPathLimits(term) { + return false + } + n.prop.push(newProposedEntry(newEntry(EntryNormal, data), _EMPTY_, term)) + return true +} + +// tryFastPathProposeMulti enqueues the given entries to the +// proposal queue, provided that the fast path is open, and that +// the given term matches the current raft term. +// Returns true if the entries were successfully enqueued. False if the +// caller should fall back to the locking proposal path. +func (n *raft) tryFastPathProposeMulti(term uint64, entries []*Entry) bool { + if term == 0 || term != n.fastPathTerm.Load() || n.State() != Leader || n.checkFastPathLimits(term) { + return false + } + _, err := n.prop.pushMany(func(yield func(*proposedEntry) bool) { + for _, e := range entries { + if !yield(newProposedEntry(e, _EMPTY_, term)) { + return + } + } + }) + return err == nil +} + +// Formal API + +// Propose will propose a new entry to the group. +// This should only be called on the leader. +func (n *raft) Propose(term uint64, data []byte) error { + if n.tryFastPathPropose(term, data) { + return nil + } + n.Lock() + defer n.Unlock() + return n.proposeLocked(term, data) +} + +func (n *raft) proposeLocked(term uint64, data []byte) error { + // Check state under lock, we might not be leader anymore. + if state := n.State(); state != Leader || term != n.term { + n.debug("Proposal ignored, not leader (state: %v, cterm: %d, term: %d)", state, term, n.term) + return errNotLeader + } + + // Error if we had a previous write error. + if werr := n.werr; werr != nil { + return werr + } + + if n.isLeaderOverrun() { + var state StreamState + n.wal.FastState(&state) + n.warn("Leader falling behind, stepping down: pindex %d, commit %d, applied %d, WAL size %s", n.pindex, n.commit, n.applied, friendlyBytes(state.Bytes)) + // Stepdown without leader transfer, likely all replicas will be overrun, and we need time to recover. + n.stepdownLocked(noLeader) + n.overrunCount++ + return errNotLeader + } + n.prop.push(newProposedEntry(newEntry(EntryNormal, data), _EMPTY_, n.term)) + return nil +} + +// ProposeMulti will propose multiple entries at once. +// This should only be called on the leader. +func (n *raft) ProposeMulti(term uint64, entries []*Entry) error { + if n.tryFastPathProposeMulti(term, entries) { + return nil + } + n.Lock() + defer n.Unlock() + // Check state under lock, we might not be leader anymore. + if state := n.State(); state != Leader || term != n.term { + n.debug("Multi proposal ignored, not leader (state: %v, cterm: %d, term: %d)", state, term, n.term) + return errNotLeader + } + + // Error if we had a previous write error. + if werr := n.werr; werr != nil { + return werr + } + + if n.isLeaderOverrun() { + var state StreamState + n.wal.FastState(&state) + n.warn("Leader falling behind, stepping down: pindex %d, commit %d, applied %d, WAL size %s", n.pindex, n.commit, n.applied, friendlyBytes(state.Bytes)) + // Stepdown without leader transfer, likely all replicas will be overrun, and we need time to recover. + n.stepdownLocked(noLeader) + n.overrunCount++ + return errNotLeader + } + _, err := n.prop.pushMany(func(yield func(*proposedEntry) bool) { + for _, e := range entries { + if !yield(newProposedEntry(e, _EMPTY_, n.term)) { + return + } + } + }) + return err +} + +// isLeaderOverrun returns whether we are overrun and should step down due to continuously increasing +// uncommitted or unapplied entries. If triggered, this means we're being severely overrun by +// incoming proposals or the system is degraded such that it's too slow (or unable) to process them. +// Stepping down means the system gets to "breathe" for a bit, until a new leader can be elected. +// Lock should be held. +func (n *raft) isLeaderOverrun() bool { + applied := max(n.applied, n.papplied) + commit := max(n.commit, n.papplied) + // We only do this past a high threshold to protect ourselves. + // Worst-case we'll have 2x the threshold, once in uncommitted and once in unapplied entries. + // Either the number of uncommitted entries is over the threshold: we're not getting quorum from our followers. + uncommittedThreshold := n.pindex > commit && n.overrun(n.pindex-commit, pauseQuorumThreshold, pauseQuorumBytes) + // Or, the number of in-memory committed but not yet applied entries is over the threshold: we're slow to apply. + unappliedThreshold := commit > applied && n.overrun(commit-applied, pauseQuorumThreshold, pauseQuorumBytes) + return uncommittedThreshold || unappliedThreshold +} + +// entryBytes estimates the byte size of the given number of entries, based on the +// average size of the entries we are currently holding in our WAL. +// Lock should be held. +func (n *raft) entryBytes(entries uint64) uint64 { + msgs := n.pindex - n.papplied + if msgs == 0 || n.bytes == 0 { + return 0 + } + return entries * n.bytes / msgs +} + +// overrun returns whether the given number of entries is over either the entry count +// threshold or the estimated byte threshold. +// Lock should be held. +func (n *raft) overrun(entries, maxEntries, maxBytes uint64) bool { + return entries > maxEntries || n.entryBytes(entries) > maxBytes +} + +// ForwardProposal will forward the proposal to the leader if known. +// If we are the leader this is the same as calling propose. +func (n *raft) ForwardProposal(entry []byte) error { + if n.State() == Leader { + n.Lock() + defer n.Unlock() + // We pass the node's term so the proposal goes through. This is unavoidable with forwarded + // proposals, normally the passed term MUST be that of the process triggering the proposals. + // So a stale process that is still running isn't allowed to make new proposals past its term. + return n.proposeLocked(n.term, entry) + } + + // TODO: Currently we do not set a reply subject, even though we are + // now capable of responding. Do this once enough time has passed, + // i.e. maybe in 2.12. + n.sendRPC(n.psubj, _EMPTY_, entry) + return nil +} + +// ProposeAddPeer is called to add a peer to the group. +func (n *raft) ProposeAddPeer(peer string) error { + n.RLock() + // Check state under lock, we might not be leader anymore. + if n.State() != Leader { + n.RUnlock() + return errNotLeader + } + // Error if we had a previous write error. + if werr := n.werr; werr != nil { + n.RUnlock() + return werr + } + if n.membChange != nil { + n.RUnlock() + return errMembershipChange + } + prop, term := n.prop, n.term + n.RUnlock() + + prop.push(newProposedEntry(newEntry(EntryAddPeer, []byte(peer)), _EMPTY_, term)) + return nil +} + +// ProposeRemovePeer is called to remove a peer from the group. +func (n *raft) ProposeRemovePeer(peer string) error { + n.RLock() + + // Error if we had a previous write error. + if werr := n.werr; werr != nil { + n.RUnlock() + return werr + } + + if n.State() != Leader { + subj := n.rpsubj + n.RUnlock() + + // Forward the proposal to the leader + n.sendRPC(subj, _EMPTY_, []byte(peer)) + return nil + } + + if n.membChange != nil { + n.RUnlock() + return errMembershipChange + } + if _, ok := n.peers[peer]; !ok { + n.RUnlock() + return errPeerNotFound + } + if len(n.peers) <= 1 { + n.RUnlock() + return errRemoveLastNode + } + + prop, term := n.prop, n.term + n.RUnlock() + + prop.push(newProposedEntry(newEntry(EntryRemovePeer, []byte(peer)), _EMPTY_, term)) + return nil +} + +// EvictPeers forcibly drops the given peers from our membership, without going +// through the log. Returns the peers that were evicted. +// +// This is only valid for managed groups, where the meta layer owns membership. +// A group that has lost quorum can not commit an EntryRemovePeer of its own, so +// it would stay stuck for as long as the downed peers are part of it. The meta +// layer has already committed their removal by quorum, so we can apply that +// decision directly and recover the ability to elect a leader. +// +// Errors for an unmanaged group, one that still has a leader and can shrink +// through its own log instead, or one where the peers left behind could still +// reach quorum without us. +func (n *raft) EvictPeers(peers []string) ([]string, error) { + n.Lock() + defer n.Unlock() + + // Only the meta layer can authorize this, so refuse for unmanaged groups. + if !n.managed { + return nil, errNotManaged + } + // If we have a leader we can remove peers through the log, which is ordered + // and therefore always preferred over deciding membership unilaterally. + if !n.Leaderless() { + return nil, errNotLeaderless + } + // Only safe to evict if the peers we leave behind can not reach quorum without + // us, otherwise they could commit entries we'd never see and we would diverge. + // Must be judged on voting membership, since a peer whose removal we appended + // but never committed can still be a voter for everyone else. + voting := n.votingPeerNames() + var remaining int + for _, peer := range voting { + if peer != n.id && !slices.Contains(peers, peer) { + remaining++ + } + } + if remaining >= len(voting)/2+1 { + return nil, errQuorumPossible + } + // Revert an uncommitted membership change if it's about a peer we're evicting, + // or about ourselves. The leader that appended our removal is gone, and the + // peers that could still commit it are the ones the meta layer peer-removed, + // so it can never become official. Reverting restores us as a member, without + // which we would stay unable to campaign even after evicting the others. + if n.membChange != nil && + (slices.Contains(peers, n.membChange.peer) || n.pendingSelfRemoval()) { + n.revertMembershipChange() + } + var evicted []string + for _, peer := range peers { + // Never evict ourselves, and never empty out the group. + if peer == n.id || len(n.peers) <= 1 { + continue + } + if _, ok := n.peers[peer]; !ok { + continue + } + // Marks the peer as removed so it can not rejoin or be re-added + // automatically, adjusts cluster size and quorum, and persists. + n.removePeer(peer) + evicted = append(evicted, peer) + } + if len(evicted) > 0 { + n.warn("Evicted peers %+v, cluster size is now %d", evicted, n.csz) + } + return evicted, nil +} + +func (n *raft) MembershipChangeInProgress() bool { + n.RLock() + defer n.RUnlock() + return n.membChange != nil +} + +// pendingSelfRemoval reports whether we have an uncommitted removal of ourselves. +// We're speculatively dropped from our own peer set at append time, but the meta +// layer can still consider us a member until the removal commits. +// Lock should be held. +func (n *raft) pendingSelfRemoval() bool { + // A removal tracks the previous membership state, an addition does not. + return n.membChange != nil && n.membChange.peer == n.id && n.membChange.prev != nil +} + +// ClusterSize reports back the total cluster size. +// This effects quorum etc. +func (n *raft) ClusterSize() int { + n.Lock() + defer n.Unlock() + return n.csz +} + +// AdjustBootClusterSize can be called to adjust the boot cluster size. +// Will error if called on a group with a leader or a previous leader. +// This can be helpful in mixed mode. +func (n *raft) AdjustBootClusterSize(csz int) error { + n.Lock() + defer n.Unlock() + + if n.leader != noLeader || n.pleader.Load() { + return errAdjustBootCluster + } + // Same floor as bootstrap. + if csz < 2 { + csz = 2 + } + // Adjust the cluster size and the number of nodes needed to establish + // a quorum. + n.csz = csz + n.recalcQuorum() + + return nil +} + +// AdjustClusterSize will change the cluster set size. +// Must be the leader. +func (n *raft) AdjustClusterSize(csz int) error { + n.Lock() + defer n.Unlock() + + // Check state under lock, we might not be leader anymore. + if n.State() != Leader { + return errNotLeader + } + // Same floor as bootstrap. + if csz < 2 { + csz = 2 + } + + // Adjust the cluster size and the number of nodes needed to establish + // a quorum. + n.csz = csz + n.recalcQuorum() + + n.sendPeerState() + return nil +} + +// recalcQuorum recalculates the number of nodes needed to establish quorum. +// While an unsafe quorum rescue is active (see RescueQuorum) the rescued quorum +// is kept, unless the natural quorum drops to be equal or below it. +// Lock should be held. +func (n *raft) recalcQuorum() { + qn := n.csz/2 + 1 + if n.rescue != nil { + if qn > n.qn { + return + } + n.warn("Unsafe quorum rescue stopped, quorum updated to %d", qn) + n.rescue.Stop() + n.rescue = nil + } + n.qn = qn +} + +// RescueQuorum unsafely lowers the number of nodes needed to establish quorum. +// This is a disaster recovery measure for a group that has permanently lost enough +// peers that it can't elect a leader anymore, allowing the surviving peers to +// re-form quorum and peer-remove the lost peers. +func (n *raft) RescueQuorum(qn int) (prev, cur int, err error) { + n.Lock() + defer n.Unlock() + + prev = n.qn + if n.State() == Closed { + return prev, 0, errNodeClosed + } + // The quorum can only be lowered. The current effective quorum is never + // larger than the peer set size, so this also bounds qn to the peer set. + if qn < 1 || qn > n.qn { + return prev, 0, errRescueBadQuorum + } + // Only allowed if the group is genuinely stuck. We must not know of a + // current leader, and must be a voting member that could participate in + // an election. + if n.leader != noLeader { + return prev, 0, errRescueLeaderKnown + } + if n.observer || n.peers[n.id] == nil { + return prev, 0, errRescueNotVoting + } + // Refuse to rescue a server with an empty log. It could never win an + // election against peers with data, but a lowered quorum could let it be + // elected off empty votes while the lost peers hold the only copies of + // the data. The rescue must be issued on a surviving server with data. + // If all servers are empty, this requires a cluster bootstrap. + if n.pindex == 0 { + return prev, 0, errRescueEmptyLog + } + + // Cancel any previous rescue. + if n.rescue != nil { + n.rescue.Stop() + } + var t *time.Timer + t = time.AfterFunc(rescueQuorumTimeout, func() { + n.Lock() + defer n.Unlock() + // Read of t must be under the lock. + n.expireRescueLocked(t) + }) + n.rescue = t + n.qn = qn + n.warn("Unsafe quorum rescue applied, quorum lowered %d -> %d for %v", prev, qn, rescueQuorumTimeout) + + // Make sure an election can happen soon. + n.resetElect(randCampaignTimeout()) + return prev, qn, nil +} + +// expireRescueLocked runs when the rescue timeout fires and restores the natural quorum. +// Lock should be held. +func (n *raft) expireRescueLocked(t *time.Timer) { + if n.State() == Closed || n.rescue != t { + return + } + // Must clear the timer first, recalcQuorum keeps the rescued quorum + // while it sees an active rescue. + n.rescue = nil + n.recalcQuorum() + n.warn("Unsafe quorum rescue expired, quorum restored to %d", n.qn) +} + +// PauseApply will allow us to pause processing of append entries onto our +// external apply queue. In effect this means that the upper layer will no longer +// receive any new entries from the Raft group. +func (n *raft) PauseApply() error { + if n.State() == Leader { + return errAlreadyLeader + } + n.Lock() + defer n.Unlock() + n.pauseApplyLocked() + return nil +} + +func (n *raft) pauseApplyLocked() { + // If we are currently not a follower, make sure we step down. + if n.State() != Follower { + n.stepdownLocked(noLeader) + } + + n.debug("Pausing our apply channel") + n.paused = true + if n.hcommit < n.commit { + n.hcommit = n.commit + } + // Also prevent us from trying to become a leader while paused and catching up. + n.resetElect(observerModeInterval) +} + +// ResumeApply will resume sending applies to the external apply queue. This +// means that we will start sending new entries to the upper layer. +func (n *raft) ResumeApply() { + n.Lock() + defer n.Unlock() + + if !n.paused { + return + } + + n.debug("Resuming our apply channel") + + // Reset before we start. + n.resetElectionTimeout() + + // Run catchup.. + if n.hcommit > n.commit { + n.debug("Resuming %d replays", n.hcommit+1-n.commit) + for index := n.commit + 1; index <= n.hcommit; index++ { + if err := n.applyCommit(index); err != nil { + n.warn("Got error on apply commit during replay: %v", err) + break + } + // We want to unlock here to allow the upper layers to call Applied() without blocking. + n.Unlock() + // Give hint to let other Go routines run. + // Might not be necessary but seems to make it more fine grained interleaving. + runtime.Gosched() + // Simply re-acquire + n.Lock() + // Need to check if we got closed. + if n.State() == Closed { + return + } + } + } + + // Clear our paused state after we apply. + n.paused = false + n.hcommit = 0 + + // If we had been selected to be the next leader campaign here now that we have resumed. + if n.lxfer { + n.xferCampaign() + } else { + n.resetElectionTimeout() + } +} + +// DrainAndReplaySnapshot will drain the apply queue and replay the snapshot. +// Our highest known commit will be preserved by pausing applies. The caller +// should make sure to call ResumeApply() when handling the snapshot from the +// queue, which will populate the rest of the committed entries in the queue. +func (n *raft) DrainAndReplaySnapshot() bool { + n.Lock() + defer n.Unlock() + // If the node was deleted, we can't replay the snapshot. + if n.deleted { + return false + } + snap, err := n.loadLastSnapshot() + if err != nil { + return false + } + n.warn("Draining and replaying snapshot") + n.pauseApplyLocked() + n.apply.drain() + // Cancel after draining, we might have sent EntryCatchup and need to get them the nil entry. + n.cancelCatchup() + n.commit = snap.lastIndex + n.apply.push(newCommittedEntry(n.commit, []*Entry{{EntrySnapshot, snap.data}})) + return true +} + +// Applied is a callback that must be called by the upper layer when it +// has successfully applied the committed entries that it received from the +// apply queue. It will return the number of entries and an estimation of the +// byte size that could be removed with a snapshot/compact. +func (n *raft) Applied(index uint64) (entries uint64, bytes uint64) { + return n.Processed(index, index) +} + +// Processed is a callback that must be called by the upper layer when it +// has processed the committed entries that it received from the apply queue, +// but it (maybe) hasn't applied all the processed entries yet. +// Used to indicate a commit was processed, even if it wasn't applied yet and +// can't be compacted away by a snapshot just yet. Which allows us to try to +// become leader if we've processed all commits, even if they're not all applied. +func (n *raft) Processed(index uint64, applied uint64) (entries uint64, bytes uint64) { + n.Lock() + defer n.Unlock() + + // Ignore if not applicable. This can happen during a reset. + if index > n.commit { + return 0, 0 + } + + // Ignore if already processed. + if index > n.processed { + n.processed = index + } + + // Ignore if already applied. + if applied > index { + applied = index + } + if applied > n.applied { + n.applied = applied + } + + // If it was set, and we reached the minimum processed index, reset and send signal to upper layer. + // We're not waiting for processed AND applied, because applying could take longer. + if n.aflr > 0 && n.processed >= n.aflr { + n.aflr = 0 + // Quick sanity-check to confirm we're still leader. + // In which case we must signal, since switchToLeader would not have done so already. + if n.State() == Leader { + if !n.leaderState.Swap(true) { + // Only update timestamp if leader state actually changed. + nowts := time.Now().UTC() + n.leaderSince.Store(&nowts) + } + n.updateLeadChange(true) + } + } + + // If we're a R1 node, and we've processed all that we needed to commit, make sure we can become + // leader as quickly as possible, so we don't wait out the election timeout. + if n.processed == n.commit && n.leader == _EMPTY_ && len(n.peers) == 1 && !n.pleader.Load() { + // Need to lower the election timeout, since only the run loop can transition. + n.resetElect(0) + } + + // Calculate the number of entries and estimate the byte size that + // we can now remove with a compaction/snapshot. + if n.applied > n.papplied { + entries = n.applied - n.papplied + } + if msgs := n.pindex - n.papplied; msgs > 0 { + bytes = entries * n.bytes / msgs + } + return entries, bytes +} + +// For capturing data needed by snapshot. +type snapshot struct { + lastTerm uint64 + lastIndex uint64 + peerstate []byte + data []byte +} + +const minSnapshotLen = 28 + +// Encodes a snapshot into a buffer for storage. +// Lock should be held. +func (n *raft) encodeSnapshot(snap *snapshot) []byte { + if snap == nil { + return nil + } + le := binary.LittleEndian + buf := make([]byte, minSnapshotLen+len(snap.peerstate)+len(snap.data)) + le.PutUint64(buf[0:], snap.lastTerm) + le.PutUint64(buf[8:], snap.lastIndex) + // Peer state + le.PutUint32(buf[16:], uint32(len(snap.peerstate))) + wi := 20 + copy(buf[wi:], snap.peerstate) + wi += len(snap.peerstate) + // data itself. + copy(buf[wi:], snap.data) + wi += len(snap.data) + + // Now do the hash for the end. + n.hh.Reset() + n.hh.Write(buf[:wi]) + var hb [highwayhash.Size64]byte + checksum := n.hh.Sum(hb[:0]) + copy(buf[wi:], checksum) + wi += len(checksum) + return buf[:wi] +} + +// SendSnapshot will send the latest snapshot as a normal AE. +// Should only be used when the upper layers know this is most recent. +// Used when restoring streams, moving a stream from R1 to R>1, etc. +func (n *raft) SendSnapshot(data []byte) error { + n.Lock() + defer n.Unlock() + // Don't check if we're leader before sending and storing, this is used on scaleup. + n.sendAppendEntryLocked([]*Entry{{EntrySnapshot, data}}, false) + return nil +} + +// Used to install a snapshot for the given term and applied index. This will release +// all of the log entries up to and including index. This should not be called with +// entries that have been applied to the FSM but have not been applied to the raft state. +func (n *raft) InstallSnapshot(data []byte, force bool) error { + n.Lock() + defer n.Unlock() + + c, err := n.createSnapshotCheckpointLocked(force) + if err != nil { + return err + } + c.n.debug("Installing snapshot of %d bytes [%d:%d]", len(data), c.term, c.applied) + snap := &snapshot{ + lastTerm: c.term, + lastIndex: c.applied, + peerstate: c.peerstate, + data: data, + } + return c.n.installSnapshot(snap) +} + +// Install the snapshot. +// Lock should be held. +func (n *raft) installSnapshot(snap *snapshot) error { + // Always reset, regardless of success or error. + // This is done even though this doesn't come from a checkpoint. We do this so we can + // interrupt/abort an asynchronously running snapshot (if it exists). Ensures the upper layer + // can't overwrite a snapshot that we installed here with an old asynchronously created one. + defer func() { + n.snapshotting = false + }() + + snapDir := filepath.Join(n.sd, snapshotsDir) + sn := fmt.Sprintf(snapFileT, snap.lastTerm, snap.lastIndex) + sfile := filepath.Join(snapDir, sn) + + n.snapLock.Lock() + err := writeFileWithSync(n.dios, sfile, n.encodeSnapshot(snap), defaultFilePerms) + n.snapLock.Unlock() + if err != nil { + // We could set write err here, but if this is a temporary situation, too many open files etc. + // we want to retry and snapshots are not fatal. + return err + } + + // Delete our previous snapshot file if it exists. + if n.snapfile != _EMPTY_ && n.snapfile != sfile { + os.Remove(n.snapfile) + } + // Remember our latest snapshot file. + n.snapfile = sfile + if _, err := n.wal.Compact(snap.lastIndex + 1); err != nil { + n.setWriteErrLocked(err) + return err + } + + // If installing a snapshot past our commits, clear the cache. + if snap.lastIndex > n.commit { + n.resetPendingEntries() + } + + var state StreamState + n.wal.FastState(&state) + n.papplied = snap.lastIndex + n.bytes = state.Bytes + return nil +} + +// CreateSnapshotCheckpoint creates a checkpoint to allow installing a snapshot asynchronously. +// Caller MUST make sure it only ever has one checkpoint handle at most, and either installs or +// aborts the checkpoint. +// See also: RaftNodeCheckpoint +func (n *raft) CreateSnapshotCheckpoint(force bool) (RaftNodeCheckpoint, error) { + n.Lock() + defer n.Unlock() + return n.createSnapshotCheckpointLocked(force) +} + +func (n *raft) createSnapshotCheckpointLocked(force bool) (*checkpoint, error) { + if n.State() == Closed { + return nil, errNodeClosed + } + if !force && n.snapshotting { + return nil, errSnapInProgress + } + + // If a write error has occurred already then stop here. + if werr := n.werr; werr != nil { + return nil, werr + } + + // Check that a catchup isn't already taking place. If it is then we won't + // allow installing snapshots until it is done. + // Unless we're forced to snapshot. We might have been catching up a peer for + // a long period, and this protects our log size from growing indefinitely. + if !force && len(n.progress) > 0 { + return nil, errCatchupsRunning + } + + if n.applied == 0 { + n.debug("Not snapshotting as there are no applied entries") + return nil, errNoSnapAvailable + } + + var term uint64 + if ae, _ := n.loadEntry(n.applied); ae != nil { + term = ae.term + ae.returnToPool() + } else { + n.debug("Not snapshotting as entry %d is not available", n.applied) + return nil, errNoSnapAvailable + } + + // Snapshot the committed peer state for the current applied index, we'll need it in the snapshot. + peerstate := encodePeerState(n.committedPeerStateLocked(n.applied)) + snapDir := filepath.Join(n.sd, snapshotsDir) + snapFile := filepath.Join(snapDir, fmt.Sprintf(snapFileT, term, n.applied)) + + n.snapshotting = true + c := &checkpoint{ + n: n, + term: term, + applied: n.applied, + papplied: n.papplied, + snapFile: snapFile, + peerstate: peerstate, + } + return c, nil +} + +type checkpoint struct { + n *raft // Reference to the RaftNode. + term uint64 // The term of the entry at applied. + applied uint64 // What applied value the snapshot will represent and what the log can be compacted to. + papplied uint64 // Previous applied value of the previous snapshot. + snapFile string // Where the snapshot should be installed. + peerstate []byte // Encoded peerstate generated when creating this checkpoint. +} + +// LoadLastSnapshot loads the last snapshot from disk when using a RaftNodeCheckpoint. +func (c *checkpoint) LoadLastSnapshot() ([]byte, error) { + c.n.Lock() + defer c.n.Unlock() + if c.n.State() == Closed { + return nil, errNodeClosed + } + if !c.n.snapshotting { + // The checkpoint can be aborted at any time, don't continue if that happened. + return nil, errSnapAborted + } + snap, err := c.n.loadLastSnapshot() + if err != nil { + return nil, err + } + if snap.lastIndex != c.papplied { + // Another snapshot was installed in the meantime. This invalidates our checkpoint. + return nil, errors.New("snapshot index mismatch") + } + return snap.data, nil +} + +// AppendEntriesSeq allows iterating over entries that can be compacted as part of a snapshot. +func (c *checkpoint) AppendEntriesSeq() iter.Seq2[*appendEntry, error] { + return func(yield func(*appendEntry, error) bool) { + for index := c.papplied + 1; index <= c.applied; index++ { + c.n.Lock() + if c.n.State() == Closed { + c.n.Unlock() + yield(nil, errNodeClosed) + return + } + if !c.n.snapshotting { + c.n.Unlock() + // The checkpoint can be aborted at any time, don't continue if that happened. + yield(nil, errSnapAborted) + return + } + // Load entry and yield to the caller while unlocked. + ae, err := c.n.loadEntry(index) + c.n.Unlock() + if err != nil { + yield(nil, err) + return + } + hasMore := yield(ae, nil) + ae.returnToPool() + if !hasMore { + return + } + } + } +} + +// Abort can be called to cancel the snapshot installation at any time. +func (c *checkpoint) Abort() { + c.n.Lock() + defer c.n.Unlock() + c.n.snapshotting = false +} + +// InstallSnapshot allows asynchronous installation of a snapshot by unlocking when +// performing operations that don't strictly need to be locked. When the lock is re-acquired +// it's checked that the node is not closed and n.snapshotting is still set to ensure we're +// still meant to install the snapshot. +// Async snapshots can only be used when using CreateSnapshotCheckpoint. +// Lock should be held. +func (c *checkpoint) InstallSnapshot(data []byte) (uint64, error) { + n := c.n + n.Lock() + defer n.Unlock() + if n.State() == Closed { + return 0, errNodeClosed + } + if !n.snapshotting { + // The checkpoint can be aborted at any time, don't continue if that happened. + return 0, errSnapAborted + } + + // Always reset, regardless of success or error. + defer func() { + n.snapshotting = false + }() + + n.debug("Installing snapshot of %d bytes [%d:%d]", len(data), c.term, c.applied) + snap := &snapshot{ + lastTerm: c.term, + lastIndex: c.applied, + peerstate: c.peerstate, + data: data, + } + encoded := n.encodeSnapshot(snap) + + // Before releasing the Raft lock, we acquire the snapshot lock to make + // sure snapshot writes remain serial. + n.snapLock.Lock() + // Unlock while writing. + n.Unlock() + err := writeFileWithSync(n.dios, c.snapFile, encoded, defaultFilePerms) + n.snapLock.Unlock() + n.Lock() + // On any failure path, drop the file we just wrote so it doesn't get + // picked up by setupLastSnapshot on restart. Skip the remove if it's the + // snapshot already adopted into n.snapfile for this term/applied, a direct + // install that was serialized after our write will have overwritten it. + if closed := n.State() == Closed; closed || !n.snapshotting || err != nil { + if c.snapFile != n.snapfile { + os.Remove(c.snapFile) + } + // If the node was closed/deleted while we were writing, the write error + // is just a consequence of that, report the close instead. + if closed { + return 0, errNodeClosed + } else if !n.snapshotting { + return 0, errSnapAborted + } + + // We could set write err here, but if this is a temporary situation, too many open files etc. + // we want to retry and snapshots are not fatal. + return 0, err + } + + // Delete our previous snapshot file if it exists. + if n.snapfile != _EMPTY_ && n.snapfile != c.snapFile { + os.Remove(n.snapfile) + } + // Remember our latest snapshot file. + n.snapfile = c.snapFile + + // Unlock while compacting. + n.Unlock() + _, err = n.wal.Compact(snap.lastIndex + 1) + n.Lock() + if n.State() == Closed { + return 0, errNodeClosed + } else if !n.snapshotting { + // The checkpoint can be aborted at any time, don't continue if that happened. + return 0, errSnapAborted + } else if err != nil { + n.setWriteErrLocked(err) + return 0, err + } + + compacted := n.bytes + var state StreamState + n.wal.FastState(&state) + n.papplied = snap.lastIndex + n.bytes = state.Bytes + + // Expose compacted size. + if n.bytes > compacted { + compacted = 0 + } else { + compacted -= n.bytes + } + return compacted, nil +} + +// NeedSnapshot returns true if it is necessary to try to install a snapshot, i.e. +// after we have finished recovering/replaying at startup, on a regular interval or +// as a part of cleaning up when shutting down. +func (n *raft) NeedSnapshot() bool { + n.RLock() + defer n.RUnlock() + return n.snapfile == _EMPTY_ && n.applied > 0 +} + +const ( + snapshotsDir = "snapshots" + snapFileT = "snap.%d.%d" +) + +// termAndIndexFromSnapfile tries to load the snapshot file and returns the term +// and index from that snapshot. +func termAndIndexFromSnapFile(sn string) (term, index uint64, err error) { + if sn == _EMPTY_ { + return 0, 0, errBadSnapName + } + fn := filepath.Base(sn) + if n, err := fmt.Sscanf(fn, snapFileT, &term, &index); err != nil || n != 2 { + return 0, 0, errBadSnapName + } + if fn != fmt.Sprintf(snapFileT, term, index) { + return 0, 0, errBadSnapName + } + return term, index, nil +} + +// setupLastSnapshot is called at startup to try and recover the last snapshot from +// the disk if possible. We will try to recover the term, index and commit/applied +// indices and then notify the upper layer what we found. Compacts the WAL if needed. +func (n *raft) setupLastSnapshot() error { + snapDir := filepath.Join(n.sd, snapshotsDir) + psnaps, err := os.ReadDir(snapDir) + if err != nil { + if os.IsNotExist(err) { + return errNoSnapAvailable + } + return err + } + + var lterm, lindex uint64 + var latest string + for _, sf := range psnaps { + sfile := filepath.Join(snapDir, sf.Name()) + var term, index uint64 + term, index, err := termAndIndexFromSnapFile(sf.Name()) + if err == nil { + if term > lterm { + lterm, lindex = term, index + latest = sfile + } else if term == lterm && index > lindex { + lindex = index + latest = sfile + } + } else { + // Clean this up, can't parse the name. + // TODO(dlc) - We could read in and check actual contents. + n.debug("Removing snapshot, can't parse name: %q", sf.Name()) + os.Remove(sfile) + } + } + if latest == _EMPTY_ { + return nil + } + + // Set latest snapshot we have. + n.Lock() + defer n.Unlock() + + n.snapfile = latest + snap, err := n.loadLastSnapshot() + if err != nil { + return err + } + + // We successfully recovered the last snapshot from the disk. + // Recover state from the snapshot and then notify the upper layer. + // Compact the WAL when we're done if needed. + n.pindex = snap.lastIndex + n.pterm = snap.lastTerm + // Explicitly only set commit, and not applied. + // Applied will move up when the snapshot is actually applied. + n.commit = snap.lastIndex + n.papplied = snap.lastIndex + // Restore the peerState + ps, err := decodePeerState(snap.peerstate) + if err != nil { + return err + } + n.processPeerState(ps) + n.extSt = ps.domainExt + + n.apply.push(newCommittedEntry(n.commit, []*Entry{{EntrySnapshot, snap.data}})) + if _, err := n.wal.Compact(snap.lastIndex + 1); err != nil { + n.setWriteErrLocked(err) + return err + } + + // Now cleanup any old entries. We only do this once we know that the + // latest snapshot was OK. + for _, sf := range psnaps { + if sfile := filepath.Join(snapDir, sf.Name()); sfile != latest { + n.debug("Removing old snapshot: %q", sfile) + os.Remove(sfile) + } + } + + return nil +} + +func (n *raft) LoadLastSnapshot() (uint64, []byte, error) { + n.Lock() + defer n.Unlock() + snap, err := n.loadLastSnapshot() + if err != nil { + return 0, nil, err + } + if snap.lastIndex != n.papplied { + return 0, nil, errors.New("snapshot index mismatch") + } + return snap.lastIndex, snap.data, nil +} + +// loadLastSnapshot will load and return our last snapshot. +// Lock should be held. +func (n *raft) loadLastSnapshot() (*snapshot, error) { + if n.snapfile == _EMPTY_ { + return nil, errNoSnapAvailable + } + + n.dios.acquire() + buf, err := os.ReadFile(n.snapfile) + n.dios.release() + + if err != nil { + n.warn("Error reading snapshot: %v", err) + return nil, err + } + if len(buf) < minSnapshotLen { + n.warn("Snapshot corrupt, too short") + return nil, errSnapshotCorrupt + } + + // Check to make sure hash is consistent. + hoff := len(buf) - 8 + lchk := buf[hoff:] + n.hh.Reset() + n.hh.Write(buf[:hoff]) + var hb [highwayhash.Size64]byte + if !bytes.Equal(lchk[:], n.hh.Sum(hb[:0])) { + n.warn("Snapshot corrupt, checksums did not match") + return nil, errSnapshotCorrupt + } + + le := binary.LittleEndian + lps := le.Uint32(buf[16:]) + snap := &snapshot{ + lastTerm: le.Uint64(buf[0:]), + lastIndex: le.Uint64(buf[8:]), + peerstate: buf[20 : 20+lps], + data: buf[20+lps : hoff], + } + + // We had a bug in 2.9.12 that would allow snapshots on last index of 0. + // Detect that and continue anyway, nothing else we can do about it. + if snap.lastIndex == 0 { + n.warn("Snapshot with last index 0 is invalid, cleaning up") + os.Remove(n.snapfile) + n.snapfile = _EMPTY_ + return nil, errNoSnapAvailable + } + + return snap, nil +} + +// Leader returns if we are the leader for our group. +// We use an atomic here now vs acquiring the read lock. +func (n *raft) Leader() bool { + if n == nil { + return false + } + return n.leaderState.Load() +} + +// LeaderSince returns how long we have been leader for, +// if applicable. +func (n *raft) LeaderSince() *time.Time { + if n == nil { + return nil + } + return n.leaderSince.Load() +} + +// stepdown immediately steps down the Raft node to the +// follower state. This will take the lock itself. +func (n *raft) stepdown(newLeader string) { + n.Lock() + defer n.Unlock() + n.stepdownLocked(newLeader) +} + +// stepdownLocked immediately steps down the Raft node to the +// follower state. This requires the lock is already held. +func (n *raft) stepdownLocked(newLeader string) { + n.debug("Stepping down") + n.switchToFollowerLocked(newLeader) +} + +// isCatchingUp returns true if a catchup is currently taking place. +func (n *raft) isCatchingUp() bool { + n.RLock() + defer n.RUnlock() + return n.catchup != nil +} + +// isCurrent is called from the healthchecks and returns true if we believe +// that the upper layer is current with the Raft layer, i.e. that it has applied +// all of the commits that we have given it. +// Optionally we can also check whether or not we're making forward progress if we +// aren't current, in which case this function may block for up to ~10ms to find out. +// Lock should be held. +func (n *raft) isCurrent(includeForwardProgress bool) bool { + // Check if we are closed. + if n.State() == Closed { + n.debug("Not current, node is closed") + return false + } + + // Check whether we've made progress on any state, 0 is invalid so not healthy. + if n.commit == 0 { + n.debug("Not current, no commits") + return false + } + + // If we were previously logging about falling behind, also log when the problem + // was cleared. + clearBehindState := func() { + if n.hcbehind { + n.warn("Health check OK, no longer falling behind") + n.hcbehind = false + } + } + + // Make sure we are the leader or we know we have heard from the leader recently. + if n.State() == Leader { + clearBehindState() + return true + } + + // Check to see that we have heard from the current leader lately. + if n.leader != noLeader && n.leader != n.id && n.catchup == nil { + okInterval := hbInterval * 2 + if ps := n.peers[n.leader]; ps == nil || time.Since(ps.ts) > okInterval { + n.debug("Not current, no recent leader contact") + return false + } + } + if cs := n.catchup; cs != nil { + // We're actively catching up, can't mark current even if commit==applied. + n.debug("Not current, still catching up pindex=%d, cindex=%d", n.pindex, cs.cindex) + return false + } + + if n.paused && n.hcommit > n.commit { + // We're currently paused, waiting to be resumed to apply pending commits. + n.debug("Not current, waiting to resume applies commit=%d, hcommit=%d", n.commit, n.hcommit) + return false + } + + if n.commit == n.applied { + // At this point if we are current, we can return saying so. + clearBehindState() + return true + } else if !includeForwardProgress { + // Otherwise, if we aren't allowed to include forward progress + // (i.e. we are checking "current" instead of "healthy") then + // give up now. + return false + } + + // Otherwise, wait for a short period of time and see if we are making any + // forward progress. + if startDelta := n.commit - n.applied; startDelta > 0 { + for i := 0; i < 10; i++ { // 10ms, in 1ms increments + n.Unlock() + time.Sleep(time.Millisecond) + n.Lock() + if n.State() == Closed { + n.debug("Node closed during health check, returning not current") + return false + } + if n.commit-n.applied < startDelta { + // The gap is getting smaller, so we're making forward progress. + clearBehindState() + return true + } + } + } + + n.hcbehind = true + n.warn("Falling behind in health check, commit %d != applied %d", n.commit, n.applied) + return false +} + +// Current returns if we are the leader for our group or an up to date follower. +func (n *raft) Current() bool { + if n == nil { + return false + } + n.Lock() + defer n.Unlock() + return n.isCurrent(false) +} + +// Healthy returns if we are the leader for our group and nearly up-to-date. +func (n *raft) Healthy() bool { + if n == nil { + return false + } + n.Lock() + defer n.Unlock() + return n.isCurrent(true) +} + +// HadPreviousLeader indicates if this group ever had a leader. +func (n *raft) HadPreviousLeader() bool { + return n.pleader.Load() +} + +// GroupLeader returns the current leader of the group. +func (n *raft) GroupLeader() string { + if n == nil { + return noLeader + } + n.RLock() + defer n.RUnlock() + return n.leader +} + +// Leaderless is a lockless way of finding out if the group has a +// leader or not. Use instead of GroupLeader in hot paths. +func (n *raft) Leaderless() bool { + if n == nil { + return true + } + // Negated because we want the default state of hasLeader to be + // false until the first setLeader() call. + return !n.hasleader.Load() +} + +// Guess the best next leader. Stepdown will check more thoroughly. +// Lock should be held. +func (n *raft) selectNextLeader() string { + nextLeader, hli := noLeader, uint64(0) + for peer, ps := range n.peers { + if peer == n.id || ps.li <= hli { + continue + } + hli = ps.li + nextLeader = peer + } + return nextLeader +} + +// StepDown will have a leader stepdown and optionally do a leader transfer. +func (n *raft) StepDown(preferred ...string) error { + n.Lock() + // Check state under lock, we might not be leader anymore. + if n.State() != Leader { + n.Unlock() + return errNotLeader + } + if len(preferred) > 1 { + n.Unlock() + return errTooManyPrefs + } + + n.debug("Being asked to stepdown") + + // See if we have up to date followers. + maybeLeader := noLeader + if len(preferred) > 0 { + if preferred[0] != _EMPTY_ { + maybeLeader = preferred[0] + } else { + preferred = nil + } + } + + // Can't pick ourselves. + if maybeLeader == n.id { + maybeLeader = noLeader + preferred = nil + } + + // If we have a preferred check it first. + if maybeLeader != noLeader { + var isHealthy bool + if ps, ok := n.peers[maybeLeader]; ok { + si, ok := n.s.nodeToInfo.Load(maybeLeader) + isHealthy = ok && !si.(nodeInfo).offline && time.Since(ps.ts) < hbInterval*3 + } + if !isHealthy { + maybeLeader = noLeader + } + } + + // If we do not have a preferred at this point pick the first healthy one. + // Make sure not ourselves. + if maybeLeader == noLeader { + for peer, ps := range n.peers { + if peer == n.id { + continue + } + si, ok := n.s.nodeToInfo.Load(peer) + isHealthy := ok && !si.(nodeInfo).offline && time.Since(ps.ts) < hbInterval*3 + if isHealthy { + maybeLeader = peer + break + } + } + } + n.Unlock() + + if len(preferred) > 0 && maybeLeader == noLeader { + n.debug("Can not transfer to preferred peer %q", preferred[0]) + } + + // If we have a new leader selected, transfer over to them. + // Send the append entry directly rather than via the proposals queue, + // as we will switch to follower state immediately and will blow away + // the contents of the proposal queue in the process. + if maybeLeader != noLeader { + n.debug("Selected %q for new leader, stepping down due to leadership transfer", maybeLeader) + ae := newEntry(EntryLeaderTransfer, []byte(maybeLeader)) + n.sendAppendEntry([]*Entry{ae}) + } + + // Force us to stepdown here. + n.stepdown(noLeader) + + return nil +} + +// Campaign will have our node start a leadership vote. +func (n *raft) Campaign() error { + n.Lock() + defer n.Unlock() + return n.campaign(randCampaignTimeout()) +} + +// CampaignImmediately will have our node start a leadership vote after minimal delay. +func (n *raft) CampaignImmediately() error { + n.Lock() + defer n.Unlock() + n.maybeLeader = true + n.resetInitializing() + return n.campaign(minCampaignTimeout / 2) +} + +func randCampaignTimeout() time.Duration { + delta := rand.Int64N(int64(maxCampaignTimeout - minCampaignTimeout)) + return (minCampaignTimeout + time.Duration(delta)) +} + +// Campaign will have our node start a leadership vote. +// Lock should be held. +func (n *raft) campaign(et time.Duration) error { + n.debug("Starting campaign") + if n.State() == Leader { + return errAlreadyLeader + } + n.resetElect(et) + return nil +} + +// xferCampaign will have our node start an immediate leadership vote. +// Lock should be held. +func (n *raft) xferCampaign() error { + n.debug("Starting transfer campaign") + if n.State() == Leader { + n.lxfer = false + return errAlreadyLeader + } + n.resetElect(10 * time.Millisecond) + return nil +} + +// State returns the current state for this node. +// Upper layers should not check State to check if we're Leader, use n.Leader() instead. +func (n *raft) State() RaftState { + return RaftState(n.state.Load()) +} + +// Progress returns the current index, commit and applied values. +func (n *raft) Progress() (index, commit, applied uint64) { + n.RLock() + defer n.RUnlock() + return n.pindex, n.commit, n.applied +} + +// Size returns number of entries and total bytes for our WAL. +func (n *raft) Size() (entries uint64, bytes uint64) { + n.RLock() + entries = n.pindex - n.papplied + bytes = n.bytes + n.RUnlock() + return entries, bytes +} + +func (n *raft) ID() string { + if n == nil { + return _EMPTY_ + } + // Lock not needed as n.id is never changed after creation. + return n.id +} + +func (n *raft) Group() string { + // Lock not needed as n.group is never changed after creation. + return n.group +} + +func (n *raft) Peers() []*Peer { + n.RLock() + defer n.RUnlock() + + var peers []*Peer + for id, ps := range n.peers { + var current bool + var lag uint64 + if id == n.id { + // We are current and have no lag when compared with ourselves. + current = true + } else if n.id == n.leader { + // We are the leader, we know how many entries this replica has persisted. + // Lag is determined by how many entries we have quorum on in our log that haven't yet + // been persisted on the replica. They are current if there's no lag. + // This will show all peers that are part of quorum as "current". + if n.commit > ps.li { + lag = n.commit - ps.li + } + current = lag == 0 + } else if id == n.leader { + // This peer is the leader, we don't know our lag, but we can report + // on whether we've seen the leader recently. + okInterval := hbInterval * 2 + current = time.Since(ps.ts) <= okInterval + } else { + // The remaining condition is another follower that we're not in contact with. + // We intentionally leave current and lag as empty. + current, lag = false, 0 + } + + p := &Peer{ + ID: id, + Current: current, + Last: ps.ts, + Lag: lag, + } + peers = append(peers, p) + } + return peers +} + +// ApplyQ returns the apply queue that new commits will be sent to for the +// upper layer to apply. +func (n *raft) ApplyQ() *ipQueue[*CommittedEntry] { return n.apply } + +// LeadChangeC returns the leader change channel, notifying when the Raft +// leader role has moved. +func (n *raft) LeadChangeC() <-chan leadChange { return n.leadc } + +// QuitC returns the quit channel, notifying when the Raft group has shut down. +func (n *raft) QuitC() <-chan struct{} { return n.quit } + +func (n *raft) Created() time.Time { + // Lock not needed as n.created is never changed after creation. + return n.created +} + +func (n *raft) Stop() { + n.shutdown() +} + +func (n *raft) WaitForStop() { + if n.state.Load() == int32(Closed) { + n.wg.Wait() + } +} + +func (n *raft) Delete() { + n.shutdown() + n.wg.Wait() + + n.Lock() + defer n.Unlock() + + n.deleted = true + if wal := n.wal; wal != nil { + wal.Delete(false) + } + os.RemoveAll(n.sd) + n.snapfile = _EMPTY_ + n.debug("Deleted") +} + +func (n *raft) IsDeleted() bool { + n.RLock() + defer n.RUnlock() + return n.deleted +} + +func (n *raft) shutdown() { + // First call to Stop or Delete should close the quit chan + // to notify the runAs goroutines to stop what they're doing. + if n.state.Swap(int32(Closed)) != int32(Closed) { + n.leaderState.Store(false) + n.leaderSince.Store(nil) + close(n.quit) + } +} + +// Reset discards this node's local raft state (log, snapshots, peer set, +// term/vote) so it can be caught up cleanly by another group with the same +// name. The caller is responsible for parking the node first (typically via +// SetObserver) if it should not compete for leadership immediately after; +// Reset itself steps the node down but does not flip observer mode. +func (n *raft) Reset() { + n.Lock() + defer n.Unlock() + + n.debug("Resetting Raft state") + + n.stepdownLocked(_EMPTY_) + + // Drop proposals and inbound entries; they are no longer meaningful + // against whatever log this node ends up following. + n.prop.drain() + n.entry.drain() + n.resp.drain() + n.apply.drain() + n.reqs.drain() + n.votes.drain() + + // Cancel any in-flight catchup so it does not race the reset. + // Cancel after draining, we might have sent EntryCatchup and need to get them the nil entry. + n.cancelCatchup() + + // Remove every snapshot under our snapshots dir, not just the one referenced + // by n.snapfile. Orphans (e.g. from a crash between install and the previous + // file's removal) would otherwise be picked up by setupLastSnapshot on the + // next restart and reseed the state we are discarding here. + snapDir := filepath.Join(n.sd, snapshotsDir) + if err := os.RemoveAll(snapDir); err != nil { + n.warn("Error removing snapshots directory during reset: %v", err) + } + if err := os.MkdirAll(snapDir, defaultDirPerms); err != nil { + n.warn("Error recreating snapshots directory during reset: %v", err) + } + n.snapfile = _EMPTY_ + + // Abort any inflight async snapshot checkpoint. + n.snapshotting = false + + // Reset the WAL, but reset these first to not trip the assertion. + n.commit, n.hcommit, n.applied, n.processed, n.papplied = 0, 0, 0, 0, 0 + n.resetWAL() + + // Reset peer set to just ourselves; a new leader will fold us back into + // the cluster's membership view via processPeerState. + n.peers = map[string]*lps{n.id: {time.Time{}, 0}} + n.removed = nil + n.adjustClusterSizeAndQuorum() + + n.term, n.vote = 0, _EMPTY_ + n.writeTermVote() + + // Persist the cleared peer state so a restart picks up the reset. + n.writePeerState(n.currentPeerStateLocked()) +} + +const ( + raftAllSubj = "$NRG.>" + raftVoteSubj = "$NRG.V.%s" + raftAppendSubj = "$NRG.AE.%s" + raftPropSubj = "$NRG.P.%s" + raftRemovePeerSubj = "$NRG.RP.%s" + raftReply = "$NRG.R.%s" + raftCatchupReply = "$NRG.CR.%s" + // Catchup progress replies happen in their own subject space for + // flow control to the leader, but not counting toward quorum. + raftCatchupProgressReply = "$NRG.CP.%s" + raftCatchupProgressPre = "$NRG.CP." +) + +// Lock should be held (due to use of random generator) +func (n *raft) newCatchupInbox() string { + var b [replySuffixLen]byte + rn := rand.Uint64() + for i, l := 0, rn; i < len(b); i++ { + b[i] = digits[l%base] + l /= base + } + return fmt.Sprintf(raftCatchupReply, b[:]) +} + +// Lock should be held (due to use of random generator) +func (n *raft) newCatchupProgressInbox() string { + var b [replySuffixLen]byte + rn := rand.Uint64() + for i, l := 0, rn; i < len(b); i++ { + b[i] = digits[l%base] + l /= base + } + return fmt.Sprintf(raftCatchupProgressReply, b[:]) +} + +func (n *raft) newInbox() string { + var b [replySuffixLen]byte + rn := rand.Uint64() + for i, l := 0, rn; i < len(b); i++ { + b[i] = digits[l%base] + l /= base + } + return fmt.Sprintf(raftReply, b[:]) +} + +// Our internal subscribe. +// Lock should be held. +func (n *raft) subscribe(subject string, cb msgHandler) (*subscription, error) { + return n.t.Subscribe(subject, cb) +} + +// Lock should be held. +func (n *raft) unsubscribe(sub *subscription) { + n.t.Unsubscribe(sub) +} + +// Lock should be held. +func (n *raft) createInternalSubs() error { + n.vsubj, n.vreply = fmt.Sprintf(raftVoteSubj, n.group), n.newInbox() + n.asubj, n.areply = fmt.Sprintf(raftAppendSubj, n.group), n.newInbox() + n.psubj = fmt.Sprintf(raftPropSubj, n.group) + n.rpsubj = fmt.Sprintf(raftRemovePeerSubj, n.group) + + // Votes + if _, err := n.subscribe(n.vreply, n.handleVoteResponse); err != nil { + return err + } + if _, err := n.subscribe(n.vsubj, n.handleVoteRequest); err != nil { + return err + } + // AppendEntry + if _, err := n.subscribe(n.areply, n.handleAppendEntryResponse); err != nil { + return err + } + if sub, err := n.subscribe(n.asubj, n.handleAppendEntry); err != nil { + return err + } else { + n.aesub = sub + } + + return nil +} + +func randElectionTimeout() time.Duration { + delta := rand.Int64N(int64(maxElectionTimeout - minElectionTimeout)) + return (minElectionTimeout + time.Duration(delta)) +} + +// Lock should be held. +func (n *raft) resetElectionTimeout() { + n.resetElect(randElectionTimeout()) +} + +func (n *raft) resetElectionTimeoutWithLock() { + n.resetElectWithLock(randElectionTimeout()) +} + +// Lock should be held. +func (n *raft) resetElect(et time.Duration) { + n.etlr = time.Now() + if n.elect == nil { + n.elect = time.NewTimer(et) + } else { + if !n.elect.Stop() { + select { + case <-n.elect.C: + default: + } + } + n.elect.Reset(et) + } +} + +func (n *raft) resetElectWithLock(et time.Duration) { + n.Lock() + n.resetElect(et) + n.Unlock() +} + +// run is the top-level runner for the Raft state machine. Depending on the +// state of the node (leader, follower, candidate, observer), this will call +// through to other functions. It is expected that this function will run for +// the entire life of the Raft node once started. +func (n *raft) run() { + s := n.s + defer s.grWG.Done() + defer n.wg.Done() + + // We want to wait for some routing to be enabled, so we will wait for + // at least a route, leaf or gateway connection to be established before + // starting the run loop. + for gw := s.gateway; ; { + s.mu.RLock() + ready, gwEnabled := s.numRemotes()+len(s.leafs) > 0, gw.enabled + s.mu.RUnlock() + if !ready && gwEnabled { + gw.RLock() + ready = len(gw.out)+len(gw.in) > 0 + gw.RUnlock() + } + if !ready { + select { + case <-s.quitCh: + return + case <-time.After(100 * time.Millisecond): + s.RateLimitWarnf("Waiting for routing to be established...") + } + } else { + break + } + } + + // We may have paused adding entries to apply queue, resume here. + // No-op if not paused. + n.ResumeApply() + + // Send nil entry to signal the upper layers we are done doing replay/restore. + n.apply.push(nil) + +runner: + for { + switch n.State() { + case Follower: + n.runAsFollower() + case Candidate: + n.runAsCandidate() + case Leader: + n.runAsLeader() + case Closed: + break runner + } + } + + // If we've reached this point then we're shutting down, either because + // the server is stopping or because the Raft group is closing/closed. + n.Lock() + defer n.Unlock() + + n.t.Close() + + // Unregistering ipQueues do not prevent them from push/pop + // just will remove them from the central monitoring map + queues := []interface { + unregister() + drain() int + }{n.reqs, n.votes, n.prop, n.entry, n.resp, n.apply} + for _, q := range queues { + q.drain() + q.unregister() + } + + n.s.unregisterRaftNode(n.group) + + if wal := n.wal; wal != nil { + wal.Stop() + } + + n.debug("Shutdown") +} + +func (n *raft) debug(format string, args ...any) { + if n.dflag { + nf := fmt.Sprintf("RAFT [%s - %s] %s", n.id, n.group, format) + n.s.Debugf(nf, args...) + } +} + +func (n *raft) warn(format string, args ...any) { + nf := fmt.Sprintf("RAFT [%s - %s] %s", n.id, n.group, format) + n.s.RateLimitWarnf(nf, args...) +} + +func (n *raft) error(format string, args ...any) { + nf := fmt.Sprintf("RAFT [%s - %s] %s", n.id, n.group, format) + n.s.Errorf(nf, args...) +} + +func (n *raft) electTimer() *time.Timer { + n.RLock() + defer n.RUnlock() + return n.elect +} + +func (n *raft) IsObserver() bool { + n.RLock() + defer n.RUnlock() + return n.observer +} + +// Sets the state to observer only. +func (n *raft) SetObserver(isObserver bool) { + n.setObserver(isObserver, extUndetermined) +} + +func (n *raft) setObserver(isObserver bool, extSt extensionState) { + n.Lock() + defer n.Unlock() + n.setObserverLocked(isObserver, extSt) +} + +func (n *raft) setObserverLocked(isObserver bool, extSt extensionState) { + wasObserver := n.observer + n.observer = isObserver + n.extSt = extSt + + // If we're leaving observer state then reset the election timer or + // we might end up waiting for up to the observerModeInterval. + if wasObserver && !isObserver { + n.resetElect(randElectionTimeout()) + } +} + +// processAppendEntries is called by the Raft state machine when there are +// new append entries to be committed and sent to the upper state machine. +func (n *raft) processAppendEntries() { + canProcess := true + if n.isClosed() { + n.debug("AppendEntry not processing inbound, closed") + canProcess = false + } + if n.outOfResources() { + n.debug("AppendEntry not processing inbound, no resources") + canProcess = false + } + // Always pop the entries, but check if we can process them. If we can't + // then the entries are effectively dropped. + aes := n.entry.pop() + if canProcess { + for _, ae := range aes { + n.processAppendEntry(ae, ae.sub) + } + } + n.entry.recycle(&aes) +} + +// runAsFollower is called by run and will block for as long as the node is +// running in the follower state. +func (n *raft) runAsFollower() { + for n.State() == Follower { + elect := n.electTimer() + + select { + case <-n.entry.ch: + // New append entries have arrived over the network. + n.processAppendEntries() + case <-n.s.quitCh: + // The server is shutting down. + return + case <-n.quit: + // The Raft node is shutting down. + return + case <-elect.C: + // The election timer has fired so we think it's time to call an election. + // If we are out of resources we just want to stay in this state for the moment. + if n.outOfResources() { + n.resetElectionTimeoutWithLock() + n.debug("Not switching to candidate, no resources") + } else if n.IsObserver() { + n.resetElectWithLock(observerModeInterval) + n.debug("Not switching to candidate, observer only") + } else if n.isCatchingUp() { + n.debug("Not switching to candidate, catching up") + // Check to see if our catchup has stalled. + n.Lock() + if n.catchupStalled() { + n.cancelCatchup() + } + n.resetElectionTimeout() + n.Unlock() + } else { + n.switchToCandidate() + return + } + case <-n.votes.ch: + // We're receiving votes from the network, probably because we have only + // just stepped down and they were already in flight. Ignore them. + n.debug("Ignoring old vote response, we have stepped down") + n.votes.popOne() + case <-n.resp.ch: + // Ignore append entry responses received from before the state change. + n.resp.drain() + case <-n.prop.ch: + // Ignore proposals received from before the state change. + n.prop.drain() + case <-n.reqs.ch: + // We've just received a vote request from the network. + // Because of drain() it is possible that we get nil from popOne(). + if voteReq, ok := n.reqs.popOne(); ok { + n.processVoteRequest(voteReq) + } + } + } +} + +// Pool for CommittedEntry re-use. +var cePool = sync.Pool{ + New: func() any { + return &CommittedEntry{} + }, +} + +// CommittedEntry is handed back to the user to apply a commit to their upper layer. +type CommittedEntry struct { + Index uint64 + Entries []*Entry +} + +// Create a new CommittedEntry. When the returned entry is no longer needed, it +// should be returned to the pool by calling ReturnToPool. +func newCommittedEntry(index uint64, entries []*Entry) *CommittedEntry { + ce := cePool.Get().(*CommittedEntry) + ce.Index, ce.Entries = index, entries + return ce +} + +// ReturnToPool returns the CommittedEntry to the pool, after which point it is +// no longer safe to reuse. +func (ce *CommittedEntry) ReturnToPool() { + if ce == nil { + return + } + if len(ce.Entries) > 0 { + for _, e := range ce.Entries { + entryPool.Put(e) + } + } + ce.Index, ce.Entries = 0, nil + cePool.Put(ce) +} + +// Pool for Entry re-use. +var entryPool = sync.Pool{ + New: func() any { + return &Entry{} + }, +} + +// Helper to create new entries. When the returned entry is no longer needed, it +// should be returned to the entryPool pool. +func newEntry(t EntryType, data []byte) *Entry { + entry := entryPool.Get().(*Entry) + entry.Type, entry.Data = t, data + return entry +} + +// Pool for appendEntry re-use. +var aePool = sync.Pool{ + New: func() any { + return &appendEntry{} + }, +} + +// appendEntry is the main struct that is used to sync raft peers. +type appendEntry struct { + leader string // The leader that this append entry came from. + term uint64 // The term when this entry was stored. + commit uint64 // The commit index of the leader when this append entry was sent. + pterm uint64 // The previous term, for checking consistency. + pindex uint64 // The previous commit index, for checking consistency. + entries []*Entry // Entries to process. + // Below fields are for internal use only: + lterm uint64 // The highest term for catchups only, as the leader understands it. (If lterm=0, use term instead) + reply string // Reply subject to respond to once committed. + sub *subscription // The subscription that the append entry came in on. + buf []byte +} + +// Create a new appendEntry. +func newAppendEntry(leader string, term, commit, pterm, pindex uint64, entries []*Entry) *appendEntry { + ae := aePool.Get().(*appendEntry) + ae.leader, ae.term, ae.commit, ae.pterm, ae.pindex, ae.entries = leader, term, commit, pterm, pindex, entries + ae.lterm, ae.reply, ae.sub, ae.buf = 0, _EMPTY_, nil, nil + return ae +} + +// Will return this append entry, and its interior entries to their respective pools. +func (ae *appendEntry) returnToPool() { + ae.entries, ae.buf, ae.sub, ae.reply = nil, nil, nil, _EMPTY_ + aePool.Put(ae) +} + +// Pool for proposedEntry re-use. +var pePool = sync.Pool{ + New: func() any { + return &proposedEntry{} + }, +} + +// Create a new single proposedEntry tagged with the term in which it was accepted. +func newProposedEntry(entry *Entry, reply string, term uint64) *proposedEntry { + pe := pePool.Get().(*proposedEntry) + pe.Entry, pe.reply, pe.term = entry, reply, term + return pe +} + +// Will return this proosed entry. +func (pe *proposedEntry) returnToPool() { + pe.Entry, pe.reply, pe.term = nil, _EMPTY_, 0 + pePool.Put(pe) +} + +type EntryType uint8 + +const ( + EntryNormal EntryType = iota + EntryOldSnapshot + EntryPeerState + EntryAddPeer + EntryRemovePeer + EntryLeaderTransfer + EntrySnapshot + // EntryCatchup signals an internal type used to signal a Raft-level catchup has started. + // After the catchup completes (or is canceled), a nil entry will be sent to signal this. + // This type of entry is purely internal and not transmitted between peers or stored in the log. + EntryCatchup +) + +func (t EntryType) String() string { + switch t { + case EntryNormal: + return "Normal" + case EntryOldSnapshot: + return "OldSnapshot" + case EntryPeerState: + return "PeerState" + case EntryAddPeer: + return "AddPeer" + case EntryRemovePeer: + return "RemovePeer" + case EntryLeaderTransfer: + return "LeaderTransfer" + case EntrySnapshot: + return "Snapshot" + } + return fmt.Sprintf("Unknown [%d]", uint8(t)) +} + +type Entry struct { + Type EntryType + Data []byte +} + +func (e *Entry) ChangesMembership() bool { + switch e.Type { + case EntryAddPeer, EntryRemovePeer: + return true + default: + return false + } +} + +func (ae *appendEntry) String() string { + return fmt.Sprintf("&{leader:%s term:%d commit:%d pterm:%d pindex:%d entries: %d}", + ae.leader, ae.term, ae.commit, ae.pterm, ae.pindex, len(ae.entries)) +} + +const appendEntryBaseLen = idLen + 4*8 + 2 + +func (ae *appendEntry) encode(b []byte) ([]byte, error) { + if ll := len(ae.leader); ll != idLen && ll != 0 { + return nil, errLeaderLen + } + if len(ae.entries) > math.MaxUint16 { + return nil, errTooManyEntries + } + + var elen uint64 + for _, e := range ae.entries { + // MaxInt32 instead of MaxUint32 deliberate here to stop int + // overflow on 32-bit platforms, still gives us ~2GB limit. + ulen := uint64(len(e.Data)) + if ulen > math.MaxInt32 { + return nil, errBadAppendEntry + } + elen += ulen + 1 + 4 // 1 is type, 4 is for size. + } + // Uvarint for lterm can be a maximum 10 bytes for a uint64. + var _lterm [10]byte + lterm := _lterm[:binary.PutUvarint(_lterm[:], ae.lterm)] + tlen := appendEntryBaseLen + elen + uint64(len(lterm)) + + var buf []byte + if uint64(cap(b)) >= tlen { + buf = b[:idLen] + } else { + buf = make([]byte, idLen, tlen) + } + + le := binary.LittleEndian + copy(buf[:idLen], ae.leader) + buf = le.AppendUint64(buf, ae.term) + buf = le.AppendUint64(buf, ae.commit) + buf = le.AppendUint64(buf, ae.pterm) + buf = le.AppendUint64(buf, ae.pindex) + buf = le.AppendUint16(buf, uint16(len(ae.entries))) + for _, e := range ae.entries { + // The +1 is safe here as we've already checked len(e.Data) + // is not greater than MaxInt32, which is less than MaxUint32. + buf = le.AppendUint32(buf, uint32(1+len(e.Data))) + buf = append(buf, byte(e.Type)) + buf = append(buf, e.Data...) + } + // This is safe because old nodes will ignore bytes after the + // encoded messages. Nodes that are aware of this will decode + // it correctly. + buf = append(buf, lterm...) + return buf, nil +} + +// This can not be used post the wire level callback since we do not copy. +func decodeAppendEntry(msg []byte, sub *subscription, reply string) (*appendEntry, error) { + if len(msg) < appendEntryBaseLen { + return nil, errBadAppendEntry + } + + le := binary.LittleEndian + + ae := newAppendEntry(string(msg[:idLen]), le.Uint64(msg[8:]), le.Uint64(msg[16:]), le.Uint64(msg[24:]), le.Uint64(msg[32:]), nil) + ae.reply, ae.sub = reply, sub + + // Decode Entries. + ne, ri := int(le.Uint16(msg[40:])), uint64(appendEntryBaseLen) + for i, max := 0, uint64(len(msg)); i < ne; i++ { + if max-ri < 4 { + return nil, errBadAppendEntry + } + ml := uint64(le.Uint32(msg[ri:])) + ri += 4 + if ml <= 0 || ri+ml > max { + return nil, errBadAppendEntry + } + entry := newEntry(EntryType(msg[ri]), msg[ri+1:ri+ml]) + ae.entries = append(ae.entries, entry) + ri += ml + } + if len(msg[ri:]) > 0 { + if lterm, n := binary.Uvarint(msg[ri:]); n > 0 { + ae.lterm = lterm + } + } + ae.buf = msg + return ae, nil +} + +// Pool for appendEntryResponse re-use. +var arPool = sync.Pool{ + New: func() any { + return &appendEntryResponse{} + }, +} + +// We want to make sure this does not change from system changing length of syshash. +const ( + idLen = 8 + appendEntryResponseLen = 24 + 1 +) + +// appendEntryResponse is our response to a received appendEntry. +type appendEntryResponse struct { + term uint64 + index uint64 + peer string + reply string // internal usage. + success bool +} + +// Create a new appendEntryResponse. +func newAppendEntryResponse(term, index uint64, peer string, success bool) *appendEntryResponse { + ar := arPool.Get().(*appendEntryResponse) + ar.term, ar.index, ar.peer, ar.success = term, index, peer, success + // Always empty out. + ar.reply = _EMPTY_ + return ar +} + +func (ar *appendEntryResponse) encode(b []byte) []byte { + var buf []byte + if cap(b) >= appendEntryResponseLen { + buf = b[:appendEntryResponseLen] + } else { + buf = make([]byte, appendEntryResponseLen) + } + le := binary.LittleEndian + le.PutUint64(buf[0:], ar.term) + le.PutUint64(buf[8:], ar.index) + copy(buf[16:16+idLen], ar.peer) + if ar.success { + buf[24] = 1 + } else { + buf[24] = 0 + } + return buf[:appendEntryResponseLen] +} + +// Track all peers we may have ever seen to use an string interns for appendEntryResponse decoding. +var peers sync.Map + +func decodeAppendEntryResponse(msg []byte) *appendEntryResponse { + if len(msg) != appendEntryResponseLen { + return nil + } + le := binary.LittleEndian + ar := arPool.Get().(*appendEntryResponse) + ar.term = le.Uint64(msg[0:]) + ar.index = le.Uint64(msg[8:]) + + peer, ok := peers.Load(string(msg[16 : 16+idLen])) + if !ok { + // We missed so store inline here. + peer = string(msg[16 : 16+idLen]) + peers.Store(peer, peer) + } + ar.peer = peer.(string) + ar.success = msg[24] == 1 + return ar +} + +// Called when a remove peer proposal has been forwarded +func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { + n.debug("Received forwarded remove peer proposal: %q", msg) + + if len(msg) != idLen { + n.warn("Received invalid peer name for remove proposal: %q", msg) + return + } + + n.RLock() + // Check state under lock, we might not be leader anymore. + if n.State() != Leader || !n.leaderState.Load() { + n.debug("Ignoring forwarded peer removal proposal, not leader") + n.RUnlock() + return + } + // Error if we had a previous write error. + if werr := n.werr; werr != nil { + n.RUnlock() + return + } + if n.membChange != nil { + n.debug("Ignoring forwarded peer removal proposal, membership changing") + n.RUnlock() + return + } + if _, ok := n.peers[string(msg)]; !ok { + n.debug("Ignoring forwarded peer removal proposal, peer not found") + n.RUnlock() + return + } + if len(n.peers) <= 1 { + n.debug("Ignoring forwarded peer removal proposal, remove last node") + n.RUnlock() + return + } + prop, term := n.prop, n.term + n.RUnlock() + + // Need to copy since this is underlying client/route buffer. + peer := copyBytes(msg) + prop.push(newProposedEntry(newEntry(EntryRemovePeer, peer), reply, term)) +} + +// Called when a peer has forwarded a proposal. +func (n *raft) handleForwardedProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { + // Need to copy since this is underlying client/route buffer. + msg = copyBytes(msg) + + n.RLock() + prop := n.prop + // Check state under lock, we might not be leader anymore. + if n.State() != Leader || !n.leaderState.Load() { + n.debug("Ignoring forwarded proposal, not leader") + n.RUnlock() + return + } + + // Ignore if we have had a write error previous. + if n.werr != nil { + n.RUnlock() + return + } + term := n.term + if n.isLeaderOverrun() { + n.RUnlock() + n.Lock() + defer n.Unlock() + // Now that we've reacquired as write lock, we need to make sure that everything we + // believed before is still true. Otherwise we've either stepped down already from + // another goroutine or we've stopped being overrun and shouldn't drop the entry. + if n.State() != Leader || !n.leaderState.Load() { + return + } else if !n.isLeaderOverrun() { + prop.push(newProposedEntry(newEntry(EntryNormal, msg), reply, n.term)) + return + } + var state StreamState + n.wal.FastState(&state) + n.warn("Leader falling behind, stepping down: pindex %d, commit %d, applied %d, WAL size %s", n.pindex, n.commit, n.applied, friendlyBytes(state.Bytes)) + // Stepdown without leader transfer, likely all replicas will be overrun, and we need time to recover. + n.stepdownLocked(noLeader) + n.overrunCount++ + return + } + // Possible that we could fall through to here from multiple connections but if + // one does end up stepping down then the proposal queue gets drained anyway. + n.RUnlock() + prop.push(newProposedEntry(newEntry(EntryNormal, msg), reply, term)) +} + +// Adds peer with the given id to our membership, +// and adjusts cluster size and quorum accordingly. +// Lock should be held. +func (n *raft) addPeer(peer string) { + // If we were on the removed list reverse that here. + if n.removed != nil { + delete(n.removed, peer) + if len(n.removed) == 0 { + n.removed = nil + } + } + + if _, ok := n.peers[peer]; !ok { + // We are not tracking this one automatically so we need + // to bump cluster size. + n.peers[peer] = &lps{time.Time{}, 0} + } + // Adjust cluster size and quorum if needed. + n.adjustClusterSizeAndQuorum() + // Write out our new state. + n.writePeerState(&peerState{n.peerNames(), n.csz, n.extSt}) +} + +// Remove the peer with the given id from our membership, +// and adjusts cluster size and quorum accordingly. +// Lock should be held. +func (n *raft) removePeer(peer string) { + if n.removed == nil { + n.removed = map[string]time.Time{} + } + n.removed[peer] = time.Now() + + delete(n.peers, peer) + // Clear observed state if it was still lingering. + if n.observed != nil { + delete(n.observed, peer) + if len(n.observed) == 0 { + n.observed = nil + } + } + n.adjustClusterSizeAndQuorum() + n.writePeerState(&peerState{n.peerNames(), n.csz, n.extSt}) +} + +// Build and send appendEntry request for the given entry that changes +// membership (EntryAddPeer / EntryRemovePeer). +// Returns true if the entry made it to the WAL and was sent to the followers +func (n *raft) sendMembershipChange(e *Entry) bool { + n.Lock() + defer n.Unlock() + + // Only makes sense to call this with entries that change membership. + // Also, ignore if we're already changing membership. + if !e.ChangesMembership() || n.membChange != nil { + return false + } + + // Set to the index where we will store the membership change. + // It needs to be before we send, since if we're cluster size 1 we try to commit immediately. + peer := string(e.Data) + n.membChange = &membChange{index: n.pindex + 1, peer: peer} + ps := n.peers[peer] + // Ignore if the peer already exists (add) or is not found (remove). + if (e.Type == EntryAddPeer && ps != nil) || (e.Type == EntryRemovePeer && ps == nil) { + n.membChange = nil + return false + } + if e.Type == EntryRemovePeer { + n.membChange.prev = ps + } + err := n.sendAppendEntryLocked([]*Entry{e}, true) + if err != nil { + n.membChange = nil + return false + } + + if e.Type == EntryAddPeer { + // Track directly, but wait for commit to be official + if _, ok := n.peers[peer]; !ok { + n.peers[peer] = &lps{time.Time{}, 0} + n.adjustClusterSizeAndQuorum() + } + } + + if e.Type == EntryRemovePeer { + // Track directly, but wait for commit to be official + if _, ok := n.peers[peer]; ok { + delete(n.peers, peer) + n.adjustClusterSizeAndQuorum() + } + if n.qn <= 1 { + n.tryCommit(n.pindex) + return true + } + } + return true +} + +const ( + maxBatchBytes = 256 * 1024 + maxBatchEntries = 4096 // larger batches showed no benefit +) + +func (n *raft) runAsLeader() { + if n.State() == Closed { + return + } + + n.Lock() + psubj, rpsubj, term := n.psubj, n.rpsubj, n.term + + // For forwarded proposals, both normal and remove peer proposals. + fsub, err := n.subscribe(psubj, n.handleForwardedProposal) + if err != nil { + n.warn("Error subscribing to forwarded proposals: %v", err) + n.stepdownLocked(noLeader) + n.Unlock() + return + } + rpsub, err := n.subscribe(rpsubj, n.handleForwardedRemovePeerProposal) + if err != nil { + n.warn("Error subscribing to forwarded remove peer proposals: %v", err) + n.unsubscribe(fsub) + n.stepdownLocked(noLeader) + n.Unlock() + return + } + + // Cleanup our subscription when we leave. + defer func() { + n.Lock() + n.unsubscribe(fsub) + n.unsubscribe(rpsub) + n.Unlock() + }() + n.Unlock() + + hb := time.NewTicker(hbInterval) + defer hb.Stop() + + lq := time.NewTicker(lostQuorumCheck) + defer lq.Stop() + + for n.State() == Leader { + select { + case <-n.s.quitCh: + return + case <-n.quit: + return + case <-n.resp.ch: + ars := n.resp.pop() + for _, ar := range ars { + n.processAppendEntryResponse(ar) + } + n.resp.recycle(&ars) + case <-n.prop.ch: + var entries []*Entry + + es, sz := n.prop.pop(), 0 + for _, b := range es { + if b.term != term { + continue + } + if b.ChangesMembership() { + n.sendMembershipChange(b.Entry) + continue + } + entries = append(entries, b.Entry) + // Increment size. + sz += len(b.Data) + 1 + // If below thresholds go ahead and send. + if sz < maxBatchBytes && len(entries) < maxBatchEntries { + continue + } + n.sendAppendEntry(entries) + // Reset our sz and entries. + // We need to re-create `entries` because there is a reference + // to it in the node's pae map. + sz, entries = 0, nil + } + if len(entries) > 0 { + n.sendAppendEntry(entries) + } + // Respond to any proposals waiting for a confirmation. + for _, pe := range es { + if pe.term == term && pe.reply != _EMPTY_ { + n.sendReply(pe.reply, nil) + } + pe.returnToPool() + } + n.prop.recycle(&es) + + case <-hb.C: + if n.notActive() { + n.sendHeartbeat() + } + case <-lq.C: + if n.lostQuorum() { + n.stepdown(noLeader) + return + } + case <-n.votes.ch: + // Because of drain() it is possible that we get nil from popOne(). + vresp, ok := n.votes.popOne() + if !ok { + continue + } + if vresp.term > n.Term() { + n.stepdown(noLeader) + return + } + case <-n.reqs.ch: + // Because of drain() it is possible that we get nil from popOne(). + if voteReq, ok := n.reqs.popOne(); ok { + n.processVoteRequest(voteReq) + } + case <-n.entry.ch: + n.processAppendEntries() + } + } +} + +// Quorum reports the quorum status. Will be called on former leaders. +func (n *raft) Quorum() bool { + n.RLock() + defer n.RUnlock() + + nc := 0 + for id, peer := range n.peers { + if id == n.id || time.Since(peer.ts) < lostQuorumInterval { + if nc++; nc >= n.qn { + return true + } + } + } + return false +} + +func (n *raft) lostQuorum() bool { + n.RLock() + defer n.RUnlock() + return n.lostQuorumLocked() +} + +func (n *raft) lostQuorumLocked() bool { + // In order to avoid false positives that can happen in heavily loaded systems + // make sure nothing is queued up that we have not processed yet. + // Also make sure we let any scale up actions settle before deciding. + if n.resp.len() != 0 || (!n.lsut.IsZero() && time.Since(n.lsut) < lostQuorumInterval) { + return false + } + + nc := 0 + for id, peer := range n.peers { + if id == n.id || time.Since(peer.ts) < lostQuorumInterval { + if nc++; nc >= n.qn { + return false + } + } + } + return true +} + +// Check for being not active in terms of sending entries. +// Used in determining if we need to send a heartbeat. +func (n *raft) notActive() bool { + n.RLock() + defer n.RUnlock() + return time.Since(n.active) > hbInterval +} + +// Return our current term. +func (n *raft) Term() uint64 { + n.RLock() + defer n.RUnlock() + return n.term +} + +// Lock should be held. +func (n *raft) loadFirstEntry() (ae *appendEntry, err error) { + var state StreamState + n.wal.FastState(&state) + return n.loadEntry(state.FirstSeq) +} + +func (n *raft) runCatchup(ar *appendEntryResponse, indexUpdatesQ *ipQueue[uint64]) { + n.Lock() + s := n.s + peer, subj, term, pterm, last := ar.peer, ar.reply, n.term, n.pterm, n.pindex + leader := n.State() == Leader // Grab while holding lock, to not race. + // Catchup progress responses for flow control happen on its own subscription, + // not counting toward quorum. + reply := n.newCatchupProgressInbox() + psub, err := n.subscribe(reply, func(_ *subscription, _ *client, _ *Account, _, _ string, msg []byte) { + if pr := decodeAppendEntryResponse(msg); pr != nil { + n.trackPeer(pr.peer) + if pr.success && pr.peer == peer { + indexUpdatesQ.push(pr.index) + } + arPool.Put(pr) + } + }) + n.Unlock() + + defer s.grWG.Done() + defer arPool.Put(ar) + + defer func() { + n.Lock() + if psub != nil { + n.unsubscribe(psub) + } + // Only remove our own progress entry. + if q, ok := n.progress[peer]; ok && q == indexUpdatesQ { + delete(n.progress, peer) + if len(n.progress) == 0 { + n.progress = nil + } + } + n.Unlock() + indexUpdatesQ.unregister() + }() + + if err != nil { + // Without the progress subscription, there's no flow control, so catchup would stall. + n.warn("Canceling catchup for %q, could not subscribe to progress inbox: %v", peer, err) + return + } + if !leader { + n.debug("Canceling catchup for %q, not leader anymore", peer) + return + } + n.debug("Running catchup for %q [%d:%d] to [%d:%d]", peer, ar.term, ar.index, pterm, last) + + const maxOutstanding = 2 * 1024 * 1024 // 2MB for now. + next, total, om := uint64(0), 0, make(map[uint64]int) + + sendNext := func() bool { + for total <= maxOutstanding { + next++ + if next > last { + return true + } + ae, err := n.loadEntry(next) + if err != nil { + if err != ErrStoreEOF { + n.warn("Got an error loading %d index: %v", next, err) + } + return true + } + // Re-encode with the lterm if needed + if ae.lterm != term { + ae.lterm = term + if ae.buf, err = ae.encode(ae.buf[:0]); err != nil { + n.warn("Got an error re-encoding append entry: %v", err) + return true + } + } + // Update our tracking total. + om[next] = len(ae.buf) + total += len(ae.buf) + n.sendRPC(subj, reply, ae.buf) + } + return false + } + + const activityInterval = 2 * time.Second + timeout := time.NewTimer(activityInterval) + defer timeout.Stop() + + stepCheck := time.NewTicker(100 * time.Millisecond) + defer stepCheck.Stop() + + // Run as long as we are leader and still not caught up. + for n.State() == Leader { + select { + case <-n.s.quitCh: + return + case <-n.quit: + return + case <-stepCheck.C: + if n.State() != Leader { + n.debug("Catching up canceled, no longer leader") + return + } + case <-timeout.C: + n.debug("Catching up for %q stalled", peer) + return + case <-indexUpdatesQ.ch: + if index, ok := indexUpdatesQ.popOne(); ok { + // Update our activity timer. + timeout.Reset(activityInterval) + // Update outstanding total. + total -= om[index] + delete(om, index) + if next == 0 { + next = index + } + // Check if we are done. + if index > last || sendNext() { + n.debug("Finished catching up") + return + } + } + } + } +} + +// Lock should be held. +func (n *raft) sendSnapshotToFollower(subject string) (uint64, error) { + snap, err := n.loadLastSnapshot() + if err != nil { + // We need to stepdown here when this happens. + n.stepdownLocked(noLeader) + return 0, err + } + // Go ahead and send the snapshot and peerstate here as first append entry to the catchup follower. + ae := n.buildAppendEntry([]*Entry{{EntrySnapshot, snap.data}, {EntryPeerState, snap.peerstate}}) + ae.pterm, ae.pindex = snap.lastTerm, snap.lastIndex + var state StreamState + n.wal.FastState(&state) + + fpIndex := state.FirstSeq - 1 + if snap.lastIndex < fpIndex && state.FirstSeq != 0 { + snap.lastIndex = fpIndex + ae.pindex = fpIndex + } + + encoding, err := ae.encode(nil) + if err != nil { + return 0, err + } + n.sendRPC(subject, n.areply, encoding) + return snap.lastIndex, nil +} + +func (n *raft) catchupFollower(ar *appendEntryResponse) { + n.debug("Being asked to catch up follower: %q", ar.peer) + n.Lock() + if n.progress == nil { + n.progress = make(map[string]*ipQueue[uint64]) + } else if q, ok := n.progress[ar.peer]; ok { + n.debug("Will cancel existing entry for catching up %q", ar.peer) + delete(n.progress, ar.peer) + q.push(n.pindex) + } + + // Check to make sure we have this entry. + start := ar.index + 1 + var state StreamState + n.wal.FastState(&state) + + if start < state.FirstSeq || (state.Msgs == 0 && start <= state.LastSeq) { + n.debug("Need to send snapshot to follower") + if lastIndex, err := n.sendSnapshotToFollower(ar.reply); err != nil { + n.error("Error sending snapshot to follower [%s]: %v", ar.peer, err) + n.Unlock() + arPool.Put(ar) + return + } else { + start = lastIndex + 1 + // If no other entries, we can just return here. + if state.Msgs == 0 || start > state.LastSeq { + n.debug("Finished catching up") + n.Unlock() + arPool.Put(ar) + return + } + n.debug("Snapshot sent, reset first catchup entry to %d", lastIndex) + } + } + + ae, err := n.loadEntry(start) + if err != nil { + n.warn("Request from follower for entry at index [%d] errored for state %+v - %v", start, state, err) + if err == ErrStoreEOF { + // If we are here we are seeing a request for an item beyond our state, meaning we should stepdown. + n.stepdownLocked(noLeader) + n.Unlock() + arPool.Put(ar) + return + } + ae, err = n.loadFirstEntry() + } + if err != nil || ae == nil { + n.warn("Could not find a starting entry for catchup request: %v", err) + // If we are here we are seeing a request for an item we do not have, meaning we should stepdown. + // This is possible on a reset of our WAL but the other side has a snapshot already. + // If we do not stepdown this can cycle. + n.stepdownLocked(noLeader) + n.Unlock() + arPool.Put(ar) + return + } + if ae.pindex != ar.index || ae.pterm != ar.term { + n.debug("Our first entry [%d:%d] does not match request from follower [%d:%d]", ae.pterm, ae.pindex, ar.term, ar.index) + } + // Create a queue for delivering updates from responses. + indexUpdates := newIPQueue[uint64](n.s, fmt.Sprintf("[ACC:%s] RAFT '%s' indexUpdates", n.accName, n.group)) + indexUpdates.push(ae.pindex) + n.progress[ar.peer] = indexUpdates + n.wg.Add(1) + n.Unlock() + started := n.s.startGoRoutine(func() { + defer n.wg.Done() + n.runCatchup(ar, indexUpdates) + }) + if !started { + n.wg.Done() + n.Lock() + if n.progress != nil && n.progress[ar.peer] == indexUpdates { + delete(n.progress, ar.peer) + } + n.Unlock() + indexUpdates.unregister() + arPool.Put(ar) + } +} + +func (n *raft) loadEntry(index uint64) (*appendEntry, error) { + var smp StoreMsg + sm, err := n.wal.LoadMsg(index, &smp) + if err != nil { + return nil, err + } + return decodeAppendEntry(sm.msg, nil, _EMPTY_) +} + +// applyCommit will update our commit index and apply the entry to the apply queue. +// lock should be held. +func (n *raft) applyCommit(index uint64) error { + if n.State() == Closed { + return errNodeClosed + } + if index <= n.commit { + n.debug("Ignoring apply commit for %d, already processed", index) + return nil + } + + if n.State() == Leader { + delete(n.acks, index) + } + + if index <= n.papplied { + return nil + } + + ae := n.pae[index] + if ae == nil { + var err error + if ae, err = n.loadEntry(index); err != nil { + if err != ErrStoreClosed && err != ErrStoreEOF { + n.warn("Got an error loading %d index: %v - will reset", index, err) + if n.State() == Leader { + n.stepdownLocked(n.selectNextLeader()) + } + // Reset and cancel any catchup. + n.resetWAL() + n.cancelCatchup() + } + return errEntryLoadFailed + } + } else { + // Size it now, the buffer is cleared below. + sz := n.entryStoreSize(ae) + defer func() { + if _, ok := n.pae[index]; ok { + n.paeBytes -= sz + delete(n.pae, index) + } + }() + } + + n.commit = index + ae.buf = nil + var committed []*Entry + + defer func() { + // Pass to the upper layers if we have normal entries. It is + // entirely possible that 'committed' might be an empty slice here, + // which will happen if we've processed updates inline (like peer + // states). In which case the upper layer will just call down with + // Applied() with no further action. + n.apply.push(newCommittedEntry(index, committed)) + // Place back in the pool. + ae.returnToPool() + }() + + for _, e := range ae.entries { + switch e.Type { + case EntryNormal: + committed = append(committed, e) + case EntryOldSnapshot: + // For old snapshots in our WAL. + committed = append(committed, newEntry(EntrySnapshot, e.Data)) + case EntrySnapshot: + committed = append(committed, e) + // If we have no snapshot, install the leader's snapshot as our own. + if len(ae.entries) == 1 && n.snapfile == _EMPTY_ && ae.commit > 0 { + n.installSnapshot(&snapshot{ + lastTerm: ae.pterm, + lastIndex: ae.commit, + peerstate: encodePeerState(n.committedPeerStateLocked(ae.commit)), + data: e.Data, + }) + } + case EntryPeerState: + if n.State() != Leader { + if ps, err := decodePeerState(e.Data); err == nil { + n.processPeerState(ps) + } + } + case EntryAddPeer: + newPeer := string(e.Data) + // Skip applying a membership change that was reverted by peer eviction, only valid + // if we won the election; a follower must always apply what the leader committed. + if n.membChange == nil && n.State() == Leader { + n.debug("Skipping reverted membership change adding peer %q", newPeer) + continue + } + n.debug("Added peer %q", newPeer) + + // Store our peer in our global peer map for all peers. + peers.LoadOrStore(newPeer, newPeer) + + n.addPeer(newPeer) + + // We pass these up as well. + committed = append(committed, e) + + // We are done with this membership change + n.membChange = nil + + case EntryRemovePeer: + peer := string(e.Data) + // Skip applying a membership change that was reverted by peer eviction, only valid + // if we won the election; a follower must always apply what the leader committed. + if n.membChange == nil && n.State() == Leader { + n.debug("Skipping reverted membership change removing peer %q", peer) + continue + } + n.debug("Removing peer %q", peer) + + n.removePeer(peer) + + // Remove from string intern map. + peers.Delete(peer) + + // We pass these up as well. + committed = append(committed, e) + + // We are done with this membership change + n.membChange = nil + + // If this is us and we are the leader signal the caller + // to attempt to stepdown. + if peer == n.id && n.State() == Leader { + return errNodeRemoved + } + } + } + return nil +} + +// Check if there is a quorum for the given index, and if +// so, commit the corresponding entry. +// Return true if the index was committed, false otherwise. +// Lock should be held. +func (n *raft) tryCommit(index uint64) (bool, error) { + acks := len(n.acks[index]) + // Count the leader if it's still part of membership + if n.peers[n.ID()] != nil { + acks += 1 + } + if acks < n.qn { + return false, nil + } + // We have a quorum + for i := n.commit + 1; i <= index; i++ { + if err := n.applyCommit(i); err != nil { + if err != errNodeClosed && err != errNodeRemoved { + n.error("Got an error applying commit for %d: %v", i, err) + } + return false, err + } + } + return true, nil +} + +// Used to track a success response. Returns true if the +// response was tracked, false if the response was ignored +// (the response is old, the index is already committed, ...) +// Lock should be held. +func (n *raft) trackResponse(ar *appendEntryResponse) bool { + // Check state under lock, we might not be leader anymore. + if n.State() != Leader { + return false + } + + ps := n.peers[ar.peer] + + // Update peer's last index. + if ps != nil && ar.index > ps.li { + ps.li = ar.index + } + + // Ignore items already committed, or skip if this is not about an entry that matches our current term. + if ar.index <= n.commit || ar.term != n.term { + assert.AlwaysOrUnreachable(ar.term <= n.term, "Raft response term mismatch", map[string]any{ + "n.accName": n.accName, + "n.group": n.group, + "n.id": n.id, + "n.term": n.term, + "ar.term": ar.term, + }) + return false + } + + // Not a peer, can't count this message towards quorum + if ps == nil { + return false + } + + // Keep track of the response + results := n.acks[ar.index] + if results == nil { + results = make(map[string]struct{}) + n.acks[ar.index] = results + } + results[ar.peer] = struct{}{} + + return true +} + +// Used to adjust cluster size and peer count based on added official peers. +// lock should be held. +func (n *raft) adjustClusterSizeAndQuorum() { + pcsz, ncsz := n.csz, len(n.peers) + n.csz = ncsz + n.recalcQuorum() + + if ncsz > pcsz { + n.debug("Expanding our clustersize: %d -> %d", pcsz, ncsz) + n.lsut = time.Now() + } else if ncsz < pcsz { + n.debug("Decreasing our clustersize: %d -> %d", pcsz, ncsz) + if n.State() == Leader { + go n.sendHeartbeat() + } + } + if ncsz != pcsz { + n.recreateInternalSubsLocked() + } +} + +// Returns true if we should count vote responses from this peer. +// Lock should be held. +func (n *raft) shouldCountVoteFromPeer(peer string) bool { + if _, ok := n.peers[peer]; ok { + return true + } + // During bootstrap, we may know fewer peer ids + // than the declared cluster size. Initially, we + // may need to accept votes from peers that are + // not yet in the peer set. + return len(n.peers) < n.csz +} + +// Track interactions with this peer. +func (n *raft) trackPeer(peer string) error { + n.Lock() + var needPeerAdd, isRemoved bool + var rts time.Time + if n.removed != nil { + rts, isRemoved = n.removed[peer] + // Removed peers can rejoin after timeout. + if isRemoved && time.Since(rts) >= peerRemoveTimeout { + isRemoved = false + } + } + if !n.managed && n.State() == Leader { + if _, ok := n.peers[peer]; !ok { + // Check if this peer had been removed previously. + needPeerAdd = !isRemoved + } + } + if ps := n.peers[peer]; ps != nil { + ps.ts = time.Now() + if n.observed != nil { + delete(n.observed, peer) + if len(n.observed) == 0 { + n.observed = nil + } + } + } else if n.managed { + // For managed groups the meta layer can assign peers before they've been + // added to our peer set. Track when we hear from them, so the upper layer + // can prefer adding peers that are demonstrably up. + if n.observed == nil { + n.observed = make(map[string]time.Time, 1) + } + // On first contact, nudge the upper layer so it can check if this unblocks adding the peer. + if _, seen := n.observed[peer]; !seen && n.leaderState.Load() { + select { + case n.leadc <- leadChange{isLeader: true, term: n.term, nudge: true}: + default: + } + } + n.observed[peer] = time.Now() + } + n.Unlock() + + if needPeerAdd { + n.ProposeAddPeer(peer) + } + return nil +} + +// LastHeardFromPeer returns when we last heard from the given peer, even if +// it's not in our peer set yet. Zero if we never heard from it. The upper +// layer uses this to judge if a meta-assigned peer is up before adding it. +func (n *raft) LastHeardFromPeer(peer string) time.Time { + n.RLock() + defer n.RUnlock() + if ps := n.peers[peer]; ps != nil { + return ps.ts + } + return n.observed[peer] +} + +func (n *raft) runAsCandidate() { + n.Lock() + // Drain old responses. + n.votes.drain() + n.Unlock() + + // Send out our request for votes. + n.requestVote() + + // We vote for ourselves. + n.votes.push(&voteResponse{term: n.term, peer: n.ID(), granted: true}) + + votes := map[string]struct{}{} + emptyVotes := map[string]struct{}{} + + for n.State() == Candidate { + elect := n.electTimer() + select { + case <-n.entry.ch: + n.processAppendEntries() + case <-n.resp.ch: + // Ignore append entry responses received from before the state change. + n.resp.drain() + case <-n.prop.ch: + // Ignore proposals received from before the state change. + n.prop.drain() + case <-n.s.quitCh: + return + case <-n.quit: + return + case <-elect.C: + n.switchToCandidate() + return + case <-n.votes.ch: + // Because of drain() it is possible that we get nil from popOne(). + vresp, ok := n.votes.popOne() + if !ok { + continue + } + n.RLock() + nterm := n.term + csz := n.csz + countVote := n.shouldCountVoteFromPeer(vresp.peer) + // While an unsafe quorum rescue is active (see RescueQuorum), grants + // from empty voters count toward quorum, but only if our own log is + // non-empty. Empty servers must never form quorum among themselves. + countEmpty := n.rescue != nil && n.pindex > 0 + n.RUnlock() + + if vresp.granted && nterm == vresp.term { + // only track peers that would be our followers + n.trackPeer(vresp.peer) + if countVote { + if !vresp.empty || countEmpty { + votes[vresp.peer] = struct{}{} + } else { + emptyVotes[vresp.peer] = struct{}{} + } + } + if n.wonElection(len(votes)) { + // Become LEADER if we have won and gotten a quorum with everyone we should hear from. + n.switchToLeader() + return + } else if len(votes)+len(emptyVotes) == csz { + // Become LEADER if we've got voted in by ALL servers. + // We couldn't get quorum based on just our normal votes. + // But, we have heard from the full cluster, and some servers came up empty. + // We know for sure we have the most up-to-date log. + n.switchToLeader() + return + } + } else if vresp.term > nterm { + // if we observe a bigger term, we should start over again or risk forming a quorum fully knowing + // someone with a better term exists. This is even the right thing to do if won == true. + n.Lock() + n.debug("Stepping down from candidate, detected higher term: %d vs %d", vresp.term, n.term) + n.term = vresp.term + n.vote = noVote + n.writeTermVote() + n.lxfer = false + n.stepdownLocked(noLeader) + n.Unlock() + } + case <-n.reqs.ch: + // Because of drain() it is possible that we get nil from popOne(). + if voteReq, ok := n.reqs.popOne(); ok { + n.processVoteRequest(voteReq) + } + } + } +} + +// handleAppendEntry handles an append entry from the wire. This function +// is an internal callback from the "asubj" append entry subscription. +func (n *raft) handleAppendEntry(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { + msg = copyBytes(msg) + if ae, err := decodeAppendEntry(msg, sub, reply); err == nil { + // Push to the new entry channel. From here one of the worker + // goroutines (runAsLeader, runAsFollower, runAsCandidate) will + // pick it up. + n.entry.push(ae) + } else { + n.warn("AppendEntry failed to be placed on internal channel: corrupt entry") + } +} + +// cancelCatchup will stop an in-flight catchup by unsubscribing from the +// catchup subscription. +// Lock should be held. +func (n *raft) cancelCatchup() { + n.debug("Canceling catchup subscription since we are now up to date") + + if n.catchup != nil && n.catchup.sub != nil { + n.unsubscribe(n.catchup.sub) + } + n.cancelCatchupSignal() + n.catchup = nil +} + +// catchupStalled will try to determine if we are stalled. This is called +// on a new entry from our leader. +// Lock should be held. +func (n *raft) catchupStalled() bool { + if n.catchup == nil { + return false + } + if n.catchup.pindex == n.pindex { + return time.Since(n.catchup.active) > 2*time.Second + } + n.catchup.pindex = n.pindex + n.catchup.active = time.Now() + return false +} + +// createCatchup will create the state needed to track a catchup as it +// runs. It then creates a unique inbox for this catchup and subscribes +// to it. The remote side will stream entries to that subject. +// Lock should be held. +func (n *raft) createCatchup(ae *appendEntry) string { + // Cleanup any old ones, but preserve whether we signaled the upper layer. + var signal bool + if n.catchup != nil { + if n.catchup.sub != nil { + n.unsubscribe(n.catchup.sub) + } + signal = n.catchup.signal + } + // Snapshot term and index. + n.catchup = &catchupState{ + cterm: ae.pterm, + cindex: ae.pindex, + pterm: n.pterm, + pindex: n.pindex, + active: time.Now(), + signal: signal, + } + inbox := n.newCatchupInbox() + sub, _ := n.subscribe(inbox, n.handleAppendEntry) + n.catchup.sub = sub + return inbox +} + +// Lock should be held. +func (n *raft) sendCatchupSignal() { + if n.catchup == nil || n.catchup.signal { + return + } + n.catchup.signal = true + // Signal to the upper layer that the following entries are catchup entries, up until the nil guard. + n.apply.push(newCommittedEntry(0, []*Entry{{EntryCatchup, nil}})) +} + +// Lock should be held. +func (n *raft) cancelCatchupSignal() { + if n.catchup == nil || !n.catchup.signal { + return + } + // Send nil entry to signal the upper layers we are done catching up. + n.apply.push(nil) +} + +// Truncate our WAL and reset. +// Lock should be held. +func (n *raft) truncateWAL(term, index uint64) { + n.debug("Truncating and repairing WAL to Term %d Index %d", term, index) + + if term == 0 && index == 0 { + if n.commit > 0 { + n.warn("Resetting WAL state") + } else { + n.debug("Clearing WAL state (no commits)") + } + } + if index < n.commit { + assert.Unreachable("WAL truncate lost commits", map[string]any{ + "n.accName": n.accName, + "n.group": n.group, + "n.id": n.id, + "term": term, + "index": index, + "commit": n.commit, + "applied": n.applied, + }) + } + + defer func() { + // Check to see if we invalidated any snapshots that might have held state + // from the entries we are truncating. + if snap, _ := n.loadLastSnapshot(); snap != nil && snap.lastIndex > index { + os.Remove(n.snapfile) + n.snapfile = _EMPTY_ + } + // Make sure to reset commit and applied if above + if n.commit > n.pindex { + n.commit = n.pindex + } + if n.processed > n.commit { + n.processed = n.commit + } + if n.applied > n.processed { + n.applied = n.processed + } + // Refresh bytes count after truncate. + var state StreamState + n.wal.FastState(&state) + n.bytes = state.Bytes + }() + + if err := n.wal.Truncate(index); err != nil { + n.warn("Error truncating WAL: %v", err) + n.setWriteErrLocked(err) + return + } + // Set after we know we have truncated properly. + n.pterm, n.pindex = term, index + + // Invalidate cached entries the WAL no longer has. + if index == 0 { + n.resetPendingEntries() + } else { + for k, ae := range n.pae { + if k > index { + n.paeBytes -= n.entryStoreSize(ae) + delete(n.pae, k) + } + } + } + + // Check if we're truncating an uncommitted membership change. + if n.membChange != nil && n.membChange.index > index { + n.revertMembershipChange() + } +} + +// Revert an uncommitted membership change that was speculatively applied to +// our peer map at append time, restoring the committed membership. +// Lock should be held. +func (n *raft) revertMembershipChange() { + peer := n.membChange.peer + if ps := n.membChange.prev; ps != nil { + // This was a removal, add it back. + n.peers[peer] = ps + // If we were on the removed list, reverse that here. + if n.removed != nil { + delete(n.removed, peer) + if len(n.removed) == 0 { + n.removed = nil + } + } + } else { + // This was an addition, remove it. + delete(n.peers, peer) + } + n.adjustClusterSizeAndQuorum() + n.writePeerState(&peerState{n.peerNames(), n.csz, n.extSt}) + n.membChange = nil +} + +// Reset our WAL. This is equivalent to truncating all data from the log. +// Lock should be held. +func (n *raft) resetWAL() { + n.truncateWAL(0, 0) +} + +// Lock should be held +func (n *raft) updateLeader(newLeader string) { + wasLeader := n.leader == n.id + n.leader = newLeader + n.hasleader.Store(newLeader != _EMPTY_) + if !n.pleader.Load() && newLeader != noLeader { + n.pleader.Store(true) + // If we were preferred to become the first leader, but didn't end up successful. + // Ensure to call lead change. When scaling from R1 to R3 we've optimized for a scale up + // not flipping leader/non-leader/leader status if the leader remains the same. But we need to + // correct that if the first leader turns out to be different. + if n.maybeLeader { + n.maybeLeader = false + if n.id != newLeader { + n.updateLeadChange(false) + } + } + } + // Reset last seen timestamps and indices. + // If we are (or were) the leader we track(ed) everyone, and don't reset. + // But if we're a follower we only track the leader, and reset all others. + if newLeader != n.id && !wasLeader { + for peer, ps := range n.peers { + // Always reset last replicated index. + ps.li = 0 + if peer == newLeader { + continue + } + // Only reset the last seen timestamp if this peer is not the leader. + ps.ts = time.Time{} + } + } +} + +// processAppendEntry will process an appendEntry. This is called either +// during recovery or from processAppendEntries when there are new entries +// to be committed. +func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) { + n.Lock() + // Don't reset here if we have been asked to assume leader position. + if !n.lxfer { + n.resetElectionTimeout() + } + + // Just return if closed or we had previous write error. + if n.State() == Closed || n.werr != nil { + n.Unlock() + return + } + + // Scratch buffer for responses. + var scratch [appendEntryResponseLen]byte + arbuf := scratch[:] + + // Grab term from append entry. But if leader explicitly defined its term, use that instead. + // This is required during catchup if the leader catches us up on older items from previous terms. + // While still allowing us to confirm they're matching our highest known term. + lterm := ae.term + if ae.lterm != 0 { + lterm = ae.lterm + } + + // Are we receiving from another leader. + if n.State() == Leader { + if lterm >= n.term { + // If the append entry term is newer than the current term, erase our + // vote. + if lterm > n.term { + n.term = lterm + n.vote = noVote + n.writeTermVote() + } else { + assert.Unreachable( + "Two leaders using the same term", + map[string]any{ + "n.accName": n.accName, + "n.group": n.group, + "n.id": n.id, + "n.term": n.term, + "ae.leader": ae.leader, + "ae.term": ae.term, + "ae.lterm": ae.lterm, + }, + ) + } + n.debug("Received append entry from another leader, stepping down to %q", ae.leader) + n.stepdownLocked(ae.leader) + } else { + // Let them know we are the leader. + ar := newAppendEntryResponse(n.term, n.pindex, n.id, false) + n.debug("AppendEntry ignoring old term from another leader") + n.sendRPC(ae.reply, _EMPTY_, ar.encode(arbuf)) + arPool.Put(ar) + n.Unlock() + return + } + } + + // If we received an append entry as a candidate then it would appear that + // another node has taken on the leader role already, so we should convert + // to a follower of that node instead. + if n.State() == Candidate { + // If we have a leader in the current term or higher, we should stepdown, + // write the term and vote if the term of the request is higher. + if lterm >= n.term { + // If the append entry term is newer than the current term, erase our + // vote. + if lterm > n.term { + n.term = lterm + n.vote = noVote + n.writeTermVote() + } + n.debug("Received append entry in candidate state from %q, converting to follower", ae.leader) + n.stepdownLocked(ae.leader) + n.updateLeadChange(false) + } + } + + // Catching up state. + catchingUp := n.catchup != nil + // Is this a new entry? New entries will be delivered on the append entry + // sub, rather than a catch-up sub. + isNew := sub != nil && sub == n.aesub + // Entries arriving on any other subscription are catchup entries being streamed + // to our dedicated catchup inbox. + isCatchup := sub != nil && !isNew + + // If we are/were catching up ignore old catchup subs, but only if catching up from an older server + // that doesn't send the leader term when catching up or if we would truncate as a result. + // We can reject old catchups from newer subs later, just by checking the append entry is on the correct term. + if !isNew && sub != nil && (ae.lterm == 0 || ae.pindex < n.pindex) && (!catchingUp || sub != n.catchup.sub) { + n.Unlock() + n.debug("AppendEntry ignoring old entry from previous catchup") + return + } + + // If this term is greater than ours. + if lterm > n.term { + n.term = lterm + n.vote = noVote + if isNew { + n.writeTermVote() + } + if n.State() != Follower { + n.debug("Term higher than ours and we are not a follower: %v, stepping down to %q", n.State(), ae.leader) + n.stepdownLocked(ae.leader) + } + } else if lterm < n.term && sub != nil && (isNew || ae.lterm != 0) { + // Anything that's below our expected highest term needs to be rejected. + // Unless we're replaying (sub=nil), in which case we'll always continue. + // For backward-compatibility we shouldn't reject if we're being caught up by an old server. + if !isNew { + n.debug("AppendEntry ignoring old entry from previous catchup") + n.Unlock() + return + } + n.debug("Rejected AppendEntry from a leader (%s) with term %d which is less than ours", ae.leader, lterm) + ar := newAppendEntryResponse(n.term, n.pindex, n.id, false) + n.Unlock() + n.sendRPC(ae.reply, _EMPTY_, ar.encode(arbuf)) + arPool.Put(ar) + return + } + + // Check state if we are catching up. + if catchingUp { + if cs := n.catchup; cs != nil && n.pterm >= cs.cterm && n.pindex >= cs.cindex { + // If we are here we are good, so if we have a catchup pending we can cancel. + n.cancelCatchup() + // Reset our notion of catching up. + catchingUp = false + } else if isNew { + var ar *appendEntryResponse + var inbox string + // Check to see if we are stalled. If so recreate our catchup state and resend response. + if n.catchupStalled() { + n.debug("Catchup may be stalled, will request again") + inbox = n.createCatchup(ae) + ar = newAppendEntryResponse(n.pterm, n.pindex, n.id, false) + } + n.Unlock() + if ar != nil { + n.sendRPC(ae.reply, inbox, ar.encode(arbuf)) + arPool.Put(ar) + } + // Ignore new while catching up or replaying. + return + } + } + + if isNew && n.leader != ae.leader && n.State() == Follower { + n.debug("AppendEntry updating leader to %q", ae.leader) + n.updateLeader(ae.leader) + n.writeTermVote() + n.resetElectionTimeout() + n.updateLeadChange(false) + } + + // Track leader directly + // But, do so after all consistency checks so we don't track an old leader. + if isNew && ae.leader != noLeader && ae.leader == n.leader { + if ps := n.peers[ae.leader]; ps != nil { + ps.ts = time.Now() + } + } + + // If commits are outpacing our applies, temporarily stop accepting new entries to avoid falling further behind. + // This encourages the leader to sync us via a snapshot instead. We use max(applied, papplied) to avoid + // incorrectly triggering this pause immediately after receiving a snapshot. + applied := max(n.applied, n.papplied) + commit := max(n.commit, n.papplied) + if sub != nil && (commit > applied || n.quorumPaused) { + diff := commit - applied + if n.quorumPaused { + if n.overrun(diff, paeWarnThreshold, paeWarnBytes) { + if catchingUp { + n.cancelCatchup() + } + n.Unlock() + return + } + // Once we're sufficiently below the threshold, we continue again. We'll likely receive a snapshot + // from the leader. + n.quorumPaused = false + var state StreamState + n.wal.FastState(&state) + n.warn("Quorum resumed: commit %d, applied %d, WAL size %s", commit, applied, friendlyBytes(state.Bytes)) + } else if n.overrun(diff, pauseQuorumThreshold, pauseQuorumBytes) { + // It takes a while until we reach the pause threshold, but once we do we enter a "cooldown period". + n.quorumPaused = true + n.overrunCount++ + var state StreamState + n.wal.FastState(&state) + n.warn("Quorum paused, falling behind: commit %d != applied %d, WAL size %s", commit, applied, friendlyBytes(state.Bytes)) + if catchingUp { + n.cancelCatchup() + } + n.Unlock() + return + } + } + + if ae.pterm != n.pterm || ae.pindex != n.pindex { + // Check if this is a lower or equal index than what we were expecting. + if ae.pindex <= n.pindex { + n.debug("AppendEntry detected pindex less than/equal to ours: [%d:%d] vs [%d:%d]", ae.pterm, ae.pindex, n.pterm, n.pindex) + var success bool + + if ae.pindex < n.commit { + // If we have already committed this entry, just mark success. + success = true + n.debug("AppendEntry pindex %d below commit %d, marking success", ae.pindex, n.commit) + } else if eae, _ := n.loadEntry(ae.pindex); eae == nil { + // If terms are equal, and we are not catching up, we have simply already processed this message. + // This can happen on server restarts based on timings of snapshots. + if ae.pterm == n.pterm && isNew { + success = true + n.debug("AppendEntry pindex %d already processed, marking success", ae.pindex) + } else if ae.pindex == n.pindex { + // Check if only our terms do not match here. + // Make sure pterms match and we take on the leader's. + // This prevents constant spinning. + n.truncateWAL(ae.pterm, ae.pindex) + } else { + snap, err := n.loadLastSnapshot() + if err == nil && snap.lastIndex == ae.pindex && snap.lastTerm == ae.pterm { + // Entry can't be found, this is normal because we have a snapshot at this index. + // Truncate back to where we've created the snapshot. + n.truncateWAL(snap.lastTerm, snap.lastIndex) + // Only continue if truncation was successful, and we ended up such that we can safely continue. + if ae.pterm == n.pterm && ae.pindex == n.pindex { + goto CONTINUE + } + } else { + // Otherwise, something has gone very wrong and we need to reset. + n.resetWAL() + } + } + } else if eae.term == ae.pterm { + // If terms match we can delete all entries past this one, and then continue storing the current entry. + n.truncateWAL(ae.pterm, ae.pindex) + // Only continue if truncation was successful, and we ended up such that we can safely continue. + if ae.pterm == n.pterm && ae.pindex == n.pindex { + goto CONTINUE + } + } else { + // If terms mismatched, delete that entry and all others past it. + // But only if we haven't already committed past this point. + if eae.pindex < n.commit { + success = true + assert.Unreachable("Truncate to earlier entry would lose commits", map[string]any{ + "n.accName": n.accName, + "n.group": n.group, + "n.id": n.id, + "n.term": n.term, + "n.pindex": n.pindex, + "n.commit": n.commit, + "n.applied": n.applied, + "ae.pindex": ae.pindex, + "ae.pterm": ae.pterm, + "ae.commit": ae.commit, + "eae.pterm": eae.pterm, + "eae.pindex": eae.pindex, + }) + } else { + n.truncateWAL(eae.pterm, eae.pindex) + } + } + // Cancel regardless if unsuccessful. + if !success { + n.cancelCatchup() + } + // Intentionally not responding. Otherwise, we could erroneously report "success". Reporting + // non-success is not needed either, and would only be wasting messages. + // For example, if we got partial catchup, and then the "real-time" messages came in very delayed. + // If we reported "success" on those "real-time" messages, we'd wrongfully be providing + // quorum while not having an up-to-date log. + n.Unlock() + return + } + + // Check if we are catching up. If we are here we know the leader did not have all of the entries + // so make sure this is a snapshot entry. If it is not start the catchup process again since it + // means we may have missed additional messages. + if catchingUp { + // This means we already entered into a catchup state but what the leader sent us did not match what we expected. + // Snapshots and peerstate will always be together when a leader is catching us up in this fashion. + if len(ae.entries) != 2 || ae.entries[0].Type != EntrySnapshot || ae.entries[1].Type != EntryPeerState { + n.warn("Expected first catchup entry to be a snapshot and peerstate, will retry") + n.cancelCatchup() + n.Unlock() + return + } + + if ps, err := decodePeerState(ae.entries[1].Data); err == nil { + n.processPeerState(ps) + // Also need to copy from client's buffer. + ae.entries[0].Data = copyBytes(ae.entries[0].Data) + } else { + n.warn("Could not parse snapshot peerstate correctly") + n.cancelCatchup() + n.Unlock() + return + } + + // Inherit state from appendEntry with the leader's snapshot. + hadPreviousSnapshot := n.snapfile != _EMPTY_ + + snap := &snapshot{ + lastTerm: ae.pterm, + lastIndex: ae.pindex, + peerstate: encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt}), + data: ae.entries[0].Data, + } + // Install the leader's snapshot as our own. + if err := n.installSnapshot(snap); err != nil { + n.setWriteErrLocked(err) + n.Unlock() + return + } + n.pindex = ae.pindex + n.pterm = ae.pterm + n.commit = ae.pindex + n.resetInitializing() + + if !hadPreviousSnapshot { + // If the first snapshot we install is received from another server, then we immediately signal + // to the upper-layer it can coalesce catchup entries. + n.sendCatchupSignal() + } + // Now send snapshot to upper levels. Only send the snapshot, not the peerstate entry. + n.apply.push(newCommittedEntry(n.commit, ae.entries[:1])) + if hadPreviousSnapshot { + // Signal catchup only after we've sent the snapshot. That ensures the upper-layer processes the snapshot + // as-is and can only coalesce other catchup entries after this one. + n.sendCatchupSignal() + } + n.Unlock() + return + } + + // Setup our state for catching up. + n.debug("AppendEntry did not match [%d:%d] with [%d:%d]", ae.pterm, ae.pindex, n.pterm, n.pindex) + inbox := n.createCatchup(ae) + ar := newAppendEntryResponse(n.pterm, n.pindex, n.id, false) + n.Unlock() + n.sendRPC(ae.reply, inbox, ar.encode(arbuf)) + arPool.Put(ar) + return + } + +CONTINUE: + // Save to our WAL if we have entries. + if ae.shouldStore() { + // Only store if an original which will have sub != nil + if sub != nil { + if err := n.storeToWAL(ae); err != nil { + if err != ErrStoreClosed { + n.warn("Error storing entry to WAL: %v", err) + } + n.Unlock() + return + } + n.cachePendingEntry(ae) + n.resetInitializing() + } else { + // This is a replay on startup so just take the appendEntry version. + n.pterm = ae.term + n.pindex = ae.pindex + 1 + } + } + + // Check to see if we have any related entries to process here. + for _, e := range ae.entries { + switch e.Type { + case EntryLeaderTransfer: + // Only process these if they are new, so no replays or catchups. + if isNew { + maybeLeader := string(e.Data) + // This is us. We need to check if we can become the leader. + if maybeLeader == n.id { + // If not an observer and not paused we are good to go. + if !n.observer && !n.paused { + n.lxfer = true + n.xferCampaign() + } else if n.paused { + // Here we can become a leader but need to wait for resume of the apply queue. + n.lxfer = true + } + } + } + case EntryAddPeer: + // When receiving or restoring, mark membership as changing. + // Set to the index where this entry was stored (pindex is now this entry's index) + if newPeer := string(e.Data); len(newPeer) == idLen { + // Track directly, but wait for commit to be official + n.membChange = &membChange{index: n.pindex, peer: newPeer} + if _, ok := n.peers[newPeer]; !ok { + n.peers[newPeer] = &lps{time.Time{}, 0} + } + n.adjustClusterSizeAndQuorum() + // Store our peer in our global peer map for all peers. + peers.LoadOrStore(newPeer, newPeer) + } + case EntryRemovePeer: + // When receiving or restoring, mark membership as changing. + // Set to the index where this entry was stored (pindex is now this entry's index) + if oldPeer := string(e.Data); len(oldPeer) == idLen { + // Track directly, but wait for commit to be official + ps, ok := n.peers[oldPeer] + if !ok { + ps = &lps{time.Time{}, 0} + } + n.membChange = &membChange{index: n.pindex, peer: oldPeer, prev: ps} + delete(n.peers, oldPeer) + n.adjustClusterSizeAndQuorum() + } + } + } + + // Make a copy of these values, as the AppendEntry might be cached and returned to the pool in applyCommit. + aeCommit := ae.commit + aeReply := ae.reply + + // Apply anything we need here. + if aeCommit > n.commit { + // If we're catching up, we might need to signal that it's okay to potentially coalesce entries from here. + if catchingUp { + n.sendCatchupSignal() + } + if n.paused { + n.hcommit = aeCommit + n.debug("Paused, not applying %d", aeCommit) + } else { + for index := n.commit + 1; index <= aeCommit; index++ { + if err := n.applyCommit(index); err != nil { + break + } + } + } + } + + // The only way for the leader to receive "success" MUST be through this path. + // Only new entries are allowed to provide quorum, since providing quorum based + // on catchup is unsafe. Catchup entries are acked through here, but only on a + // dedicated progress inbox. + var ar *appendEntryResponse + if isNew || (isCatchup && strings.HasPrefix(aeReply, raftCatchupProgressPre)) { + ar = newAppendEntryResponse(n.pterm, n.pindex, n.id, true) + } + n.Unlock() + + // Success. Send our response. + if ar != nil { + n.sendRPC(aeReply, _EMPTY_, ar.encode(arbuf)) + arPool.Put(ar) + } +} + +// resetInitializing resets the notion of initializing. +// If we were scaling up, also leaves observer mode. +// Lock should be held. +func (n *raft) resetInitializing() { + n.initializing = false + if n.scaleUp { + n.scaleUp = false + n.setObserverLocked(false, extUndetermined) + } +} + +// processPeerState is called when a peer state entry is received +// over the wire or when we're updating known peers. +// Lock should be held. +func (n *raft) processPeerState(ps *peerState) { + // Update our version of peers to that of the leader. Calculate + // the number of nodes needed to establish a quorum. + n.csz = ps.clusterSize + n.recalcQuorum() + + old := n.peers + n.peers = make(map[string]*lps) + for _, peer := range ps.knownPeers { + if lp := old[peer]; lp != nil { + n.peers[peer] = lp + delete(old, peer) + } else { + n.peers[peer] = &lps{time.Time{}, 0} + } + // If we were on the removed list reverse that here. + if n.removed != nil { + delete(n.removed, peer) + if len(n.removed) == 0 { + n.removed = nil + } + } + } + // Any remaining old nodes are marked as removed, so they can't be + // re-added via automatic peer tracking. + if len(old) > 0 { + if n.removed == nil { + n.removed = map[string]time.Time{} + } + now := time.Now() + for peer := range old { + n.removed[peer] = now + } + } + n.debug("Update peers from leader to %+v", n.peers) + n.writePeerState(ps) +} + +// processAppendEntryResponse is called when we receive an append entry +// response from another node. They will send a confirmation to tell us +// whether they successfully committed the entry or not. +func (n *raft) processAppendEntryResponse(ar *appendEntryResponse) { + n.trackPeer(ar.peer) + + if ar.success { + // The remote node successfully committed the append entry. + // They agree with our leadership and are happy with the state of the log. + // In this case ar.term was populated with the remote's pterm. If this matches + // our term, we can use it to check for quorum and up our commit. + var err error + var committed bool + + n.Lock() + if n.trackResponse(ar) { + committed, err = n.tryCommit(ar.index) + } + n.Unlock() + + // Leader was peer-removed. Attempt a step-down to + // a new leader before shutting down. + if err == errNodeRemoved { + n.StepDown() + n.Stop() + } + + // Send a heartbeat if there is no other message lined + // up, so that followers can apply without waiting for + // the next message. + if committed && n.prop.len() == 0 { + n.sendHeartbeat() + } + + arPool.Put(ar) + } else if ar.reply != _EMPTY_ { + // The remote node didn't commit the append entry, and they believe they + // are behind and have specified a reply subject, so let's try to catch them up. + // In this case ar.term was populated with the remote's pterm. + n.catchupFollower(ar) + } else { + n.Lock() + if ar.term > n.term { + // The remote node didn't commit the append entry, it looks like + // they are on a newer term than we are. Step down. + // In this case ar.term was populated with the remote's term. + n.term = ar.term + n.vote = noVote + n.writeTermVote() + n.warn("Detected another leader with higher term, will stepdown") + n.stepdownLocked(noLeader) + } + n.Unlock() + // Either way, return back to pool. + arPool.Put(ar) + } +} + +// handleAppendEntryResponse processes responses to append entries. +func (n *raft) handleAppendEntryResponse(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + ar := decodeAppendEntryResponse(msg) + if ar == nil { + n.error("Received malformed append entry response") + return + } + ar.reply = reply + n.resp.push(ar) +} + +func (n *raft) buildAppendEntry(entries []*Entry) *appendEntry { + return newAppendEntry(n.id, n.term, n.commit, n.pterm, n.pindex, entries) +} + +// Determine if we should store an entry. This stops us from storing +// heartbeat messages. +func (ae *appendEntry) shouldStore() bool { + return ae != nil && len(ae.entries) > 0 +} + +// Store our append entry to our WAL. +// lock should be held. +func (n *raft) storeToWAL(ae *appendEntry) error { + if ae == nil { + return fmt.Errorf("raft: Missing append entry for storage") + } + if n.werr != nil { + return n.werr + } + + seq, _, err := n.wal.StoreMsg(_EMPTY_, nil, ae.buf, 0) + if err != nil { + n.setWriteErrLocked(err) + return err + } + + // Sanity checking for now. + if index := ae.pindex + 1; index != seq { + n.warn("Wrong index, ae is %+v, index stored was %d, n.pindex is %d, will reset", ae, seq, n.pindex) + if n.State() == Leader { + n.stepdownLocked(n.selectNextLeader()) + } + // Reset and cancel any catchup. + n.resetWAL() + n.cancelCatchup() + return errEntryStoreFailed + } + + n.bytes += n.entryStoreSize(ae) + n.pterm = ae.term + n.pindex = seq + return nil +} + +const ( + pauseQuorumThreshold = 100_000 + pauseQuorumBytes = 1 * 1024 * 1024 * 1024 // 1GB + + paeDropThreshold = 20_000 + paeDropBytes = 256 * 1024 * 1024 // 256MB + + paeWarnThreshold = 10_000 + paeWarnModulo = 5_000 + paeWarnBytes = 128 * 1024 * 1024 // 128MB + paeWarnBytesModulo = 32 * 1024 * 1024 // 32MB +) + +func (n *raft) sendAppendEntry(entries []*Entry) { + n.Lock() + defer n.Unlock() + n.sendAppendEntryLocked(entries, true) +} + +// Returns nil if an appendEntry was appended to our WAL and sent to followers, +// an error otherwise. +func (n *raft) sendAppendEntryLocked(entries []*Entry, checkLeader bool) error { + // Safeguard against sending an append entry right after a stepdown from a different goroutine. + // Specifically done while holding the lock to not race. + if checkLeader && n.State() != Leader { + n.debug("Not sending append entry, not leader") + return errNotLeader + } + + // The raft mutex remains held while entries are stored + // and sent to followers. If the leader is ready to take + // more proposals (no errors, not overrun), allow Propose + // and ProposeMulti to enqueue proposals for the current + // term without acquiring the raft mutex. + if checkLeader && n.werr == nil && !n.isLeaderOverrun() && !n.checkFastPathLimits(n.term) { + n.fastPathTerm.Store(n.term) + defer n.fastPathTerm.Store(0) + } + + ae := n.buildAppendEntry(entries) + + var err error + var scratch [1024]byte + ae.buf, err = ae.encode(scratch[:]) + if err != nil { + return err + } + + // If we have entries store this in our wal. + shouldStore := ae.shouldStore() + if shouldStore { + if err := n.storeToWAL(ae); err != nil { + return err + } + n.active = time.Now() + n.cachePendingEntry(ae) + } + n.sendRPC(n.asubj, n.areply, ae.buf) + if !shouldStore { + ae.returnToPool() + } + if n.qn <= 1 { + n.tryCommit(n.pindex) + } + return nil +} + +// cachePendingEntry saves append entries in memory for faster processing during applyCommit. +// Only save so many however to avoid memory bloat. +func (n *raft) cachePendingEntry(ae *appendEntry) { + // Invalidate any cache entry at this index, we might have + // stored it previously with a different value. + if pae, ok := n.pae[n.pindex]; ok { + n.paeBytes -= n.entryStoreSize(pae) + delete(n.pae, n.pindex) + } + if l := uint64(len(n.pae)); l < paeDropThreshold && n.paeBytes < paeDropBytes { + prev := n.paeBytes + n.pae[n.pindex], l = ae, l+1 + n.paeBytes += n.entryStoreSize(ae) + if l >= paeWarnThreshold && l%paeWarnModulo == 0 { + n.warn("%d append entries pending", len(n.pae)) + } else if b := n.paeBytes; b >= paeWarnBytes { + // Only log on transition past another paeWarnBytesModulo. + if b/paeWarnBytesModulo != prev/paeWarnBytesModulo { + n.warn("%d append entries pending, %s", l, friendlyBytes(b)) + } + } + } +} + +// entryStoreSize returns the byte size of the given append entry as stored in our WAL. +// Lock should be held. +func (n *raft) entryStoreSize(ae *appendEntry) uint64 { + if ae == nil || ae.buf == nil { + return 0 + } + if n.wtype == FileStorage { + return fileStoreMsgSize(_EMPTY_, nil, ae.buf) + } + return memStoreMsgSize(_EMPTY_, nil, ae.buf) +} + +// resetPendingEntries clears our append entry cache. +// Lock should be held. +func (n *raft) resetPendingEntries() { + if len(n.pae) > 0 { + n.pae = make(map[uint64]*appendEntry) + } + n.paeBytes = 0 +} + +type extensionState uint16 + +const ( + extUndetermined = extensionState(iota) + extExtended + extNotExtended +) + +type peerState struct { + knownPeers []string + clusterSize int + domainExt extensionState +} + +func peerStateBufSize(ps *peerState) int { + return 4 + 4 + (idLen * len(ps.knownPeers)) + 2 +} + +func encodePeerState(ps *peerState) []byte { + le := binary.LittleEndian + buf := make([]byte, peerStateBufSize(ps)) + le.PutUint32(buf[0:], uint32(ps.clusterSize)) + le.PutUint32(buf[4:], uint32(len(ps.knownPeers))) + wi := 8 + for _, peer := range ps.knownPeers { + copy(buf[wi:], peer) + wi += idLen + } + le.PutUint16(buf[wi:], uint16(ps.domainExt)) + return buf +} + +func decodePeerState(buf []byte) (*peerState, error) { + if len(buf) < 8 { + return nil, errCorruptPeers + } + le := binary.LittleEndian + ps := &peerState{clusterSize: int(le.Uint32(buf[0:]))} + expectedPeers := int(le.Uint32(buf[4:])) + buf = buf[8:] + ri := 0 + for i, n := 0, expectedPeers; i < n && ri+idLen <= len(buf); i++ { + ps.knownPeers = append(ps.knownPeers, string(buf[ri:ri+idLen])) + ri += idLen + } + if len(ps.knownPeers) != expectedPeers { + return nil, errCorruptPeers + } + if len(buf[ri:]) >= 2 { + ps.domainExt = extensionState(le.Uint16(buf[ri:])) + } + return ps, nil +} + +func (n *raft) PeerNames() []string { + n.RLock() + defer n.RUnlock() + return n.peerNames() +} + +// VotingPeerNames returns every peer that could still be voting on entries, +// which is our peer set plus a peer we speculatively dropped for a removal +// that has not committed yet. See votingPeerNames. +func (n *raft) VotingPeerNames() []string { + n.RLock() + defer n.RUnlock() + return n.votingPeerNames() +} + +func (n *raft) votingPeerNames() []string { + peers := n.peerNames() + // Only a removal tracks the previous membership state, an addition does not, + // and there can only ever be one membership change inflight. + if n.membChange != nil && n.membChange.prev != nil && !slices.Contains(peers, n.membChange.peer) { + peers = append(peers, n.membChange.peer) + } + return peers +} + +// Lock should be held. +func (n *raft) peerNames() []string { + peers := make([]string, 0, len(n.peers)) + for name := range n.peers { + peers = append(peers, name) + } + return peers +} + +func (n *raft) currentPeerState() *peerState { + n.RLock() + ps := n.currentPeerStateLocked() + n.RUnlock() + return ps +} + +func (n *raft) currentPeerStateLocked() *peerState { + return &peerState{n.peerNames(), n.csz, n.extSt} +} + +// committedPeerStateLocked builds peer state from the committed membership as of the given index. +// Lock should be held. +func (n *raft) committedPeerStateLocked(index uint64) *peerState { + // No pending change or index already covers the pending change. + if n.membChange == nil || n.membChange.index <= index { + return &peerState{n.peerNames(), n.csz, n.extSt} + } + + // Reconstruct the committed peer set by reverting the speculative change. + peer := n.membChange.peer + if n.membChange.prev != nil { + // This was a removal; the peer is still a committed member, add it back. + names := make([]string, 0, len(n.peers)+1) + for name := range n.peers { + names = append(names, name) + } + if _, ok := n.peers[peer]; !ok { + names = append(names, peer) + } + return &peerState{names, len(names), n.extSt} + } + + // This was an addition; exclude the not-yet-committed peer. + names := make([]string, 0, len(n.peers)) + for name := range n.peers { + if name == peer { + continue + } + names = append(names, name) + } + return &peerState{names, len(names), n.extSt} +} + +// sendPeerState will send our current peer state to the cluster. +// Lock should be held. +func (n *raft) sendPeerState() { + n.sendAppendEntryLocked([]*Entry{{EntryPeerState, encodePeerState(n.currentPeerStateLocked())}}, true) +} + +// Send a heartbeat. +func (n *raft) SendHeartbeat() { + if n.State() == Leader { + n.sendHeartbeat() + } +} + +// Send a heartbeat. +func (n *raft) sendHeartbeat() { + n.sendAppendEntry(nil) +} + +type voteRequest struct { + term uint64 + lastTerm uint64 + lastIndex uint64 + candidate string + // internal only. + reply string +} + +const voteRequestLen = 24 + idLen + +func (vr *voteRequest) encode() []byte { + var buf [voteRequestLen]byte + le := binary.LittleEndian + le.PutUint64(buf[0:], vr.term) + le.PutUint64(buf[8:], vr.lastTerm) + le.PutUint64(buf[16:], vr.lastIndex) + copy(buf[24:24+idLen], vr.candidate) + + return buf[:voteRequestLen] +} + +func decodeVoteRequest(msg []byte, reply string) *voteRequest { + if len(msg) != voteRequestLen { + return nil + } + + le := binary.LittleEndian + return &voteRequest{ + term: le.Uint64(msg[0:]), + lastTerm: le.Uint64(msg[8:]), + lastIndex: le.Uint64(msg[16:]), + candidate: string(copyBytes(msg[24 : 24+idLen])), + reply: reply, + } +} + +const peerStateFile = "peers.idx" + +// Lock should be held. +func (n *raft) writePeerState(ps *peerState) { + pse := encodePeerState(ps) + if bytes.Equal(n.wps, pse) { + return + } + // Stamp latest and write the peer state file. + n.wps = pse + if err := writePeerState(n.dios, n.sd, ps); err != nil && !n.isClosed() { + n.setWriteErrLocked(err) + n.warn("Error writing peer state file for %q: %v", n.group, err) + } +} + +// Writes out our peer state outside of a specific raft context. +func writePeerState(dios *diskIOSemaphore, sd string, ps *peerState) error { + psf := filepath.Join(sd, peerStateFile) + if _, err := os.Stat(psf); err != nil && !os.IsNotExist(err) { + return err + } + return writeFileWithSync(dios, psf, encodePeerState(ps), defaultFilePerms) +} + +func readPeerState(dios *diskIOSemaphore, sd string) (ps *peerState, err error) { + dios.acquire() + buf, err := os.ReadFile(filepath.Join(sd, peerStateFile)) + dios.release() + + if err != nil { + return nil, err + } + return decodePeerState(buf) +} + +const ( + termVoteFile = "tav.idx" + termLen = 8 // uint64 + termVoteLen = idLen + termLen +) + +// Writes out our term & vote outside of a specific raft context. +func writeTermVote(dios *diskIOSemaphore, sd string, wtv []byte) error { + psf := filepath.Join(sd, termVoteFile) + if _, err := os.Stat(psf); err != nil && !os.IsNotExist(err) { + return err + } + return writeFileWithSync(dios, psf, wtv, defaultFilePerms) +} + +// readTermVote will read the largest term and who we voted from to stable storage. +// Lock should be held. +func (n *raft) readTermVote() (term uint64, voted string, err error) { + n.dios.acquire() + buf, err := os.ReadFile(filepath.Join(n.sd, termVoteFile)) + n.dios.release() + + if err != nil { + return 0, noVote, err + } + if len(buf) < termLen { + // Not enough bytes for the uint64 below, so avoid a panic. + return 0, noVote, nil + } + le := binary.LittleEndian + term = le.Uint64(buf[0:]) + if len(buf) < termVoteLen { + return term, noVote, nil + } + voted = string(buf[8:]) + return term, voted, nil +} + +// Lock should be held. +func (n *raft) setWriteErrLocked(err error) { + // Check if we are closed already. + if n.State() == Closed { + return + } + // Ignore if already set. + if n.werr == err || err == nil { + return + } + // Ignore non-write errors. + if err == ErrStoreClosed || + err == ErrStoreEOF || + err == ErrStoreMsgNotFound || + err == errNoPending || + err == errPartialCache { + return + } + // If this is a not found report but do not disable. + if os.IsNotExist(err) { + n.warn("Resource not found: %v", err) + return + } + n.error("Critical write error: %v", err) + n.werr = err + // Abort any inflight async snapshot checkpoint. + n.snapshotting = false + n.shutdown() + assert.Unreachable("Raft encountered write error", map[string]any{ + "n.accName": n.accName, + "n.group": n.group, + "n.id": n.id, + "err": err, + }) + + if isPermissionError(err) { + go n.s.handleWritePermissionError() + } + + if isOutOfSpaceErr(err) { + // For now since this can be happening all under the covers, we will call up and disable JetStream. + go n.s.handleOutOfSpace(nil) + } +} + +// Helper to check if we are closed when we do not hold a lock already. +func (n *raft) isClosed() bool { + return n.State() == Closed +} + +// GetWriteErr returns the write error (if any). +func (n *raft) GetWriteErr() error { + n.RLock() + defer n.RUnlock() + return n.werr +} + +// Capture our write error if any and hold. +func (n *raft) setWriteErr(err error) { + n.Lock() + defer n.Unlock() + n.setWriteErrLocked(err) +} + +// writeTermVote will record the largest term and who we voted for to stable +// storage. It returns the write error, if any, so that callers which need the +// term/vote to be durable before acting on it (e.g. granting or requesting a +// vote) can avoid doing so when the persist fails. +// Lock should be held. +func (n *raft) writeTermVote() error { + if n.werr != nil { + return n.werr + } + + var buf [termVoteLen]byte + le := binary.LittleEndian + le.PutUint64(buf[0:], n.term) + copy(buf[8:], n.vote) + b := buf[:8+len(n.vote)] + + // If the term and vote hasn't changed then don't rewrite to disk. + if bytes.Equal(n.wtv, b) { + return nil + } + // Stamp latest and write the term & vote file. + n.wtv = b + if err := writeTermVote(n.dios, n.sd, n.wtv); err != nil && !n.isClosed() { + // Clear wtv since we failed. + n.wtv = nil + n.setWriteErrLocked(err) + n.warn("Error writing term and vote file for %q: %v", n.group, err) + return err + } + return nil +} + +// voteResponse is a response to a vote request. +type voteResponse struct { + term uint64 + peer string + granted bool + empty bool // "Empty vote", whether this peer's log is empty. +} + +const voteResponseLen = 8 + 8 + 1 + +func (vr *voteResponse) encode() []byte { + var buf [voteResponseLen]byte + le := binary.LittleEndian + le.PutUint64(buf[0:], vr.term) + copy(buf[8:], vr.peer) + if vr.granted { + buf[16] |= 1 + } + if vr.empty { + buf[16] |= 2 + } + return buf[:voteResponseLen] +} + +func decodeVoteResponse(msg []byte) *voteResponse { + if len(msg) != voteResponseLen { + return nil + } + le := binary.LittleEndian + vr := &voteResponse{term: le.Uint64(msg[0:]), peer: string(msg[8:16])} + vr.granted = msg[16]&1 != 0 + vr.empty = msg[16]&2 != 0 + return vr +} + +func (n *raft) handleVoteResponse(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { + vr := decodeVoteResponse(msg) + n.debug("Received a voteResponse %+v", vr) + if vr == nil { + n.error("Received malformed vote response for %q", n.group) + return + } + + if state := n.State(); state != Candidate && state != Leader { + n.debug("Ignoring old vote response, we have stepped down") + return + } + + n.votes.push(vr) +} + +func (n *raft) processVoteRequest(vr *voteRequest) error { + // To simplify calling code, we can possibly pass `nil` to this function. + // If that is the case, does not consider it an error. + if vr == nil { + return nil + } + n.debug("Received a voteRequest %+v", vr) + + n.Lock() + + vresp := &voteResponse{n.term, n.id, false, n.pindex == 0} + defer n.debug("Sending a voteResponse %+v -> %q", vresp, vr.reply) + + // Ignore if we are newer. This is important so that we don't accidentally process + // votes from a previous term if they were still in flight somewhere. + if vr.term < n.term { + n.Unlock() + n.sendReply(vr.reply, vresp.encode()) + return nil + } + + // If this is a higher term go ahead and stepdown. + if vr.term > n.term { + if n.State() != Follower { + n.debug("Stepping down from %s, detected higher term: %d vs %d", + strings.ToLower(n.State().String()), vr.term, n.term) + n.stepdownLocked(noLeader) + } + n.cancelCatchup() + n.term = vr.term + n.vote = noVote + n.writeTermVote() + // Clear current Leader since it belonged to an old term. + n.updateLeader(noLeader) + } + + // Only way we get to yes is through here. + voteOk := n.vote == noVote || n.vote == vr.candidate + + // If we have an empty log, but are initializing. + if voteOk && vresp.empty && n.initializing { + // Reset notion of having an empty log if we're voting during initialization/scale up. + // Ensures they only need quorum, and not need to hear from all servers. + vresp.empty = false + } + + // Other server's log needs to be equal or more up-to-date than ours. + if voteOk && (vr.lastTerm > n.pterm || vr.lastTerm == n.pterm && vr.lastIndex >= n.pindex) { + // Persist our vote before granting it. Raft requires the vote to be durable before + // we respond, otherwise a restart could forget the vote and let us grant a second + // vote in the same term, which would break election safety. + n.term = vr.term + n.vote = vr.candidate + if err := n.writeTermVote(); err != nil { + n.vote = noVote + n.warn("Not granting vote for %q, could not persist term and vote: %v", n.group, err) + } else { + vresp.granted = true + n.resetElectionTimeout() + } + } else if n.vote == noVote && n.State() != Candidate { + // We have a more up-to-date log, and haven't voted yet. + // Start campaigning earlier, but only if not candidate already, as that would short-circuit us. + n.resetElect(randCampaignTimeout()) + } + + // Term might have changed, make sure response has the most current + vresp.term = n.term + + n.Unlock() + + n.sendReply(vr.reply, vresp.encode()) + + return nil +} + +func (n *raft) handleVoteRequest(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { + vr := decodeVoteRequest(msg, reply) + if vr == nil { + n.error("Received malformed vote request for %q", n.group) + return + } + n.reqs.push(vr) +} + +func (n *raft) requestVote() { + n.Lock() + if n.State() != Candidate { + n.Unlock() + return + } + n.vote = n.id + if err := n.writeTermVote(); err != nil { + // Make sure that our self-vote is persisted durably before campaigning, otherwise + // we could vote for someone else in the same term after a restart. + n.vote = noVote + n.Unlock() + n.warn("Not requesting votes for %q, could not persist term and vote: %v", n.group, err) + return + } + vr := voteRequest{n.term, n.pterm, n.pindex, n.id, _EMPTY_} + subj, reply := n.vsubj, n.vreply + n.Unlock() + + n.debug("Sending out voteRequest %+v", vr) + + // Now send it out. + n.sendRPC(subj, reply, vr.encode()) +} + +func (n *raft) sendRPC(subject, reply string, msg []byte) { + n.t.Publish(subject, reply, msg) +} + +func (n *raft) sendReply(subject string, msg []byte) { + n.t.Publish(subject, _EMPTY_, msg) +} + +func (n *raft) wonElection(votes int) bool { + return votes >= n.QuorumNeeded() +} + +// QuorumNeeded returns the current effective quorum size. +func (n *raft) QuorumNeeded() int { + n.RLock() + qn := n.qn + n.RUnlock() + return qn +} + +// InRescue returns whether an unsafe quorum rescue is currently active. +func (n *raft) InRescue() bool { + n.RLock() + defer n.RUnlock() + return n.rescue != nil +} + +// leadChange signals a leadership change to the upper layer. The term +// identifies the leadership epoch the signal belongs to. +type leadChange struct { + isLeader bool + term uint64 + nudge bool // Signal that a new peer was observed, only if the group is managed. +} + +// Lock should be held. +func (n *raft) updateLeadChange(isLeader bool) { + lc := leadChange{isLeader: isLeader, term: n.term} + // We don't care about values that have not been consumed (transitory states), + // so we dequeue any state that is pending and push the new one. + for { + select { + case n.leadc <- lc: + return + default: + select { + case <-n.leadc: + default: + // May have been consumed by the "reader" go routine, so go back + // to the top of the loop and try to send again. + } + } + } +} + +// Lock should be held. +func (n *raft) switchState(state RaftState) bool { +retry: + pstate := n.State() + if pstate == Closed { + return false + } + + // Set our state. If something else has changed our state + // then retry, this will either be a Stop or Delete call. + if !n.state.CompareAndSwap(int32(pstate), int32(state)) { + goto retry + } + + // Reset the election timer. + n.resetElectionTimeout() + + var leadChanged bool + if pstate == Leader && state != Leader { + leadChanged = true + n.updateLeadChange(false) + // Disable fast path proposals before draining the proposal queue below. + n.fastPathTerm.Store(0) + // Drain the append entry response and proposal queues. + n.resp.drain() + n.prop.drain() + } else if state == Leader && pstate != Leader { + // Don't updateLeadChange here, it will be done in switchToLeader or after initial messages are applied. + leadChanged = true + n.resetPendingEntries() + } + + n.writeTermVote() + return leadChanged +} + +const ( + noLeader = _EMPTY_ + noVote = _EMPTY_ +) + +func (n *raft) switchToFollower(leader string) { + n.Lock() + defer n.Unlock() + + n.switchToFollowerLocked(leader) +} + +func (n *raft) switchToFollowerLocked(leader string) { + if n.State() == Closed { + return + } + + n.debug("Switching to follower") + + n.aflr = 0 + n.leaderState.Store(false) + n.leaderSince.Store(nil) + n.lxfer = false + + // Reset acks, we can't assume acks from a previous term are still valid in another term. + if len(n.acks) > 0 { + n.acks = make(map[uint64]map[string]struct{}) + } + n.updateLeader(leader) + n.switchState(Follower) +} + +func (n *raft) switchToCandidate() { + if n.State() == Closed { + return + } + + n.Lock() + defer n.Unlock() + + // If we are catching up or are in observer mode we can not switch. + // Avoid petitioning to become leader if we're behind on applies. + if n.observer || n.paused || n.processed < n.commit { + n.resetElect(minElectionTimeout / 4) + return + } + + // Do not campaign with an uncommitted membership change about us, + // otherwise we would try to become leader outside our peer set. + if n.membChange != nil && n.membChange.peer == n.id { + // The election timer fired, so we've not heard from a leader. Clear it + // like campaigning would, or we'd keep reporting we have one and the + // upper layer could never recognize the group as stuck. + n.updateLeader(noLeader) + n.resetElect(minElectionTimeout) + return + } + + // For managed groups the meta layer can assign us before the group leader has + // added us to the peer set. Do not campaign while we're not a member ourselves. + if n.managed && n.peers[n.id] == nil { + n.updateLeader(noLeader) + n.resetElect(minElectionTimeout) + return + } + + if n.State() != Candidate { + n.debug("Switching to candidate") + } else { + if n.lostQuorumLocked() && time.Since(n.llqrt) > lostQuorumSignal { + // We signal to the upper layers such that can alert on quorum lost. + n.updateLeadChange(false) + n.llqrt = time.Now() + } + } + // Increment the term. + n.term++ + n.vote = noVote + // Reset quorum paused. If it was previously set, we checked above that we've applied all committed entries. + n.quorumPaused = false + // Clear current Leader. + n.updateLeader(noLeader) + n.switchState(Candidate) +} + +func (n *raft) switchToLeader() { + if n.State() == Closed { + return + } + + n.Lock() + defer n.Unlock() + + n.debug("Switching to leader") + + n.lxfer = false + n.updateLeader(n.id) + n.switchState(Leader) + + // To send out our initial peer state. + // In our implementation this is equivalent to sending a NOOP-entry upon becoming leader. + // Wait for this message (and potentially more) to be applied. + // It's important to wait signaling we're leader if we're not up-to-date yet, as that + // would mean we're in a consistent state compared with the previous leader. + n.sendPeerState() + n.aflr = n.pindex +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..0be7ef8 --- /dev/null +++ b/server/server.go @@ -0,0 +1,4808 @@ +// Copyright 2012-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 ( + "bytes" + "context" + "crypto/fips140" + "crypto/tls" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "log" + "math/rand/v2" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "regexp" + "runtime" + "runtime/pprof" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + // Allow dynamic profiling. + _ "net/http/pprof" + + "expvar" + + "github.com/klauspost/compress/s2" + "github.com/nats-io/jwt/v2" + "github.com/nats-io/nats-server/v2/logger" + "github.com/nats-io/nkeys" + "github.com/nats-io/nuid" +) + +const ( + // Interval for the first PING for non client connections. + firstPingInterval = time.Second + + // This is for the first ping for client connections. + firstClientPingInterval = 2 * time.Second +) + +// These are protocol versions sent between server connections: ROUTER, LEAF and +// GATEWAY. We may have protocol versions that have a meaning only for a certain +// type of connections, but we don't have to have separate enums for that. +// However, it is CRITICAL to not change the order of those constants since they +// are exchanged between servers. When adding a new protocol version, add to the +// end of the list, don't try to group them by connection types. +const ( + // RouteProtoZero is the original Route protocol from 2009. + // http://nats.io/documentation/internals/nats-protocol/ + RouteProtoZero = iota + // RouteProtoInfo signals a route can receive more then the original INFO block. + // This can be used to update remote cluster permissions, etc... + RouteProtoInfo + // RouteProtoV2 is the new route/cluster protocol that provides account support. + RouteProtoV2 + // MsgTraceProto indicates that this server understands distributed message tracing. + MsgTraceProto +) + +// Will return the latest server-to-server protocol versions, unless the +// option to override it is set. +func (s *Server) getServerProto() int { + opts := s.getOpts() + // Initialize with the latest protocol version. + proto := MsgTraceProto + // For tests, we want to be able to make this server behave + // as an older server so check this option to see if we should override. + if opts.overrideProto < 0 { + // The option overrideProto is set to 0 by default (when creating an + // Options structure). Since this is the same value than the original + // proto RouteProtoZero, tests call setServerProtoForTest() with the + // desired protocol level, which sets it as negative value equal to: + // (wantedProto + 1) * -1. Here we compute back the real value. + proto = (opts.overrideProto * -1) - 1 + } + return proto +} + +// Used by tests. +func setServerProtoForTest(wantedProto int) int { + return (wantedProto + 1) * -1 +} + +// Info is the information sent to clients, routes, gateways, and leaf nodes, +// to help them understand information about this server. +type Info struct { + ID string `json:"server_id"` + Name string `json:"server_name"` + Version string `json:"version"` + Proto int `json:"proto"` + GitCommit string `json:"git_commit,omitempty"` + GoVersion string `json:"go"` + Host string `json:"host"` + Port int `json:"port"` + Headers bool `json:"headers"` + AuthRequired bool `json:"auth_required,omitempty"` + TLSRequired bool `json:"tls_required,omitempty"` + TLSVerify bool `json:"tls_verify,omitempty"` + TLSAvailable bool `json:"tls_available,omitempty"` + MaxPayload int32 `json:"max_payload"` + JetStream bool `json:"jetstream,omitempty"` + IP string `json:"ip,omitempty"` + CID uint64 `json:"client_id,omitempty"` + ClientIP string `json:"client_ip,omitempty"` + Nonce string `json:"nonce,omitempty"` + Cluster string `json:"cluster,omitempty"` + Dynamic bool `json:"cluster_dynamic,omitempty"` + Domain string `json:"domain,omitempty"` + ClientConnectURLs []string `json:"connect_urls,omitempty"` // Contains URLs a client can connect to. + WSConnectURLs []string `json:"ws_connect_urls,omitempty"` // Contains URLs a ws client can connect to. + LameDuckMode bool `json:"ldm,omitempty"` + Compression string `json:"compression,omitempty"` + ConnectInfo bool `json:"connect_info,omitempty"` // When true this is the server INFO response to CONNECT + RemoteAccount string `json:"remote_account,omitempty"` // Lets the client or leafnode side know the remote account that they bind to. + IsSystemAccount bool `json:"acc_is_sys,omitempty"` // Indicates if the account is a system account. + JSApiLevel int `json:"api_lvl,omitempty"` + + // Route Specific + Import *SubjectPermission `json:"import,omitempty"` + Export *SubjectPermission `json:"export,omitempty"` + LNOC bool `json:"lnoc,omitempty"` + LNOCU bool `json:"lnocu,omitempty"` + LN bool `json:"ln,omitempty"` // Support for LS+/LS- leaf interest using a sentinel origin cluster, implies LNOC+LNOCU + InfoOnConnect bool `json:"info_on_connect,omitempty"` // When true the server will respond to CONNECT with an INFO + RoutePoolSize int `json:"route_pool_size,omitempty"` + RoutePoolIdx int `json:"route_pool_idx,omitempty"` + RouteAccount string `json:"route_account,omitempty"` + RouteAccReqID string `json:"route_acc_add_reqid,omitempty"` + GossipMode byte `json:"gossip_mode,omitempty"` + + // Gateways Specific + Gateway string `json:"gateway,omitempty"` // Name of the origin Gateway (sent by gateway's INFO) + GatewayURLs []string `json:"gateway_urls,omitempty"` // Gateway URLs in the originating cluster (sent by gateway's INFO) + GatewayURL string `json:"gateway_url,omitempty"` // Gateway URL on that server (sent by route's INFO) + GatewayCmd byte `json:"gateway_cmd,omitempty"` // Command code for the receiving server to know what to do + GatewayCmdPayload []byte `json:"gateway_cmd_payload,omitempty"` // Command payload when needed + GatewayNRP bool `json:"gateway_nrp,omitempty"` // Uses new $GNR. prefix for mapped replies + GatewayIOM bool `json:"gateway_iom,omitempty"` // Indicate that all accounts will be switched to InterestOnly mode "right away" + + // LeafNode Specific + LeafNodeURLs []string `json:"leafnode_urls,omitempty"` // LeafNode URLs that the server can reconnect to. + + XKey string `json:"xkey,omitempty"` // Public server's x25519 key. +} + +// Server is our main struct. +type Server struct { + // Fields accessed with atomic operations need to be 64-bit aligned + gcid uint64 + // How often user logon fails due to the issuer account not being pinned. + pinnedAccFail uint64 + stats + scStats + staleStats + mu sync.RWMutex + reloadMu sync.RWMutex // Write-locked when a config reload is taking place ONLY + kp nkeys.KeyPair + xkp nkeys.KeyPair + xpub string + info Info + configFile string + optsMu sync.RWMutex + opts *Options + running atomic.Bool + shutdown atomic.Bool + listener net.Listener + listenerErr error + gacc *Account + sys *internal + sysAcc atomic.Pointer[Account] + js atomic.Pointer[jetStream] + dios *diskIOSemaphore + isMetaLeader atomic.Bool + jsClustered atomic.Bool + accounts sync.Map + tmpAccounts sync.Map // Temporarily stores accounts that are being built + activeAccounts int32 + accResolver AccountResolver + clients map[uint64]*client + routes map[string][]*client + remoteRoutePoolSize map[string]int // Map for remote's configure route pool size + routesPoolSize int // Configured pool size + routesReject bool // During reload, we may want to reject adding routes until some conditions are met + routesNoPool int // Number of routes that don't use pooling (connecting to older server for instance) + accRoutes map[string]map[string]*client // Key is account name, value is key=remoteID/value=route connection + accRouteByHash sync.Map // Key is account name, value is nil or a pool index + accAddedCh chan struct{} + accAddedReqID string + leafs map[uint64]*client + users map[string]*User + nkeys map[string]*NkeyUser + totalClients uint64 + closed *closedRingBuffer + done chan bool + start time.Time + http net.Listener + httpHandler http.Handler + httpBasePath string + profiler net.Listener + httpReqStats map[string]uint64 + routeListener net.Listener + routeListenerErr error + routeInfo Info + routeResolver netResolver + routesToSelf map[string]struct{} + routeTLSName string + leafNodeListener net.Listener + leafNodeListenerErr error + leafNodeInfo Info + leafNodeInfoJSON []byte + leafURLsMap refCountedUrlSet + leafNodeOpts struct { + resolver netResolver + dialTimeout time.Duration + dialer leafNodeDialer + } + leafRemoteCfgs map[*leafNodeCfg]struct{} + rmLeafRemoteCfgs map[string]*leafNodeCfg + leafRemoteAccounts sync.Map + leafNodeEnabled bool + leafDisableConnect bool // Used in test only + leafNoCluster bool // Indicate that this server has only remotes and no cluster defined + + quitCh chan struct{} + startupComplete chan struct{} + shutdownComplete chan struct{} + + // Tracking Go routines + grMu sync.Mutex + grTmpClients map[uint64]*client + grRunning bool + grWG sync.WaitGroup // to wait on various go routines + + cproto int64 // number of clients supporting async INFO + configTime time.Time // last time config was loaded + + logging struct { + sync.RWMutex + logger Logger + trace int32 + debug int32 + traceSysAcc int32 + } + + clientConnectURLs []string + + // Used internally for quick look-ups. + clientConnectURLsMap refCountedUrlSet + + // For Gateways + gatewayListener net.Listener // Accept listener + gatewayListenerErr error + gateway *srvGateway + + // Used by tests to check that http.Servers do + // not set any timeout. + monitoringServer *http.Server + profilingServer *http.Server + + // LameDuck mode + ldm bool + ldmCh chan bool + + // Trusted public operator keys. + trustedKeys []string + // map of trusted keys to operator setting StrictSigningKeyUsage + strictSigningKeyUsage map[string]struct{} + + // We use this to minimize mem copies for requests to monitoring + // endpoint /varz (when it comes from http). + varzMu sync.Mutex + varz *Varz + // This is set during a config reload if we detect that we have + // added/removed routes. The monitoring code then check that + // to know if it should update the cluster's URLs array. + varzUpdateRouteURLs bool + + // Keeps a sublist of subscriptions attached to leafnode connections + // for the $GNR.*.*.*.> subject so that a server can send back a mapped + // gateway reply. + gwLeafSubs *Sublist + + // Used for expiration of mapped GW replies + gwrm struct { + w int32 + ch chan time.Duration + m sync.Map + } + + // For eventIDs + eventIdsMu sync.Mutex + eventIds *nuid.NUID + + // Websocket structure + websocket srvWebsocket + + // MQTT structure + mqtt srvMQTT + + // OCSP monitoring + ocsps []*OCSPMonitor + + // OCSP peer verification (at least one TLS block) + ocspPeerVerify bool + + // OCSP response cache + ocsprc OCSPResponseCache + + // exporting account name the importer experienced issues with + incompleteAccExporterMap sync.Map + + // Holds cluster name under different lock for mapping + cnMu sync.RWMutex + cn string + + // For registering raft nodes with the server. + rnMu sync.RWMutex + raftNodes map[string]RaftNode + + // For mapping from a raft node name back to a server name and cluster. Node has to be in the same domain. + nodeToInfo sync.Map + + // For out of resources to not log errors too fast. + rerrMu sync.Mutex + rerrLast time.Time + + connRateCounter *rateCounter + + // If there is a system account configured, to still support the $G account, + // the server will create a fake user and add it to the list of users. + // Keep track of what that user name is for config reload purposes. + sysAccOnlyNoAuthUser string + + // IPQueues map + ipQueues sync.Map + + // To limit logging frequency + rateLimitLogging sync.Map + rateLimitLoggingCh chan time.Duration + + // Total outstanding catchup bytes in flight. + gcbMu sync.RWMutex + gcbOut int64 + gcbOutMax int64 // Taken from JetStreamMaxCatchup or defaultMaxTotalCatchupOutBytes + // A global chanel to kick out stalled catchup sequences. + gcbKick chan struct{} + + // Total outbound syncRequests + syncOutSem chan struct{} + + // Queue to process JS API requests that come from routes (or gateways) + jsAPIRoutedReqs *ipQueue[*jsAPIRoutedReq] + jsAPIRoutedInfoReqs *ipQueue[*jsAPIRoutedReq] + + // Delayed API responses. + delayedAPIResponses *ipQueue[*delayedAPIResponse] + + // Whether moving NRG traffic into accounts is permitted on this server. + // Controls whether or not the account NRG capability is set in statsz. + // Currently used by unit tests to simulate nodes not supporting account NRG. + accountNRGAllowed atomic.Bool + + // List of proxies trusted keys in `KeyPair` form so we can do signature + // verification when processing incoming proxy connections. + proxiesKeyPairs []nkeys.KeyPair + proxiedConns map[string]map[uint64]*client +} + +// For tracking JS nodes. +type nodeInfo struct { + name string + version string + cluster string + domain string + id string + tags jwt.TagList + cfg *JetStreamConfig + stats *JetStreamStats + offline bool + js bool + binarySnapshots bool // Deprecated, binary snapshots are always used; versions 2.10.0+ support it. + accountNRG bool +} + +// selectable reports whether we've had STATSZ from this node. Peer selection +// skips nodes without config or stats, so until then there's nothing we could +// place on it. +func (ni nodeInfo) selectable() bool { + return ni.cfg != nil && ni.stats != nil +} + +type stats struct { + inMsgs int64 + inBytes int64 + inClientMsgs int64 + inClientBytes int64 + outMsgs int64 + outBytes int64 + outClientMsgs int64 + outClientBytes int64 + slowConsumers int64 + staleConnections int64 + stalls int64 +} + +// scStats includes the total and per connection counters of Slow Consumers. +type scStats struct { + clients atomic.Uint64 + routes atomic.Uint64 + leafs atomic.Uint64 + gateways atomic.Uint64 +} + +// staleStats includes the total and per connection counters of Stale Connections. +type staleStats struct { + clients atomic.Uint64 + routes atomic.Uint64 + leafs atomic.Uint64 + gateways atomic.Uint64 +} + +// This is used by tests so we can run all server tests with a default route +// or leafnode compression mode. For instance: +// go test -race -v ./server -cluster_compression=fast +var ( + testDefaultClusterCompression string + testDefaultLeafNodeCompression string +) + +// Compression modes. +const ( + CompressionNotSupported = "not supported" + CompressionOff = "off" + CompressionAccept = "accept" + CompressionS2Auto = "s2_auto" + CompressionS2Uncompressed = "s2_uncompressed" + CompressionS2Fast = "s2_fast" + CompressionS2Better = "s2_better" + CompressionS2Best = "s2_best" +) + +// defaultCompressionS2AutoRTTThresholds is the default of RTT thresholds for +// the CompressionS2Auto mode. +var defaultCompressionS2AutoRTTThresholds = []time.Duration{ + // [0..10ms] -> CompressionS2Uncompressed + 10 * time.Millisecond, + // ]10ms..50ms] -> CompressionS2Fast + 50 * time.Millisecond, + // ]50ms..100ms] -> CompressionS2Better + 100 * time.Millisecond, + // ]100ms..] -> CompressionS2Best +} + +// For a given user provided string, matches to one of the compression mode +// constant and updates the provided string to that constant. Returns an +// error if the provided compression mode is not known. +// The parameter `chosenModeForOn` indicates which compression mode to use +// when the user selects "on" (or enabled, true, etc..). This is because +// we may have different defaults depending on where the compression is used. +func validateAndNormalizeCompressionOption(c *CompressionOpts, chosenModeForOn string) error { + if c == nil { + return nil + } + cmtl := strings.ToLower(c.Mode) + // First, check for the "on" case so that we set to the default compression + // mode for that. The other switch/case will finish setup if needed (for + // instance if the default mode is s2Auto). + switch cmtl { + case "on", "enabled", "true": + cmtl = chosenModeForOn + default: + } + // Check (again) with the proper mode. + switch cmtl { + case "not supported", "not_supported": + c.Mode = CompressionNotSupported + case "disabled", "off", "false": + c.Mode = CompressionOff + case "accept": + c.Mode = CompressionAccept + case "auto", "s2_auto": + var rtts []time.Duration + if len(c.RTTThresholds) == 0 { + rtts = defaultCompressionS2AutoRTTThresholds + } else { + for _, n := range c.RTTThresholds { + // Do not error on negative, but simply set to 0 + if n < 0 { + n = 0 + } + // Make sure they are properly ordered. However, it is possible + // to have a "0" anywhere in the list to indicate that this + // compression level should not be used. + if l := len(rtts); l > 0 && n != 0 { + for _, v := range rtts { + if n < v { + return fmt.Errorf("RTT threshold values %v should be in ascending order", c.RTTThresholds) + } + } + } + rtts = append(rtts, n) + } + if len(rtts) > 0 { + // Trim 0 that are at the end. + stop := -1 + for i := len(rtts) - 1; i >= 0; i-- { + if rtts[i] != 0 { + stop = i + break + } + } + rtts = rtts[:stop+1] + } + if len(rtts) > 4 { + // There should be at most values for "uncompressed", "fast", + // "better" and "best" (when some 0 are present). + return fmt.Errorf("compression mode %q should have no more than 4 RTT thresholds: %v", c.Mode, c.RTTThresholds) + } else if len(rtts) == 0 { + // But there should be at least 1 if the user provided the slice. + // We would be here only if it was provided by say with values + // being a single or all zeros. + return fmt.Errorf("compression mode %q requires at least one RTT threshold", c.Mode) + } + } + c.Mode = CompressionS2Auto + c.RTTThresholds = rtts + case "fast", "s2_fast": + c.Mode = CompressionS2Fast + case "better", "s2_better": + c.Mode = CompressionS2Better + case "best", "s2_best": + c.Mode = CompressionS2Best + default: + return fmt.Errorf("unsupported compression mode %q", c.Mode) + } + return nil +} + +// Returns `true` if the compression mode `m` indicates that the server +// will negotiate compression with the remote server, `false` otherwise. +// Note that the provided compression mode is assumed to have been +// normalized and validated. +func needsCompression(m string) bool { + return m != _EMPTY_ && m != CompressionOff && m != CompressionNotSupported +} + +// Compression is asymmetric, meaning that one side can have a different +// compression level than the other. However, we need to check for cases +// when this server `scm` or the remote `rcm` do not support compression +// (say older server, or test to make it behave as it is not), or have +// the compression off. +// Note that `scm` is assumed to not be "off" or "not supported". +func selectCompressionMode(scm, rcm string) (mode string, err error) { + if rcm == CompressionNotSupported || rcm == _EMPTY_ { + return CompressionNotSupported, nil + } + switch rcm { + case CompressionOff: + // If the remote explicitly disables compression, then we won't + // use compression. + return CompressionOff, nil + case CompressionAccept: + // If the remote is ok with compression (but is not initiating it), + // and if we too are in this mode, then it means no compression. + if scm == CompressionAccept { + return CompressionOff, nil + } + // Otherwise use our compression mode. + return scm, nil + case CompressionS2Auto, CompressionS2Uncompressed, CompressionS2Fast, CompressionS2Better, CompressionS2Best: + // This case is here to make sure that if we don't recognize a + // compression setting, we error out. + if scm == CompressionAccept { + // If our compression mode is "accept", then we will use the remote + // compression mode, except if it is "auto", in which case we will + // default to "fast". This is not a configuration (auto in one + // side and accept in the other) that would be recommended. + if rcm == CompressionS2Auto { + return CompressionS2Fast, nil + } + // Use their compression mode. + return rcm, nil + } + // Otherwise use our compression mode. + return scm, nil + default: + return _EMPTY_, fmt.Errorf("unsupported route compression mode %q", rcm) + } +} + +// If the configured compression mode is "auto" then will return that, +// otherwise will return the given `cm` compression mode. +func compressionModeForInfoProtocol(co *CompressionOpts, cm string) string { + if co.Mode == CompressionS2Auto { + return CompressionS2Auto + } + return cm +} + +// Given a connection RTT and a list of thresholds durations, this +// function will return an S2 compression level such as "uncompressed", +// "fast", "better" or "best". For instance, with the following slice: +// [5ms, 10ms, 15ms, 20ms], a RTT of up to 5ms will result +// in the compression level "uncompressed", ]5ms..10ms] will result in +// "fast" compression, etc.. +// However, the 0 value allows for disabling of some compression levels. +// For instance, the following slice: [0, 0, 20, 30] means that a RTT of +// [0..20ms] would result in the "better" compression - effectively disabling +// the use of "uncompressed" and "fast", then anything above 20ms would +// result in the use of "best" level (the 30 in the list has no effect +// and the list could have been simplified to [0, 0, 20]). +func selectS2AutoModeBasedOnRTT(rtt time.Duration, rttThresholds []time.Duration) string { + var idx int + var found bool + for i, d := range rttThresholds { + if rtt <= d { + idx = i + found = true + break + } + } + if !found { + // If we did not find but we have all levels, then use "best", + // otherwise use the last one in array. + if l := len(rttThresholds); l >= 3 { + idx = 3 + } else { + idx = l - 1 + } + } + switch idx { + case 0: + return CompressionS2Uncompressed + case 1: + return CompressionS2Fast + case 2: + return CompressionS2Better + } + return CompressionS2Best +} + +// Returns an array of s2 WriterOption based on the route compression mode. +// So far we return a single option, but this way we can call s2.NewWriter() +// with a nil []s2.WriterOption, but not with a nil s2.WriterOption, so +// this is more versatile. +func s2WriterOptions(cm string) []s2.WriterOption { + _opts := [2]s2.WriterOption{} + opts := append( + _opts[:0], + s2.WriterConcurrency(1), // Stop asynchronous flushing in separate goroutines + ) + switch cm { + case CompressionS2Uncompressed: + return append(opts, s2.WriterUncompressed()) + case CompressionS2Fast: + return opts + case CompressionS2Best: + return append(opts, s2.WriterBestCompression()) + case CompressionS2Better: + return append(opts, s2.WriterBetterCompression()) + default: + return nil + } +} + +// New will setup a new server struct after parsing the options. +// DEPRECATED: Use NewServer(opts) +func New(opts *Options) *Server { + s, _ := NewServer(opts) + return s +} + +func NewServerFromConfig(opts *Options) (*Server, error) { + if opts.ConfigFile != _EMPTY_ && opts.configDigest == "" { + if err := opts.ProcessConfigFile(opts.ConfigFile); err != nil { + return nil, err + } + } + return NewServer(opts) +} + +// NewServer will setup a new server struct after parsing the options. +// Could return an error if options can not be validated. +// The provided Options type should not be re-used afterwards. +// Either use Options.Clone() to pass a copy, or make a new one. +func NewServer(opts *Options) (*Server, error) { + setBaselineOptions(opts) + + // Process TLS options, including whether we require client certificates. + tlsReq := opts.TLSConfig != nil + verify := (tlsReq && opts.TLSConfig.ClientAuth == tls.RequireAndVerifyClientCert) + + // Create our server's nkey identity. + kp, _ := nkeys.CreateServer() + pub, _ := kp.PublicKey() + + // Create an xkey for encrypting messages from this server. + var xkp nkeys.KeyPair + var xpub string + if !fips140.Enabled() { + xkp, _ = nkeys.CreateCurveKeys() + xpub, _ = xkp.PublicKey() + } + + serverName := pub + if opts.ServerName != _EMPTY_ { + serverName = opts.ServerName + } + + httpBasePath := normalizeBasePath(opts.HTTPBasePath) + + // Validate some options. This is here because we cannot assume that + // server will always be started with configuration parsing (that could + // report issues). Its options can be (incorrectly) set by hand when + // server is embedded. If there is an error, return nil. + if err := validateOptions(opts); err != nil { + return nil, err + } + + info := Info{ + ID: pub, + XKey: xpub, + Version: VERSION, + Proto: PROTO, + GitCommit: gitCommit, + GoVersion: runtime.Version(), + Name: serverName, + Host: opts.Host, + Port: opts.Port, + AuthRequired: false, + TLSRequired: tlsReq && !opts.AllowNonTLS, + TLSVerify: verify, + MaxPayload: opts.MaxPayload, + JetStream: opts.JetStream, + Headers: !opts.NoHeaderSupport, + Cluster: opts.Cluster.Name, + Domain: opts.JetStreamDomain, + JSApiLevel: JSApiLevel, + } + + if tlsReq && !info.TLSRequired { + info.TLSAvailable = true + } + + now := time.Now() + + s := &Server{ + kp: kp, + xkp: xkp, + xpub: xpub, + configFile: opts.ConfigFile, + info: info, + opts: opts, + done: make(chan bool, 1), + start: now, + configTime: now, + gwLeafSubs: NewSublistWithCache(), + httpBasePath: httpBasePath, + eventIds: nuid.New(), + routesToSelf: make(map[string]struct{}), + httpReqStats: make(map[string]uint64), // Used to track HTTP requests + rateLimitLoggingCh: make(chan time.Duration, 1), + leafNodeEnabled: opts.LeafNode.Port != 0 || len(opts.LeafNode.Remotes) > 0, + syncOutSem: make(chan struct{}, maxConcurrentSyncRequests), + dios: newDiskIOSemaphore(opts.JetStreamConcurrentIOs), + } + + // Delayed API response queue. Create regardless if JetStream is configured + // or not (since it can be enabled/disabled with config reload, we want this + // queue to exist at all times). + s.delayedAPIResponses = newIPQueue[*delayedAPIResponse](s, "delayed API responses") + + // By default we'll allow account NRG. + s.accountNRGAllowed.Store(true) + + // Fill up the maximum in flight syncRequests for this server. + // Used in JetStream catchup semantics. + for i := 0; i < maxConcurrentSyncRequests; i++ { + s.syncOutSem <- struct{}{} + } + + if opts.TLSRateLimit > 0 { + s.connRateCounter = newRateCounter(opts.tlsConfigOpts.RateLimit) + } + + // Trusted root operator keys. + if !s.processTrustedKeys() { + return nil, fmt.Errorf("Error processing trusted operator keys") + } + + // If we have solicited leafnodes but no clustering and no clustername. + // However we may need a stable clustername so use the server name. + if len(opts.LeafNode.Remotes) > 0 && opts.Cluster.Port == 0 && opts.Cluster.Name == _EMPTY_ { + s.leafNoCluster = true + opts.Cluster.Name = opts.ServerName + } + + if opts.Cluster.Name != _EMPTY_ { + // Also place into mapping cn with cnMu lock. + s.cnMu.Lock() + s.cn = opts.Cluster.Name + s.cnMu.Unlock() + } + + s.mu.Lock() + defer s.mu.Unlock() + + // If there are proxies trusted public keys in the configuration + // this will fill create the corresponding list of nkeys.KeyPair + // that we can use for signature verification. + s.processProxiesTrustedKeys() + + // Place ourselves in the JetStream nodeInfo if needed. + if opts.JetStream { + ourNode := getHash(serverName) + s.nodeToInfo.Store(ourNode, nodeInfo{ + name: serverName, + version: VERSION, + cluster: opts.Cluster.Name, + domain: opts.JetStreamDomain, + id: info.ID, + tags: opts.Tags, + cfg: &JetStreamConfig{MaxMemory: opts.JetStreamMaxMemory, MaxStore: opts.JetStreamMaxStore, CompressOK: true}, + stats: nil, + offline: false, + js: true, + binarySnapshots: true, + accountNRG: true, + }) + } + + s.routeResolver = opts.Cluster.resolver + if s.routeResolver == nil { + s.routeResolver = net.DefaultResolver + } + + // Used internally for quick look-ups. + s.clientConnectURLsMap = make(refCountedUrlSet) + s.websocket.connectURLsMap = make(refCountedUrlSet) + s.leafURLsMap = make(refCountedUrlSet) + + // Ensure that non-exported options (used in tests) are properly set. + s.setLeafNodeNonExportedOptions() + + // Setup OCSP Stapling and OCSP Peer. This will abort server from starting if there + // are no valid staples and OCSP Stapling policy is set to Always or MustStaple. + if err := s.enableOCSP(); err != nil { + return nil, err + } + + // Call this even if there is no gateway defined. It will + // initialize the structure so we don't have to check for + // it to be nil or not in various places in the code. + if err := s.newGateway(opts); err != nil { + return nil, err + } + + // If we have a cluster definition but do not have a cluster name, create one. + if opts.Cluster.Port != 0 && opts.Cluster.Name == _EMPTY_ { + s.info.Cluster = nuid.Next() + } else if opts.Cluster.Name != _EMPTY_ { + // Likewise here if we have a cluster name set. + s.info.Cluster = opts.Cluster.Name + } + + // This is normally done in the AcceptLoop, once the + // listener has been created (possibly with random port), + // but since some tests may expect the INFO to be properly + // set after New(), let's do it now. + s.setInfoHostPort() + + // For tracking clients + s.clients = make(map[uint64]*client) + + // For tracking closed clients. + s.closed = newClosedRingBuffer(opts.MaxClosedClients) + + // For tracking connections that are not yet registered + // in s.routes, but for which readLoop has started. + s.grTmpClients = make(map[uint64]*client) + + // For tracking routes and their remote ids + s.initRouteStructures(opts) + + // For tracking leaf nodes. + s.leafs = make(map[uint64]*client) + + // Used to kick out all go routines possibly waiting on server + // to shutdown. + s.quitCh = make(chan struct{}) + + // Closed when startup is complete. ReadyForConnections() will block on + // this before checking the presence of listening sockets. + s.startupComplete = make(chan struct{}) + + // Closed when Shutdown() is complete. Allows WaitForShutdown() to block + // waiting for complete shutdown. + s.shutdownComplete = make(chan struct{}) + + // Check for configured account resolvers. + if err := s.configureResolver(); err != nil { + return nil, err + } + // If there is an URL account resolver, do basic test to see if anyone is home. + if ar := opts.AccountResolver; ar != nil { + if ur, ok := ar.(*URLAccResolver); ok { + if _, err := ur.Fetch(_EMPTY_); err != nil { + return nil, err + } + } + } + // For other resolver: + // In operator mode, when the account resolver depends on an external system and + // the system account can't be fetched, inject a temporary one. + if ar := s.accResolver; len(opts.TrustedOperators) == 1 && ar != nil && + opts.SystemAccount != _EMPTY_ && opts.SystemAccount != DEFAULT_SYSTEM_ACCOUNT { + if _, ok := ar.(*MemAccResolver); !ok { + s.mu.Unlock() + var a *Account + // perform direct lookup to avoid warning trace + if _, err := fetchAccount(ar, opts.SystemAccount); err == nil { + a, _ = s.lookupAccount(opts.SystemAccount) + } + s.mu.Lock() + if a == nil { + sac := NewAccount(opts.SystemAccount) + sac.Issuer = opts.TrustedOperators[0].Issuer + sac.signingKeys = map[string]jwt.Scope{} + sac.signingKeys[opts.SystemAccount] = nil + s.registerAccountNoLock(sac) + } + } + } + + // For tracking accounts + if _, err := s.configureAccounts(false); err != nil { + return nil, err + } + + // Used to setup Authorization. + s.configureAuthorization() + + // Start signal handler + s.handleSignals() + + return s, nil +} + +// Initializes route structures based on pooling and/or per-account routes. +// +// Server lock is held on entry +func (s *Server) initRouteStructures(opts *Options) { + s.routes = make(map[string][]*client) + if ps := opts.Cluster.PoolSize; ps > 0 { + s.routesPoolSize = ps + } else { + s.routesPoolSize = 1 + } + // If we have per-account routes, we create accRoutes and initialize it + // with nil values. The presence of an account as the key will allow us + // to know if a given account is supposed to have dedicated routes. + if l := len(opts.Cluster.PinnedAccounts); l > 0 { + s.accRoutes = make(map[string]map[string]*client, l) + for _, acc := range opts.Cluster.PinnedAccounts { + s.accRoutes[acc] = make(map[string]*client) + } + } +} + +func (s *Server) logRejectedTLSConns() { + defer s.grWG.Done() + t := time.NewTicker(time.Second) + defer t.Stop() + for { + select { + case <-s.quitCh: + return + case <-t.C: + blocked := s.connRateCounter.countBlocked() + if blocked > 0 { + s.Warnf("Rejected %d connections due to TLS rate limiting", blocked) + } + } + } +} + +// clusterName returns our cluster name which could be dynamic. +func (s *Server) ClusterName() string { + s.mu.RLock() + cn := s.info.Cluster + s.mu.RUnlock() + return cn +} + +// Grabs cluster name with cluster name specific lock. +func (s *Server) cachedClusterName() string { + s.cnMu.RLock() + cn := s.cn + s.cnMu.RUnlock() + return cn +} + +// setClusterName will update the cluster name for this server. +func (s *Server) setClusterName(name string) { + s.mu.Lock() + var resetCh chan struct{} + if s.sys != nil && s.info.Cluster != name { + // can't hold the lock as go routine reading it may be waiting for lock as well + resetCh = s.sys.resetCh + } + s.info.Cluster = name + s.routeInfo.Cluster = name + + // Need to close solicited leaf nodes. The close has to be done outside of the server lock. + var leafs []*client + for _, c := range s.leafs { + c.mu.Lock() + if c.leaf != nil && c.leaf.remote != nil { + leafs = append(leafs, c) + } + c.mu.Unlock() + } + s.mu.Unlock() + + // Also place into mapping cn with cnMu lock. + s.cnMu.Lock() + s.cn = name + s.cnMu.Unlock() + + for _, l := range leafs { + l.closeConnection(ClusterNameConflict) + } + if resetCh != nil { + select { + case resetCh <- struct{}{}: + case <-s.quitCh: + } + } + s.Noticef("Cluster name updated to %s", name) +} + +// Return whether the cluster name is dynamic. +func (s *Server) isClusterNameDynamic() bool { + // We need to lock the whole "Cluster.Name" check and not use s.getOpts() + // because otherwise this could cause a data race with setting the name in + // route.go's processRouteConnect(). + s.optsMu.RLock() + dynamic := s.opts.Cluster.Name == _EMPTY_ + s.optsMu.RUnlock() + return dynamic +} + +// Returns our configured serverName. +func (s *Server) serverName() string { + return s.getOpts().ServerName +} + +// ClientURL returns the URL used to connect clients. +// Helpful in tests and with in-process servers using a random client port (-1). +func (s *Server) ClientURL() string { + // FIXME(dlc) - should we add in user and pass if defined single? + opts := s.getOpts() + var u url.URL + u.Scheme = "nats" + if opts.TLSConfig != nil { + u.Scheme = "tls" + } + u.Host = net.JoinHostPort(opts.Host, fmt.Sprintf("%d", opts.Port)) + return u.String() +} + +// WebsocketURL returns the URL used to connect websocket clients. +// Helpful in tests and with in-process servers using a random websocket port (-1). +func (s *Server) WebsocketURL() string { + opts := s.getOpts() + var u url.URL + u.Scheme = "ws" + if opts.Websocket.TLSConfig != nil { + u.Scheme = "wss" + } + u.Host = net.JoinHostPort(opts.Websocket.Host, fmt.Sprintf("%d", opts.Websocket.Port)) + return u.String() +} + +func validateCluster(o *Options) error { + if o.Cluster.Name != _EMPTY_ && strings.Contains(o.Cluster.Name, " ") { + return ErrClusterNameHasSpaces + } + if p := o.Cluster.Permissions; p != nil { + perms := &Permissions{Publish: p.Import, Subscribe: p.Export} + if err := checkClusterPermissionSubjects(perms); err != nil { + return err + } + } + if o.Cluster.Compression.Mode != _EMPTY_ { + if err := validateAndNormalizeCompressionOption(&o.Cluster.Compression, CompressionS2Fast); err != nil { + return err + } + } + if err := validatePinnedCerts(o.Cluster.TLSPinnedCerts); err != nil { + return fmt.Errorf("cluster: %v", err) + } + // Check that cluster name if defined matches any gateway name. + // Note that we have already verified that the gateway name does not have spaces. + if o.Gateway.Name != _EMPTY_ && o.Gateway.Name != o.Cluster.Name { + if o.Cluster.Name != _EMPTY_ { + return ErrClusterNameConfigConflict + } + // Set this here so we do not consider it dynamic. + o.Cluster.Name = o.Gateway.Name + } + clusterName := o.Cluster.Name + if clusterName == _EMPTY_ && len(o.LeafNode.Remotes) > 0 && o.Cluster.Port == 0 { + clusterName = o.ServerName + } + if clusterName == leafNoOriginCluster { + return ErrClusterNameReserved + } + if l := len(o.Cluster.PinnedAccounts); l > 0 { + if o.Cluster.PoolSize < 0 { + return fmt.Errorf("pool_size cannot be negative if pinned accounts are specified") + } + m := make(map[string]struct{}, l) + for _, a := range o.Cluster.PinnedAccounts { + if _, exists := m[a]; exists { + return fmt.Errorf("found duplicate account name %q in pinned accounts list %q", a, o.Cluster.PinnedAccounts) + } + m[a] = struct{}{} + } + } + return nil +} + +func validatePinnedCerts(pinned PinnedCertSet) error { + re := regexp.MustCompile("^[a-f0-9]{64}$") + for certId := range pinned { + entry := strings.ToLower(certId) + if !re.MatchString(entry) { + return fmt.Errorf("error parsing 'pinned_certs' key %s does not look like lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo", entry) + } + } + return nil +} + +func validateOptions(o *Options) error { + if o.LameDuckDuration > 0 && o.LameDuckGracePeriod >= o.LameDuckDuration { + return fmt.Errorf("lame duck grace period (%v) should be strictly lower than lame duck duration (%v)", + o.LameDuckGracePeriod, o.LameDuckDuration) + } + if int64(o.MaxPayload) > o.MaxPending { + return fmt.Errorf("max_payload (%v) cannot be higher than max_pending (%v)", + o.MaxPayload, o.MaxPending) + } + if o.ServerName != _EMPTY_ && strings.Contains(o.ServerName, " ") { + return errors.New("server name cannot contain spaces") + } + // Check that the trust configuration is correct. + if err := validateTrustedOperators(o); err != nil { + return err + } + // Check on leaf nodes which will require a system + // account when gateways are also configured. + if err := validateLeafNode(o); err != nil { + return err + } + // Check that authentication is properly configured. + if err := validateAuth(o); err != nil { + return err + } + // Check that proxies is properly configured. + if err := validateProxies(o); err != nil { + return err + } + // Check that gateway is properly configured. Returns no error + // if there is no gateway defined. + if err := validateGatewayOptions(o); err != nil { + return err + } + // Check that cluster name if defined matches any gateway name. + if err := validateCluster(o); err != nil { + return err + } + if err := validateMQTTOptions(o); err != nil { + return err + } + if err := validateJetStreamOptions(o); err != nil { + return err + } + // Finally check websocket options. + return validateWebsocketOptions(o) +} + +func (s *Server) getOpts() *Options { + s.optsMu.RLock() + opts := s.opts + s.optsMu.RUnlock() + return opts +} + +func (s *Server) setOpts(opts *Options) { + s.optsMu.Lock() + s.opts = opts + s.optsMu.Unlock() +} + +func (s *Server) globalAccount() *Account { + s.mu.RLock() + gacc := s.gacc + s.mu.RUnlock() + return gacc +} + +// Used to setup or update Accounts. +// Returns a map that indicates which accounts have had their stream imports +// changed (in case of an update in configuration reload). +// Lock is held upon entry, but will be released/reacquired in this function. +func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error) { + awcsti := make(map[string]struct{}) + + // Create the global account. + if s.gacc == nil { + s.gacc = NewAccount(globalAccountName) + s.registerAccountNoLock(s.gacc) + } + + opts := s.getOpts() + + // We need to track service imports since we can not swap them out (unsub and re-sub) + // until the proper server struct accounts have been swapped in properly. Doing it in + // place could lead to data loss or server panic since account under new si has no real + // account and hence no sublist, so will panic on inbound message. + siMap := make(map[*Account][][]byte) + + // Check opts and walk through them. We need to copy them here + // so that we do not keep a real one sitting in the options. + for _, acc := range opts.Accounts { + var a *Account + create := true + // For the global account, we want to skip the reload process + // and fall back into the "create" case which will in that + // case really be just an update (shallowCopy will make sure + // that mappings are copied over). + if reloading && acc.Name != globalAccountName { + if ai, ok := s.accounts.Load(acc.Name); ok { + a = ai.(*Account) + // Before updating the account, check if stream imports have changed. + if !a.checkStreamImportsEqual(acc) { + awcsti[acc.Name] = struct{}{} + } + a.mu.Lock() + // Collect the sids for the service imports since we are going to + // replace with new ones. + var sids [][]byte + for _, sis := range a.imports.services { + for _, si := range sis { + if si.sid != nil { + sids = append(sids, si.sid) + } + } + } + // Setup to process later if needed. + if len(sids) > 0 || len(acc.imports.services) > 0 { + siMap[a] = sids + } + + // Now reset all export/imports fields since they are going to be + // filled in shallowCopy() + a.imports.streams, a.imports.services = nil, nil + a.exports.streams, a.exports.services = nil, nil + // We call shallowCopy from the account `acc` (the one in Options) + // and pass `a` (our existing account) to get it updated. + acc.shallowCopy(a) + a.mu.Unlock() + create = false + } + } + // Track old mappings if global account. + var oldGMappings []*mapping + if create { + if acc.Name == globalAccountName { + a = s.gacc + a.mu.Lock() + oldGMappings = append(oldGMappings, a.mappings...) + a.mu.Unlock() + } else { + a = NewAccount(acc.Name) + } + // Locking matters in the case of an update of the global account + a.mu.Lock() + acc.shallowCopy(a) + a.mu.Unlock() + // Will be a no-op in case of the global account since it is already registered. + s.registerAccountNoLock(a) + } + + // The `acc` account is stored in options, not in the server, and these can be cleared. + acc.sl, acc.clients, acc.mappings = nil, nil, nil + + // Check here if we have been reloaded and we have a global account with mappings that may have changed. + // If we have leafnodes they need to be updated. + if reloading && a == s.gacc { + a.mu.Lock() + mappings := make(map[string]*mapping) + if len(a.mappings) > 0 && a.nleafs > 0 { + for _, em := range a.mappings { + mappings[em.src] = em + } + } + a.mu.Unlock() + if len(mappings) > 0 || len(oldGMappings) > 0 { + a.lmu.RLock() + for _, lc := range a.lleafs { + for _, em := range mappings { + lc.forceAddToSmap(em.src) + } + // Remove any old ones if needed. + for _, em := range oldGMappings { + // Only remove if not in the new ones. + if _, ok := mappings[em.src]; !ok { + lc.forceRemoveFromSmap(em.src) + } + } + } + a.lmu.RUnlock() + } + } + + // If we see an account defined using $SYS we will make sure that is set as system account. + if acc.Name == DEFAULT_SYSTEM_ACCOUNT && opts.SystemAccount == _EMPTY_ { + opts.SystemAccount = DEFAULT_SYSTEM_ACCOUNT + } + } + + // Now that we have this we need to remap any referenced accounts in + // import or export maps to the new ones. + swapApproved := func(ea *exportAuth) { + for sub, a := range ea.approved { + var acc *Account + if v, ok := s.accounts.Load(a.Name); ok { + acc = v.(*Account) + } + ea.approved[sub] = acc + } + } + var numAccounts int + s.accounts.Range(func(k, v any) bool { + numAccounts++ + acc := v.(*Account) + acc.mu.Lock() + // Exports + for _, se := range acc.exports.streams { + if se != nil { + swapApproved(&se.exportAuth) + } + } + for _, se := range acc.exports.services { + if se != nil { + // Swap over the bound account for service exports. + if se.acc != nil { + if v, ok := s.accounts.Load(se.acc.Name); ok { + se.acc = v.(*Account) + } + } + swapApproved(&se.exportAuth) + } + } + // Imports + for _, si := range acc.imports.streams { + if v, ok := s.accounts.Load(si.acc.Name); ok { + si.acc = v.(*Account) + } + } + for _, sis := range acc.imports.services { + for _, si := range sis { + if v, ok := s.accounts.Load(si.acc.Name); ok { + si.acc = v.(*Account) + + // It is possible to allow for latency tracking inside your + // own account, so lock only when not the same account. + if si.acc == acc { + si.se = si.acc.getServiceExport(si.to) + continue + } + si.acc.mu.RLock() + si.se = si.acc.getServiceExport(si.to) + si.acc.mu.RUnlock() + } + } + } + // Make sure the subs are running, but only if not reloading. + if len(acc.imports.services) > 0 && acc.ic == nil && !reloading { + acc.ic = s.createInternalAccountClient() + acc.ic.acc = acc + // Need to release locks to invoke this function. + acc.mu.Unlock() + s.mu.Unlock() + acc.addAllServiceImportSubs() + s.mu.Lock() + acc.mu.Lock() + } + acc.updated = time.Now() + acc.mu.Unlock() + return true + }) + + // Check if we need to process service imports pending from above. + // This processing needs to be after we swap in the real accounts above. + for acc, sids := range siMap { + c := acc.ic + for _, sid := range sids { + c.processUnsub(sid) + } + acc.addAllServiceImportSubs() + s.mu.Unlock() + s.registerSystemImports(acc) + s.mu.Lock() + } + + // Set the system account if it was configured. + // Otherwise create a default one. + if opts.SystemAccount != _EMPTY_ { + // Lock may be acquired in lookupAccount, so release to call lookupAccount. + s.mu.Unlock() + acc, err := s.lookupAccount(opts.SystemAccount) + s.mu.Lock() + if err == nil && s.sys != nil && acc != s.sys.account { + // sys.account.clients (including internal client)/respmap/etc... are transferred separately + s.sys.account = acc + s.sysAcc.Store(acc) + } + if err != nil { + return awcsti, fmt.Errorf("error resolving system account: %v", err) + } + + // If we have defined a system account here check to see if its just us and the $G account. + // We would do this to add user/pass to the system account. If this is the case add in + // no-auth-user for $G. + // Only do this if non-operator mode and we did not have an authorization block defined. + if len(opts.TrustedOperators) == 0 && numAccounts == 2 && opts.NoAuthUser == _EMPTY_ && !opts.authBlockDefined { + // If we come here from config reload, let's not recreate the fake user name otherwise + // it will cause currently clients to be disconnected. + uname := s.sysAccOnlyNoAuthUser + if uname == _EMPTY_ { + // Create a unique name so we do not collide. + var b [8]byte + rn := rand.Int64() + for i, l := 0, rn; i < len(b); i++ { + b[i] = digits[l%base] + l /= base + } + uname = fmt.Sprintf("nats-%s", b[:]) + s.sysAccOnlyNoAuthUser = uname + } + opts.Users = append(opts.Users, &User{Username: uname, Password: uname[6:], Account: s.gacc}) + opts.NoAuthUser = uname + } + } + + // Add any required exports from system account. + if s.sys != nil { + sysAcc := s.sys.account + s.mu.Unlock() + s.addSystemAccountExports(sysAcc) + s.mu.Lock() + } + + return awcsti, nil +} + +// Setup the account resolver. For memory resolver, make sure the JWTs are +// properly formed but do not enforce expiration etc. +// Lock is held on entry, but may be released/reacquired during this call. +func (s *Server) configureResolver() error { + opts := s.getOpts() + s.accResolver = opts.AccountResolver + if opts.AccountResolver != nil { + // For URL resolver, set the TLSConfig if specified. + if opts.AccountResolverTLSConfig != nil { + if ar, ok := opts.AccountResolver.(*URLAccResolver); ok { + if t, ok := ar.c.Transport.(*http.Transport); ok { + t.CloseIdleConnections() + t.TLSClientConfig = opts.AccountResolverTLSConfig.Clone() + } + } + } + if len(opts.resolverPreloads) > 0 { + // Lock ordering is account resolver -> server, so we need to release + // the lock and reacquire it when done with account resolver's calls. + ar := s.accResolver + s.mu.Unlock() + defer s.mu.Lock() + if ar.IsReadOnly() { + return fmt.Errorf("resolver preloads only available for writeable resolver types MEM/DIR/CACHE_DIR") + } + for k, v := range opts.resolverPreloads { + _, err := jwt.DecodeAccountClaims(v) + if err != nil { + return fmt.Errorf("preload account error for %q: %v", k, err) + } + ar.Store(k, v) + } + } + } + return nil +} + +// This will check preloads for validation issues. +func (s *Server) checkResolvePreloads() { + opts := s.getOpts() + // We can just check the read-only opts versions here, that way we do not need + // to grab server lock or access s.accResolver. + for k, v := range opts.resolverPreloads { + claims, err := jwt.DecodeAccountClaims(v) + if err != nil { + s.Errorf("Preloaded account [%s] not valid", k) + continue + } + // Check if it is expired. + vr := jwt.CreateValidationResults() + claims.Validate(vr) + if vr.IsBlocking(true) { + s.Warnf("Account [%s] has validation issues:", k) + for _, v := range vr.Issues { + s.Warnf(" - %s", v.Description) + } + } + } +} + +// Determines if we are in pre NATS 2.0 setup with no accounts. +func (s *Server) globalAccountOnly() bool { + var hasOthers bool + + if s.trustedKeys != nil { + return false + } + + s.mu.RLock() + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + // Ignore global and system + if acc == s.gacc || (s.sys != nil && acc == s.sys.account) { + return true + } + hasOthers = true + return false + }) + s.mu.RUnlock() + + return !hasOthers +} + +// Determines if this server is in standalone mode, meaning no routes or gateways. +func (s *Server) standAloneMode() bool { + opts := s.getOpts() + return opts.Cluster.Port == 0 && opts.Gateway.Port == 0 +} + +func (s *Server) configuredRoutes() int { + return len(s.getOpts().Routes) +} + +// activePeers is used in bootstrapping raft groups like the JetStream meta controller. +func (s *Server) ActivePeers() (peers []string) { + s.nodeToInfo.Range(func(k, v any) bool { + si := v.(nodeInfo) + if !si.offline { + peers = append(peers, k.(string)) + } + return true + }) + return peers +} + +// isTrustedIssuer will check that the issuer is a trusted public key. +// This is used to make sure an account was signed by a trusted operator. +func (s *Server) isTrustedIssuer(issuer string) bool { + s.mu.RLock() + defer s.mu.RUnlock() + // If we are not running in trusted mode and there is no issuer, that is ok. + if s.trustedKeys == nil && issuer == _EMPTY_ { + return true + } + for _, tk := range s.trustedKeys { + if tk == issuer { + return true + } + } + return false +} + +// processTrustedKeys will process binary stamped and +// options-based trusted nkeys. Returns success. +func (s *Server) processTrustedKeys() bool { + s.strictSigningKeyUsage = map[string]struct{}{} + opts := s.getOpts() + if trustedKeys != _EMPTY_ && !s.initStampedTrustedKeys() { + return false + } else if opts.TrustedKeys != nil { + for _, key := range opts.TrustedKeys { + if !nkeys.IsValidPublicOperatorKey(key) { + return false + } + } + s.trustedKeys = append([]string(nil), opts.TrustedKeys...) + for _, claim := range opts.TrustedOperators { + if !claim.StrictSigningKeyUsage { + continue + } + for _, key := range claim.SigningKeys { + s.strictSigningKeyUsage[key] = struct{}{} + } + } + } + return true +} + +// checkTrustedKeyString will check that the string is a valid array +// of public operator nkeys. +func checkTrustedKeyString(keys string) []string { + tks := strings.Fields(keys) + if len(tks) == 0 { + return nil + } + // Walk all the keys and make sure they are valid. + for _, key := range tks { + if !nkeys.IsValidPublicOperatorKey(key) { + return nil + } + } + return tks +} + +// initStampedTrustedKeys will check the stamped trusted keys +// and will set the server field 'trustedKeys'. Returns whether +// it succeeded or not. +func (s *Server) initStampedTrustedKeys() bool { + // Check to see if we have an override in options, which will cause us to fail. + if len(s.getOpts().TrustedKeys) > 0 { + return false + } + tks := checkTrustedKeyString(trustedKeys) + if len(tks) == 0 { + return false + } + s.trustedKeys = tks + return true +} + +// PrintAndDie is exported for access in other packages. +func PrintAndDie(msg string) { + fmt.Fprintln(os.Stderr, msg) + os.Exit(1) +} + +// PrintServerAndExit will print our version and exit. +func PrintServerAndExit() { + fmt.Printf("nats-server: v%s\n", VERSION) + os.Exit(0) +} + +// ProcessCommandLineArgs takes the command line arguments +// validating and setting flags for handling in case any +// sub command was present. +func ProcessCommandLineArgs(cmd *flag.FlagSet) (showVersion bool, showHelp bool, err error) { + if len(cmd.Args()) > 0 { + arg := cmd.Args()[0] + switch strings.ToLower(arg) { + case "version": + return true, false, nil + case "help": + return false, true, nil + default: + return false, false, fmt.Errorf("unrecognized command: %q", arg) + } + } + + return false, false, nil +} + +// Public version. +func (s *Server) Running() bool { + return s.isRunning() +} + +// Protected check on running state +func (s *Server) isRunning() bool { + return s.running.Load() +} + +func (s *Server) logPid() error { + pidStr := strconv.Itoa(os.Getpid()) + return os.WriteFile(s.getOpts().PidFile, []byte(pidStr), defaultFilePerms) +} + +// numReservedAccounts will return the number of reserved accounts configured in the server. +// Currently this is 1, one for the global default account. +func (s *Server) numReservedAccounts() int { + return 1 +} + +// NumActiveAccounts reports number of active accounts on this server. +func (s *Server) NumActiveAccounts() int32 { + return atomic.LoadInt32(&s.activeAccounts) +} + +// incActiveAccounts() just adds one under lock. +func (s *Server) incActiveAccounts() { + atomic.AddInt32(&s.activeAccounts, 1) +} + +// decActiveAccounts() just subtracts one under lock. +func (s *Server) decActiveAccounts() { + atomic.AddInt32(&s.activeAccounts, -1) +} + +// This should be used for testing only. Will be slow since we have to +// range over all accounts in the sync.Map to count. +func (s *Server) numAccounts() int { + count := 0 + s.mu.RLock() + s.accounts.Range(func(k, v any) bool { + count++ + return true + }) + s.mu.RUnlock() + return count +} + +// NumLoadedAccounts returns the number of loaded accounts. +func (s *Server) NumLoadedAccounts() int { + return s.numAccounts() +} + +// LookupOrRegisterAccount will return the given account if known or create a new entry. +func (s *Server) LookupOrRegisterAccount(name string) (account *Account, isNew bool) { + s.mu.Lock() + defer s.mu.Unlock() + if v, ok := s.accounts.Load(name); ok { + return v.(*Account), false + } + acc := NewAccount(name) + s.registerAccountNoLock(acc) + return acc, true +} + +// RegisterAccount will register an account. The account must be new +// or this call will fail. +func (s *Server) RegisterAccount(name string) (*Account, error) { + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.accounts.Load(name); ok { + return nil, ErrAccountExists + } + acc := NewAccount(name) + s.registerAccountNoLock(acc) + return acc, nil +} + +// SetSystemAccount will set the internal system account. +// If root operators are present it will also check validity. +func (s *Server) SetSystemAccount(accName string) error { + // Lookup from sync.Map first. + if v, ok := s.accounts.Load(accName); ok { + return s.setSystemAccount(v.(*Account)) + } + + // If we are here we do not have local knowledge of this account. + // Do this one by hand to return more useful error. + ac, jwt, err := s.fetchAccountClaims(accName) + if err != nil { + return err + } + acc := s.buildInternalAccount(ac) + acc.claimJWT = jwt + // Due to race, we need to make sure that we are not + // registering twice. + if racc := s.registerAccount(acc); racc != nil { + return nil + } + return s.setSystemAccount(acc) +} + +// SystemAccount returns the system account if set. +func (s *Server) SystemAccount() *Account { + return s.sysAcc.Load() +} + +// GlobalAccount returns the global account. +// Default clients will use the global account. +func (s *Server) GlobalAccount() *Account { + s.mu.RLock() + defer s.mu.RUnlock() + return s.gacc +} + +// SetDefaultSystemAccount will create a default system account if one is not present. +func (s *Server) SetDefaultSystemAccount() error { + if _, isNew := s.LookupOrRegisterAccount(DEFAULT_SYSTEM_ACCOUNT); !isNew { + return nil + } + s.Debugf("Created system account: %q", DEFAULT_SYSTEM_ACCOUNT) + return s.SetSystemAccount(DEFAULT_SYSTEM_ACCOUNT) +} + +// Assign a system account. Should only be called once. +// This sets up a server to send and receive messages from +// inside the server itself. +func (s *Server) setSystemAccount(acc *Account) error { + if acc == nil { + return ErrMissingAccount + } + // Don't try to fix this here. + if acc.IsExpired() { + return ErrAccountExpired + } + // If we are running with trusted keys for an operator + // make sure we check the account is legit. + if !s.isTrustedIssuer(acc.Issuer) { + return ErrAccountValidation + } + + s.mu.Lock() + + if s.sys != nil { + s.mu.Unlock() + return ErrAccountExists + } + + // This is here in an attempt to quiet the race detector and not have to place + // locks on fast path for inbound messages and checking service imports. + acc.mu.Lock() + if acc.imports.services == nil { + acc.imports.services = make(map[string][]*serviceImport) + } + acc.mu.Unlock() + + s.sys = &internal{ + account: acc, + client: s.createInternalSystemClient(), + seq: 1, + sid: 1, + servers: make(map[string]*serverUpdate), + replies: make(map[string]msgHandler), + sendq: newIPQueue[*pubMsg](s, "System sendQ"), + recvq: newIPQueue[*inSysMsg](s, "System recvQ"), + recvqp: newIPQueue[*inSysMsg](s, "System recvQ Pings"), + resetCh: make(chan struct{}), + sq: s.newSendQ(acc), + statsz: statsHBInterval, + orphMax: 5 * eventsHBInterval, + chkOrph: 3 * eventsHBInterval, + } + recvq, recvqp := s.sys.recvq, s.sys.recvqp + s.sys.wg.Add(1) + s.mu.Unlock() + + // Store in atomic for fast lookup. + s.sysAcc.Store(acc) + + // Register with the account. + s.sys.client.registerWithAccount(acc) + + s.addSystemAccountExports(acc) + + // Start our internal loop to serialize outbound messages. + // We do our own wg here since we will stop first during shutdown. + go s.internalSendLoop(&s.sys.wg) + + // Start the internal loop for inbound messages. + go s.internalReceiveLoop(recvq) + // Start the internal loop for inbound STATSZ/Ping messages. + go s.internalReceiveLoop(recvqp) + + // Start up our general subscriptions + s.initEventTracking() + + // Track for dead remote servers. + s.wrapChk(s.startRemoteServerSweepTimer)() + + // Send out statsz updates periodically. + s.wrapChk(s.startStatszTimer)() + + // If we have existing accounts make sure we enable account tracking. + s.mu.Lock() + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + s.enableAccountTracking(acc) + return true + }) + s.mu.Unlock() + + return nil +} + +// Creates an internal system client. +func (s *Server) createInternalSystemClient() *client { + return s.createInternalClient(SYSTEM) +} + +// Creates an internal jetstream client. +func (s *Server) createInternalJetStreamClient() *client { + return s.createInternalClient(JETSTREAM) +} + +// Creates an internal client for Account. +func (s *Server) createInternalAccountClient() *client { + return s.createInternalClient(ACCOUNT) +} + +// Internal clients. kind should be SYSTEM, JETSTREAM or ACCOUNT +func (s *Server) createInternalClient(kind int) *client { + if !isInternalClient(kind) { + return nil + } + now := time.Now() + c := &client{srv: s, kind: kind, opts: internalOpts, msubs: -1, mpay: -1, start: now, last: now} + c.initClient() + c.echo = false + c.headers = true + c.flags.set(noReconnect) + return c +} + +// Determine if accounts should track subscriptions for +// efficient propagation. +// Lock should be held on entry. +func (s *Server) shouldTrackSubscriptions() bool { + opts := s.getOpts() + return (opts.Cluster.Port != 0 || opts.Gateway.Port != 0) +} + +// Invokes registerAccountNoLock under the protection of the server lock. +// That is, server lock is acquired/released in this function. +// See registerAccountNoLock for comment on returned value. +func (s *Server) registerAccount(acc *Account) *Account { + s.mu.Lock() + racc := s.registerAccountNoLock(acc) + s.mu.Unlock() + return racc +} + +// Helper to set the sublist based on preferences. +func (s *Server) setAccountSublist(acc *Account) { + if acc != nil && acc.sl == nil { + acc.sl = NewSublistForServer(s) + } +} + +// Registers an account in the server. +// Due to some locking considerations, we may end-up trying +// to register the same account twice. This function will +// then return the already registered account. +// Lock should be held on entry. +func (s *Server) registerAccountNoLock(acc *Account) *Account { + // We are under the server lock. Lookup from map, if present + // return existing account. + if a, _ := s.accounts.Load(acc.Name); a != nil { + s.tmpAccounts.Delete(acc.Name) + return a.(*Account) + } + // Finish account setup and store. + s.setAccountSublist(acc) + + acc.mu.Lock() + s.setRouteInfo(acc) + if acc.clients == nil { + acc.clients = make(map[*client]struct{}) + } + + // If we are capable of routing we will track subscription + // information for efficient interest propagation. + // During config reload, it is possible that account was + // already created (global account), so use locking and + // make sure we create only if needed. + // TODO(dlc)- Double check that we need this for GWs. + if acc.rm == nil && s.opts != nil && s.shouldTrackSubscriptions() { + acc.rm = make(map[string]int32) + acc.lws = make(map[string]int32) + } + acc.srv = s + acc.updated = time.Now() + accName := acc.Name + jsEnabled := len(acc.jsLimits) > 0 + acc.mu.Unlock() + + if opts := s.getOpts(); opts != nil && len(opts.JsAccDefaultDomain) > 0 { + if defDomain, ok := opts.JsAccDefaultDomain[accName]; ok { + if jsEnabled { + s.Warnf("Skipping Default Domain %q, set for JetStream enabled account %q", defDomain, accName) + } else if defDomain != _EMPTY_ { + for src, dest := range generateJSMappingTable(defDomain) { + // flip src and dest around so the domain is inserted + s.Noticef("Adding default domain mapping %q -> %q to account %q %p", dest, src, accName, acc) + if err := acc.AddMapping(dest, src); err != nil { + s.Errorf("Error adding JetStream default domain mapping: %v", err) + } + } + } + } + } + + s.accounts.Store(acc.Name, acc) + s.tmpAccounts.Delete(acc.Name) + s.enableAccountTracking(acc) + + // Can not have server lock here. + s.mu.Unlock() + s.registerSystemImports(acc) + // Starting 2.9.0, we are phasing out the optimistic mode, so change + // the account to interest-only mode (except if instructed not to do + // it in some tests). + if s.gateway.enabled && !gwDoNotForceInterestOnlyMode { + s.switchAccountToInterestMode(acc.GetName()) + } + s.mu.Lock() + + return nil +} + +// Sets the account's routePoolIdx depending on presence or not of +// pooling or per-account routes. Also updates a map used by +// gateway code to retrieve a route based on some route hash. +// +// Both Server and Account lock held on entry. +func (s *Server) setRouteInfo(acc *Account) { + // If there is a dedicated route configured for this account + if _, ok := s.accRoutes[acc.Name]; ok { + // We want the account name to be in the map, but we don't + // need a value (we could store empty string) + s.accRouteByHash.Store(acc.Name, nil) + // Set the route pool index to -1 so that it is easy when + // ranging over accounts to exclude those accounts when + // trying to get accounts for a given pool index. + acc.routePoolIdx = accDedicatedRoute + } else { + // If pool size more than 1, we will compute a hash code and + // use modulo to assign to an index of the pool slice. For 1 + // and below, all accounts will be bound to the single connection + // at index 0. + acc.routePoolIdx = computeRoutePoolIdx(s.routesPoolSize, acc.Name) + if s.routesPoolSize > 1 { + s.accRouteByHash.Store(acc.Name, acc.routePoolIdx) + } + } +} + +// lookupAccount is a function to return the account structure +// associated with an account name. +// Lock MUST NOT be held upon entry. +func (s *Server) lookupAccount(name string) (*Account, error) { + return s.lookupOrFetchAccount(name, true) +} + +// lookupOrFetchAccount is a function to return the account structure +// associated with an account name. +// Lock MUST NOT be held upon entry. +func (s *Server) lookupOrFetchAccount(name string, fetch bool) (*Account, error) { + var acc *Account + if v, ok := s.accounts.Load(name); ok { + acc = v.(*Account) + } + if acc != nil { + // If we are expired and we have a resolver, then + // return the latest information from the resolver. + if acc.IsExpired() { + s.Debugf("Requested account [%s] has expired", name) + if s.AccountResolver() != nil && fetch { + if err := s.updateAccount(acc); err != nil { + // This error could mask expired, so just return expired here. + return nil, ErrAccountExpired + } + } else { + return nil, ErrAccountExpired + } + } + return acc, nil + } + // If we have a resolver see if it can fetch the account. + if s.AccountResolver() == nil || !fetch { + return nil, ErrMissingAccount + } + return s.fetchAccount(name) +} + +// LookupAccount is a public function to return the account structure +// associated with name. +func (s *Server) LookupAccount(name string) (*Account, error) { + return s.lookupAccount(name) +} + +// This will fetch new claims and if found update the account with new claims. +// Lock MUST NOT be held upon entry. +func (s *Server) updateAccount(acc *Account) error { + acc.mu.RLock() + // TODO(dlc) - Make configurable + if !acc.incomplete && time.Since(acc.updated) < time.Second { + acc.mu.RUnlock() + s.Debugf("Requested account update for [%s] ignored, too soon", acc.Name) + return ErrAccountResolverUpdateTooSoon + } + acc.mu.RUnlock() + claimJWT, err := s.fetchRawAccountClaims(acc.Name) + if err != nil { + return err + } + return s.updateAccountWithClaimJWT(acc, claimJWT) +} + +// updateAccountWithClaimJWT will check and apply the claim update. +// Lock MUST NOT be held upon entry. +func (s *Server) updateAccountWithClaimJWT(acc *Account, claimJWT string) error { + if acc == nil { + return ErrMissingAccount + } + acc.mu.RLock() + sameClaim := acc.claimJWT != _EMPTY_ && acc.claimJWT == claimJWT && !acc.incomplete + acc.mu.RUnlock() + if sameClaim { + s.Debugf("Requested account update for [%s], same claims detected", acc.Name) + return nil + } + accClaims, _, err := s.verifyAccountClaims(claimJWT) + if err == nil && accClaims != nil { + acc.mu.Lock() + // if an account is updated with a different operator signing key, we want to + // show a consistent issuer. + acc.Issuer = accClaims.Issuer + if acc.Name != accClaims.Subject { + acc.mu.Unlock() + return ErrAccountValidation + } + acc.mu.Unlock() + s.UpdateAccountClaims(acc, accClaims) + acc.mu.Lock() + // needs to be set after update completed. + // This causes concurrent calls to return with sameClaim=true if the change is effective. + acc.claimJWT = claimJWT + acc.mu.Unlock() + return nil + } + return err +} + +// fetchRawAccountClaims will grab raw account claims iff we have a resolver. +// Lock is NOT held upon entry. +func (s *Server) fetchRawAccountClaims(name string) (string, error) { + accResolver := s.AccountResolver() + if accResolver == nil { + return _EMPTY_, ErrNoAccountResolver + } + // Need to do actual Fetch + start := time.Now() + claimJWT, err := fetchAccount(accResolver, name) + fetchTime := time.Since(start) + if fetchTime > time.Second { + s.Warnf("Account [%s] fetch took %v", name, fetchTime) + } else { + s.Debugf("Account [%s] fetch took %v", name, fetchTime) + } + if err != nil { + s.Warnf("Account fetch failed: %v", err) + return "", err + } + return claimJWT, nil +} + +// fetchAccountClaims will attempt to fetch new claims if a resolver is present. +// Lock is NOT held upon entry. +func (s *Server) fetchAccountClaims(name string) (*jwt.AccountClaims, string, error) { + claimJWT, err := s.fetchRawAccountClaims(name) + if err != nil { + return nil, _EMPTY_, err + } + var claim *jwt.AccountClaims + claim, claimJWT, err = s.verifyAccountClaims(claimJWT) + if claim != nil && claim.Subject != name { + return nil, _EMPTY_, ErrAccountValidation + } + return claim, claimJWT, err +} + +// verifyAccountClaims will decode and validate any account claims. +func (s *Server) verifyAccountClaims(claimJWT string) (*jwt.AccountClaims, string, error) { + accClaims, err := jwt.DecodeAccountClaims(claimJWT) + if err != nil { + return nil, _EMPTY_, err + } + if !s.isTrustedIssuer(accClaims.Issuer) { + return nil, _EMPTY_, ErrAccountValidation + } + vr := jwt.CreateValidationResults() + accClaims.Validate(vr) + if vr.IsBlocking(true) { + return nil, _EMPTY_, ErrAccountValidation + } + return accClaims, claimJWT, nil +} + +// This will fetch an account from a resolver if defined. +// Lock is NOT held upon entry. +func (s *Server) fetchAccount(name string) (*Account, error) { + accClaims, claimJWT, err := s.fetchAccountClaims(name) + if accClaims == nil { + return nil, err + } + acc := s.buildInternalAccount(accClaims) + // Due to possible race, if registerAccount() returns a non + // nil account, it means the same account was already + // registered and we should use this one. + if racc := s.registerAccount(acc); racc != nil { + // Update with the new claims in case they are new. + if err = s.updateAccountWithClaimJWT(racc, claimJWT); err != nil { + return nil, err + } + return racc, nil + } + // The sub imports may have been setup but will not have had their + // subscriptions properly setup. Do that here. + var needImportSubs bool + + acc.mu.Lock() + acc.claimJWT = claimJWT + if len(acc.imports.services) > 0 { + if acc.ic == nil { + acc.ic = s.createInternalAccountClient() + acc.ic.acc = acc + } + needImportSubs = true + } + acc.mu.Unlock() + + // Do these outside the lock. + if needImportSubs { + acc.addAllServiceImportSubs() + } + + return acc, nil +} + +// Start up the server, this will not block. +// +// WaitForShutdown can be used to block and wait for the server to shutdown properly if needed +// after calling s.Shutdown() +func (s *Server) Start() { + s.Noticef("Starting nats-server") + + gc := gitCommit + if gc == _EMPTY_ { + gc = "not set" + } + + // Snapshot server options. + opts := s.getOpts() + + // Capture if this server is a leaf that has no cluster, so we don't + // display the cluster name if that is the case. + s.mu.RLock() + leafNoCluster := s.leafNoCluster + s.mu.RUnlock() + + var clusterName string + if !leafNoCluster { + clusterName = s.ClusterName() + } + + s.Noticef(" Version: %s", VERSION) + s.Noticef(" Git: [%s]", gc) + s.Debugf(" Go build: %s", s.info.GoVersion) + if clusterName != _EMPTY_ { + s.Noticef(" Cluster: %s", clusterName) + } + s.Noticef(" Name: %s", s.info.Name) + if opts.JetStream { + s.Noticef(" Node: %s", getHash(s.info.Name)) + } + s.Noticef(" ID: %s", s.info.ID) + s.printFeatureFlags(opts) + + defer s.Noticef("Server is ready") + + // Check for insecure configurations. + s.checkAuthforWarnings() + + // Avoid RACE between Start() and Shutdown() + s.running.Store(true) + s.mu.Lock() + // Update leafNodeEnabled in case options have changed post NewServer() + // and before Start() (we should not be able to allow that, but server has + // direct reference to user-provided options - at least before a Reload() is + // performed. + s.leafNodeEnabled = opts.LeafNode.Port != 0 || len(opts.LeafNode.Remotes) > 0 + s.mu.Unlock() + + s.grMu.Lock() + s.grRunning = true + s.grMu.Unlock() + + s.startRateLimitLogExpiration() + + // Pprof http endpoint for the profiler. + if opts.ProfPort != 0 { + s.StartProfiler() + } else { + // It's still possible to access this profile via a SYS endpoint, so set + // this anyway. (Otherwise StartProfiler would have called it.) + s.setBlockProfileRate(opts.ProfBlockRate) + } + + if opts.ConfigFile != _EMPTY_ { + var cd string + if opts.configDigest != "" { + cd = fmt.Sprintf("(%s)", opts.configDigest) + } + s.Noticef("Using configuration file: %s %s", opts.ConfigFile, cd) + } + + hasOperators := len(opts.TrustedOperators) > 0 + if hasOperators { + s.Noticef("Trusted Operators") + } + for _, opc := range opts.TrustedOperators { + s.Noticef(" System : %q", opc.Audience) + s.Noticef(" Operator: %q", opc.Name) + s.Noticef(" Issued : %v", time.Unix(opc.IssuedAt, 0)) + switch opc.Expires { + case 0: + s.Noticef(" Expires : Never") + default: + s.Noticef(" Expires : %v", time.Unix(opc.Expires, 0)) + } + } + if hasOperators && opts.SystemAccount == _EMPTY_ { + s.Warnf("Trusted Operators should utilize a System Account") + } + if opts.MaxPayload > MAX_PAYLOAD_MAX_SIZE { + s.Warnf("Maximum payloads over %v are generally discouraged and could lead to poor performance", + friendlyBytes(int64(MAX_PAYLOAD_MAX_SIZE))) + } + + if len(opts.JsAccDefaultDomain) > 0 { + s.Warnf("The option `default_js_domain` is a temporary backwards compatibility measure and will be removed") + } + + // If we have a memory resolver, check the accounts here for validation exceptions. + // This allows them to be logged right away vs when they are accessed via a client. + if hasOperators && len(opts.resolverPreloads) > 0 { + s.checkResolvePreloads() + } + + // Log the pid to a file. + if opts.PidFile != _EMPTY_ { + if err := s.logPid(); err != nil { + s.Fatalf("Could not write pidfile: %v", err) + return + } + } + + // Setup system account which will start the eventing stack. + if sa := opts.SystemAccount; sa != _EMPTY_ { + if err := s.SetSystemAccount(sa); err != nil { + s.Fatalf("Can't set system account: %v", err) + return + } + } else if !opts.NoSystemAccount { + // We will create a default system account here. + s.SetDefaultSystemAccount() + } + + // Start monitoring before enabling other subsystems of the + // server to be able to monitor during startup. + if err := s.StartMonitoring(); err != nil { + s.Fatalf("Can't start monitoring: %v", err) + return + } + + // Start up resolver machinery. + if ar := s.AccountResolver(); ar != nil { + if err := ar.Start(s); err != nil { + s.Fatalf("Could not start resolver: %v", err) + return + } + // In operator mode, when the account resolver depends on an external system and + // the system account is the bootstrapping account, start fetching it. + if len(opts.TrustedOperators) == 1 && opts.SystemAccount != _EMPTY_ && opts.SystemAccount != DEFAULT_SYSTEM_ACCOUNT { + opts := s.getOpts() + _, isMemResolver := ar.(*MemAccResolver) + if v, ok := s.accounts.Load(opts.SystemAccount); !isMemResolver && ok && v.(*Account).claimJWT == _EMPTY_ { + s.Noticef("Using bootstrapping system account") + s.startGoRoutine(func() { + defer s.grWG.Done() + t := time.NewTicker(time.Second) + defer t.Stop() + for { + select { + case <-s.quitCh: + return + case <-t.C: + sacc := s.SystemAccount() + if claimJWT, err := fetchAccount(ar, opts.SystemAccount); err != nil { + continue + } else if err = s.updateAccountWithClaimJWT(sacc, claimJWT); err != nil { + continue + } + s.Noticef("System account fetched and updated") + return + } + } + }) + } + } + } + + // Start expiration of mapped GW replies, regardless if + // this server is configured with gateway or not. + s.startGWReplyMapExpiration() + + // Check if JetStream has been enabled. This needs to be after + // the system account setup above. JetStream will create its + // own system account if one is not present. + if opts.JetStream { + // Make sure someone is not trying to enable on the system account. + if sa := s.SystemAccount(); sa != nil && len(sa.jsLimits) > 0 { + s.Fatalf("Not allowed to enable JetStream on the system account") + } + cfg := &JetStreamConfig{ + StoreDir: opts.StoreDir, + SyncInterval: opts.SyncInterval, + SyncAlways: opts.SyncAlways, + Strict: !opts.NoJetStreamStrict, + MaxMemory: opts.JetStreamMaxMemory, + MaxStore: opts.JetStreamMaxStore, + Domain: opts.JetStreamDomain, + CompressOK: true, + UniqueTag: opts.JetStreamUniqueTag, + } + if err := s.EnableJetStream(cfg); err != nil { + s.Fatalf("Can't start JetStream: %v", err) + return + } + } else { + // Check to see if any configured accounts have JetStream enabled. + sa, ga := s.SystemAccount(), s.GlobalAccount() + var hasSys, hasGlobal bool + var total int + + s.accounts.Range(func(k, v any) bool { + total++ + acc := v.(*Account) + if acc == sa { + hasSys = true + } else if acc == ga { + hasGlobal = true + } + acc.mu.RLock() + hasJs := len(acc.jsLimits) > 0 + acc.mu.RUnlock() + if hasJs { + s.checkJetStreamExports() + acc.enableAllJetStreamServiceImportsAndMappings() + } + return true + }) + // If we only have the system account and the global account and we are not standalone, + // go ahead and enable JS on $G in case we are in simple mixed mode setup. + if total == 2 && hasSys && hasGlobal && !s.standAloneMode() { + ga.mu.Lock() + ga.jsLimits = map[string]JetStreamAccountLimits{ + _EMPTY_: dynamicJSAccountLimits, + } + ga.mu.Unlock() + s.checkJetStreamExports() + ga.enableAllJetStreamServiceImportsAndMappings() + } + } + + // Delayed API response handling. Start regardless of JetStream being + // currently configured or not (since it can be enabled/disabled with + // configuration reload). + s.startGoRoutine(s.delayedAPIResponder) + + // Start OCSP Stapling monitoring for TLS certificates if enabled. Hook TLS handshake for + // OCSP check on peers (LEAF and CLIENT kind) if enabled. + s.startOCSPMonitoring() + + // Configure OCSP Response Cache for peer OCSP checks if enabled. + s.initOCSPResponseCache() + + // Start up gateway if needed. Do this before starting the routes, because + // we want to resolve the gateway host:port so that this information can + // be sent to other routes. + if opts.Gateway.Port != 0 { + s.startGateways() + } + + // Start websocket server if needed. Do this before starting the routes, and + // leaf node because we want to resolve the gateway host:port so that this + // information can be sent to other routes. + if opts.Websocket.Port != 0 { + s.startWebsocketServer() + } + + // Start up listen if we want to accept leaf node connections. + if opts.LeafNode.Port != 0 { + // Will resolve or assign the advertise address for the leafnode listener. + // We need that in StartRouting(). + s.startLeafNodeAcceptLoop() + } + + // Solicit remote servers for leaf node connections. + if len(opts.LeafNode.Remotes) > 0 { + s.solicitLeafNodeRemotes(opts.LeafNode.Remotes) + } + + // TODO (ik): I wanted to refactor this by starting the client + // accept loop first, that is, it would resolve listen spec + // in place, but start the accept-for-loop in a different go + // routine. This would get rid of the synchronization between + // this function and StartRouting, which I also would have wanted + // to refactor, but both AcceptLoop() and StartRouting() have + // been exported and not sure if that would break users using them. + // We could mark them as deprecated and remove in a release or two... + + // The Routing routine needs to wait for the client listen + // port to be opened and potential ephemeral port selected. + clientListenReady := make(chan struct{}) + + // MQTT + if opts.MQTT.Port != 0 { + s.startMQTT() + } + + // Start up routing as well if needed. + if opts.Cluster.Port != 0 { + s.startGoRoutine(func() { + s.StartRouting(clientListenReady) + }) + } + + if opts.PortsFileDir != _EMPTY_ { + s.logPorts() + } + + if opts.TLSRateLimit > 0 { + s.startGoRoutine(s.logRejectedTLSConns) + } + + // We've finished starting up. + close(s.startupComplete) + + // Wait for clients. + if !opts.DontListen { + s.AcceptLoop(clientListenReady) + } + + // Bring OSCP Response cache online after accept loop started in anticipation of NATS-enabled cache types + s.startOCSPResponseCache() +} + +func (s *Server) isShuttingDown() bool { + return s.shutdown.Load() +} + +// Shutdown will shutdown the server instance by kicking out the AcceptLoop +// and closing all associated clients. +func (s *Server) Shutdown() { + if s == nil { + return + } + // Prevent issues with multiple calls. + if !s.shutdown.CompareAndSwap(false, true) { + return + } + // This is for JetStream R1 Pull Consumers to allow signaling + // that pending pull requests are invalid. + s.signalPullConsumers() + + // Transfer off any raft nodes that we are a leader by stepping them down. + s.stepdownRaftNodes() + + // Shutdown the eventing system as needed. + // This is done first to send out any messages for + // account status. We will also clean up any + // eventing items associated with accounts. + s.shutdownEventing() + + s.mu.Lock() + s.Noticef("Initiating Shutdown...") + + accRes := s.accResolver + + opts := s.getOpts() + + s.running.Store(false) + s.grMu.Lock() + s.grRunning = false + s.grMu.Unlock() + s.mu.Unlock() + + if accRes != nil { + accRes.Close() + } + + // Now check and shutdown jetstream. + s.shutdownJetStream() + + // Now shutdown the nodes + s.shutdownRaftNodes() + + s.mu.Lock() + conns := make(map[uint64]*client) + + // Copy off the clients + for i, c := range s.clients { + conns[i] = c + } + // Copy off the connections that are not yet registered + // in s.routes, but for which the readLoop has started + s.grMu.Lock() + for i, c := range s.grTmpClients { + conns[i] = c + } + s.grMu.Unlock() + // Copy off the routes + s.forEachRoute(func(r *client) { + r.mu.Lock() + conns[r.cid] = r + r.mu.Unlock() + }) + // Copy off the gateways + s.getAllGatewayConnections(conns) + + // Copy off the leaf nodes + for i, c := range s.leafs { + conns[i] = c + } + + // Number of done channel responses we expect. + doneExpected := 0 + + // Kick client AcceptLoop() + if s.listener != nil { + doneExpected++ + s.listener.Close() + s.listener = nil + } + + // Kick websocket server + doneExpected += s.closeWebsocketServer() + + // Kick MQTT accept loop + if s.mqtt.listener != nil { + doneExpected++ + s.mqtt.listener.Close() + s.mqtt.listener = nil + } + + // Kick leafnodes AcceptLoop() + if s.leafNodeListener != nil { + doneExpected++ + s.leafNodeListener.Close() + s.leafNodeListener = nil + } + + // Kick route AcceptLoop() + if s.routeListener != nil { + doneExpected++ + s.routeListener.Close() + s.routeListener = nil + } + + // Kick Gateway AcceptLoop() + if s.gatewayListener != nil { + doneExpected++ + s.gatewayListener.Close() + s.gatewayListener = nil + } + + // Kick HTTP monitoring if its running + if s.http != nil { + doneExpected++ + s.http.Close() + s.http = nil + } + + // Kick Profiling if its running + if s.profiler != nil { + doneExpected++ + s.profiler.Close() + } + + s.mu.Unlock() + + // Release go routines that wait on that channel + close(s.quitCh) + + // Close client and route connections + for _, c := range conns { + c.setNoReconnect() + c.closeConnection(ServerShutdown) + } + + // Block until the accept loops exit + for doneExpected > 0 { + <-s.done + doneExpected-- + } + + // Wait for go routines to be done. + s.grWG.Wait() + + if opts.PortsFileDir != _EMPTY_ { + s.deletePortsFile(opts.PortsFileDir) + } + + s.Noticef("Server Exiting..") + + // Stop OCSP Response Cache + if s.ocsprc != nil { + s.ocsprc.Stop(s) + } + + // Close logger if applicable. It allows tests on Windows + // to be able to do proper cleanup (delete log file). + s.logging.RLock() + log := s.logging.logger + s.logging.RUnlock() + if log != nil { + if l, ok := log.(*logger.Logger); ok { + l.Close() + } + } + // Notify that the shutdown is complete + close(s.shutdownComplete) +} + +// Close the websocket server if running. If so, returns 1, else 0. +// Server lock held on entry. +func (s *Server) closeWebsocketServer() int { + ws := &s.websocket + ws.mu.Lock() + hs := ws.server + if hs != nil { + ws.server = nil + ws.listener = nil + } + ws.mu.Unlock() + if hs != nil { + hs.Close() + return 1 + } + return 0 +} + +// WaitForShutdown will block until the server has been fully shutdown. +func (s *Server) WaitForShutdown() { + <-s.shutdownComplete +} + +// AcceptLoop is exported for easier testing. +func (s *Server) AcceptLoop(clr chan struct{}) { + // If we were to exit before the listener is setup properly, + // make sure we close the channel. + defer func() { + if clr != nil { + close(clr) + } + }() + + if s.isShuttingDown() { + return + } + + // Snapshot server options. + opts := s.getOpts() + + // Setup state that can enable shutdown + s.mu.Lock() + hp := net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port)) + l, e := s.getServerListener(hp) + s.listenerErr = e + if e != nil { + s.mu.Unlock() + s.Fatalf("Error listening on port: %s, %q", hp, e) + return + } + s.Noticef("Listening for client connections on %s", + net.JoinHostPort(opts.Host, strconv.Itoa(l.Addr().(*net.TCPAddr).Port))) + + // Alert if PROXY protocol is enabled + if opts.ProxyProtocol { + s.Noticef("PROXY protocol enabled for client connections") + } + + // Alert of TLS enabled. + if opts.TLSConfig != nil { + // "TLS Handshake First" without a fallback delay always requires the + // handshake, which overrides "allow_non_tls". + tlsHandshakeFirstOnly := opts.TLSHandshakeFirst && opts.TLSHandshakeFirstFallback == 0 + if opts.AllowNonTLS && !tlsHandshakeFirstOnly { + s.Noticef("TLS available for client connections") + } else { + s.Noticef("TLS required for client connections") + } + if tlsHandshakeFirstOnly { + s.Warnf("Clients that are not using \"TLS Handshake First\" option will fail to connect") + } + } + + // If server was started with RANDOM_PORT (-1), opts.Port would be equal + // to 0 at the beginning this function. So we need to get the actual port + if opts.Port == 0 { + // Write resolved port back to options. + opts.Port = l.Addr().(*net.TCPAddr).Port + } + + // Now that port has been set (if it was set to RANDOM), set the + // server's info Host/Port with either values from Options or + // ClientAdvertise. + if err := s.setInfoHostPort(); err != nil { + s.Fatalf("Error setting server INFO with ClientAdvertise value of %s, err=%v", opts.ClientAdvertise, err) + l.Close() + s.mu.Unlock() + return + } + // Keep track of client connect URLs. We may need them later. + s.clientConnectURLs = s.getClientConnectURLs() + s.listener = l + + go s.acceptConnections(l, "Client", func(conn net.Conn) { s.createClient(conn) }, + func(_ error) bool { + if s.isLameDuckMode() { + // Signal that we are not accepting new clients + s.ldmCh <- true + // Now wait for the Shutdown... + <-s.quitCh + return true + } + return false + }) + s.mu.Unlock() + + // Let the caller know that we are ready + close(clr) + clr = nil +} + +// getServerListener returns a network listener for the given host-port address. +// If the Server already has an active listener (s.listener), it returns that listener +// along with any previous error (s.listenerErr). Otherwise, it creates and returns +// a new TCP listener on the specified address using natsListen. +func (s *Server) getServerListener(hp string) (net.Listener, error) { + if s.listener != nil { + return s.listener, s.listenerErr + } + + return natsListen("tcp", hp) +} + +// InProcessConn returns an in-process connection to the server, +// avoiding the need to use a TCP listener for local connectivity +// within the same process. This can be used regardless of the +// state of the DontListen option. +func (s *Server) InProcessConn() (net.Conn, error) { + pl, pr := net.Pipe() + if !s.startGoRoutine(func() { + s.createClientInProcess(pl) + s.grWG.Done() + }) { + pl.Close() + pr.Close() + return nil, fmt.Errorf("failed to create connection") + } + return pr, nil +} + +func (s *Server) acceptConnections(l net.Listener, acceptName string, createFunc func(conn net.Conn), errFunc func(err error) bool) { + tmpDelay := ACCEPT_MIN_SLEEP + + for { + conn, err := l.Accept() + if err != nil { + if errFunc != nil && errFunc(err) { + return + } + if tmpDelay = s.acceptError(acceptName, err, tmpDelay); tmpDelay < 0 { + break + } + continue + } + tmpDelay = ACCEPT_MIN_SLEEP + if !s.startGoRoutine(func() { + s.reloadMu.RLock() + createFunc(conn) + s.reloadMu.RUnlock() + s.grWG.Done() + }) { + conn.Close() + } + } + s.Debugf(acceptName + " accept loop exiting..") + s.done <- true +} + +// This function sets the server's info Host/Port based on server Options. +// Note that this function may be called during config reload, this is why +// Host/Port may be reset to original Options if the ClientAdvertise option +// is not set (since it may have previously been). +func (s *Server) setInfoHostPort() error { + // When this function is called, opts.Port is set to the actual listen + // port (if option was originally set to RANDOM), even during a config + // reload. So use of s.opts.Port is safe. + opts := s.getOpts() + if opts.ClientAdvertise != _EMPTY_ { + h, p, err := parseHostPort(opts.ClientAdvertise, opts.Port) + if err != nil { + return err + } + s.info.Host = h + s.info.Port = p + } else { + s.info.Host = opts.Host + s.info.Port = opts.Port + } + return nil +} + +// StartProfiler is called to enable dynamic profiling. +func (s *Server) StartProfiler() { + if s.isShuttingDown() { + return + } + + // Snapshot server options. + opts := s.getOpts() + + port := opts.ProfPort + + // Check for Random Port + if port == -1 { + port = 0 + } + + s.mu.Lock() + hp := net.JoinHostPort(opts.Host, strconv.Itoa(port)) + l, err := net.Listen("tcp", hp) + + if err != nil { + s.mu.Unlock() + s.Fatalf("error starting profiler: %s", err) + return + } + s.Noticef("profiling port: %d", l.Addr().(*net.TCPAddr).Port) + + srv := &http.Server{ + Addr: hp, + Handler: http.DefaultServeMux, + MaxHeaderBytes: 1 << 20, + ReadTimeout: time.Second * 5, + } + s.profiler = l + s.profilingServer = srv + + s.setBlockProfileRate(opts.ProfBlockRate) + + go func() { + // if this errors out, it's probably because the server is being shutdown + err := srv.Serve(l) + if err != nil { + if !s.isShuttingDown() { + s.Fatalf("error starting profiler: %s", err) + } + } + srv.Close() + s.done <- true + }() + s.mu.Unlock() +} + +func (s *Server) setBlockProfileRate(rate int) { + // Passing i ProfBlockRate <= 0 here will disable or > 0 will enable. + runtime.SetBlockProfileRate(rate) + + if rate > 0 { + s.Warnf("Block profiling is enabled (rate %d), this may have a performance impact", rate) + } +} + +// StartHTTPMonitoring will enable the HTTP monitoring port. +// DEPRECATED: Should use StartMonitoring. +func (s *Server) StartHTTPMonitoring() { + s.startMonitoring(false) +} + +// StartHTTPSMonitoring will enable the HTTPS monitoring port. +// DEPRECATED: Should use StartMonitoring. +func (s *Server) StartHTTPSMonitoring() { + s.startMonitoring(true) +} + +// StartMonitoring starts the HTTP or HTTPs server if needed. +func (s *Server) StartMonitoring() error { + // Snapshot server options. + opts := s.getOpts() + + // Specifying both HTTP and HTTPS ports is a misconfiguration + if opts.HTTPPort != 0 && opts.HTTPSPort != 0 { + return fmt.Errorf("can't specify both HTTP (%v) and HTTPs (%v) ports", opts.HTTPPort, opts.HTTPSPort) + } + var err error + if opts.HTTPPort != 0 { + err = s.startMonitoring(false) + } else if opts.HTTPSPort != 0 { + if opts.TLSConfig == nil { + return fmt.Errorf("TLS cert and key required for HTTPS") + } + err = s.startMonitoring(true) + } + return err +} + +// HTTP endpoints +const ( + RootPath = "/" + VarzPath = "/varz" + ConnzPath = "/connz" + RoutezPath = "/routez" + GatewayzPath = "/gatewayz" + LeafzPath = "/leafz" + SubszPath = "/subsz" + StackszPath = "/stacksz" + AccountzPath = "/accountz" + AccountStatzPath = "/accstatz" + JszPath = "/jsz" + HealthzPath = "/healthz" + IPQueuesPath = "/ipqueuesz" + RaftzPath = "/raftz" + ExpvarzPath = "/debug/vars" +) + +func (s *Server) basePath(p string) string { + return path.Join(s.httpBasePath, p) +} + +type captureHTTPServerLog struct { + s *Server + prefix string +} + +func (cl *captureHTTPServerLog) Write(p []byte) (int, error) { + var buf [128]byte + var b = buf[:0] + + b = append(b, []byte(cl.prefix)...) + offset := 0 + if bytes.HasPrefix(p, []byte("http:")) { + offset = 6 + } + b = append(b, p[offset:]...) + cl.s.Errorf(string(b)) + return len(p), nil +} + +// The TLS configuration is passed to the listener when the monitoring +// "server" is setup. That prevents TLS configuration updates on reload +// from being used. By setting this function in tls.Config.GetConfigForClient +// we instruct the TLS handshake to ask for the tls configuration to be +// used for a specific client. We don't care which client, we always use +// the same TLS configuration. +func (s *Server) getMonitoringTLSConfig(_ *tls.ClientHelloInfo) (*tls.Config, error) { + opts := s.getOpts() + tc := opts.TLSConfig.Clone() + tc.ClientAuth = tls.NoClientCert + return tc, nil +} + +// Start the monitoring server +func (s *Server) startMonitoring(secure bool) error { + if s.isShuttingDown() { + return nil + } + + // Snapshot server options. + opts := s.getOpts() + + var ( + hp string + err error + httpListener net.Listener + port int + ) + + monitorProtocol := "http" + + if secure { + monitorProtocol += "s" + port = opts.HTTPSPort + if port == -1 { + port = 0 + } + hp = net.JoinHostPort(opts.HTTPHost, strconv.Itoa(port)) + config := opts.TLSConfig.Clone() + if !s.ocspPeerVerify { + config.GetConfigForClient = s.getMonitoringTLSConfig + config.ClientAuth = tls.NoClientCert + } + httpListener, err = tls.Listen("tcp", hp, config) + + } else { + port = opts.HTTPPort + if port == -1 { + port = 0 + } + hp = net.JoinHostPort(opts.HTTPHost, strconv.Itoa(port)) + httpListener, err = net.Listen("tcp", hp) + } + + if err != nil { + return fmt.Errorf("can't listen to the monitor port: %v", err) + } + + rport := httpListener.Addr().(*net.TCPAddr).Port + s.Noticef("Starting %s monitor on %s", monitorProtocol, net.JoinHostPort(opts.HTTPHost, strconv.Itoa(rport))) + + mux := http.NewServeMux() + + // Root + mux.HandleFunc(s.basePath(RootPath), s.HandleRoot) + // Varz + mux.HandleFunc(s.basePath(VarzPath), s.HandleVarz) + // Connz + mux.HandleFunc(s.basePath(ConnzPath), s.HandleConnz) + // Routez + mux.HandleFunc(s.basePath(RoutezPath), s.HandleRoutez) + // Gatewayz + mux.HandleFunc(s.basePath(GatewayzPath), s.HandleGatewayz) + // Leafz + mux.HandleFunc(s.basePath(LeafzPath), s.HandleLeafz) + // Subz + mux.HandleFunc(s.basePath(SubszPath), s.HandleSubsz) + // Subz alias for backwards compatibility + mux.HandleFunc(s.basePath("/subscriptionsz"), s.HandleSubsz) + // Stacksz + mux.HandleFunc(s.basePath(StackszPath), s.HandleStacksz) + // Accountz + mux.HandleFunc(s.basePath(AccountzPath), s.HandleAccountz) + // Accstatz + mux.HandleFunc(s.basePath(AccountStatzPath), s.HandleAccountStatz) + // Jsz + mux.HandleFunc(s.basePath(JszPath), s.HandleJsz) + // Healthz + mux.HandleFunc(s.basePath(HealthzPath), s.HandleHealthz) + // IPQueuesz + mux.HandleFunc(s.basePath(IPQueuesPath), s.HandleIPQueuesz) + // Raftz + mux.HandleFunc(s.basePath(RaftzPath), s.HandleRaftz) + // Expvarz + mux.Handle(s.basePath(ExpvarzPath), expvar.Handler()) + + // Do not set a WriteTimeout because it could cause cURL/browser + // to return empty response or unable to display page if the + // server needs more time to build the response. + srv := &http.Server{ + Addr: hp, + Handler: mux, + MaxHeaderBytes: 1 << 20, + ErrorLog: log.New(&captureHTTPServerLog{s, "monitoring: "}, _EMPTY_, 0), + ReadHeaderTimeout: time.Second * 5, + } + s.mu.Lock() + s.http = httpListener + s.httpHandler = mux + s.monitoringServer = srv + s.mu.Unlock() + + go func() { + if err := srv.Serve(httpListener); err != nil { + if !s.isShuttingDown() { + s.Fatalf("Error starting monitor on %q: %v", hp, err) + } + } + srv.Close() + s.mu.Lock() + s.httpHandler = nil + s.mu.Unlock() + s.done <- true + }() + + return nil +} + +// HTTPHandler returns the http.Handler object used to handle monitoring +// endpoints. It will return nil if the server is not configured for +// monitoring, or if the server has not been started yet (Server.Start()). +func (s *Server) HTTPHandler() http.Handler { + s.mu.Lock() + defer s.mu.Unlock() + return s.httpHandler +} + +// Perform a conditional deep copy due to reference nature of [Client|WS]ConnectURLs. +// If updates are made to Info, this function should be consulted and updated. +// Assume lock is held. +func (s *Server) copyInfo() Info { + info := s.info + if len(info.ClientConnectURLs) > 0 { + info.ClientConnectURLs = append([]string(nil), s.info.ClientConnectURLs...) + } + if len(info.WSConnectURLs) > 0 { + info.WSConnectURLs = append([]string(nil), s.info.WSConnectURLs...) + } + return info +} + +// tlsMixConn is used when we can receive both TLS and non-TLS connections on same port. +type tlsMixConn struct { + net.Conn + pre *bytes.Buffer +} + +// Read for our mixed multi-reader. +func (c *tlsMixConn) Read(b []byte) (int, error) { + if c.pre != nil { + n, err := c.pre.Read(b) + if c.pre.Len() == 0 { + c.pre = nil + } + return n, err + } + return c.Conn.Read(b) +} + +func (s *Server) createClient(conn net.Conn) *client { + return s.createClientEx(conn, false) +} + +func (s *Server) createClientInProcess(conn net.Conn) *client { + return s.createClientEx(conn, true) +} + +func (s *Server) createClientEx(conn net.Conn, inProcess bool) *client { + // Snapshot server options. + opts := s.getOpts() + + maxPay := int32(opts.MaxPayload) + maxSubs := int32(opts.MaxSubs) + // For system, maxSubs of 0 means unlimited, so re-adjust here. + if maxSubs == 0 { + maxSubs = -1 + } + now := time.Now() + + c := &client{ + srv: s, + nc: conn, + opts: defaultOpts, + mpay: maxPay, + msubs: maxSubs, + start: now, + last: now, + iproc: inProcess, + } + + c.registerWithAccount(s.globalAccount()) + + var info Info + var authRequired bool + + s.mu.Lock() + // Grab JSON info string + info = s.copyInfo() + if s.nonceRequired() { + // Nonce handling + var raw [nonceLen]byte + nonce := raw[:] + s.generateNonce(nonce) + info.Nonce = string(nonce) + } + c.nonce = []byte(info.Nonce) + authRequired = info.AuthRequired + + // Check to see if we have auth_required set but we also have a no_auth_user. + // If so set back to false. + if info.AuthRequired && opts.NoAuthUser != _EMPTY_ && opts.NoAuthUser != s.sysAccOnlyNoAuthUser { + info.AuthRequired = false + } + + // Check to see if this is an in-process connection with tls_required. + // If so, set as not required, but available. + if inProcess && info.TLSRequired { + info.TLSRequired = false + info.TLSAvailable = true + } + + s.totalClients++ + s.mu.Unlock() + + // Grab lock + c.mu.Lock() + if authRequired { + c.flags.set(expectConnect) + } + + // Initialize + c.initClient() + + c.Debugf("Client connection created") + + // Save info.TLSRequired value since we may neeed to change it back and forth. + orgInfoTLSReq := info.TLSRequired + + var tlsFirstFallback time.Duration + // Check if we should do TLS first. + tlsFirst := opts.TLSConfig != nil && opts.TLSHandshakeFirst + if tlsFirst { + // Make sure info.TLSRequired is set to true (it could be false + // if AllowNonTLS is enabled). + info.TLSRequired = true + // Get the fallback delay value if applicable. + if f := opts.TLSHandshakeFirstFallback; f > 0 { + tlsFirstFallback = f + } else if inProcess { + // For in-process connection, we will always have a fallback + // delay. It allows support for non-TLS, TLS and "TLS First" + // in-process clients to successfully connect. + tlsFirstFallback = DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY + } + } + + // Decide if we are going to require TLS or not and generate INFO json. + // If we have ProxyProtocol enabled then we won't include the client + // IP in the initial INFO, as that would leak the proxy IP itself. + // In that case we'll send another INFO after the client introduces itself. + tlsRequired := info.TLSRequired + infoBytes := c.generateClientInfoJSON(info, !opts.ProxyProtocol) + + // Send our information, except if TLS and TLSHandshakeFirst is requested. + if !tlsFirst { + // Need to be sent in place since writeLoop cannot be started until + // TLS handshake is done (if applicable). + c.sendProtoNow(infoBytes) + } + + // Unlock to register + c.mu.Unlock() + + // Register with the server. + s.mu.Lock() + // If server is not running, Shutdown() may have already gathered the + // list of connections to close. It won't contain this one, so we need + // to bail out now otherwise the readLoop started down there would not + // be interrupted. Skip also if in lame duck mode. + if !s.isRunning() || s.ldm { + // There are some tests that create a server but don't start it, + // and use "async" clients and perform the parsing manually. Such + // clients would branch here (since server is not running). However, + // when a server was really running and has been shutdown, we must + // close this connection. + if s.isShuttingDown() { + conn.Close() + } + s.mu.Unlock() + return c + } + + // If there is a max connections specified, check that adding + // this new client would not push us over the max + if opts.MaxConn < 0 || (opts.MaxConn > 0 && len(s.clients) >= opts.MaxConn) { + s.mu.Unlock() + c.maxConnExceeded() + return nil + } + s.clients[c.cid] = c + + s.mu.Unlock() + + // Re-Grab lock + c.mu.Lock() + + var pre []byte + // We need first to check for "TLS First" fallback delay. + if !c.isClosed() && tlsFirstFallback > 0 { + // We wait and see if we are getting any data. Since we did not send + // the INFO protocol yet, only clients that use TLS first should be + // sending data (the TLS handshake). We don't really check the content: + // if it is a rogue agent and not an actual client performing the + // TLS handshake, the error will be detected when performing the + // handshake on our side. + pre = make([]byte, 4) + nc := c.nc + c.mu.Unlock() + _ = nc.SetReadDeadline(time.Now().Add(tlsFirstFallback)) + n, _ := io.ReadFull(nc, pre[:]) + _ = nc.SetReadDeadline(time.Time{}) + c.mu.Lock() + // If we get any data (regardless of possible timeout), we will proceed + // with the TLS handshake. + if n > 0 { + pre = pre[:n] + } else { + // We did not get anything so we will send the INFO protocol. + pre = nil + + // Restore the original info.TLSRequired value if it is + // different that the current value and regenerate infoBytes. + if orgInfoTLSReq != info.TLSRequired { + info.TLSRequired = orgInfoTLSReq + infoBytes = c.generateClientInfoJSON(info, !opts.ProxyProtocol) + } + c.sendProtoNow(infoBytes) + // Set the boolean to false for the rest of the function. + tlsFirst = false + } + } + // If we have both TLS and non-TLS allowed we need to see which + // one the client wants. We'll always allow this for in-process + // connections. + sniffTLS := !tlsFirst && opts.TLSConfig != nil && (inProcess || opts.AllowNonTLS) + if !c.isClosed() && sniffTLS { + pre = make([]byte, 6) // Minimum 6 bytes for proxy proto in next step. + nc := c.nc + c.mu.Unlock() + _ = nc.SetReadDeadline(time.Now().Add(secondsToDuration(opts.TLSTimeout))) + n, _ := io.ReadFull(nc, pre[:]) + _ = nc.SetReadDeadline(time.Time{}) + c.mu.Lock() + pre = pre[:n] + tlsRequired = n > 0 && pre[0] == 0x16 + } + + // Check for proxy protocol if enabled. The PROXY header is sent as + // plaintext before any TLS handshake per the spec, so we must read it + // before doing TLS even when TLS is required. Any bytes read past the + // header are kept in `pre` and replayed into the TLS handshake (or the + // non-TLS protocol parser) by the tlsMixConn wrapper used below. + if !c.isClosed() && opts.ProxyProtocol { + if len(pre) == 0 { + // There has been no pre-read yet, do so so we can work out + // if the client is trying to negotiate PROXY. + pre = make([]byte, 6) + nc := c.nc + c.mu.Unlock() + _ = nc.SetReadDeadline(time.Now().Add(proxyProtoReadTimeout)) + n, _ := io.ReadFull(nc, pre) + _ = nc.SetReadDeadline(time.Time{}) + c.mu.Lock() + pre = pre[:n] + } + conn = &tlsMixConn{conn, bytes.NewBuffer(pre)} + c.mu.Unlock() + addr, proxyPre, err := readProxyProtoHeader(conn) + c.mu.Lock() + if c.isClosed() { + c.mu.Unlock() + return nil + } + if err != nil && err != errProxyProtoUnrecognized { + // err != errProxyProtoUnrecognized implies that we detected a proxy + // protocol header but we failed to parse it, so don't continue. + c.mu.Unlock() + s.Warnf("Error reading PROXY protocol header from %s: %v", conn.RemoteAddr(), err) + c.closeConnection(ProtocolViolation) + return nil + } + // If addr is nil, it was a LOCAL/UNKNOWN command (health check) + // Use the connection as-is + if addr != nil { + c.nc = &proxyConn{ + Conn: conn, + remoteAddr: addr, + } + // These were set already by initClient, override them. + c.host = addr.srcIP.String() + c.port = addr.srcPort + } + // At this point, err is either: + // - nil => we parsed the proxy protocol header successfully and + // proxyPre holds any bytes read past the header + // - errProxyProtoUnrecognized => we didn't detect proxy protocol at + // all and proxyPre holds the bytes that detection consumed (which + // may include bytes read directly from the socket beyond the + // original pre-read), so they must be replayed to the next step. + pre = proxyPre + if err == nil && sniffTLS { + // If we sniffed for TLS-vs-plaintext above, the byte we looked + // at belonged to the PROXY header, not to the client's actual + // traffic. Re-evaluate using the first byte that follows the + // header (reading it from the connection if needed). + if len(pre) == 0 { + buf := make([]byte, 1) + nc := c.nc + c.mu.Unlock() + _ = nc.SetReadDeadline(time.Now().Add(secondsToDuration(opts.TLSTimeout))) + n, _ := io.ReadFull(nc, buf) + _ = nc.SetReadDeadline(time.Time{}) + c.mu.Lock() + pre = buf[:n] + } + tlsRequired = len(pre) > 0 && pre[0] == 0x16 + } + // Because we have ProxyProtocol enabled, our earlier INFO message didn't + // include the client_ip. If we need to send it again then we will include + // it, but sending it here immediately can confuse clients who have just + // PING'd. + infoBytes = c.generateClientInfoJSON(info, true) + } + + // Check for TLS + if !c.isClosed() && tlsRequired { + if s.connRateCounter != nil && !s.connRateCounter.allow() { + c.mu.Unlock() + c.sendErr("Connection throttling is active. Please try again later.") + c.closeConnection(MaxConnectionsExceeded) + return nil + } + + // If we have a prebuffer create a multi-reader. + if len(pre) > 0 { + c.nc = &tlsMixConn{c.nc, bytes.NewBuffer(pre)} + // Clear pre so it is not parsed. + pre = nil + } + // Performs server-side TLS handshake. + if err := c.doTLSServerHandshake(_EMPTY_, opts.TLSConfig, opts.TLSTimeout, opts.TLSPinnedCerts); err != nil { + c.mu.Unlock() + return nil + } + } + + // Now, send the INFO if it was delayed + if !c.isClosed() && tlsFirst { + c.flags.set(didTLSFirst) + c.sendProtoNow(infoBytes) + } + + // Connection could have been closed while sending the INFO proto. + if c.isClosed() { + c.mu.Unlock() + // We need to call closeConnection() to make sure that proper cleanup is done. + c.closeConnection(WriteError) + return nil + } + + // Check for Auth. We schedule this timer after the TLS handshake to avoid + // the race where the timer fires during the handshake and causes the + // server to write bad data to the socket. See issue #432. + if authRequired { + c.setAuthTimer(secondsToDuration(opts.AuthTimeout)) + } + + // Do final client initialization + + // Set the Ping timer. Will be reset once connect was received. + c.setPingTimer() + + // Spin up the read loop. + s.startGoRoutine(func() { c.readLoop(pre) }) + + // Spin up the write loop. + s.startGoRoutine(func() { c.writeLoop() }) + + if tlsRequired { + c.Debugf("TLS handshake complete") + cs := c.nc.(*tls.Conn).ConnectionState() + c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite)) + } + + c.mu.Unlock() + + return c +} + +// This will save off a closed client in a ring buffer such that +// /connz can inspect. Useful for debugging, etc. +func (s *Server) saveClosedClient(c *client, nc net.Conn, subs map[string]*subscription, reason ClosedState) { + now := time.Now() + + s.accountDisconnectEvent(c, now, reason.String()) + + c.mu.Lock() + + cc := &closedClient{} + cc.fill(c, nc, now, false) + // Note that cc.fill is using len(c.subs), which may have been set to nil by now, + // so replace cc.NumSubs with len(subs). + cc.NumSubs = uint32(len(subs)) + cc.Stop = &now + cc.Reason = reason.String() + + // Do subs, do not place by default in main ConnInfo + if len(subs) > 0 { + cc.subs = make([]SubDetail, 0, len(subs)) + for _, sub := range subs { + cc.subs = append(cc.subs, newSubDetail(sub)) + } + } + // Hold user as well. + cc.user = c.getRawAuthUser() + // Hold account name if not the global account. + if c.acc != nil && c.acc.Name != globalAccountName { + cc.acc = c.acc.Name + } + cc.JWT = redactBearerJWT(c.opts.JWT) + cc.IssuerKey = issuerForClient(c) + cc.Tags = c.tags + cc.NameTag = c.nameTag + c.mu.Unlock() + + // Place in the ring buffer + if s.closed != nil { + s.closed.append(cc) + } +} + +// Adds to the list of client and websocket clients connect URLs. +// If there was a change, an INFO protocol is sent to registered clients +// that support async INFO protocols. +// Server lock held on entry. +func (s *Server) addConnectURLsAndSendINFOToClients(curls, wsurls []string) { + s.updateServerINFOAndSendINFOToClients(curls, wsurls, true) +} + +// Removes from the list of client and websocket clients connect URLs. +// If there was a change, an INFO protocol is sent to registered clients +// that support async INFO protocols. +// Server lock held on entry. +func (s *Server) removeConnectURLsAndSendINFOToClients(curls, wsurls []string) { + s.updateServerINFOAndSendINFOToClients(curls, wsurls, false) +} + +// Updates the list of client and websocket clients connect URLs and if any change +// sends an async INFO update to clients that support it. +// Server lock held on entry. +func (s *Server) updateServerINFOAndSendINFOToClients(curls, wsurls []string, add bool) { + remove := !add + // Will return true if we need alter the server's Info object. + updateMap := func(urls []string, m refCountedUrlSet) bool { + wasUpdated := false + for _, url := range urls { + if add && m.addUrl(url) { + wasUpdated = true + } else if remove && m.removeUrl(url) { + wasUpdated = true + } + } + return wasUpdated + } + cliUpdated := updateMap(curls, s.clientConnectURLsMap) + wsUpdated := updateMap(wsurls, s.websocket.connectURLsMap) + + updateInfo := func(infoURLs *[]string, urls []string, m refCountedUrlSet) { + // Recreate the info's slice from the map + *infoURLs = (*infoURLs)[:0] + // Add this server client connect ULRs first... + *infoURLs = append(*infoURLs, urls...) + // Then the ones from the map + for url := range m { + *infoURLs = append(*infoURLs, url) + } + } + if cliUpdated { + updateInfo(&s.info.ClientConnectURLs, s.clientConnectURLs, s.clientConnectURLsMap) + } + if wsUpdated { + updateInfo(&s.info.WSConnectURLs, s.websocket.connectURLs, s.websocket.connectURLsMap) + } + if cliUpdated || wsUpdated { + // Send to all registered clients that support async INFO protocols. + s.sendAsyncInfoToClients(cliUpdated, wsUpdated) + } +} + +// Handle closing down a connection when the handshake has timedout. +func tlsTimeout(c *client, conn *tls.Conn) { + c.mu.Lock() + closed := c.isClosed() + c.mu.Unlock() + // Check if already closed + if closed { + return + } + cs := conn.ConnectionState() + if !cs.HandshakeComplete { + if c.kind == CLIENT || c.kind == LEAF { + c.Debugf("TLS handshake timeout") + } else { + c.Errorf("TLS handshake timeout") + } + c.sendErr("Secure Connection - TLS Required") + c.closeConnection(TLSHandshakeError) + } +} + +// Seems silly we have to write these +func tlsVersion(ver uint16) string { + switch ver { + case tls.VersionTLS10: + return "1.0" + case tls.VersionTLS11: + return "1.1" + case tls.VersionTLS12: + return "1.2" + case tls.VersionTLS13: + return "1.3" + } + return fmt.Sprintf("Unknown [0x%x]", ver) +} + +func tlsVersionFromString(ver string) (uint16, error) { + switch ver { + case "1.0": + return tls.VersionTLS10, nil + case "1.1": + return tls.VersionTLS11, nil + case "1.2": + return tls.VersionTLS12, nil + case "1.3": + return tls.VersionTLS13, nil + } + return 0, fmt.Errorf("unknown version: %v", ver) +} + +// Remove a client or route from our internal accounting. +func (s *Server) removeClient(c *client) { + // kind is immutable, so can check without lock + switch c.kind { + case CLIENT: + c.mu.Lock() + cid := c.cid + updateProtoInfoCount := false + if c.kind == CLIENT && c.opts.Protocol >= ClientProtoInfo { + updateProtoInfoCount = true + } + proxyKey := c.proxyKey + c.mu.Unlock() + + s.mu.Lock() + delete(s.clients, cid) + if updateProtoInfoCount { + s.cproto-- + } + if proxyKey != _EMPTY_ { + s.removeProxiedConn(proxyKey, cid) + } + s.mu.Unlock() + case ROUTER: + s.removeRoute(c) + case GATEWAY: + s.removeRemoteGatewayConnection(c) + case LEAF: + s.removeLeafNodeConnection(c) + } +} + +// Remove the connection with id `cid` from the map of connections +// under the public key `key` of the trusted proxies. +// +// Server lock must be held on entry. +func (s *Server) removeProxiedConn(key string, cid uint64) { + conns := s.proxiedConns[key] + delete(conns, cid) + if len(conns) == 0 { + delete(s.proxiedConns, key) + } +} + +func (s *Server) removeFromTempClients(cid uint64) { + s.grMu.Lock() + delete(s.grTmpClients, cid) + s.grMu.Unlock() +} + +func (s *Server) addToTempClients(cid uint64, c *client) bool { + added := false + s.grMu.Lock() + if s.grRunning { + s.grTmpClients[cid] = c + added = true + } + s.grMu.Unlock() + return added +} + +///////////////////////////////////////////////////////////////// +// These are some helpers for accounting in functional tests. +///////////////////////////////////////////////////////////////// + +// NumRoutes will report the number of registered routes. +func (s *Server) NumRoutes() int { + s.mu.RLock() + defer s.mu.RUnlock() + return s.numRoutes() +} + +// numRoutes will report the number of registered routes. +// Server lock held on entry +func (s *Server) numRoutes() int { + var nr int + s.forEachRoute(func(c *client) { + nr++ + }) + return nr +} + +// NumRemotes will report number of registered remotes. +func (s *Server) NumRemotes() int { + s.mu.RLock() + defer s.mu.RUnlock() + return s.numRemotes() +} + +// numRemotes will report number of registered remotes. +// Server lock held on entry +func (s *Server) numRemotes() int { + return len(s.routes) +} + +// NumLeafNodes will report number of leaf node connections. +func (s *Server) NumLeafNodes() int { + s.mu.RLock() + defer s.mu.RUnlock() + return len(s.leafs) +} + +// NumClients will report the number of registered clients. +func (s *Server) NumClients() int { + s.mu.RLock() + defer s.mu.RUnlock() + return len(s.clients) +} + +// GetClient will return the client associated with cid. +func (s *Server) GetClient(cid uint64) *client { + return s.getClient(cid) +} + +// getClient will return the client associated with cid. +func (s *Server) getClient(cid uint64) *client { + s.mu.RLock() + defer s.mu.RUnlock() + return s.clients[cid] +} + +// GetLeafNode returns the leafnode associated with the cid. +func (s *Server) GetLeafNode(cid uint64) *client { + s.mu.RLock() + defer s.mu.RUnlock() + return s.leafs[cid] +} + +// NumSubscriptions will report how many subscriptions are active. +func (s *Server) NumSubscriptions() uint32 { + s.mu.RLock() + defer s.mu.RUnlock() + return s.numSubscriptions() +} + +// numSubscriptions will report how many subscriptions are active. +// Lock should be held. +func (s *Server) numSubscriptions() uint32 { + var subs int + s.accounts.Range(func(k, v any) bool { + acc := v.(*Account) + subs += acc.TotalSubs() + return true + }) + return uint32(subs) +} + +// NumSlowConsumers will report the number of slow consumers. +func (s *Server) NumSlowConsumers() int64 { + return atomic.LoadInt64(&s.slowConsumers) +} + +// NumStalledClients will report the total number of times clients have been stalled. +func (s *Server) NumStalledClients() int64 { + return atomic.LoadInt64(&s.stalls) +} + +// NumSlowConsumersClients will report the number of slow consumers clients. +func (s *Server) NumSlowConsumersClients() uint64 { + return s.scStats.clients.Load() +} + +// NumSlowConsumersRoutes will report the number of slow consumers routes. +func (s *Server) NumSlowConsumersRoutes() uint64 { + return s.scStats.routes.Load() +} + +// NumSlowConsumersGateways will report the number of slow consumers leafs. +func (s *Server) NumSlowConsumersGateways() uint64 { + return s.scStats.gateways.Load() +} + +// NumSlowConsumersLeafs will report the number of slow consumers leafs. +func (s *Server) NumSlowConsumersLeafs() uint64 { + return s.scStats.leafs.Load() +} + +// NumStaleConnections will report the number of stale connections. +func (s *Server) NumStaleConnections() int64 { + return atomic.LoadInt64(&s.staleConnections) +} + +// NumStaleConnectionsClients will report the number of stale client connections. +func (s *Server) NumStaleConnectionsClients() uint64 { + return s.staleStats.clients.Load() +} + +// NumStaleConnectionsRoutes will report the number of stale route connections. +func (s *Server) NumStaleConnectionsRoutes() uint64 { + return s.staleStats.routes.Load() +} + +// NumStaleConnectionsGateways will report the number of stale gateway connections. +func (s *Server) NumStaleConnectionsGateways() uint64 { + return s.staleStats.gateways.Load() +} + +// NumStaleConnectionsLeafs will report the number of stale leaf connections. +func (s *Server) NumStaleConnectionsLeafs() uint64 { + return s.staleStats.leafs.Load() +} + +// ConfigTime will report the last time the server configuration was loaded. +func (s *Server) ConfigTime() time.Time { + s.mu.RLock() + defer s.mu.RUnlock() + return s.configTime +} + +// Addr will return the net.Addr object for the current listener. +func (s *Server) Addr() net.Addr { + s.mu.RLock() + defer s.mu.RUnlock() + if s.listener == nil { + return nil + } + return s.listener.Addr() +} + +// MonitorAddr will return the net.Addr object for the monitoring listener. +func (s *Server) MonitorAddr() *net.TCPAddr { + s.mu.RLock() + defer s.mu.RUnlock() + if s.http == nil { + return nil + } + return s.http.Addr().(*net.TCPAddr) +} + +// ClusterAddr returns the net.Addr object for the route listener. +func (s *Server) ClusterAddr() *net.TCPAddr { + s.mu.RLock() + defer s.mu.RUnlock() + if s.routeListener == nil { + return nil + } + return s.routeListener.Addr().(*net.TCPAddr) +} + +// ProfilerAddr returns the net.Addr object for the profiler listener. +func (s *Server) ProfilerAddr() *net.TCPAddr { + s.mu.RLock() + defer s.mu.RUnlock() + if s.profiler == nil { + return nil + } + return s.profiler.Addr().(*net.TCPAddr) +} + +func (s *Server) readyForConnections(d time.Duration) error { + // Snapshot server options. + opts := s.getOpts() + + type info struct { + ok bool + err error + } + chk := make(map[string]info) + + end := time.Now().Add(d) + for time.Now().Before(end) { + s.mu.RLock() + chk["server"] = info{ok: s.listener != nil || opts.DontListen, err: s.listenerErr} + chk["route"] = info{ok: (opts.Cluster.Port == 0 || s.routeListener != nil), err: s.routeListenerErr} + chk["gateway"] = info{ok: (opts.Gateway.Name == _EMPTY_ || s.gatewayListener != nil), err: s.gatewayListenerErr} + chk["leafnode"] = info{ok: (opts.LeafNode.Port == 0 || s.leafNodeListener != nil), err: s.leafNodeListenerErr} + chk["websocket"] = info{ok: (opts.Websocket.Port == 0 || s.websocket.listener != nil), err: s.websocket.listenerErr} + chk["mqtt"] = info{ok: (opts.MQTT.Port == 0 || s.mqtt.listener != nil), err: s.mqtt.listenerErr} + s.mu.RUnlock() + + var numOK int + for _, inf := range chk { + if inf.ok { + numOK++ + } + } + if numOK == len(chk) { + // In the case of DontListen option (no accept loop), we still want + // to make sure that Start() has done all the work, so we wait on + // that. + if opts.DontListen { + select { + case <-s.startupComplete: + case <-time.After(d): + return fmt.Errorf("failed to be ready for connections after %s: startup did not complete", d) + } + } + return nil + } + if d > 25*time.Millisecond { + time.Sleep(25 * time.Millisecond) + } + } + + failed := make([]string, 0, len(chk)) + for name, inf := range chk { + if inf.ok && inf.err != nil { + failed = append(failed, fmt.Sprintf("%s(ok, but %s)", name, inf.err)) + } + if !inf.ok && inf.err == nil { + failed = append(failed, name) + } + if !inf.ok && inf.err != nil { + failed = append(failed, fmt.Sprintf("%s(%s)", name, inf.err)) + } + } + + return fmt.Errorf( + "failed to be ready for connections after %s: %s", + d, strings.Join(failed, ", "), + ) +} + +// ReadyForConnections returns `true` if the server is ready to accept clients +// and, if routing is enabled, route connections. If after the duration +// `dur` the server is still not ready, returns `false`. +func (s *Server) ReadyForConnections(dur time.Duration) bool { + return s.readyForConnections(dur) == nil +} + +// Quick utility to function to tell if the server supports headers. +func (s *Server) supportsHeaders() bool { + if s == nil { + return false + } + return !(s.getOpts().NoHeaderSupport) +} + +// ID returns the server's ID +func (s *Server) ID() string { + return s.info.ID +} + +// NodeName returns the node name for this server. +func (s *Server) NodeName() string { + return getHash(s.info.Name) +} + +// Name returns the server's name. This will be the same as the ID if it was not set. +func (s *Server) Name() string { + return s.info.Name +} + +func (s *Server) String() string { + return s.info.Name +} + +type pprofLabels map[string]string + +func setGoRoutineLabels(tags ...pprofLabels) { + var labels []string + for _, m := range tags { + for k, v := range m { + labels = append(labels, k, v) + } + } + if len(labels) > 0 { + pprof.SetGoroutineLabels( + pprof.WithLabels(context.Background(), pprof.Labels(labels...)), + ) + } +} + +func (s *Server) startGoRoutine(f func(), tags ...pprofLabels) bool { + var started bool + s.grMu.Lock() + defer s.grMu.Unlock() + if s.grRunning { + s.grWG.Add(1) + go func() { + setGoRoutineLabels(tags...) + f() + }() + started = true + } + return started +} + +// getClientConnectURLs returns suitable URLs for clients to connect to the listen +// port based on the server options' Host and Port. If the Host corresponds to +// "any" interfaces, this call returns the list of resolved IP addresses. +// If ClientAdvertise is set, returns the client advertise host and port. +// The server lock is assumed held on entry. +func (s *Server) getClientConnectURLs() []string { + // Snapshot server options. + opts := s.getOpts() + // Ignore error here since we know that if there is client advertise, the + // parseHostPort is correct because we did it right before calling this + // function in Server.New(). + urls, _ := s.getConnectURLs(opts.ClientAdvertise, opts.Host, opts.Port) + return urls +} + +// Generic version that will return an array of URLs based on the given +// advertise, host and port values. +func (s *Server) getConnectURLs(advertise, host string, port int) ([]string, error) { + urls := make([]string, 0, 1) + + // short circuit if advertise is set + if advertise != "" { + h, p, err := parseHostPort(advertise, port) + if err != nil { + return nil, err + } + urls = append(urls, net.JoinHostPort(h, strconv.Itoa(p))) + } else { + sPort := strconv.Itoa(port) + _, ips, err := s.getNonLocalIPsIfHostIsIPAny(host, true) + for _, ip := range ips { + urls = append(urls, net.JoinHostPort(ip, sPort)) + } + if err != nil || len(urls) == 0 { + // We are here if s.opts.Host is not "0.0.0.0" nor "::", or if for some + // reason we could not add any URL in the loop above. + // We had a case where a Windows VM was hosed and would have err == nil + // and not add any address in the array in the loop above, and we + // ended-up returning 0.0.0.0, which is problematic for Windows clients. + // Check for 0.0.0.0 or :: specifically, and ignore if that's the case. + if host == "0.0.0.0" || host == "::" { + s.Errorf("Address %q can not be resolved properly", host) + } else { + urls = append(urls, net.JoinHostPort(host, sPort)) + } + } + } + return urls, nil +} + +// Returns an array of non local IPs if the provided host is +// 0.0.0.0 or ::. It returns the first resolved if `all` is +// false. +// The boolean indicate if the provided host was 0.0.0.0 (or ::) +// so that if the returned array is empty caller can decide +// what to do next. +func (s *Server) getNonLocalIPsIfHostIsIPAny(host string, all bool) (bool, []string, error) { + ip := net.ParseIP(host) + // If this is not an IP, we are done + if ip == nil { + return false, nil, nil + } + // If this is not 0.0.0.0 or :: we have nothing to do. + if !ip.IsUnspecified() { + return false, nil, nil + } + s.Debugf("Get non local IPs for %q", host) + var ips []string + ifaces, _ := net.Interfaces() + for _, i := range ifaces { + addrs, _ := i.Addrs() + for _, addr := range addrs { + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + ipStr := ip.String() + // Skip non global unicast addresses + if !ip.IsGlobalUnicast() || ip.IsUnspecified() { + ip = nil + continue + } + s.Debugf(" ip=%s", ipStr) + ips = append(ips, ipStr) + if !all { + break + } + } + } + return true, ips, nil +} + +// if the ip is not specified, attempt to resolve it +func resolveHostPorts(addr net.Listener) []string { + hostPorts := make([]string, 0) + hp := addr.Addr().(*net.TCPAddr) + port := strconv.Itoa(hp.Port) + if hp.IP.IsUnspecified() { + var ip net.IP + ifaces, _ := net.Interfaces() + for _, i := range ifaces { + addrs, _ := i.Addrs() + for _, addr := range addrs { + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + hostPorts = append(hostPorts, net.JoinHostPort(ip.String(), port)) + case *net.IPAddr: + ip = v.IP + hostPorts = append(hostPorts, net.JoinHostPort(ip.String(), port)) + default: + continue + } + } + } + } else { + hostPorts = append(hostPorts, net.JoinHostPort(hp.IP.String(), port)) + } + return hostPorts +} + +// format the address of a net.Listener with a protocol +func formatURL(protocol string, addr net.Listener) []string { + hostports := resolveHostPorts(addr) + for i, hp := range hostports { + hostports[i] = fmt.Sprintf("%s://%s", protocol, hp) + } + return hostports +} + +// Ports describes URLs that the server can be contacted in +type Ports struct { + Nats []string `json:"nats,omitempty"` + Monitoring []string `json:"monitoring,omitempty"` + Cluster []string `json:"cluster,omitempty"` + Profile []string `json:"profile,omitempty"` + WebSocket []string `json:"websocket,omitempty"` +} + +// PortsInfo attempts to resolve all the ports. If after maxWait the ports are not +// resolved, it returns nil. Otherwise it returns a Ports struct +// describing ports where the server can be contacted +func (s *Server) PortsInfo(maxWait time.Duration) *Ports { + if s.readyForListeners(maxWait) { + opts := s.getOpts() + + s.mu.RLock() + tls := s.info.TLSRequired + listener := s.listener + httpListener := s.http + clusterListener := s.routeListener + profileListener := s.profiler + wsListener := s.websocket.listener + wss := s.websocket.tls + s.mu.RUnlock() + + ports := Ports{} + + if listener != nil { + natsProto := "nats" + if tls { + natsProto = "tls" + } + ports.Nats = formatURL(natsProto, listener) + } + + if httpListener != nil { + monProto := "http" + if opts.HTTPSPort != 0 { + monProto = "https" + } + ports.Monitoring = formatURL(monProto, httpListener) + } + + if clusterListener != nil { + clusterProto := "nats" + if opts.Cluster.TLSConfig != nil { + clusterProto = "tls" + } + ports.Cluster = formatURL(clusterProto, clusterListener) + } + + if profileListener != nil { + ports.Profile = formatURL("http", profileListener) + } + + if wsListener != nil { + protocol := wsSchemePrefix + if wss { + protocol = wsSchemePrefixTLS + } + ports.WebSocket = formatURL(protocol, wsListener) + } + + return &ports + } + + return nil +} + +// Returns the portsFile. If a non-empty dirHint is provided, the dirHint +// path is used instead of the server option value +func (s *Server) portFile(dirHint string) string { + dirname := s.getOpts().PortsFileDir + if dirHint != "" { + dirname = dirHint + } + if dirname == _EMPTY_ { + return _EMPTY_ + } + return filepath.Join(dirname, fmt.Sprintf("%s_%d.ports", filepath.Base(os.Args[0]), os.Getpid())) +} + +// Delete the ports file. If a non-empty dirHint is provided, the dirHint +// path is used instead of the server option value +func (s *Server) deletePortsFile(hintDir string) { + portsFile := s.portFile(hintDir) + if portsFile != "" { + if err := os.Remove(portsFile); err != nil { + s.Errorf("Error cleaning up ports file %s: %v", portsFile, err) + } + } +} + +// Writes a file with a serialized Ports to the specified ports_file_dir. +// The name of the file is `exename_pid.ports`, typically nats-server_pid.ports. +// if ports file is not set, this function has no effect +func (s *Server) logPorts() { + opts := s.getOpts() + portsFile := s.portFile(opts.PortsFileDir) + if portsFile != _EMPTY_ { + go func() { + info := s.PortsInfo(5 * time.Second) + if info == nil { + s.Errorf("Unable to resolve the ports in the specified time") + return + } + data, err := json.Marshal(info) + if err != nil { + s.Errorf("Error marshaling ports file: %v", err) + return + } + if err := os.WriteFile(portsFile, data, 0666); err != nil { + s.Errorf("Error writing ports file (%s): %v", portsFile, err) + return + } + + }() + } +} + +// waits until a calculated list of listeners is resolved or a timeout +func (s *Server) readyForListeners(dur time.Duration) bool { + end := time.Now().Add(dur) + for time.Now().Before(end) { + s.mu.RLock() + listeners := s.serviceListeners() + s.mu.RUnlock() + if len(listeners) == 0 { + return false + } + + ok := true + for _, l := range listeners { + if l == nil { + ok = false + break + } + } + if ok { + return true + } + select { + case <-s.quitCh: + return false + case <-time.After(25 * time.Millisecond): + // continue - unable to select from quit - we are still running + } + } + return false +} + +// returns a list of listeners that are intended for the process +// if the entry is nil, the interface is yet to be resolved +func (s *Server) serviceListeners() []net.Listener { + listeners := make([]net.Listener, 0) + opts := s.getOpts() + listeners = append(listeners, s.listener) + if opts.Cluster.Port != 0 { + listeners = append(listeners, s.routeListener) + } + if opts.HTTPPort != 0 || opts.HTTPSPort != 0 { + listeners = append(listeners, s.http) + } + if opts.ProfPort != 0 { + listeners = append(listeners, s.profiler) + } + if opts.Websocket.Port != 0 { + listeners = append(listeners, s.websocket.listener) + } + return listeners +} + +// Returns true if in lame duck mode. +func (s *Server) isLameDuckMode() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.ldm +} + +// LameDuckShutdown will perform a lame duck shutdown of NATS, whereby +// the client listener is closed, existing client connections are +// kicked, Raft leaderships are transferred, JetStream is shutdown +// and then finally shutdown the NATS Server itself. +// This function blocks and will not return until the NATS Server +// has completed the entire shutdown operation. +func (s *Server) LameDuckShutdown() { + s.lameDuckMode() +} + +// This function will close the client listener then close the clients +// at some interval to avoid a reconnect storm. +// We will also transfer any raft leaders and shutdown JetStream. +func (s *Server) lameDuckMode() { + s.mu.Lock() + // Check if there is actually anything to do + if s.isShuttingDown() || s.ldm || s.listener == nil { + s.mu.Unlock() + return + } + s.Noticef("Entering lame duck mode, stop accepting new clients") + s.ldm = true + s.sendLDMShutdownEventLocked() + expected := 1 + s.listener.Close() + s.listener = nil + expected += s.closeWebsocketServer() + s.ldmCh = make(chan bool, expected) + opts := s.getOpts() + gp := opts.LameDuckGracePeriod + // For tests, we want the grace period to be in some cases bigger + // than the ldm duration, so to by-pass the validateOptions() check, + // we use negative number and flip it here. + if gp < 0 { + gp *= -1 + } + s.mu.Unlock() + + // If we are running any raftNodes transfer leaders. + if hadTransfers := s.transferRaftLeaders(); hadTransfers { + // They will transfer leadership quickly, but wait here for a second. + select { + case <-time.After(time.Second): + case <-s.quitCh: + return + } + } + + // Now check and shutdown jetstream. + s.shutdownJetStream() + + // Now shutdown the nodes + s.shutdownRaftNodes() + + // Wait for accept loops to be done to make sure that no new + // client can connect + for i := 0; i < expected; i++ { + <-s.ldmCh + } + + s.mu.Lock() + // Need to recheck few things + if s.isShuttingDown() || len(s.clients) == 0 { + s.mu.Unlock() + // If there is no client, we need to call Shutdown() to complete + // the LDMode. If server has been shutdown while lock was released, + // calling Shutdown() should be no-op. + s.Shutdown() + return + } + dur := int64(opts.LameDuckDuration) + dur -= int64(gp) + if dur <= 0 { + dur = int64(time.Second) + } + numClients := int64(len(s.clients)) + batch := 1 + // Sleep interval between each client connection close. + var si int64 + if numClients != 0 { + si = dur / numClients + } + if si < 1 { + // Should not happen (except in test with very small LD duration), but + // if there are too many clients, batch the number of close and + // use a tiny sleep interval that will result in yield likely. + si = 1 + batch = int(numClients / dur) + } else if si > int64(time.Second) { + // Conversely, there is no need to sleep too long between clients + // and spread say 10 clients for the 2min duration. Sleeping no + // more than 1sec. + si = int64(time.Second) + } + + // Now capture all clients + clients := make([]*client, 0, len(s.clients)) + for _, client := range s.clients { + clients = append(clients, client) + } + // Now that we know that no new client can be accepted, + // send INFO to routes and clients to notify this state. + s.sendLDMToRoutes() + s.sendLDMToClients() + s.mu.Unlock() + + t := time.NewTimer(gp) + // Delay start of closing of client connections in case + // we have several servers that we want to signal to enter LD mode + // and not have their client reconnect to each other. + select { + case <-t.C: + s.Noticef("Closing existing clients") + case <-s.quitCh: + t.Stop() + return + } + for i, client := range clients { + client.closeConnection(ServerShutdown) + if i == len(clients)-1 { + break + } + if batch == 1 || i%batch == 0 { + // We pick a random interval which will be at least si/2 + v := rand.Int64N(si) + if v < si/2 { + v = si / 2 + } + t.Reset(time.Duration(v)) + // Sleep for given interval or bail out if kicked by Shutdown(). + select { + case <-t.C: + case <-s.quitCh: + t.Stop() + return + } + } + } + s.Shutdown() + s.WaitForShutdown() +} + +// Send an INFO update to routes with the indication that this server is in LDM mode. +// Server lock is held on entry. +func (s *Server) sendLDMToRoutes() { + s.routeInfo.LameDuckMode = true + infoJSON := generateInfoJSON(&s.routeInfo) + s.forEachRemote(func(r *client) { + r.mu.Lock() + r.enqueueProto(infoJSON) + r.mu.Unlock() + }) + // Clear now so that we notify only once, should we have to send other INFOs. + s.routeInfo.LameDuckMode = false +} + +// Send an INFO update to clients with the indication that this server is in +// LDM mode and with only URLs of other nodes. +// Server lock is held on entry. +func (s *Server) sendLDMToClients() { + s.info.LameDuckMode = true + // Clear this so that if there are further updates, we don't send our URLs. + s.clientConnectURLs = s.clientConnectURLs[:0] + if s.websocket.connectURLs != nil { + s.websocket.connectURLs = s.websocket.connectURLs[:0] + } + // Reset content first. + s.info.ClientConnectURLs = s.info.ClientConnectURLs[:0] + s.info.WSConnectURLs = s.info.WSConnectURLs[:0] + // Only add the other nodes if we are allowed to. + if !s.getOpts().Cluster.NoAdvertise { + for url := range s.clientConnectURLsMap { + s.info.ClientConnectURLs = append(s.info.ClientConnectURLs, url) + } + for url := range s.websocket.connectURLsMap { + s.info.WSConnectURLs = append(s.info.WSConnectURLs, url) + } + } + // Send to all registered clients that support async INFO protocols. + s.sendAsyncInfoToClients(true, true) + // We now clear the info.LameDuckMode flag so that if there are + // cluster updates and we send the INFO, we don't have the boolean + // set which would cause multiple LDM notifications to clients. + s.info.LameDuckMode = false +} + +// If given error is a net.Error and is temporary, sleeps for the given +// delay and double it, but cap it to ACCEPT_MAX_SLEEP. The sleep is +// interrupted if the server is shutdown. +// An error message is displayed depending on the type of error. +// Returns the new (or unchanged) delay, or a negative value if the +// server has been or is being shutdown. +func (s *Server) acceptError(acceptName string, err error, tmpDelay time.Duration) time.Duration { + if !s.isRunning() { + return -1 + } + //lint:ignore SA1019 We want to retry on a bunch of errors here. + if ne, ok := err.(net.Error); ok && ne.Temporary() { // nolint:staticcheck + s.Errorf("Temporary %s Accept Error(%v), sleeping %dms", acceptName, ne, tmpDelay/time.Millisecond) + select { + case <-time.After(tmpDelay): + case <-s.quitCh: + return -1 + } + tmpDelay *= 2 + if tmpDelay > ACCEPT_MAX_SLEEP { + tmpDelay = ACCEPT_MAX_SLEEP + } + } else { + s.Errorf("%s Accept error: %v", acceptName, err) + } + return tmpDelay +} + +var errNoIPAvail = errors.New("no IP available") + +func (s *Server) getRandomIP(resolver netResolver, url string, excludedAddresses map[string]struct{}) (string, error) { + host, port, err := net.SplitHostPort(url) + if err != nil { + return "", err + } + // If already an IP, skip. + if net.ParseIP(host) != nil { + return url, nil + } + ips, err := resolver.LookupHost(context.Background(), host) + if err != nil { + return "", fmt.Errorf("lookup for host %q: %v", host, err) + } + if len(excludedAddresses) > 0 { + for i := 0; i < len(ips); i++ { + ip := ips[i] + addr := net.JoinHostPort(ip, port) + if _, excluded := excludedAddresses[addr]; excluded { + if len(ips) == 1 { + ips = nil + break + } + ips[i] = ips[len(ips)-1] + ips = ips[:len(ips)-1] + i-- + } + } + if len(ips) == 0 { + return "", errNoIPAvail + } + } + var address string + if len(ips) == 0 { + s.Warnf("Unable to get IP for %s, will try with %s: %v", host, url, err) + address = url + } else { + var ip string + if len(ips) == 1 { + ip = ips[0] + } else { + ip = ips[rand.Int32N(int32(len(ips)))] + } + // add the port + address = net.JoinHostPort(ip, port) + } + return address, nil +} + +// Returns true for the first attempt and depending on the nature +// of the attempt (first connect or a reconnect), when the number +// of attempts is equal to the configured report attempts. +func (s *Server) shouldReportConnectErr(firstConnect bool, attempts int) bool { + opts := s.getOpts() + if firstConnect { + if attempts == 1 || attempts%opts.ConnectErrorReports == 0 { + return true + } + return false + } + if attempts == 1 || attempts%opts.ReconnectErrorReports == 0 { + return true + } + return false +} + +func (s *Server) updateRemoteSubscription(acc *Account, sub *subscription, delta int32) { + s.updateRouteSubscriptionMap(acc, sub, delta) + if s.gateway.enabled { + s.gatewayUpdateSubInterest(acc.Name, sub, delta) + } + + acc.updateLeafNodes(sub, delta) +} + +func (s *Server) startRateLimitLogExpiration() { + interval := time.Second + s.startGoRoutine(func() { + defer s.grWG.Done() + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-s.quitCh: + return + case interval = <-s.rateLimitLoggingCh: + ticker.Reset(interval) + case <-ticker.C: + s.rateLimitLogging.Range(func(k, v any) bool { + start := v.(time.Time) + if time.Since(start) >= interval { + s.rateLimitLogging.Delete(k) + } + return true + }) + } + } + }) +} + +func (s *Server) changeRateLimitLogInterval(d time.Duration) { + if d <= 0 { + return + } + select { + case s.rateLimitLoggingCh <- d: + default: + } +} + +// DisconnectClientByID disconnects a client by connection ID +func (s *Server) DisconnectClientByID(id uint64) error { + if s == nil { + return ErrServerNotRunning + } + if client := s.getClient(id); client != nil { + client.closeConnection(Kicked) + return nil + } else if client = s.GetLeafNode(id); client != nil { + client.closeConnection(Kicked) + return nil + } + return errors.New("no such client or leafnode id") +} + +// LDMClientByID sends a Lame Duck Mode info message to a client by connection ID +func (s *Server) LDMClientByID(id uint64) error { + if s == nil { + return ErrServerNotRunning + } + s.mu.RLock() + c := s.clients[id] + if c == nil { + s.mu.RUnlock() + return errors.New("no such client id") + } + info := s.copyInfo() + info.LameDuckMode = true + s.mu.RUnlock() + c.mu.Lock() + defer c.mu.Unlock() + if c.opts.Protocol >= ClientProtoInfo && c.flags.isSet(firstPongSent) { + // sendInfo takes care of checking if the connection is still + // valid or not, so don't duplicate tests here. + c.Debugf("Sending Lame Duck Mode info to client") + c.enqueueProto(c.generateClientInfoJSON(info, true)) + return nil + } else { + return errors.New("client does not support Lame Duck Mode or is not ready to receive the notification") + } +} + +func (s *Server) diskIOSemaphore() *diskIOSemaphore { + if s == nil || s.dios == nil { + return defaultDiskIOSemaphore() + } + return s.dios +} diff --git a/server/stream.go b/server/stream.go new file mode 100644 index 0000000..50d02f3 --- /dev/null +++ b/server/stream.go @@ -0,0 +1,9775 @@ +// 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/v2" + "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 uint64 `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"` + Desired *DesiredClusterInfo `json:"desired,omitempty"` +} + +// DesiredClusterInfo shows information of the desired set of servers +// that should make up the stream or consumer. +type DesiredClusterInfo struct { + Created time.Time `json:"created"` + Name string `json:"name,omitempty"` + Replicas []*DesiredPeerInfo `json:"replicas,omitempty"` + Origin *DesiredClusterInfoOrigin `json:"origin,omitempty"` + Status *DesiredClusterInfoStatus `json:"status,omitempty"` +} + +type DesiredClusterInfoOrigin struct { + // Original replicas before it was updated. + Replicas int `json:"replicas"` + // Original placement before it was updated. + Placement *Placement `json:"placement,omitempty"` + // Original retention before it was updated. + Retention *RetentionPolicy `json:"retention,omitempty"` +} + +// DesiredPeerInfo is a minimal version of PeerInfo that shows information about the desired peer set. +type DesiredPeerInfo struct { + Name string `json:"name"` // Name is the unique name for the peer + Offline bool `json:"offline,omitempty"` // Offline indicates if it has not been seen recently + Peer string `json:"peer"` // Peer is the unique ID for the peer +} + +// MigrationStatusType classifies a migration status by what has to change for the +// migration to advance, so it can be matched on without parsing the status line. +type MigrationStatusType string + +const ( + MigrationStatusMeta MigrationStatusType = "meta" // The meta leader must record or advance desired state. + MigrationStatusMembership MigrationStatusType = "membership" // A proposed membership change must commit. + MigrationStatusSnapshot MigrationStatusType = "snapshot" // A snapshot must be installed. + MigrationStatusCatchup MigrationStatusType = "catchup" // Peers must become store-current. + MigrationStatusQuorum MigrationStatusType = "quorum" // More peers must come online before we can act without losing quorum. + MigrationStatusBlocked MigrationStatusType = "blocked" // Another asset must move first, i.e. the stream/consumer ordering constraint. + MigrationStatusUnavailable MigrationStatusType = "unavailable" // Nothing to do here, we're shutting down, or the assignment is gone. +) + +type DesiredClusterInfoStatus struct { + // Description is a short status line describing what the group leader is currently + // doing to move this group toward its desired state, or what it's waiting on. + Description string `json:"description"` + // Type classifies Description by what has to change for the migration to + // advance, so it can be matched on without parsing the status line. + Type MigrationStatusType `json:"type"` + // Err is the underlying failure behind this status, if it had one. Only set + // for faults that persist across cycles, never for races that resolve themselves. + Err string `json:"err,omitempty"` +} + +// mstat builds a migration status of the given type. The line is a short, stable +// phrase; keep it matchable by leaving anything unbounded to Err. +func mstat(t MigrationStatusType, format string, args ...any) *DesiredClusterInfoStatus { + if len(args) == 0 { + return &DesiredClusterInfoStatus{Type: t, Description: format} + } + return &DesiredClusterInfoStatus{Type: t, Description: fmt.Sprintf(format, args...)} +} + +// withErr attaches the underlying failure, unless it's one that resolves itself on +// the next cycle. Those would flap in and out of the status for no good reason. +func (status *DesiredClusterInfoStatus) withErr(err error) *DesiredClusterInfoStatus { + if err == nil || + errors.Is(err, ErrStoreClosed) || + errors.Is(err, errNotLeader) || + errors.Is(err, errNodeClosed) || + errors.Is(err, errMembershipChange) || + errors.Is(err, errNoSnapAvailable) { + return status + } + status.Err = err.Error() + return status +} + +// 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 nanoseconds since this peer was last seen + 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 + Pending bool `json:"pending,omitempty"` // Pending indicates the peer is part of the assignment, but is not a peer of the Raft group yet or is being removed. + // 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"` + Seq uint64 `json:"seq,omitempty"` + 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. + + // Isolates direct get and msg get reads from writes (ingest, atomic batch + // commits, purges and deletes), so those reads never observe a partially + // applied atomic batch or rollup. Other read paths (consumer delivery, + // mirrors/sources, mset.mu-based info) are sequentially consistent and may + // observe a prefix of an inflight batch. + isolateMu sync.RWMutex + + // 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 + + // Whether the stream layer has completed leader setup via setLeader. Unlike isLeader(), + // which reads the raft node's current state, this tracks our own processed leadership. + leader atomic.Bool + + 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. + term uint64 // Raft term, used to determine if we are still the leader for the current term (if applicable, 0 otherwise). + lmsgId string // The de-duplication message ID of the last message stored in the stream. + consumers map[string]*consumer // The consumers for this stream. + 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. + ddwin time.Duration // The dedupe window, mirrors cfg.Duplicates. + 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. + restoring bool + restoreLeader bool + restoreTerm uint64 + + // 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]*catchupPeer // Peers being caught up out of band, see catchupPeer. + 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. + monitorMu sync.Mutex // Serializes monitorWg's Add against Wait to prevent a WaitGroup reuse panic. + + // 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. +} + +// 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. + schedule bool // Marks whether the last message is a schedule. +} + +// 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. +} + +// StreamSourceState is the tracked state for a single stream source, as +// persisted by the store so it survives a restart or a leader change. +type StreamSourceState struct { + // Seq is the last stream message sequence number seen from the source. + Seq uint64 + // Ident is the identity of the source stream, used to detect that it was + // recreated. Empty if the source did not report one, i.e. it is an older + // server or a pre-2.10 message header. + Ident string +} + +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. + ident string // The identity of this source, a recreated stream results in a different ident. + 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. + rc bool // Recreate based on a stream identity mismatch. + 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) +} + +func (a *Account) addStreamForRestore(config *StreamConfig) (*stream, error) { + return a.addStreamWithAssignmentAndMode(config, nil, nil, false, false, true) +} + +// 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) { + return a.addStreamWithAssignmentAndMode(config, fsConfig, sa, pedantic, recovering, false) +} + +func (a *Account) addStreamWithAssignmentAndMode(config *StreamConfig, fsConfig *FileStoreConfig, sa *streamAssignment, pedantic, recovering, restoring 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() + // A restore must create its own stream. Another restore or create may + // have claimed the name since the restore's initial existence check. + if restoring { + return nil, NewJSStreamNameExistRestoreFailedError() + } + // 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, tier, 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, + ddwin: cfg.Duplicates, + 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(), + restoring: restoring, + } + + // 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 the stream is backed by a Raft log, we can relax + // SyncAlways so that we flush and sync whenever a + // stream snapshot is created. We can safely recover + // from the snapshot and the tail of the log. + if fsCfg.SyncAlways && config.Replicas > 1 { + fsCfg.SyncOnFlush = true + } + + if err := mset.setupStore(fsCfg, recovering || restoring); 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, 0); 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() + if recovering && !restoring { + mset.store.Ready() + } + + 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 = copyStrings(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 + + // Stop catching up peers if they're no longer part of the group. + for peer := range mset.catchups { + if !slices.Contains(peers, peer) { + mset.clearCatchupPeerLocked(peer) + } + } + + // 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. + mset.signalUpdate() +} + +// signalUpdate wakes the monitor routine so it re-evaluates the stream assignment. +func (mset *stream) signalUpdate() { + select { + case mset.uch <- struct{}{}: + default: + } +} + +// identity returns a hash of the stream's creation time, used to detect stream recreation. +func (mset *stream) identity() string { + return getHash(mset.createdTime().UTC().Format(time.RFC3339Nano)) +} + +// identity returns a hash of the stream's creation time, used to detect stream recreation. +// JS lock should be held. +func (sa *streamAssignment) identity() string { + return getHash(sa.Created.UTC().Format(time.RFC3339Nano)) +} + +// Returned by monitorQuitC once the monitor was signaled to quit, so a monitor +// that only gets there afterwards still exits immediately. +var closedMonitorQuitC = func() chan struct{} { + ch := make(chan struct{}) + close(ch) + return ch +}() + +func (mset *stream) monitorQuitC() <-chan struct{} { + if mset == nil { + return nil + } + mset.mu.Lock() + defer mset.mu.Unlock() + // Don't recreate, a concurrent stopMonitoring would wait forever for a monitor + // that can't be signaled anymore. startMonitorWg creates it per generation. + if mset.mqch == nil { + return closedMonitorQuitC + } + 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, term uint64) error { + mset.mu.Lock() + if mset.restoring { + mset.restoreLeader, mset.restoreTerm = isLeader, term + mset.mu.Unlock() + return nil + } + wasLeader := mset.leader.Swap(isLeader) + + // We can skip the teardown if we were leader before and are still the leader now. + // But only at term 1, since that means scale up from or down to an unreplicated config. + // R1 assets have no raft node and use the coerced term 1. + if term < 1 { + term = 1 + } + skipTeardown := wasLeader && isLeader && term == 1 + mset.term = term + if !skipTeardown { + // 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) + mset.store.ResetState() + } + + // 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 !skipTeardown && mset.batches != nil { + mset.batches.mu.Lock() + for batchId, b := range mset.batches.fast { + mset.batches.fastBatchReset(mset, batchId, b) + } + mset.batches.mu.Unlock() + } + } + 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 +} + +func (mset *stream) completeRestore() error { + mset.mu.Lock() + if !mset.restoring { + mset.mu.Unlock() + return nil + } + mset.restoring = false + isLeader, term := mset.restoreLeader, mset.restoreTerm + mset.restoreLeader, mset.restoreTerm = false, 0 + mset.mu.Unlock() + if isLeader { + if err := mset.setLeader(true, term); err != nil { + return err + } + } + mset.store.Ready() + 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() { + var smv StoreMsg + var state StreamState + mset.store.FastState(&state) + + if state.LastSeq > 0 { + if sm, err := mset.store.LoadMsg(state.LastSeq, &smv); err == nil { + mset.lmsgId = getMsgId(sm.hdr) + } + } + + if mset.ddwin <= 0 { + return + } + + // We have some messages. Lookup starting sequence by duplicate time window. + sseq := mset.store.GetSeqFromTime(time.Now().Add(-mset.ddwin)) + if sseq == 0 { + return + } + + 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}) + } + } + } +} + +// Returns true if the underlying store should use the +// Raft log for replaying the tail of stream during +// recovery. +func (mset *stream) shouldReplayFromWAL() bool { + if mset == nil || mset.raftNode() == nil || mset.store.Type() != FileStorage { + return false + } + fs, ok := mset.store.(*fileStore) + if !ok { + return false + } + return fs.syncOnFlush.Load() +} + +// prepareForWALReplay truncates any stream filestore tail past the last +// sequence of the given snapshot, in preparation of replaying the Raft log. +func (mset *stream) prepareForWALReplay(snap *StreamReplicatedState) error { + if mset == nil { + return nil + } + + mset.mu.Lock() + defer mset.mu.Unlock() + + var snapSeq, clfs uint64 + if snap != nil { + snapSeq = snap.LastSeq + clfs = snap.Failed + } else if mset.cfg.FirstSeq > 0 { + snapSeq = mset.cfg.FirstSeq - 1 + } + + var state StreamState + mset.store.FastState(&state) + if state.LastSeq > snapSeq { + mset.srv.Debugf("Truncate to snapshot sequence %d", snapSeq) + if err := mset.store.Truncate(snapSeq); err != nil { + return err + } + mset.store.FastState(&state) + } + + mset.lseq = state.LastSeq + mset.setCLFS(clfs) + + mset.ddMu.Lock() + if mset.ddtmr != nil { + mset.ddtmr.Stop() + mset.ddtmr = nil + } + mset.ddmap = nil + mset.ddarr = nil + mset.ddindex = 0 + mset.lmsgId = _EMPTY_ + mset.rebuildDedupe() + mset.ddMu.Unlock() + return nil +} + +// 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 + store := mset.store + if fs, ok := store.(*fileStore); ok { + fs.setCreatedTime(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 + } + // Read the other stream's subjects under its cfgMu. + mset.cfgMu.RLock() + msubjects := mset.cfg.Subjects + mset.cfgMu.RUnlock() + for _, subj := range msubjects { + for _, tsubj := range subjects { + if SubjectsCollide(tsubj, subj) { + return true + } + } + } + } + return false +} + +// StreamDefaultDuplicatesWindow default duplicates window. +const StreamDefaultDuplicatesWindow = 2 * time.Minute + +// Do not hold the jetStream lock, it will be read-locked internally. +func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic bool) (StreamConfig, *ApiError) { + if js := s.getJetStream(); js != nil { + js.mu.RLock() + defer js.mu.RUnlock() + } + return s.checkStreamCfgLocked(config, acc, pedantic) +} + +// jetStream lock (read or write) should be held, if JetStream is enabled. +func (s *Server) checkStreamCfgLocked(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.AllowMsgSchedules { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message schedules")) + } + 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.Discard == DiscardNew { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("message scheduling cannot use discard new")) + } + 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 { + if sa := js.streamAssignmentOrInflight(acc.Name, streamName); sa != nil { + cfg = *sa.Config.clone() + exists = true + } + } + } 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. + if sa := js.streamAssignmentOrInflight(acc.Name, cfg.Mirror.Name); sa != nil { + if pedantic && cfg.MirrorDirect != sa.Config.AllowDirect { + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("origin stream has direct get set, mirror has it disabled")) + } + cfg.MirrorDirect = sa.Config.AllowDirect + } + } + } 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, the latter will be read-locked internally. +func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig, s *Server, pedantic bool) (*StreamConfig, error) { + js, _ := jsa.jetStreamAndClustered() + js.mu.RLock() + defer js.mu.RUnlock() + return jsa.configUpdateCheckLocked(old, new, s, pedantic) +} + +// Do not hold jsAccount lock. The jetStream lock (read or write) should be held. +func (jsa *jsAccount) configUpdateCheckLocked(old, new *StreamConfig, s *Server, pedantic bool) (*StreamConfig, error) { + cfg, apiErr := s.checkStreamCfgLocked(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")) + } + singleServerMode := !s.JetStreamIsClustered() && s.standAloneMode() + if singleServerMode && cfg.Replicas > 1 { + return nil, ApiErrors[JSStreamReplicasNotSupportedErr] + } + // 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 + + // 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 additional 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() + } + if isClustered { + _, reserved = js.tieredStreamAndReservationCount(acc.Name, tier, &cfg) + } + // reserved covers only the other streams. checkAllLimits adds this stream's + // footprint via cfg.MaxBytes, which is currently maxBytesDiff, so it only + // adds the diff. Add the remaining (newMaxBytes - maxBytesDiff) here so the + // two together equal this stream's true new footprint, even when Replicas + // changes on update. + reserved = addSaturate(reserved, accountReservation(tier, cfg.Replicas, newMaxBytes-maxBytesDiff)) + if err := js.checkAllLimits(&selected, tier, &cfg, reserved, 0); 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. + oldInactiveThreshold, newInactiveThreshold := ocfg.ConsumerLimits.InactiveThreshold, cfg.ConsumerLimits.InactiveThreshold + oldMaxAckPending, newMaxAckPending := ocfg.ConsumerLimits.MaxAckPending, cfg.ConsumerLimits.MaxAckPending + updateLimits := (newInactiveThreshold > 0 && oldInactiveThreshold != newInactiveThreshold) || + (newMaxAckPending > 0 && oldMaxAckPending != newMaxAckPending) + + // Only check if not clustered. The meta leader performs clustered checks. + if updateLimits && !mset.js.isClustered() { + var errorConsumers []string + mset.mu.RLock() + clist := make([]*consumer, 0, len(mset.consumers)) + for _, c := range mset.consumers { + clist = append(clist, c) + } + mset.mu.RUnlock() + for _, c := range clist { + c.mu.RLock() + name, ccfg := c.name, c.cfg + if (newInactiveThreshold > 0 && ccfg.InactiveThreshold > newInactiveThreshold) || + (newMaxAckPending > 0 && ccfg.MaxAckPending > newMaxAckPending) { + errorConsumers = append(errorConsumers, name) + } + c.mu.RUnlock() + } + 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() + + var needsStartingSeqNum map[string]struct{} + 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{}{} + } + + // 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) + + 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.cfgMu.Lock() + mset.cfg.Sources = append(mset.cfg.Sources, s) + mset.cfgMu.Unlock() + + 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 + if needsStartingSeqNum == nil { + needsStartingSeqNum = make(map[string]struct{}) + } + 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) + } + } + } + + // 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 atomic publish is disabled, delete any in-progress batches. + if !cfg.AllowAtomicPublish { + mset.deleteAtomicBatches(false) + } + // 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() + + // All replicas track msg ids, so update the dedupe window everywhere. + mset.ddMu.Lock() + mset.ddwin = cfg.Duplicates + mset.ddMu.Unlock() + + // If we're changing retention, 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) + // Switching to Interest without any consumers drops all messages. + if cfg.Retention == InterestPolicy && len(toUpdate) == 0 && ss.Msgs > 0 { + mset.store.Compact(ss.LastSeq + 1) + } + 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() + } + active := mset.active + 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, + }) + } + } + + if err = mset.store.UpdateConfig(cfg); err != nil { + return err + } + + // Newly added sources can now setup their source consumers, since the store scanned + // if the source was pre-existing as part of the config update above. + if active && len(needsStartingSeqNum) > 0 { + mset.mu.Lock() + if mset.active { + mset.setStartingSequenceForSources(needsStartingSeqNum) + for iName := range needsStartingSeqNum { + // A concurrent update could have removed or replaced the source + // in the meantime, only set up the ones still present. + if si, ok := mset.sources[iName]; ok { + mset.setupSourceConsumer(iName, si.sseq+1, time.Time{}) + } + } + } + mset.mu.Unlock() + } + + 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 := s.infoReplySubject(source.External.Domain(), acc.Name, sourceName, consumerName) + 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) { + // Purges mutate message state, so hold the isolation lock like any other + // write so direct get requests don't observe a partially applied purge. + mset.isolateMu.Lock() + defer mset.isolateMu.Unlock() + 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 + } else if doPurge && preq != nil && preq.Subject != _EMPTY_ && !o.isFilterSubsetOf(preq.Subject) { + isWider = 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 + } + // Deletes mutate message state, so hold the isolation lock like any other write. + mset.isolateMu.Lock() + defer mset.isolateMu.Unlock() + 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 + } + // Deletes mutate message state, so hold the isolation lock like any other write. + mset.isolateMu.Lock() + defer mset.isolateMu.Unlock() + 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() + // Use the persisted source state for the sequences, si.sseq can be ahead of what's stored. + var sourcesState map[string]StreamSourceState + if mset.store != nil { + sourcesState = mset.store.SourcesState() + } + sis = make([]*StreamSourceInfo, 0, len(mset.sources)) + for _, si := range mset.sources { + ssi := mset.sourceInfo(si) + if ssi == nil { + continue + } + // Only report the sequence if the persisted state belongs to the same identity (if known). + if sss, ok := sourcesState[si.iname]; ok && !si.rc && (sss.Ident == _EMPTY_ || si.ident == sss.Ident) { + ssi.Seq = sss.Seq + } + sis = append(sis, ssi) + } + 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: if not seen past the health check interval, it's stale. + stale := func(si *sourceInfo) bool { + return time.Since(time.Unix(0, si.last.Load())) > sourceHealthCheckInterval + } + shouldRetry := func(si *sourceInfo) bool { + if si != nil && !si.sip && (si.sub == nil || clientClosed(si.sub.client) || stale(si)) { + // Skip if a recreate is already scheduled and we can't cancel it. + if t, ok := mset.sourceSetupSchedules[si.iname]; ok { + if !t.Stop() { + return false + } + delete(mset.sourceSetupSchedules, si.iname) + } + si.fails = 0 + 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 { + if err := mset.skipMsgs(mset.mirror.sseq+1, sseq-1); err != nil { + mset.mirror.sseq = osseq + mset.mirror.dseq = odseq + mset.mu.Unlock() + mset.retryMirrorConsumer() + return false + } + 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.cfgMu.Lock() + mset.cfg.MirrorDirect = false + mset.cfgMu.Unlock() + } + } + + // 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, term := mset.node, mset.term + mset.mu.Unlock() + + var err error + if node != nil { + if stype == FileStorage && isFileStoreMsgTooLarge(fileStoreMsgSize(m.subj, m.hdr, m.msg)) { + err = ErrMsgTooLarge + } else if js.limitsExceeded(stype) { + s.resourcesExceededError(stype) + err = ApiErrors[JSInsufficientResourcesErr] + } else { + err = node.Propose(term, 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.ShutdownJetStream() + 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. + lseq := mset.lastSeq() + mset.mu.Lock() + if mset.mirror != nil { + mset.mirror.lag = olag + mset.mirror.dseq = odseq + mset.mirror.sseq = osseq + if sseq <= lseq { + mset.mirror.sseq = lseq + } + } + 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) error { + node, store := mset.node, mset.store + // If we are not clustered we can short circuit now with store.SkipMsgs + if node == nil { + if err := store.SkipMsgs(start, end-start+1); err != nil { + return err + } + mset.lseq = end + return nil + } + + // 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) { + return node.Propose(mset.term, encodeDeleteRange(&DeleteRange{First: start, Num: end - start + 1})) + } + + 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 { + if err := node.ProposeMulti(mset.term, entries); err != nil { + return err + } + // 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 { + return node.ProposeMulti(mset.term, entries) + } + return nil +} + +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 + +// How long we wait for a response from a consumer reset request for a durable source or +// mirror. Shorter than a create, since the consumer is only repositioned, not stood up. +var srcDurableConsumerWaitTime = 5 * 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 + } + } + + newReplySubscription := func() (string, chan *JSApiConsumerCreateResponse, *subscription, error) { + respCh := make(chan *JSApiConsumerCreateResponse, 1) + consumer := req.Config.Name + if durableDeliverSubject != _EMPTY_ { + consumer = mirror.cname + } + reply := mset.srv.infoReplySubject(ext.Domain(), mset.acc.Name, mset.cfg.Mirror.Name, consumer) + 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: + } + }) + return reply, respCh, crSub, err + } + reply, respCh, crSub, err := newReplySubscription() + 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() { + // A durable consumer only needs to be reset, so don't wait as long as we would + // for a consumer that still has to be created from scratch. + timeout := srcConsumerWaitTime + if durableDeliverSubject != _EMPTY_ { + timeout = srcDurableConsumerWaitTime + } + + var retry bool + var timedOut 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() + if timedOut && durableDeliverSubject != _EMPTY_ { + mset.startProbeSubscription(mset.mirror, durableDeliverSubject, func() { + // Skip if one is already running. + if t := mset.mirrorConsumerSetup; t != nil && !t.Stop() { + return + } + mset.mirrorConsumerSetup = nil + 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 || ccr.ConsumerInfo.Config == 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() + // Recreate the reply subscription so we don't get stale responses from other servers. + mset.unsubscribe(crSub) + if reply, respCh, crSub, err = newReplySubscription(); err != nil { + mirror.err = NewJSMirrorConsumerSetupFailedError(err, Unless(err)) + retry = true + mset.mu.Unlock() + return + } + mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, nil, b, nil, 0)) + mset.mu.Unlock() + goto SELECT + } + mset.unsubscribe(crSub) + cerr := ccr.Error + if cerr == nil { + cerr = NewJSMirrorConsumerSetupFailedError(errors.New("invalid consumer create response")) + } + mset.srv.Warnf("JetStream error response for create mirror consumer: %+v", cerr) + mirror.err = cerr + // 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 { + // Local helper: abort consumer setup, leaving the mirror in its + // pre-setup state so the retry path can re-create it cleanly. + failSetup := func(setupErr error) { + mset.cancelSourceInfo(mirror) + mirror.err = NewJSMirrorConsumerSetupFailedError(setupErr, Unless(setupErr)) + retry = true + mset.mu.Unlock() + } + // 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 { + if _, err := mset.store.PurgeEx(_EMPTY_, ccr.ConsumerInfo.Delivered.Stream+1, 0); err != nil { + failSetup(err) + return + } + mset.lseq = ccr.ConsumerInfo.Delivered.Stream + } else if err := mset.skipMsgs(state.LastSeq+1, ccr.ConsumerInfo.Delivered.Stream); err != nil { + failSetup(err) + return + } + } + + // 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(timeout): + mset.unsubscribe(crSub) + // We already waited long enough, let's retry now. + retry, timedOut = true, 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) + } +} + +// startProbeSubscription watches a durable consumer's deliver subject after a request for +// it timed out, and stays up for as long as we are backing off. When we receive a heartbeat +// or message, we can short-circuit on retrying the reset request. +// Lock should be held. +func (mset *stream) startProbeSubscription(si *sourceInfo, deliverSubject string, retry func()) { + if si == nil || si.sub != nil || deliverSubject == _EMPTY_ { + return + } + sub, err := mset.subscribeInternal(deliverSubject, func(_ *subscription, _ *client, _ *Account, _, _ string, _ []byte) { + mset.mu.Lock() + defer mset.mu.Unlock() + // Only short-circuit a backoff we are still in. si.fails is back to zero if we + // already did so, and si.sip means a request is in flight that tears us down anyway. + if si.sub == nil || si.sip || si.fails == 0 { + return + } + // si.fails only drives the retry backoff, which does not apply to a consumer we + // can see is alive, so clear it and ask again now. + si.fails = 0 + retry() + }) + if err != nil { + return + } + si.sub = sub +} + +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() && !si.rc { + 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 + + type consumerCreateResp struct { + ccr *JSApiConsumerCreateResponse + ident string + } + newReplySubscription := func() (string, chan *consumerCreateResp, *subscription, error) { + respCh := make(chan *consumerCreateResp, 1) + consumer := req.Config.Name + if durableDeliverSubject != _EMPTY_ { + consumer = si.cname + } + reply := mset.srv.infoReplySubject(ext.Domain(), mset.acc.Name, si.name, consumer) + crSub, err := mset.subscribeInternal(reply, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + hdr, 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 + } + var ident string + if si := sliceHeader(JSStreamIdentity, hdr); si != nil { + ident = string(si) + } + select { + case respCh <- &consumerCreateResp{ccr: &ccr, ident: ident}: + default: + } + }) + return reply, respCh, crSub, err + } + reply, respCh, crSub, err := newReplySubscription() + 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 + + // Three API levels can be tried: + // - API level 5: stream recreation detection. + // - API level 4: durable sourcing, AckFlowControl, and consumer reset. + // - not specified: used as fallback. + apiLevel := 5 + hdr := genHeader(nil, JSRequiredApiLevel, strconv.Itoa(apiLevel)) + hdr = genHeader(hdr, JSStreamIdentity, si.ident) + if durableDeliverSubject != _EMPTY_ { + // Send the consumer reset request + mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, hdr, nil, nil, 0)) + } else { + // Marshal request. + b, _ := json.Marshal(req) + + // Send the consumer create request + mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, hdr, b, nil, 0)) + } + + go func() { + // A durable consumer only needs to be reset, so don't wait as long as we would + // for a consumer that still has to be created from scratch. + timeout := srcConsumerWaitTime + if durableDeliverSubject != _EMPTY_ { + timeout = srcDurableConsumerWaitTime + } + + var retry bool + var recreate bool + var timedOut 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 { + if !recreate { + si.fails++ + } + // Cancel here since we can not do anything with this consumer at this point. + mset.cancelSourceInfo(si) + mset.setupSourceConsumer(iname, seq, startTime) + if timedOut && durableDeliverSubject != _EMPTY_ { + mset.startProbeSubscription(si, durableDeliverSubject, func() { + // Drop the scheduled retry, we are going again right now. + if t, ok := mset.sourceSetupSchedules[iname]; ok { + // Skip if one is already running. + if !t.Stop() { + return + } + delete(mset.sourceSetupSchedules, iname) + } + mset.setupSourceConsumer(iname, seq, startTime) + }) + } + } else { + // Clear on success. + si.fails = 0 + } + } + mset.mu.Unlock() + }() + + SELECT: + select { + case resp := <-respCh: + ccr, streamIdentity := resp.ccr, resp.ident + 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 || ccr.ConsumerInfo.Config == nil { + // Retry after unsetting the expected stream's identity. + if ccr.Error != nil && ccr.Error.ErrCode == uint16(JSConsumerStreamIdentityMismatchF) { + mset.unsubscribe(crSub) + seq, si.sseq, si.dseq = 0, 0, 0 + si.ident, si.rc = _EMPTY_, true + retry, recreate = true, true + mset.mu.Unlock() + return + } + + // If the responding server doesn't support certain request settings, retry a 'downgraded' request. + if req.Config.Sourcing && ccr.Error != nil && + (ccr.Error.ErrCode == uint16(JSRequiredApiLevelErr) || ccr.Error.ErrCode == uint16(JSInvalidJSONErr)) { + if apiLevel > 4 { + apiLevel-- + // Recreate the reply subscription so we don't get stale responses from other servers. + mset.unsubscribe(crSub) + if reply, respCh, crSub, err = newReplySubscription(); err != nil { + si.err = NewJSSourceConsumerSetupFailedError(err, Unless(err)) + retry = true + mset.mu.Unlock() + return + } + // Retry without stream identity. + hdr := genHeader(nil, JSRequiredApiLevel, strconv.Itoa(apiLevel)) + var b []byte + if durableDeliverSubject == _EMPTY_ { + b, _ = json.Marshal(req) + } + mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, hdr, b, nil, 0)) + mset.mu.Unlock() + goto SELECT + } + + // If the responding server doesn't support sourcing consumers, retry without it. + // 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() + // Recreate the reply subscription so we don't get stale responses from other servers. + mset.unsubscribe(crSub) + if reply, respCh, crSub, err = newReplySubscription(); err != nil { + si.err = NewJSSourceConsumerSetupFailedError(err, Unless(err)) + retry = true + mset.mu.Unlock() + return + } + 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. + cerr := ccr.Error + if cerr == nil { + cerr = NewJSSourceConsumerSetupFailedError(errors.New("invalid consumer create response")) + } + mset.srv.Warnf("JetStream error response for stream %s create source consumer %s: %+v", mset.cfg.Name, si.name, cerr) + si.err = cerr + // 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, + }, + ) + } + + // Capture identity to detect stream recreation. + recreated := si.rc || (streamIdentity != _EMPTY_ && si.ident != _EMPTY_ && si.ident != streamIdentity) + if recreated { + mset.srv.Warnf("JetStream source stream %q for stream '%s > %s' was recreated, resetting source state", + si.name, mset.acc.Name, mset.cfg.Name) + si.sseq = 0 + } + si.ident, si.rc = streamIdentity, false + + // 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(timeout): + mset.unsubscribe(crSub) + // We already waited long enough, let's retry now. + retry, timedOut = true, 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(mset.term, 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, ident := mset.node, si.ident + 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, ident)) + + // 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.ShutdownJetStream() + } 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). +// The source's ident must be passed in, as it can be mutated under the stream lock, which is not held here. +// Format: [] +func (si *sourceInfo) genSourceHeader(orig, reply, ident 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) + if ident != _EMPTY_ { + b.WriteByte(' ') + b.WriteString(ident) + } + 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 +// Format: [] +func streamAndSeq(shdr string) (string, string, uint64, string) { + if strings.HasPrefix(shdr, jsAckPre) { + streamName, iname, seq := streamAndSeqFromAckReply(shdr) + return streamName, iname, seq, _EMPTY_ + } + // New version which is stream index name sequence + fields := strings.Split(shdr, " ") + nFields := len(fields) + + if nFields != 2 && nFields <= 3 { + return _EMPTY_, _EMPTY_, 0, _EMPTY_ + } + + streamName := fields[0] + if nFields >= 4 { + iname := strings.Join([]string{fields[0], fields[2], fields[3]}, " ") + seq := uint64(parseAckReplyNum(fields[1])) + var identity string + if nFields >= 6 { + // Copy, since it's stored on the sourceInfo. + identity = copyString(fields[5]) + } + return streamName, iname, seq, identity + } else { + seq := uint64(parseAckReplyNum(fields[1])) + return streamName, _EMPTY_, seq, _EMPTY_ + } + +} + +// Lock should be held. +func (mset *stream) setStartingSequenceForSources(iNames map[string]struct{}) { + // The store contains an index of the source state, use it here without doing lookups ourselves. + sourcesState := mset.store.SourcesState() + for iName := range iNames { + if si, ok := mset.sources[iName]; ok { + if sss, ok := sourcesState[iName]; ok { + si.sseq = sss.Seq + si.dseq = 0 + si.ident, si.rc = sss.Ident, false + } else { + // If it doesn't exist, only reset the delivery sequence. + si.dseq = 0 + } + } + } +} + +// Resets the SourceInfo for all the sources, seeding each with the +// persisted starting sequence and identity. +// lock should be held. +func (mset *stream) resetSourceInfo() { + // Reset if needed. + mset.stopSourceConsumers() + mset.sources = make(map[string]*sourceInfo) + + // The store contains an index of the source state, use it here without doing lookups ourselves. + // Seed the source infos with it right away, the actual consumer setup can be delayed and until + // then we'd otherwise have no idea how far we've sourced already. + var sourcesState map[string]StreamSourceState + if mset.store != nil { + sourcesState = mset.store.SourcesState() + } + + 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} + } + if sss, ok := sourcesState[ssi.iname]; ok { + si.sseq, si.ident = sss.Seq, sss.Ident + } + mset.sources[ssi.iname] = si + } +} + +// 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.resetSourceInfo() + + // 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) 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, recovering bool) error { + mset.mu.Lock() + switch mset.cfg.Storage { + case MemoryStorage: + ms, err := newMemStoreWithMode(&mset.cfg, recovering) + 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 := newFileStoreWithCreatedAndMode(cfg, mset.cfg, mset.created, prf, oldprf, recovering) + 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() { + mset.mu.RLock() + if mset.isLeader() { + md := streamMsgDelete{Seq: seq, NoErase: true, Stream: mset.cfg.Name} + mset.node.Propose(mset.term, 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. + var ss StreamState + mset.store.FastState(&ss) + mset.clsMu.RLock() + for _, o := range mset.cList { + o.streamNumPendingLocked() + o.removeRedeliveredBelow(ss.FirstSeq) + } + 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.ddMu.Lock() + defer mset.ddMu.Unlock() + tmrNext := mset.ddwin + window := int64(tmrNext) + + 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.ddwin <= 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.ddwin, 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: +// ......$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 + } + seq, ok := parseUint64(stringToBytes(reply[o+1 : p])) + // Reject math.MaxUint64 to prevent b.lseq overflowing on the next b.lseq++. + if !ok || seq == 0 || seq == math.MaxUint64 { + return nil, true + } + b.seq = seq + p = o + 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 parseUint64(bseq) +} + +// 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.getCfgName() + 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 + + store, s := mset.store, mset.srv + name := mset.getCfgName() + + // Ensure this read request is isolated and doesn't interleave with writes. + mset.isolateMu.RLock() + defer mset.isolateMu.RUnlock() + + // 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()) + var state StreamState + store.FastState(&state) + // 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 <= 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 + store.FastState(&state) + upToSeq = state.LastSeq + } + + var lseq uint64 + var sentBytes, sent int + total, np, err := store.MultiLastMsgs(req.MultiLastFor, req.Seq, upToSeq, maxAllowedResponses, func(sm *StoreMsg, np uint64) bool { + hdr := sm.hdr + ts := time.Unix(0, sm.ts).UTC() + + 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 { + return false + } + sent++ + if req.Batch > 0 && sent >= req.Batch { + return false + } + return true + }) + if err != nil { + var hdr []byte + switch { + case err == ErrTooManyResults: + hdr = []byte("NATS/1.0 413 Too Many Results\r\n\r\n") + case total > 0: + // A resolved message failed to load. Errors are folded into a 404 + // regardless of cause, so internal errors don't leak to clients. + hdr = []byte("NATS/1.0 404 Message Not Found\r\n\r\n") + default: + 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 total == 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 + } + + // 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 + } + + store, s := mset.store, mset.srv + name := mset.getCfgName() + + // Ensure this read request is isolated and doesn't interleave with writes. + mset.isolateMu.RLock() + defer mset.isolateMu.RUnlock() + + 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. + var state StreamState + store.FastState(&state) + if state.LastSeq > 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. +// needIsolation should be false only if the caller already holds isolateMu +// across a whole atomic batch; mset.mu must NOT be held by the caller. +func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, mt *msgTrace, sourced bool, needIsolation bool) error { + return mset.processJetStreamMsgWithBatch(subject, reply, hdr, msg, lseq, ts, mt, sourced, needIsolation, nil, false) +} + +func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, mt *msgTrace, sourced bool, needIsolation bool, fastBatch *FastBatch, deferResponse bool) (retErr error) { + if mt != nil && !deferResponse { + // 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 needIsolation { + mset.isolateMu.Lock() + defer mset.isolateMu.Unlock() + } + // Hold the stream lock for the lifetime of processing a single message. + 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{} + + if canConsistencyCheck && stype == FileStorage && isFileStoreMsgTooLarge(fileStoreMsgSize(subject, hdr, msg)) { + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSStreamStoreFailedError(ErrMsgTooLarge) + response, _ := json.Marshal(resp) + outq.sendMsg(reply, response) + } + return ErrMsgTooLarge + } + + 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 + + // 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() + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return ErrMaxPayload + } + + if canConsistencyCheck && len(hdr) > math.MaxUint16 { + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSStreamHeaderExceedsMaximumError() + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return ErrMaxPayload + } + + 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 { + // Check that the to-be-purged subject is a schedule message. + // We still allow this message through if there exists no message for this subject, + // to remain backward-compatible. An "expected at sequence" check can still be + // performed to make this stricter. + var smv StoreMsg + sm, _ := store.LoadLastMsg(bytesToString(scheduler), &smv) + if sm != nil && len(sliceHeader(JSSchedulePattern, sm.hdr)) == 0 { + apiErr := NewJSMessageSchedulesSchedulerInvalidError() + 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 + } + + // Non-sourced messages aren't allowed to have the stream source header. + if !sourced && len(sliceHeader(JSStreamSource, hdr)) > 0 { + apiErr := NewJSMessageSourceHdrNotAllowedError() + 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 sources[origStream] == nil { + 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 + } + } + + // Header processing above may have changed the message, such as when a + // counter increment generates its value payload. Check the resulting size + // against the stream limit as well as checking the inbound size early. + // 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.SkipMsgNoInterest(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 tsubj != _EMPTY_ && !IsValidPublishSubject(tsubj) { + s.RateLimitWarnf("Stream '%s > %s' suppressing republish with invalid subject %q", accName, name, tsubj) + tsubj = _EMPTY_ // ... stops the republish. + } + 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.ShutdownJetStream() + 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(mset.term, 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() + + // Errors before message processing are reported here. A coalesced R1 + // durability boundary also defers the successful trace until its final + // sync; other successful traces are sent from processJetStreamMsg. + deferResponse := false + if mt != nil { + defer func() { + if retErr != nil || deferResponse { + 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() + + // Parse the batch commit header. + var commit, commitEob, commitInvalid bool + if c := sliceHeader(JSBatchCommit, hdr); c != nil { + commitEob = bytes.Equal(c, []byte("eob")) + commitInvalid = !commitEob && !bytes.Equal(c, []byte("1")) + commit = true + } + + // If not clustered, the commit message runs the consistency checks and + // commits straight to the store below, so hold the isolation lock across + // that whole section. + if !isClustered && commit { + mset.isolateMu.Lock() + defer mset.isolateMu.Unlock() + } + 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 + } + size := batchSeq + // Don't count the "End Of Batch" marker toward the batch size. + if commitEob { + size-- + } + if size > 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 + } + + // Reject the batch if the commit is not recognized. + if commitInvalid { + b.cleanupLocked(batchId, batches) + batches.mu.Unlock() + mset.mu.Unlock() + err := NewJSAtomicPublishInvalidBatchCommitError() + return respondError(err) + } + + // 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 + } + size := batchSeq + // Don't count the "End Of Batch" marker toward the batch size. + if commitEob { + size-- + } + if size > 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 + } + + type checkedMsg struct { + subj string + hdr []byte + msg []byte + } + var ( + entries []*Entry + checked []checkedMsg + 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) + } else { + // Preserve the (possibly rewritten) headers and message, for example for counters and + // scheduled messages, so the commit below stores the transformed message. + // Need to copy, the staged message buffer is reused on every load. + checked = append(checked, checkedMsg{bsubj, copyBytes(bhdr), copyBytes(bmsg)}) + } + 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. isolateMu stays held until return + // via the defer above, but release mset.mu and batches.mu. + mset.mu.Unlock() + batches.mu.Unlock() + + fs, hasFileStore := mset.store.(*fileStore) + coalesceSync := false + if hasFileStore { + fs.mu.Lock() + coalesceSync = fs.syncAlways.Load() + if coalesceSync { + fs.updateDurabilitySettingsLocked(true) + } + fs.mu.Unlock() + } + deferResponse = coalesceSync + durabilityReset := !coalesceSync + defer func() { + if !durabilityReset { + fs.mu.Lock() + fs.resetDurabilitySettingsLocked() + fs.mu.Unlock() + } + }() + + for seq := uint64(1); seq <= batchSeq; seq++ { + // Use the checked (and possibly rewritten) message from above, not the raw staged + // message, so transformations like counter increments and scheduled message + // rollups are persisted just like in the clustered proposal path. + cm := checked[seq-1] + bsubj, bhdr, bmsg = cm.subj, cm.hdr, cm.msg + var _reply string + if seq == batchSeq { + if !coalesceSync { + _reply = reply + } + // If committed by EOB, the last message must get the normal commit header. + if commitEob { + bhdr = genHeader(bhdr, JSBatchCommit, "1") + } + } + // If errored, assume all subsequent calls will fail too (for example, store is closed). + // Don't clean up the batch so that a restart can try to recover it. + if err = mset.processJetStreamMsgWithBatch(bsubj, _reply, bhdr, bmsg, 0, 0, mt, false, false, nil, deferResponse); err != nil { + return err + } + } + if coalesceSync { + if err = fs.FlushAllPending(); err != nil { + mset.setWriteErr(err) + return err + } + fs.mu.Lock() + fs.resetDurabilitySettingsLocked() + fs.mu.Unlock() + durabilityReset = true + mset.mu.RLock() + lseq, pubAck, outq := mset.lseq, mset.pubAck, mset.outq + mset.mu.RUnlock() + if canRespond && !mt.traceOnly() { + var buf [256]byte + response := append(buf[:0], pubAck...) + response = append(response, strconv.FormatUint(lseq, 10)...) + response = append(response, fmt.Sprintf(",\"batch\":%q,\"count\":%d}", batchId, batchSeq)...) + outq.sendMsg(reply, response) + } + } + // Re-acquire for the cleanup below. If a concurrent staging error + // already cleaned the batch up, cleanupLocked is a no-op. + batches.mu.Lock() + } else { + term := mset.term + 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(term, 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, term := mset.srv, mset.js, mset.jsa, mset.cfg.Storage, mset.cfg.Replicas, mset.tier, mset.outq, mset.node, mset.term + 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. + // Unless this is the commit message, in which case we must still complete it below. + if batch.gapOk && !batch.commit { + 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 there is none pending, correct the persisted sequence as we need to commit below. + if b.pending == 0 { + b.pseq = 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, false) + } + err = commitSingleMsg(diff, mset, subject, reply, hdr, msg, name, jsa, mt, node, term, 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() + } + o.stopMonitoring() + } +} + +// 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() + + // If we're shutting down, wait for the monitor routine, which attempts to snapshot. + if isShuttingDown && !deleteFlag { + mset.stopMonitoring() + } + + 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.stopMonitoring() + } + } + } + + 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.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.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) { + if _, ok := mset.consumers[o.name]; ok { + return + } + mset.consumers[o.name] = o + if o.direct || o.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 c, ok := mset.consumers[o.name]; !ok || c != o { + return + } + delete(mset.consumers, o.name) + + if (o.direct || o.sourcing) && mset.sourcingConsumers > 0 { + mset.sourcingConsumers-- + } + + // 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() +} + +// 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 { + if o.direct { + num++ + } + } + 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 + } + // Ignore direct/sourcing consumers. + if o.direct || o.sourcing { + continue + } + o.mu.RLock() + 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 + // V2 snapshots intentionally do not run the v1 checkMsgs pre-scan. Unlike + // v1's raw file copy, v2 reads each message through LoadNextMsg, which uses + // cacheLookup/msgFromBufEx and verifies the per-record checksum when a + // freshly loaded cache entry is first read. Corrupt records therefore make + // LoadNextMsg fail and abort the snapshot instead of silently entering the + // backup. + return mset.js.CreateStreamSnapshotV2(store, deadline, includeConsumers, mset.streamAssignment()) +} + +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, tier, &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() + } +} + +// startMonitorWg registers a pending monitor goroutine on monitorWg. It is +// held under monitorMu so that the monitorWg.Add can never race a concurrent +// monitorWg.Wait in stopMonitoring. The corresponding monitorWg.Done is done by +// the monitor goroutine directly and must not be wrapped with monitorMu. +func (mset *stream) startMonitorWg() { + mset.monitorMu.Lock() + // Quit channel for this new monitor generation. Not if the stream is (being) + // stopped, stop won't signal again so the monitor must exit right away instead. + mset.mu.Lock() + if mset.mqch == nil && !mset.closed.Load() { + mset.mqch = make(chan struct{}) + } + mset.mu.Unlock() + mset.monitorWg.Add(1) + mset.monitorMu.Unlock() +} + +// stopMonitoring signals any running monitor goroutine to quit and waits for +// it to fully exit. +func (mset *stream) stopMonitoring() { + // monitorMu is held across both the quit signal and the wait so that a + // concurrent startMonitorWg cannot slip a new monitor generation in + // between. + mset.monitorMu.Lock() + defer mset.monitorMu.Unlock() + mset.signalMonitorQuit() + mset.monitorWg.Wait() +} + +// 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 +} + +// 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() + } +}