diff --git a/go.mod b/go.mod index 8d249ec43a..7e4440abd1 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/libregraph/lico v0.67.0 github.com/mna/pigeon v1.3.0 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/nats-io/nats-server/v2 v2.14.3 + github.com/nats-io/nats-server/v2 v2.14.4 github.com/nats-io/nats.go v1.52.0 github.com/olekukonko/tablewriter v1.1.4 github.com/onsi/ginkgo v1.16.5 @@ -135,7 +135,7 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/alexedwards/argon2id v1.0.0 // indirect github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 // indirect - github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect + github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index e497b4c827..75aa4c8341 100644 --- a/go.sum +++ b/go.sum @@ -115,8 +115,8 @@ github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/ github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= -github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= +github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op h1:p2zFsAzvhIpFya8AIOHIbWf7NGvO34QpLGclyf7nXj8= +github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= @@ -894,8 +894,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc= github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc= -github.com/nats-io/nats-server/v2 v2.14.3 h1:+xjydPt7rkit67G+04TN0mcO2n+8nveZE7tK/PPV53A= -github.com/nats-io/nats-server/v2 v2.14.3/go.mod h1:5IlCtBzfwyzQzPMjmoJ9W2/LKmnJRtNyuOs/OT+NHDY= +github.com/nats-io/nats-server/v2 v2.14.4 h1:efgjZ8cdExAKRuqSg8UPJFprb+l7NlBtSDPhDlw3rO4= +github.com/nats-io/nats-server/v2 v2.14.4/go.mod h1:BltdpOYestjbtQSnVO2zGHdg5SGBZjt+GYTgB9LZq/I= github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc= github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= diff --git a/vendor/github.com/antithesishq/antithesis-sdk-go/assert/assert.go b/vendor/github.com/antithesishq/antithesis-sdk-go/assert/assert.go index eff6fa96bd..2d50c62460 100644 --- a/vendor/github.com/antithesishq/antithesis-sdk-go/assert/assert.go +++ b/vendor/github.com/antithesishq/antithesis-sdk-go/assert/assert.go @@ -6,7 +6,7 @@ // // These functions are no-ops with minimal performance overhead when called outside of the Antithesis environment. However, if the environment variable ANTITHESIS_SDK_LOCAL_OUTPUT is set, these functions will log to the file pointed to by that variable using a structured JSON format defined [here]. This allows you to make use of the Antithesis assertions package in your regular testing, or even in production. In particular, very few assertions frameworks offer a convenient way to define [Sometimes assertions], but they can be quite useful even outside Antithesis. // -// Each function in this package takes a parameter called message, which is a human readable identifier used to aggregate assertions. Antithesis generates one test property per unique message and this test property will be named "" in the [triage report]. +// Each function in this package takes a parameter called message, which is a human readable identifier used to aggregate assertions. Antithesis generates one test property per unique message and this test property will be named "" in the [triage report]. Message must be provided as a string literal. // // This test property either passes or fails, which depends upon the evaluation of every assertion that shares its message. Different assertions in different parts of the code should have different message, but the same assertion should always have the same message even if it is moved to a different file. // diff --git a/vendor/github.com/antithesishq/antithesis-sdk-go/internal/emit.go b/vendor/github.com/antithesishq/antithesis-sdk-go/internal/emit.go index a932f5cf4a..9a0eda76b8 100644 --- a/vendor/github.com/antithesishq/antithesis-sdk-go/internal/emit.go +++ b/vendor/github.com/antithesishq/antithesis-sdk-go/internal/emit.go @@ -38,7 +38,7 @@ type libHandler interface { } const ( - errorLogLinePrefix = "[* antithesis-sdk-go *]" + errorLogLinePrefix = "[* antithesis-sdk-go *]" ) var handler libHandler diff --git a/vendor/github.com/antithesishq/antithesis-sdk-go/internal/sdk_const.go b/vendor/github.com/antithesishq/antithesis-sdk-go/internal/sdk_const.go index e520f15202..44f7b58457 100644 --- a/vendor/github.com/antithesishq/antithesis-sdk-go/internal/sdk_const.go +++ b/vendor/github.com/antithesishq/antithesis-sdk-go/internal/sdk_const.go @@ -3,7 +3,7 @@ package internal // -------------------------------------------------------------------------------- // Versions // -------------------------------------------------------------------------------- -const SDK_Version = "0.7.0" +const SDK_Version = "0.7.2" const Protocol_Version = "1.1.0" // -------------------------------------------------------------------------------- diff --git a/vendor/github.com/antithesishq/antithesis-sdk-go/internal/voidstar_handler.go b/vendor/github.com/antithesishq/antithesis-sdk-go/internal/voidstar_handler.go index eb410fd887..dab2363554 100644 --- a/vendor/github.com/antithesishq/antithesis-sdk-go/internal/voidstar_handler.go +++ b/vendor/github.com/antithesishq/antithesis-sdk-go/internal/voidstar_handler.go @@ -4,8 +4,8 @@ package internal import ( "fmt" - "unsafe" "os" + "unsafe" ) // -------------------------------------------------------------------------------- diff --git a/vendor/github.com/nats-io/nats-server/v2/server/auth.go b/vendor/github.com/nats-io/nats-server/v2/server/auth.go index 476667ae36..4d9598b7d1 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/auth.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/auth.go @@ -194,6 +194,53 @@ func (p *Permissions) clone() *Permissions { return clone } +// wsCanMapCerts returns true if the websocket listener collects client +// certificates, which mqtt and leafnode connections over it need to be mapped. +func (o *Options) wsCanMapCerts() bool { + return o.Websocket.Port != 0 && o.Websocket.TLSConfig != nil && + o.Websocket.TLSConfig.ClientAuth >= tls.RequestClientCert +} + +// canTLSMap returns true if verify_and_map on any listener that uses the users +// table could select this user, which makes it a certificate identity. Cluster +// and gateway map onto their own configured user, so they do not count. +func (o *Options) canTLSMap(u *User) bool { + // The mapping lookup skips users not allowed on the connection type. + allowed := func(cts ...string) bool { + if len(u.AllowedConnectionTypes) == 0 { + return true + } + for _, ct := range cts { + if _, ok := u.AllowedConnectionTypes[ct]; ok { + return true + } + } + return false + } + // Only a listener that is started can map, and in-process connections have + // no TLS state, so they never do. + if o.TLSMap && !o.DontListen && allowed(jwt.ConnectionTypeStandard) { + return true + } + if o.Websocket.TLSMap && o.Websocket.Port != 0 && allowed(jwt.ConnectionTypeWebsocket) { + return true + } + // Mqtt over websocket maps with the mqtt listener, not the websocket one, + // but takes its certificate from the websocket transport. + if o.MQTT.TLSMap && o.MQTT.Port != 0 && + (allowed(jwt.ConnectionTypeMqtt) || + o.wsCanMapCerts() && allowed(jwt.ConnectionTypeMqttWS)) { + return true + } + // Leafnodes only use the users table when the leafnode block sets no + // credentials of its own, see isLeafNodeAuthorized. + return o.LeafNode.TLSMap && o.LeafNode.Port != 0 && + o.LeafNode.Username == _EMPTY_ && o.LeafNode.Nkey == _EMPTY_ && + len(o.LeafNode.Users) == 0 && + (allowed(jwt.ConnectionTypeLeafnode) || + o.wsCanMapCerts() && allowed(jwt.ConnectionTypeLeafnodeWS)) +} + // checkAuthforWarnings will look for insecure settings and log concerns. // Lock is assumed held. func (s *Server) checkAuthforWarnings() { @@ -205,7 +252,7 @@ func (s *Server) checkAuthforWarnings() { for _, u := range s.users { // Skip warn if using TLS certs based auth // unless a password has been left in the config. - if u.Password == _EMPTY_ && opts.TLSMap { + if u.Password == _EMPTY_ && opts.canTLSMap(u) { continue } // Check if this is our internal sys client created on the fly. @@ -1164,6 +1211,13 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au if proxyRequired = user.ProxyRequired; proxyRequired && !trustedProxy { return setProxyAuthError(ErrAuthProxyRequired) } + // A user with no password is a certificate only identity when mapping is + // enabled on another listener, so do not let comparePasswords match two + // empty passwords. The no_auth_user needs no credentials either way. + if !tlsMap && user.Password == _EMPTY_ && user.Username != noAuthUser && opts.canTLSMap(user) { + c.Debugf("User %q requires a client certificate", user.Username) + return false + } ok = comparePasswords(user.Password, c.opts.Password) // If we are authorized, register the user which will properly setup any permissions // for pub/sub authorizations. @@ -1657,15 +1711,44 @@ func validateAuth(o *Options) error { if err := validateAllowedConnectionTypes(u.AllowedConnectionTypes); err != nil { return err } + if err := validatePermissionSubjects(u.Permissions); err != nil { + return fmt.Errorf("invalid permissions for user %q: %w", u.Username, err) + } } for _, u := range o.Nkeys { if err := validateAllowedConnectionTypes(u.AllowedConnectionTypes); err != nil { return err } + if err := validatePermissionSubjects(u.Permissions); err != nil { + return fmt.Errorf("invalid permissions for nkey %q: %w", u.Nkey, err) + } } return validateNoAuthUser(o, o.NoAuthUser) } +func validatePermissionSubjects(p *Permissions) error { + if p == nil { + return nil + } + if p.Publish != nil { + if err := checkPermSubjectArray(p.Publish.Allow, false); err != nil { + return fmt.Errorf("publish allow: %w", err) + } + if err := checkPermSubjectArray(p.Publish.Deny, false); err != nil { + return fmt.Errorf("publish deny: %w", err) + } + } + if p.Subscribe != nil { + if err := checkPermSubjectArray(p.Subscribe.Allow, true); err != nil { + return fmt.Errorf("subscribe allow: %w", err) + } + if err := checkPermSubjectArray(p.Subscribe.Deny, true); err != nil { + return fmt.Errorf("subscribe deny: %w", err) + } + } + return nil +} + func validateAllowedConnectionTypes(m map[string]struct{}) error { for ct := range m { ctuc := strings.ToUpper(ct) diff --git a/vendor/github.com/nats-io/nats-server/v2/server/avl/seqset.go b/vendor/github.com/nats-io/nats-server/v2/server/avl/seqset.go index f4fa127df6..61f74c1693 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/avl/seqset.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/avl/seqset.go @@ -42,6 +42,22 @@ type SequenceSet struct { // Insert will insert the sequence into the set. // The tree will be balanced inline. func (ss *SequenceSet) Insert(seq uint64) { + // If a node covering seq already exists, setting a bit can not change the + // tree shape, so skip the recursive descent and rebalance checks. + for n := ss.root; n != nil; { + if seq < n.base { + n = n.l + } else if seq >= n.base+numEntries { + n = n.r + } else { + n.set(seq, &ss.changed) + if ss.changed { + ss.changed = false + ss.size++ + } + return + } + } if ss.root = ss.root.insert(seq, &ss.changed, &ss.nodes); ss.changed { ss.changed = false ss.size++ @@ -187,17 +203,32 @@ func (ss *SequenceSet) Clone() *SequenceSet { return css } +// Equal returns whether the two sets contain exactly the same sequences. +func (ss *SequenceSet) Equal(other *SequenceSet) bool { + if ss.IsEmpty() || other.IsEmpty() { + return ss.IsEmpty() && other.IsEmpty() + } + if ss.size != other.size { + return false + } + // Sizes are equal, so a one-way membership check suffices. + equal := true + ss.Range(func(seq uint64) bool { + equal = other.Exists(seq) + return equal + }) + return equal +} + // Union will union this SequenceSet with ssa. func (ss *SequenceSet) Union(ssa ...*SequenceSet) { for _, sa := range ssa { sa.root.nodeIter(func(n *node) { for nb, b := range n.bits { - for pos := uint64(0); b != 0; pos++ { - if b&1 == 1 { - seq := n.base + (uint64(nb) * uint64(bitsPerBucket)) + pos - ss.Insert(seq) - } - b >>= 1 + base := n.base + uint64(nb)*bitsPerBucket + for b != 0 { + ss.Insert(base + uint64(bits.TrailingZeros64(b))) + b &= b - 1 } } }) @@ -302,8 +333,11 @@ func decodev2(buf []byte) (*SequenceSet, int, error) { sz := int(le.Uint32(buf[index+4:])) index += 8 - expectedLen := minLen + (nn * ((numBuckets+1)*8 + 2)) - if len(buf) < expectedLen { + // nn is decoded as a uint32 but held in an int. On 32-bit builds a value + // above MaxInt32 turns negative and nn*perNode below overflows, so the + // length check would pass for a short buffer and the following make/reads + // run off the end. Compare with division so the bound holds on every arch. + if nn < 0 || nn > (len(buf)-minLen)/((numBuckets+1)*8+2) { return nil, -1, ErrBadEncoding } @@ -335,8 +369,9 @@ func decodev1(buf []byte) (*SequenceSet, int, error) { const v1NumBuckets = 64 - expectedLen := minLen + (nn * ((v1NumBuckets+1)*8 + 2)) - if len(buf) < expectedLen { + // See decodev2: guard the node count without overflowing the multiply so + // the bound stays correct on 32-bit builds too. + if nn < 0 || nn > (len(buf)-minLen)/((v1NumBuckets+1)*8+2) { return nil, -1, ErrBadEncoding } @@ -347,12 +382,9 @@ func decodev1(buf []byte) (*SequenceSet, int, error) { for nb := uint64(0); nb < v1NumBuckets; nb++ { n := le.Uint64(buf[index:]) // Walk all set bits and insert sequences manually for this decode from v1. - for pos := uint64(0); n != 0; pos++ { - if n&1 == 1 { - seq := base + (nb * uint64(bitsPerBucket)) + pos - ss.Insert(seq) - } - n >>= 1 + for n != 0 { + ss.Insert(base + (nb * uint64(bitsPerBucket)) + uint64(bits.TrailingZeros64(n))) + n &= n - 1 } index += 8 } @@ -527,10 +559,13 @@ func (n *node) clear(seq uint64, deleted *bool) bool { seq -= n.base i := seq / bitsPerBucket mask := uint64(1) << (seq % bitsPerBucket) - if (n.bits[i] & mask) != 0 { - n.bits[i] &^= mask - *deleted = true + if (n.bits[i] & mask) == 0 { + // Nothing cleared, and nodes in the tree are never empty, + // so no need to scan the buckets. + return false } + n.bits[i] &^= mask + *deleted = true for _, b := range n.bits { if b != 0 { return false @@ -663,11 +698,13 @@ func (n *node) iter(f func(uint64) bool) bool { if ok := n.l.iter(f); !ok { return false } - for num := n.base; num < n.base+numEntries; num++ { - if n.exists(num) { - if ok := f(num); !ok { + for i, b := range n.bits { + base := n.base + uint64(i)*bitsPerBucket + for b != 0 { + if ok := f(base + uint64(bits.TrailingZeros64(b))); !ok { return false } + b &= b - 1 } } if ok := n.r.iter(f); !ok { diff --git a/vendor/github.com/nats-io/nats-server/v2/server/client.go b/vendor/github.com/nats-io/nats-server/v2/server/client.go index 12cc4db683..b5c5fcfaf7 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/client.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/client.go @@ -287,7 +287,7 @@ type client struct { subs map[string]*subscription replies map[string]*resp mperms *msgDeny - darray []string + darray []*subscription // Parsed subscribe deny entries used to build the delivery-time filter. pcd map[*client]struct{} atmr *time.Timer expires time.Time @@ -450,15 +450,22 @@ type resp struct { // succeed but no message sent on foo should be delivered. type msgDeny struct { deny *Sublist - dcache map[string]bool + 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 - _qs [32]byte + sub *subscription + qs []byte + qsubs []*subscription + _qs [32]byte + _qsubs [4]*subscription } const ( @@ -1072,6 +1079,9 @@ func (c *client) updateDefaultPermissions(perms *Permissions) bool { 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 { @@ -1079,6 +1089,9 @@ func splitSubjectQueue(sq string) ([]byte, []byte, error) { } 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 } @@ -1099,15 +1112,35 @@ func (c *client) setPermissions(perms *Permissions) { 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)} - c.perms.pub.allow.Insert(sub) + 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 { - sub := &subscription{subject: []byte(pubSubject)} - c.perms.pub.deny.Insert(sub) + 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) + } } } @@ -1131,12 +1164,12 @@ func (c *client) setPermissions(perms *Permissions) { c.Errorf("%s", err.Error()) continue } - c.perms.sub.allow.Insert(sub) + 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() - // Also hold onto this array for later. - c.darray = perms.Subscribe.Deny } for _, subSubject := range perms.Subscribe.Deny { sub := &subscription{} @@ -1145,7 +1178,12 @@ func (c *client) setPermissions(perms *Permissions) { c.Errorf("%s", err.Error()) continue } - c.perms.sub.deny.Insert(sub) + 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) } } @@ -1195,14 +1233,14 @@ func (c *client) publicPermissions() *Permissions { subs := _subs[:0] c.perms.sub.allow.All(&subs) for _, sub := range subs { - perms.Subscribe.Allow = append(perms.Subscribe.Allow, string(sub.subject)) + 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, string(sub.subject)) + perms.Subscribe.Deny = append(perms.Subscribe.Deny, subjectQueueString(sub)) } } // Responses. @@ -1214,6 +1252,13 @@ func (c *client) publicPermissions() *Permissions { 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 ( @@ -1235,37 +1280,48 @@ func (c *client) mergeDenyPermissions(what denyType, denyPubs []string) { if c.perms.pub.deny == nil { c.perms.pub.deny = NewSublistForServer(c.srv) } - mergeDenyPerm(&c.perms.pub, denyPubs) + 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() } - mergeDenyPerm(&c.perms.sub, denyPubs) + 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) { +func mergeDenyPerm(p *perm, denyPubs []string, allowQueue bool) []*subscription { + var inserted []*subscription FOR_DENY: - for _, subj := range denyPubs { - r := p.deny.Match(subj) + 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 string(s.subject) == subj { + if bytes.Equal(s.subject, subject) && bytes.Equal(s.queue, queue) { continue FOR_DENY } } } for _, s := range r.psubs { - if string(s.subject) == subj { + if bytes.Equal(s.subject, subject) && len(queue) == 0 { continue FOR_DENY } } - sub := &subscription{subject: []byte(subj)} - p.deny.Insert(sub) + 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 @@ -1301,9 +1357,9 @@ func (c *client) setExpiration(claims *jwt.ClaimsData, validFor time.Duration) { // messages based on a deny clause for subscriptions. // Lock should be held. func (c *client) loadMsgDenyFilter() { - c.mperms = &msgDeny{NewSublistWithCache(), make(map[string]bool)} + c.mperms = &msgDeny{NewSublistWithCache(), make(map[msgDenyKey]bool)} for _, sub := range c.darray { - c.mperms.deny.Insert(&subscription{subject: []byte(sub)}) + c.mperms.deny.Insert(&subscription{subject: sub.subject, queue: sub.queue}) } } @@ -3339,21 +3395,28 @@ 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 && subjectHasWildcard(subject) { + 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(sub, subject) { + if SubjectsCollide(bytesToString(sub.subject), subject) { c.loadMsgDenyFilter() break } } } - return true } func queueMatches(queue string, qsubs [][]*subscription) bool { @@ -3496,19 +3559,21 @@ func (c *client) processUnsub(arg []byte) error { // 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 string) bool { - if denied, ok := c.mperms.dcache[subject]; ok { +func (c *client) checkDenySub(subject, queue string) bool { + key := msgDenyKey{subject, queue} + if denied, ok := c.mperms.dcache[key]; ok { return denied - } else if np, _ := c.mperms.deny.NumInterest(subject); np != 0 { - c.mperms.dcache[subject] = true - return true - } else { - c.mperms.dcache[subject] = false } + 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 false + return denied } // Create a message header for routes or leafnodes. Header and origin cluster aware. @@ -3691,7 +3756,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su 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) { + if client == nil || (c == client && !client.echo && !sub.si && !sub.rsi) { if client != nil && mt != nil { client.mu.Lock() mt.addEgressEvent(client, sub, errMsgTraceNoEcho) @@ -3704,7 +3769,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su // 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)) { + if client.mperms != nil && client.checkDenySub(string(subject), bytesToString(sub.queue)) { mt.addEgressEvent(client, sub, errMsgTraceSubDeny) client.mu.Unlock() return false @@ -4067,8 +4132,8 @@ func (c *client) pruneReplyPerms() { // deliverMsg. func (c *client) pruneDenyCache() { r := 0 - for subject := range c.mperms.dcache { - delete(c.mperms.dcache, subject) + for key := range c.mperms.dcache { + delete(c.mperms.dcache, key) if r++; r > pruneSize { break } @@ -5083,6 +5148,7 @@ func (c *client) addSubToRouteTargets(sub *subscription) { if sub.queue != nil { rt.qs = append(rt.qs, sub.queue...) rt.qs = append(rt.qs, ' ') + rt.qsubs = append(rt.qsubs, sub) } return } @@ -5101,10 +5167,49 @@ func (c *client) addSubToRouteTargets(sub *subscription) { 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. @@ -5553,9 +5658,6 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver, // 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) - if flags&pmrCollectQueueNames != 0 { - queues = append(queues, rsub.queue) - } } } @@ -5585,6 +5687,10 @@ sendToRoutesOrLeafs: // 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 @@ -5617,6 +5723,11 @@ sendToRoutesOrLeafs: 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 { @@ -5999,10 +6110,13 @@ func (c *client) processSubsOnConfigReload(awcsti map[string]struct{}) { 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. - canSub := c.canSubscribe(string(sub.subject)) - canQSub := sub.queue != nil && c.canSubscribe(string(sub.subject), string(sub.queue)) - - if !canSub && !canQSub { + 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) @@ -6025,8 +6139,13 @@ func (c *client) processSubsOnConfigReload(awcsti map[string]struct{}) { // Unsubscribe all that need to be removed and report back to client and logs. for _, sub := range removed { c.unsubscribe(acc, sub, true, true) - 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()) + 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()) + } } } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/const.go b/vendor/github.com/nats-io/nats-server/v2/server/const.go index 4fe5052ea5..d56085a0ca 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/const.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/const.go @@ -66,7 +66,7 @@ func init() { const ( // VERSION is the current version for the server. - VERSION = "2.14.3" + VERSION = "2.14.4" // PROTO is the currently supported protocol. // 0 was the original diff --git a/vendor/github.com/nats-io/nats-server/v2/server/consumer.go b/vendor/github.com/nats-io/nats-server/v2/server/consumer.go index afb7ec03ab..760bd9dcb9 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/consumer.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/consumer.go @@ -502,6 +502,7 @@ type consumer struct { // 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). infoSub *subscription lqsent time.Time prm map[string]struct{} @@ -1435,7 +1436,7 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri o.resetStartingSeq(0, _EMPTY_, false) } if config.Direct || standalone { - o.setLeader(true) + o.setLeader(true, 0) } // This is always true in single server mode. @@ -1602,18 +1603,112 @@ func (o *consumer) isLeader() bool { return o.leader.Load() } -func (o *consumer) setLeader(isLeader bool) error { - o.mu.RLock() +func (o *consumer) setLeader(isLeader bool, term uint64) error { + o.mu.Lock() mset, closed := o.mset, o.closed - movingToClustered := o.node != nil && o.pch == nil - movingToNonClustered := o.node == nil && o.pch != nil 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()) - o.mu.RUnlock() + + // 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 + // 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 { @@ -1637,35 +1732,6 @@ func (o *consumer) setLeader(isLeader bool) error { o.mu.Unlock() } - if wasLeader { - // If we detect we are scaling up, make sure to create clustered routines and channels. - if movingToClustered { - o.mu.Lock() - // We are moving from R1 to clustered. - o.pch = make(chan struct{}, 1) - go o.loopAndForwardProposals(o.qch) - if o.phead != nil { - select { - case o.pch <- struct{}{}: - default: - } - } - o.mu.Unlock() - } 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. - o.mu.Lock() - pch := o.pch - o.pch = nil - select { - case pch <- struct{}{}: - default: - } - o.mu.Unlock() - } - return nil - } - mset.mu.RLock() s, jsa, stream := mset.srv, mset.jsa, mset.getCfgName() mset.mu.RUnlock() @@ -1770,8 +1836,10 @@ func (o *consumer) setLeader(isLeader bool) error { 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() @@ -1811,67 +1879,16 @@ func (o *consumer) setLeader(isLeader bool) error { } // If we are R>1 spin up our proposal loop. - if node != nil { + 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(qch) + o.loopAndForwardProposals(node, qch, pch, term) }() } - - } else { - // Shutdown the go routines and the subscriptions. - o.mu.Lock() - 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 - // 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() - // If we were the leader make sure to drain queued up acks. - if wasLeader { - 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 - } - o.mu.Unlock() } return nil } @@ -2454,6 +2471,9 @@ func (acc *Account) checkNewConsumerConfig(cfg, ncfg *ConsumerConfig) error { 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") + } if cfg.OptStartSeq != ncfg.OptStartSeq { return errors.New("start sequence can not be updated") } @@ -2893,25 +2913,14 @@ func (o *consumer) resetLocalStartingSeq(seq uint64) { o.ldt, o.lat = time.Time{}, time.Time{} } -func (o *consumer) loopAndForwardProposals(qch chan struct{}) { - // On exit make sure we nil out pch. - defer func() { - o.mu.Lock() - o.pch = nil - o.mu.Unlock() - }() - - o.mu.RLock() - node, pch := o.node, o.pch - o.mu.RUnlock() - - if node == nil || pch == nil { +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 o.node == nil || !o.node.Leader() { + if node == nil || !node.Leader() || o.term != term { o.mu.Unlock() return errors.New("no longer leader") } @@ -2925,14 +2934,14 @@ func (o *consumer) loopAndForwardProposals(qch chan struct{}) { entries = append(entries, newEntry(EntryNormal, proposal.data)) sz += len(proposal.data) if sz > maxBatch { - node.ProposeMulti(entries) + 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(entries) + node.ProposeMulti(term, entries) } return nil } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/dios.go b/vendor/github.com/nats-io/nats-server/v2/server/dios.go new file mode 100644 index 0000000000..8ad3aff319 --- /dev/null +++ b/vendor/github.com/nats-io/nats-server/v2/server/dios.go @@ -0,0 +1,85 @@ +// Copyright 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 ( + "sync/atomic" + "time" +) + +const defaultConcurrentIOs = 4096 +const minConcurrentIOs = 4 +const maxConcurrentIOs = 8192 + +// Used to limit number of disk IO calls in flight since they could all be blocking an OS thread. +// https://github.com/nats-io/nats-server/issues/2742 +type diskIOSemaphore struct { + ch chan struct{} + waiters atomic.Int64 + waits atomic.Uint64 + waitNanos atomic.Uint64 + maxWaitNanos atomic.Uint64 +} + +func newDiskIOSemaphore(n int) *diskIOSemaphore { + n = max(minConcurrentIOs, min(n, maxConcurrentIOs)) + d := &diskIOSemaphore{ch: make(chan struct{}, n)} + for range n { + d.ch <- struct{}{} + } + return d +} + +func defaultDiskIOSemaphore() *diskIOSemaphore { + // The disk IO semaphore used to be sized based on the number + // of CPU cores. That policy led to poor use of devices that + // can handle many requests in parallel, so simply cap the + // number of concurrent IO requests handed to the Go runtime. + return newDiskIOSemaphore(defaultConcurrentIOs) +} + +func (d *diskIOSemaphore) acquire() { + select { + case <-d.ch: + return + default: + // No slot available, count this + // waiter before blocking. + d.waiters.Add(1) + start := time.Now() + <-d.ch + waited := time.Since(start) + d.waiters.Add(-1) + d.countWait(uint64(waited.Nanoseconds())) + } +} + +func (d *diskIOSemaphore) countWait(ns uint64) { + d.waits.Add(1) + d.waitNanos.Add(ns) + for { + cur := d.maxWaitNanos.Load() + if ns <= cur || d.maxWaitNanos.CompareAndSwap(cur, ns) { + return + } + } +} + +func (d *diskIOSemaphore) release() { + d.ch <- struct{}{} +} + +func (d *diskIOSemaphore) cap() int { + return cap(d.ch) +} diff --git a/vendor/github.com/nats-io/nats-server/v2/server/filestore.go b/vendor/github.com/nats-io/nats-server/v2/server/filestore.go index c860461c14..17f1a7b8c2 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/filestore.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/filestore.go @@ -215,56 +215,58 @@ type fileStore struct { scheduling *MsgScheduling sdm *SDMMeta lpex time.Time // Last PurgeEx call. + dios *diskIOSemaphore } // 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 - syncAlways bool - noCompact bool - closed bool - ttls uint64 // How many msgs have TTLs? - schedules uint64 // How many msgs have schedules? + 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. + syncAlways bool + 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 @@ -277,6 +279,9 @@ type cache struct { 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 { @@ -411,6 +416,7 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim if fcfg.SyncInterval == 0 { fcfg.SyncInterval = defaultSyncInterval } + dios := fcfg.srv.diskIOSemaphore() // Check the directory if stat, err := os.Stat(fcfg.StoreDir); os.IsNotExist(err) { @@ -426,12 +432,13 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim } tmpfile.Close() - <-dios + dios.acquire() os.Remove(tmpfile.Name()) - dios <- struct{}{} + dios.release() fs = &fileStore{ fcfg: fcfg, + dios: dios, psim: stree.NewSubjectTree[psi](), bim: make(map[uint32]*msgBlock), cfg: FileStreamInfo{Created: created, StreamConfig: cfg}, @@ -1050,6 +1057,37 @@ func getMsgBlockBuf(sz int) (buf []byte) { } } +// 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) { @@ -1202,6 +1240,10 @@ func (fs *fileStore) recoverMsgBlock(index uint32) (*msgBlock, error) { // 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 } @@ -1433,6 +1475,7 @@ func (mb *msgBlock) convertCipher() error { } // Reset the cache since we just read everything in. + mb.cache.stopRecycle() mb.cache = nil mb.ecache.Set(nil) @@ -1440,19 +1483,19 @@ func (mb *msgBlock) convertCipher() error { // the old keyfile back. if err := fs.genEncryptionKeysForBlock(mb); err != nil { keyFile := filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index)) - fs.writeFileWithOptionalSync(keyFile, ekey, defaultFilePerms) + writeFileWithSync(fs.dios, keyFile, ekey, defaultFilePerms) return err } mb.bek.XORKeyStream(buf, buf) - <-dios + mb.fs.dios.acquire() err = os.WriteFile(mb.mfn, buf, defaultFilePerms) - dios <- struct{}{} + mb.fs.dios.release() if err != nil { return err } return nil } - return fmt.Errorf("unable to recover keys") + return errKeyInvalid } // Convert a plaintext block to encrypted. @@ -1476,6 +1519,7 @@ func (mb *msgBlock) convertToEncrypted() error { 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 @@ -1486,9 +1530,9 @@ func (mb *msgBlock) convertToEncrypted() error { return err } mb.bek.XORKeyStream(buf, buf) - <-dios + mb.fs.dios.acquire() err = os.WriteFile(mb.mfn, buf, defaultFilePerms) - dios <- struct{}{} + mb.fs.dios.release() if err != nil { return err } @@ -1609,9 +1653,9 @@ func (mb *msgBlock) rebuildStateFromBufLocked(buf []byte, allowTruncate bool) (* if mb.mfd != nil { fd = mb.mfd } else { - <-dios + mb.fs.dios.acquire() fd, err = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms) - dios <- struct{}{} + mb.fs.dios.release() if err == nil { defer fd.Close() } @@ -1878,15 +1922,15 @@ func (fs *fileStore) recoverFullState() (rerr error) { defer fs.mu.Unlock() // Check for any left over purged messages. - <-dios + fs.dios.acquire() if err := fs.recoverPartialPurge(); err != nil { - dios <- struct{}{} + 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) - dios <- struct{}{} + fs.dios.release() if err != nil { if !os.IsNotExist(err) { @@ -2115,12 +2159,12 @@ func (fs *fileStore) recoverFullState() (rerr error) { mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) var dirs []os.DirEntry - <-dios + fs.dios.acquire() if f, err := os.Open(mdir); err == nil { dirs, _ = f.ReadDir(-1) f.Close() } - dios <- struct{}{} + fs.dios.release() var index uint32 for _, fi := range dirs { @@ -2166,10 +2210,10 @@ func (fs *fileStore) recoverFullState() (rerr error) { // Lock should be held. func (fs *fileStore) recoverTTLState() error { // See if we have a timed hash wheel for TTLs. - <-dios + fs.dios.acquire() fn := filepath.Join(fs.fcfg.StoreDir, msgDir, ttlStreamStateFile) buf, err := os.ReadFile(fn) - dios <- struct{}{} + fs.dios.release() if err != nil && !os.IsNotExist(err) { return err @@ -2253,10 +2297,10 @@ func (fs *fileStore) recoverTTLState() error { // Lock should be held. func (fs *fileStore) recoverMsgSchedulingState() error { // See if we have a timed hash wheel for TTLs. - <-dios + fs.dios.acquire() fn := filepath.Join(fs.fcfg.StoreDir, msgDir, msgSchedulingStreamStateFile) buf, err := os.ReadFile(fn) - dios <- struct{}{} + fs.dios.release() if err != nil && !os.IsNotExist(err) { return err @@ -2378,9 +2422,9 @@ func (fs *fileStore) cleanupOldMeta() { mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) fs.mu.RUnlock() - <-dios + fs.dios.acquire() f, err := os.Open(mdir) - dios <- struct{}{} + fs.dios.release() if err != nil { return } @@ -2405,20 +2449,20 @@ func (fs *fileStore) recoverMsgs() error { defer fs.mu.Unlock() // Check for any left over purged messages. - <-dios + fs.dios.acquire() if err := fs.recoverPartialPurge(); err != nil { - dios <- struct{}{} + fs.dios.release() return err } mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) f, err := os.Open(mdir) if err != nil { - dios <- struct{}{} + fs.dios.release() return errNotReadable } dirs, err := f.ReadDir(-1) f.Close() - dios <- struct{}{} + fs.dios.release() if err != nil { return errNotReadable @@ -2488,6 +2532,18 @@ func (fs *fileStore) recoverMsgs() error { 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 } @@ -3260,6 +3316,13 @@ func (mb *msgBlock) filteredPendingLocked(filter string, wc bool, sseq uint64) ( } } + needsCleanup := mb.cache == nil + defer func() { + if needsCleanup { + mb.finishedWithCache() + } + }() + if filter == _EMPTY_ { filter, wc = fwcs, true } @@ -3339,7 +3402,6 @@ func (mb *msgBlock) filteredPendingLocked(filter string, wc bool, sseq uint64) ( } shouldExpire = true } - defer mb.finishedWithCache() _tsa, _fsa := [32]string{}, [32]string{} tsa, fsa := _tsa[:0], _fsa[:0] @@ -3908,12 +3970,14 @@ func (fs *fileStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed i delete(subs, bytesToString(bsubj)) } else { // Need to search for the real last since recorded last is > maxSeq. - var didLoad bool + needsCleanup := mb.cache == nil if mb.cacheNotLoaded() { if ierr = mb.loadMsgsWithLock(); ierr != nil { + if needsCleanup { + mb.finishedWithCache() + } return false } - didLoad = true } var smv StoreMsg fseq := atomic.LoadUint64(&mb.first.seq) @@ -3928,7 +3992,7 @@ func (fs *fileStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed i delete(subs, ssubj) break } - if didLoad { + if needsCleanup { mb.finishedWithCache() } } @@ -4727,6 +4791,7 @@ func (mb *msgBlock) setupWriteCache(buf []byte) error { // 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() @@ -4813,9 +4878,9 @@ func (fs *fileStore) newMsgBlockForWrite() (*msgBlock, error) { } mb.hh = hh - <-dios + fs.dios.acquire() mfd, err := os.OpenFile(mb.mfn, os.O_CREATE|os.O_RDWR, defaultFilePerms) - dios <- struct{}{} + fs.dios.release() if err != nil { if isPermissionError(err) { @@ -4859,11 +4924,14 @@ func (fs *fileStore) genEncryptionKeysForBlock(mb *msgBlock) error { if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) { return err } - err = fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms) + sync := fs.syncAlways.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 } @@ -4878,9 +4946,11 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t mmp := uint64(fs.cfg.MaxMsgsPer) var psmc uint64 psmax := mmp > 0 && len(subj) > 0 + var info *psi if psmax { - if info, ok := fs.psim.Find(stringToBytes(subj)); ok { - psmc = info.total + 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 } } @@ -4890,7 +4960,7 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t // the message here since it could cause replicas to drift. if discardNewCheck && fs.cfg.Discard == DiscardNew { var asl bool - if psmax && psmc >= mmp { + 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. @@ -4900,7 +4970,12 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t if fseq, err = fs.firstSeqForSubj(subj); err != nil { return err } - asl = true + // 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 @@ -4940,11 +5015,12 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t } // Adjust top level tracking of per subject msg counts. - var info *psi - var ok bool if len(subj) > 0 && fs.psim != nil { index := fs.lmb.index - if info, ok = fs.psim.Find(stringToBytes(subj)); ok { + if info == nil { + info, _ = fs.psim.Find(stringToBytes(subj)) + } + if info != nil { info.total++ if index > info.lblk { info.lblk = index @@ -4968,41 +5044,33 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t fs.state.LastTime = now // Enforce per message limits. - // We snapshotted psmc before our actual write, so >= comparison needed. - if psmax && psmc >= mmp { + 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 ok, err := fs.removeMsgViaLimits(fseq); err != nil { + if _, err = fs.removeMsgViaLimits(fseq); err != nil && err != ErrStoreMsgNotFound { return err - } else if ok { - // Make sure we are below the limit. - if psmc--; psmc >= mmp { - bsubj := stringToBytes(subj) - for info, ok := fs.psim.Find(bsubj); ok && info.total > mmp; info, ok = fs.psim.Find(bsubj) { - if seq, err := fs.firstSeqForSubj(subj); err != nil { - return err - } else if seq == 0 { - break - } else if ok, err = fs.removeMsgViaLimits(seq); err != nil { - return err - } else if !ok { - break - } - } - } - } else if mb := fs.selectMsgBlock(fseq); mb != nil { - // If we are here we could not remove fseq from above, so rebuild. - var ld *LostStreamData - if ld, _, err = mb.rebuildState(); err != nil { - return err - } else if ld != nil { - fs.rebuildStateLocked(ld) - } + } + 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. @@ -5418,10 +5486,20 @@ func (fs *fileStore) firstSeqForSubj(subj string) (uint64, error) { 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() @@ -5438,6 +5516,9 @@ func (fs *fileStore) firstSeqForSubj(subj string) (uint64, 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() @@ -5456,7 +5537,7 @@ func (fs *fileStore) firstSeqForSubj(subj string) (uint64, error) { if shouldExpire { // Expire this cache before moving on. mb.tryForceExpireCacheLocked() - } else { + } else if needsCleanup { mb.finishedWithCache() } mb.mu.Unlock() @@ -5790,16 +5871,18 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim // We used to not have to load in the messages except with callbacks or the filtered subject state (which is now always on). // Now just load regardless. // TODO(dlc) - Figure out a way not to have to load it in, we need subject tracking outside main data block. - var didLoad bool + needsCleanup := mb.cache == nil if mb.cacheNotLoaded() { if err := mb.loadMsgsWithLock(); err != nil { + if needsCleanup { + mb.finishedWithCache() + } mb.mu.Unlock() return false, err } - didLoad = true } finishedWithCache := func() { - if didLoad { + if needsCleanup { mb.finishedWithCache() } } @@ -6065,7 +6148,7 @@ func (mb *msgBlock) compact() error { // 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 *avl.SequenceSet) error { +func (mb *msgBlock) compactWithFloor(floor uint64, fsDmap *interiorDeletes) error { wasLoaded := mb.cache != nil && mb.cacheAlreadyLoaded() if !wasLoaded { if err := mb.loadMsgsWithLock(); err != nil { @@ -6172,9 +6255,9 @@ func (mb *msgBlock) compactWithFloor(floor uint64, fsDmap *avl.SequenceSet) erro // We will write to a new file and mv/rename it in case of failure. mfn := filepath.Join(mb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(newScan, mb.index)) - <-dios + mb.fs.dios.acquire() err := os.WriteFile(mfn, nbuf, defaultFilePerms) - dios <- struct{}{} + mb.fs.dios.release() if err != nil { _ = os.Remove(mfn) return err @@ -6673,6 +6756,7 @@ func (mb *msgBlock) clearCache() { buf := mbcache.buf mb.cache = nil mb.ecache.Set(nil) + mbcache.stopRecycle() recycleMsgBlockBuf(buf) } @@ -6717,6 +6801,10 @@ func (mb *msgBlock) tryExpireWriteCache() []byte { 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 } @@ -6771,6 +6859,7 @@ func (mb *msgBlock) tryExpireCacheLocked() { // 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) } @@ -7169,9 +7258,9 @@ func (mb *msgBlock) enableForWriting(fip bool) error { if mb.mfd != nil { return nil } - <-dios + mb.fs.dios.acquire() mfd, err := os.OpenFile(mb.mfn, os.O_CREATE|os.O_RDWR, defaultFilePerms) - dios <- struct{}{} + mb.fs.dios.release() if err != nil { return fmt.Errorf("error opening msg block file [%q]: %v", mb.mfn, err) } @@ -7279,11 +7368,13 @@ func (mb *msgBlock) writeMsgRecordLocked(rl, seq uint64, subj string, mhdr, msg // 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 @@ -7508,7 +7599,7 @@ func (fs *fileStore) checkLastBlock(rl uint64) (lmb *msgBlock, err error) { func (fs *fileStore) writeMsgRecord(seq uint64, ts int64, subj string, hdr, msg []byte) (uint64, error) { // Get size for this message. rl := fileStoreMsgSize(subj, hdr, msg) - if rl&hbit != 0 || rl > rlBadThresh { + if isFileStoreMsgTooLarge(rl) { return 0, ErrMsgTooLarge } // Grab our current last message block. @@ -7564,9 +7655,9 @@ func (mb *msgBlock) recompressOnDiskIfNeeded() error { // header, in which case we do nothing. // 2. The block will be uncompressed, in which case we will compress it // and then write it back out to disk, re-encrypting if necessary. - <-dios + mb.fs.dios.acquire() origBuf, err := os.ReadFile(mb.mfn) - dios <- struct{}{} + mb.fs.dios.release() if err != nil { return fmt.Errorf("failed to read original block from disk: %w", err) @@ -7620,9 +7711,9 @@ func (mb *msgBlock) atomicOverwriteFile(buf []byte, allowCompress bool) error { // operation if something goes wrong), create a new temporary file. We will // write out the new block here and then swap the files around afterwards // once everything else has succeeded correctly. - <-dios + mb.fs.dios.acquire() tmpFD, err := os.OpenFile(tmpFN, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultFilePerms) - dios <- struct{}{} + mb.fs.dios.release() if err != nil { return fmt.Errorf("failed to create temporary file: %w", err) @@ -7764,8 +7855,7 @@ func (fs *fileStore) syncBlocks() { fs.setWriteErr(err) } - var fsDmapLoaded bool - var fsDmap avl.SequenceSet + var fsDmap *interiorDeletes var markDirty bool for _, mb := range blks { @@ -7810,10 +7900,12 @@ func (fs *fileStore) syncBlocks() { } // 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. @@ -7822,9 +7914,8 @@ func (fs *fileStore) syncBlocks() { // 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 !fsDmapLoaded { - fsDmapLoaded = true - fsDmap = fs.deleteMap() + if fsDmap == nil { + fsDmap = deleteMap(blks) } fs.mu.RLock() mb.mu.Lock() @@ -7834,7 +7925,7 @@ func (fs *fileStore) syncBlocks() { fs.mu.RUnlock() continue } - err := mb.compactWithFloor(firstSeq, &fsDmap) + err := mb.compactWithFloor(firstSeq, fsDmap) // If this compact removed all raw bytes due to tombstone cleanup, schedule to remove. shouldRemove := mb.rbytes == 0 mb.mu.Unlock() @@ -7860,6 +7951,14 @@ func (fs *fileStore) syncBlocks() { } } + // 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() @@ -7869,9 +7968,9 @@ func (fs *fileStore) syncBlocks() { if mb.mfd != nil { fd = mb.mfd } else { - <-dios + fs.dios.acquire() fd, err = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms) - dios <- struct{}{} + fs.dios.release() didOpen = true if err != nil && !os.IsNotExist(err) { mb.mu.Unlock() @@ -7918,9 +8017,9 @@ func (fs *fileStore) syncBlocks() { fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile) var fd *os.File var err error - <-dios + fs.dios.acquire() fd, err = os.OpenFile(fn, os.O_RDWR, defaultFilePerms) - dios <- struct{}{} + fs.dios.release() if err != nil && !os.IsNotExist(err) { fs.setWriteErr(err) return @@ -8052,6 +8151,7 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error { // 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 { @@ -8202,6 +8302,7 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error { mb.cache.wp = int(lbuf) mb.ttls = ttls mb.schedules = schedules + mb.cache.registerRecycle() return nil } @@ -8231,9 +8332,9 @@ func (mb *msgBlock) writeAt(buf []byte, woff int64) (int, error) { mb.mockWriteErr = false return 0, errors.New("mock write error") } - <-dios + mb.fs.dios.acquire() n, err := mb.mfd.WriteAt(buf, woff) - dios <- struct{}{} + mb.fs.dios.release() return n, err } @@ -8393,9 +8494,9 @@ func (mb *msgBlock) fssNotLoaded() bool { // Lock should be held func (mb *msgBlock) openBlock() (*os.File, error) { // Gate with concurrent IO semaphore. - <-dios + mb.fs.dios.acquire() f, err := os.Open(mb.mfn) - dios <- struct{}{} + mb.fs.dios.release() return f, err } @@ -8440,9 +8541,9 @@ func (mb *msgBlock) loadBlock(buf []byte) ([]byte, error) { buf = getMsgBlockBuf(sz) } - <-dios + mb.fs.dios.acquire() n, err := io.ReadFull(f, buf[:sz]) - dios <- struct{}{} + mb.fs.dios.release() // On success capture raw bytes size. if err == nil { mb.rbytes = uint64(n) @@ -8609,6 +8710,7 @@ var ( 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") @@ -9066,18 +9168,20 @@ func (fs *fileStore) loadLastLocked(subj string, sm *StoreMsg) (lsm *StoreMsg, e return nil, err } } - var didLoad bool + 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 } - didLoad = true } lsm, err = mb.cacheLookup(l, sm) } - if didLoad { + if needsCleanup { mb.finishedWithCache() } mb.mu.Unlock() @@ -9596,6 +9700,12 @@ 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) } @@ -10093,30 +10203,39 @@ func (fs *fileStore) Purge() (uint64, error) { return fs.purge(0) } -func (fs *fileStore) purge(fseq uint64) (purged uint64, rerr error) { +func (fs *fileStore) purge(fseq uint64) (uint64, error) { if fs.isClosed() { return 0, ErrStoreClosed } - // Persist any write errors. - defer func() { - if rerr != nil { - fs.mu.Lock() - fs.setWriteErr(rerr) - fs.mu.Unlock() - } - }() - fs.mu.Lock() + 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 { - fs.mu.Unlock() - return 0, err + return 0, 0, err } - purged = fs.state.Msgs - rbytes := int64(fs.state.Bytes) + purged, bytes = fs.state.Msgs, fs.state.Bytes fs.state.FirstSeq = fs.state.LastSeq + 1 fs.state.FirstTime = time.Time{} @@ -10140,8 +10259,7 @@ func (fs *fileStore) purge(fseq uint64) (purged uint64, rerr error) { // Make sure we have a lmb to write to. if _, err := fs.newMsgBlockForWrite(); err != nil { - fs.mu.Unlock() - return purged, err + return purged, bytes, err } lmb := fs.lmb @@ -10153,18 +10271,15 @@ func (fs *fileStore) purge(fseq uint64) (purged uint64, rerr error) { // 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 { - fs.mu.Unlock() - return purged, err + 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 { - fs.mu.Unlock() - return purged, err + return purged, bytes, err } if err := lmb.closeFDs(); err != nil { - fs.mu.Unlock() - return purged, err + return purged, bytes, err } fs.blks = nil @@ -10181,37 +10296,32 @@ func (fs *fileStore) purge(fseq uint64) (purged uint64, rerr error) { mdir := filepath.Join(fs.fcfg.StoreDir, msgDir) ndir := filepath.Join(fs.fcfg.StoreDir, newMsgDir) pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir) - <-dios + 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 { - dios <- struct{}{} - fs.mu.Unlock() - return purged, err + fs.dios.release() + return purged, bytes, err } } else if !os.IsNotExist(err) { - dios <- struct{}{} - fs.mu.Unlock() - return purged, err + fs.dios.release() + return purged, bytes, err } if _, err := os.Stat(pdir); err == nil { if err = os.RemoveAll(pdir); err != nil { - dios <- struct{}{} - fs.mu.Unlock() - return purged, err + fs.dios.release() + return purged, bytes, err } } else if !os.IsNotExist(err) { - dios <- struct{}{} - fs.mu.Unlock() - return purged, err + fs.dios.release() + return purged, bytes, err } // Create directory to move the new tombstone to. if err := os.MkdirAll(ndir, defaultDirPerms); err != nil { - dios <- struct{}{} - fs.mu.Unlock() - return purged, err + 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 @@ -10220,54 +10330,38 @@ func (fs *fileStore) purge(fseq uint64) (purged uint64, rerr error) { b := filepath.Join(mdir, mbf) a := filepath.Join(ndir, mbf) if err := os.Rename(b, a); err != nil && !os.IsNotExist(err) { - dios <- struct{}{} - fs.mu.Unlock() - return purged, err + fs.dios.release() + return purged, bytes, err } } // Purge all remaining messages. if err := os.Rename(mdir, pdir); err != nil { - dios <- struct{}{} - fs.mu.Unlock() - return purged, err + 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 { - dios <- struct{}{} - fs.mu.Unlock() - return purged, err + fs.dios.release() + return purged, bytes, err } - dios <- struct{}{} + fs.dios.release() // Remove the purged messages directory asynchronously. go func() { - <-dios + fs.dios.acquire() _ = os.RemoveAll(pdir) - dios <- struct{}{} + fs.dios.release() }() // Re-enable writing for the lmb. lmb.mu.Lock() - err := lmb.enableForWriting(fs.fip) + err = lmb.enableForWriting(fs.fip) lmb.mu.Unlock() if err != nil { - 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 { - cb(-int64(purged), -rbytes, 0, _EMPTY_) + return purged, bytes, err } - return purged, nil + return purged, bytes, nil } // Lock and dios should be held. @@ -10322,48 +10416,56 @@ func (fs *fileStore) Compact(seq uint64) (uint64, error) { return fs.compact(seq) } -func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { +func (fs *fileStore) compact(seq uint64) (uint64, error) { if fs.isClosed() { return 0, ErrStoreClosed } - if seq == 0 { - return fs.purge(seq) - } + var err error + var purged, bytes uint64 fs.mu.Lock() - // Always return previous write errors. - if err := fs.werr; err != nil { - fs.mu.Unlock() - return 0, err + if seq == 0 || seq > fs.state.LastSeq { + purged, bytes, err = fs.purgeLocked(seq) + } else { + purged, bytes, err = fs.compactLocked(seq) } - // Same as purge all. - if lseq := fs.state.LastSeq; seq > lseq { + if err != nil { + fs.setWriteErr(err) fs.mu.Unlock() - return fs.purge(seq) + 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 { - fs.mu.Unlock() - return purged, nil + return 0, 0, nil } // We have to delete interior messages. smb := fs.selectMsgBlock(seq) if smb == nil { - fs.mu.Unlock() - return 0, nil + return 0, 0, nil } - // Persist any write errors. - defer func() { - if rerr != nil { - fs.mu.Lock() - fs.setWriteErr(rerr) - fs.mu.Unlock() - } - }() - - var bytes uint64 - // All msgblocks up to this one can be thrown away. var deleted int for _, mb := range fs.blks { @@ -10376,8 +10478,7 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { // Make sure we do subject cleanup as well. if err := mb.ensurePerSubjectInfoLoaded(); err != nil { mb.mu.Unlock() - fs.mu.Unlock() - return 0, err + return 0, 0, err } mb.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool { subj := bytesToString(bsubj) @@ -10390,14 +10491,12 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { err := mb.dirtyCloseWithRemove(true) mb.mu.Unlock() if err != nil { - fs.mu.Unlock() - return purged, err + return purged, bytes, err } deleted++ } var smv StoreMsg - var err error var tombs []msgId smb.mu.Lock() @@ -10411,11 +10510,10 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { if smb.cacheNotLoaded() { if err = smb.loadMsgsWithLock(); err != nil { smb.mu.Unlock() - fs.mu.Unlock() - return purged, err + return purged, bytes, err } defer func() { - // The lock is released once we get here, so need to re-acquire. + // The block lock is released once we get here, so need to re-acquire. smb.mu.Lock() smb.finishedWithCache() smb.mu.Unlock() @@ -10442,8 +10540,7 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { // Update fss if _, err := smb.removeSeqPerSubject(sm.subj, mseq); err != nil { smb.mu.Unlock() - fs.mu.Unlock() - return purged, err + return purged, bytes, err } fs.removePerSubject(sm.subj) tombs = append(tombs, msgId{sm.seq, sm.ts}) @@ -10456,8 +10553,7 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { if smb != fs.lmb { if err = smb.dirtyCloseWithRemove(true); err != nil { smb.mu.Unlock() - fs.mu.Unlock() - return purged, err + return purged, bytes, err } deleted++ } else { @@ -10490,8 +10586,7 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { moff, _, _, err = smb.slotInfo(int(atomic.LoadUint64(&smb.first.seq) - smb.cache.fseq)) if err != nil { smb.mu.Unlock() - fs.mu.Unlock() - return purged, err + return purged, bytes, err } else if moff >= uint32(len(smb.cache.buf)) { goto SKIP } @@ -10507,8 +10602,7 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { originalSize := len(nbuf) if nbuf, err = smb.cmp.Compress(nbuf); err != nil { smb.mu.Unlock() - fs.mu.Unlock() - return purged, err + return purged, bytes, err } meta := &CompressionInfo{ Algorithm: smb.cmp, @@ -10522,8 +10616,7 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { bek, err := genBlockEncryptionKey(smb.fs.fcfg.Cipher, smb.seed, smb.nonce) if err != nil { smb.mu.Unlock() - fs.mu.Unlock() - return purged, err + return purged, bytes, err } // For future writes make sure to set smb.bek to keep counter correct. smb.bek = bek @@ -10532,20 +10625,18 @@ func (fs *fileStore) compact(seq uint64) (purged uint64, rerr error) { // We will write to a new file and mv/rename it in case of failure. mfn := filepath.Join(smb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(newScan, smb.index)) - <-dios + fs.dios.acquire() err = os.WriteFile(mfn, nbuf, defaultFilePerms) - dios <- struct{}{} + fs.dios.release() if err != nil { _ = os.Remove(mfn) smb.mu.Unlock() - fs.mu.Unlock() - return purged, err + return purged, bytes, err } if err = os.Rename(mfn, smb.mfn); err != nil { _ = os.Remove(mfn) smb.mu.Unlock() - fs.mu.Unlock() - return purged, err + return purged, bytes, err } // Make sure to remove fss state. @@ -10565,15 +10656,13 @@ SKIP: if len(tombs) > 0 { for _, tomb := range tombs { if err = fs.writeTombstoneNoFlush(tomb.seq, tomb.ts); err != nil { - fs.mu.Unlock() - return purged, err + 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 { - fs.mu.Unlock() - return purged, err + return purged, bytes, err } } } @@ -10613,19 +10702,8 @@ SKIP: // after we release the lock. os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)) fs.dirty++ - cb := fs.scb - fs.mu.Unlock() - - // Force a new index.db to be written. - if purged > 0 { - fs.forceWriteFullState() - } - - if cb != nil && purged > 0 { - cb(-int64(purged), -int64(bytes), 0, _EMPTY_) - } - return purged, err + return purged, bytes, nil } // Will completely reset our store. @@ -11271,10 +11349,13 @@ func (mb *msgBlock) removeSeqPerSubject(subj string, seq uint64) (uint64, error) // 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() } @@ -11411,16 +11492,22 @@ func (mb *msgBlock) generatePerSubjectInfo() error { return nil } + needsCleanup := mb.cache == nil if mb.cacheNotLoaded() { if err := mb.loadMsgsWithLock(); err != nil { + if needsCleanup { + mb.finishedWithCache() + } return err } - // indexCacheBuf can produce fss now, so if non-nil we are good. - if mb.fss != nil { - return nil - } + } + 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() @@ -11516,19 +11603,19 @@ func (fs *fileStore) populateGlobalPerSubjectInfo(mb *msgBlock) error { // 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(dir string) error { - <-dios +func removeAllWithRetry(dios *diskIOSemaphore, dir string) error { + dios.acquire() err := os.RemoveAll(dir) - dios <- struct{}{} + dios.release() if err == nil { return nil } ttl := time.Now().Add(time.Second) for time.Now().Before(ttl) { time.Sleep(10 * time.Millisecond) - <-dios + dios.acquire() err = os.RemoveAll(dir) - dios <- struct{}{} + dios.release() if err == nil { return nil } @@ -11637,11 +11724,11 @@ func (fs *fileStore) Delete(inline bool) error { // 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(ndir); err != nil { + if err := removeAllWithRetry(fs.dios, ndir); err != nil { return err } } else { - go removeAllWithRetry(ndir) + go removeAllWithRetry(fs.dios, ndir) } return nil } @@ -11937,10 +12024,10 @@ func (fs *fileStore) _writeFullState(force bool) error { // Write our update index.db // Protect with dios. - <-dios + fs.dios.acquire() err := os.WriteFile(fn, buf, defaultFilePerms) // if file system is not writable isPermissionError is set to true - dios <- struct{}{} + fs.dios.release() if err != nil { return err } @@ -12345,51 +12432,61 @@ func (fs *fileStore) EncodedStreamState(failed uint64) ([]byte, error) { } } - // Encoded is Msgs, Bytes, FirstSeq, LastSeq, Failed, NumDeleted and optional DeletedBlocks - var buf [1024]byte - buf[0], buf[1] = streamStateMagic, streamStateVersion - n := hdrLen - n += binary.PutUvarint(buf[n:], fs.state.Msgs) - n += binary.PutUvarint(buf[n:], fs.state.Bytes) - n += binary.PutUvarint(buf[n:], fs.state.FirstSeq) - n += binary.PutUvarint(buf[n:], fs.state.LastSeq) - n += binary.PutUvarint(buf[n:], failed) - n += binary.PutUvarint(buf[n:], uint64(numDeleted)) - - b := buf[0:n] + // 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 dbs DeleteBlocks if numDeleted > 0 { - var scratch [4 * 1024]byte - fs.readLockAllMsgBlocks() defer fs.readUnlockAllMsgBlocks() + var sz int + dbs, sz = fs.deleteBlocks() + total += sz + } - for _, db := range fs.deleteBlocks() { - switch db := db.(type) { - case *DeleteRange: - first, _, num := db.State() - scratch[0] = runLengthMagic - i := 1 - i += binary.PutUvarint(scratch[i:], first) - i += binary.PutUvarint(scratch[i:], num) - b = append(b, scratch[0:i]...) - case *avl.SequenceSet: - buf := db.Encode(scratch[:0]) - b = append(b, buf...) - default: - return nil, errors.New("no impl") + b := make([]byte, 0, total) + b = append(b, streamStateMagic, streamStateVersion) + 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) + 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. +// and gaps between blocks, as well as their total binary encoded size. // All blocks should be at least read locked. -func (fs *fileStore) deleteBlocks() DeleteBlocks { - var dbs DeleteBlocks +func (fs *fileStore) deleteBlocks() (dbs DeleteBlocks, sz int) { var prevLast uint64 var prevRange *DeleteRange var msgsSinceGap bool @@ -12404,46 +12501,82 @@ func (fs *fileStore) deleteBlocks() DeleteBlocks { // 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 + return dbs, sz } -// deleteMap returns all interior deletes for each block based on the mb.dmap. -// Specifically, this will not contain any deletes for blocks that have 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. -func (fs *fileStore) deleteMap() (dmap avl.SequenceSet) { - fs.mu.RLock() - defer fs.mu.RUnlock() +// 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. +} - fs.readLockAllMsgBlocks() - defer fs.readUnlockAllMsgBlocks() +// 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) +} - for _, mb := range fs.blks { - if mb.dmap.Size() > 0 { - mb.dmap.Range(func(seq uint64) bool { - dmap.Insert(seq) - return true - }) +// 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 dmap + return &v } // SyncDeleted will make sure this stream has same deleted state as dbs. @@ -12467,7 +12600,7 @@ func (fs *fileStore) SyncDeleted(dbs DeleteBlocks) error { lseq := fs.state.LastSeq fs.readLockAllMsgBlocks() - mdbs := fs.deleteBlocks() + 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 { @@ -12530,12 +12663,43 @@ func pruneDeleteBlock(db DeleteBlock, blocks DeleteBlocks) (bool, DeleteBlocks) } if aFirst == bFirst && aLast == bLast && aNum == bNum { - return true, blocks[1:] + // 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 //////////////////////////////////////////////////////////////////////////////// @@ -13199,29 +13363,6 @@ func (o *consumerFileStore) encryptState(buf []byte) ([]byte, error) { return o.aek.Seal(nonce, nonce, buf, nil), nil } -// Used to limit number of disk IO calls in flight since they could all be blocking an OS thread. -// https://github.com/nats-io/nats-server/issues/2742 -var dios chan struct{} - -// Used to setup our simplistic counting semaphore using buffered channels. -// golang.org's semaphore seemed a bit heavy. -func init() { - // Limit ourselves to a sensible number of blocking I/O calls. Range between - // 4-16 concurrent disk I/Os based on CPU cores, or 50% of cores if greater - // than 32 cores. - mp := runtime.GOMAXPROCS(-1) - nIO := min(16, max(4, mp)) - if mp > 32 { - // If the system has more than 32 cores then limit dios to 50% of cores. - nIO = max(16, min(mp, mp/2)) - } - dios = make(chan struct{}, nIO) - // Fill it up to start. - for i := 0; i < nIO; i++ { - dios <- struct{}{} - } -} - func (o *consumerFileStore) writeState(buf []byte) error { // Check if we have the index file open. o.mu.Lock() @@ -13402,9 +13543,9 @@ func (o *consumerFileStore) stateWithCopyLocked(doCopy bool) (*ConsumerState, er } // Read the state in here from disk.. - <-dios + o.fs.dios.acquire() buf, err := os.ReadFile(o.ifn) - dios <- struct{}{} + o.fs.dios.release() if err != nil && !os.IsNotExist(err) { return nil, err @@ -13665,7 +13806,7 @@ func (o *consumerFileStore) delete(streamDeleted bool) error { // If our stream was not deleted this will remove the directories. if odir != _EMPTY_ && !streamDeleted { - if err := removeAllWithRetry(odir); err != nil { + if err := removeAllWithRetry(o.fs.dios, odir); err != nil { return err } } @@ -13806,27 +13947,25 @@ func (alg StoreCompression) Decompress(buf []byte) ([]byte, error) { // sets O_SYNC on the open file if SyncAlways is set. The dios semaphore is // handled automatically by this function, so don't wrap calls to it in dios. func (fs *fileStore) writeFileWithOptionalSync(name string, data []byte, perm fs.FileMode) error { - return writeAtomically(name, data, perm, fs.syncAlways.Load()) + return writeAtomically(fs.dios, name, data, perm, fs.syncAlways.Load()) } -func writeFileWithSync(name string, data []byte, perm fs.FileMode) error { - return writeAtomically(name, data, perm, true) +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(name string, data []byte, perm fs.FileMode, sync bool) error { +func writeAtomically(dios *diskIOSemaphore, name string, data []byte, perm fs.FileMode, sync bool) error { tmp := name + ".tmp" flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC if sync { flags = flags | os.O_SYNC } - <-dios - defer func() { - dios <- struct{}{} - }() + dios.acquire() + defer dios.release() f, err := os.OpenFile(tmp, flags, perm) if err != nil { return err @@ -13848,18 +13987,53 @@ func writeAtomically(name string, data []byte, perm fs.FileMode, sync bool) erro if sync && canFsyncDirectories { // To ensure that the file rename was persisted on all filesystems, // also try to flush the directory metadata. - var d *os.File - if d, err = os.Open(filepath.Dir(name)); err != nil { + if err = syncDir(name); err != nil { return err } - if err = d.Sync(); err != nil { - // Close fd, but ignore its error since sync takes precedence. - _ = d.Close() - 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 } - if err = d.Close(); err != 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(name); err != nil { return err } } return nil } + +// Dios should already be held. +func syncDir(name string) error { + var d *os.File + var err error + if d, err = os.Open(filepath.Dir(name)); 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/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go index 73ae3ad802..80f5f197ab 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go @@ -716,7 +716,7 @@ func (s *Server) disableJetStream(deleteState bool) error { func (s *Server) enableJetStreamAccounts() error { // Reuse the same task workers across all accounts, so that we don't explode // with a large number of goroutines on multi-account systems. - tq := parallelTaskQueue(len(dios)) + tq := parallelTaskQueue(min(64, s.diskIOSemaphore().cap())) defer close(tq) // If we have no configured accounts setup then setup imports on global account. diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_api.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_api.go index 5d10e57fec..f19e636cd1 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_api.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_api.go @@ -2925,7 +2925,7 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac 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(encodeDeleteConsumerAssignment(ca)); err != nil { + 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)) @@ -2935,7 +2935,7 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac nc++ } sa := &streamAssignment{Group: osa.Group, Config: osa.Config, Subject: subject, Client: osa.Client, Created: osa.Created} - if err = meta.Propose(encodeDeleteStreamAssignment(sa)); err != nil { + 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)) @@ -4186,7 +4186,7 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) return } - if !IsValidSubject(req.DeliverSubject) { + if !IsValidPublishSubject(req.DeliverSubject) { resp.Error = NewJSSnapshotDeliverSubjectInvalidError() s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) return @@ -4230,7 +4230,10 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun }) // Now do the real streaming. - s.streamSnapshot(acc, mset, sr, &req) + if err := s.streamSnapshot(acc, mset, sr, &req); err != nil { + s.Warnf("Snapshot of stream '%s > %s' failed: %v", mset.jsa.account.Name, mset.name(), err) + return + } end := time.Now().UTC() @@ -4263,7 +4266,7 @@ 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) { +func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult, req *JSApiStreamSnapshotRequest) error { chunkSize, wndSize := req.ChunkSize, req.WindowSize if chunkSize == 0 { chunkSize = defaultSnapshotChunkSize @@ -4288,7 +4291,9 @@ func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult, // Check interest for the snapshot deliver subject. inch := make(chan bool, 1) - acc.sl.RegisterNotification(req.DeliverSubject, inch) + 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 { @@ -4356,6 +4361,7 @@ func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult, done: mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + return nil } // For determining consumer request type. @@ -5287,7 +5293,7 @@ func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account setStaticConsumerMetadata(nca.Config) eca := encodeAddConsumerAssignment(nca) - if err = meta.Propose(eca); err != nil { + if err = meta.Propose(cc.term, eca); err != nil { js.mu.Unlock() return } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go index 1ac6636d3e..1968ce8a05 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go @@ -130,8 +130,8 @@ func getBatchStoreDir(storeDir, streamName, batchId string) (string, string) { func newBatchStore(mset *stream, batchId string, replicas int, storage StorageType, storeDir, streamName string) (StreamStore, error) { if replicas == 1 && storage == FileStorage { bname, storeDir := getBatchStoreDir(storeDir, streamName, batchId) - fcfg := FileStoreConfig{AsyncFlush: true, BlockSize: defaultLargeBlockSize, StoreDir: storeDir} s := mset.srv + fcfg := FileStoreConfig{AsyncFlush: true, BlockSize: defaultLargeBlockSize, StoreDir: storeDir, srv: s} prf := s.jsKeyGen(s.getOpts().JetStreamKey, mset.acc.Name) if prf != nil { // We are encrypted here, fill in correct cipher selection. @@ -546,6 +546,12 @@ func checkMsgHeadersPreClusteredProposal( var incr *big.Int var hasSchedule bool + // Do this before staging any proposal state. All clustered publish paths, + // including atomic and fast batches, use this helper. + if mset.store.Type() == FileStorage && isFileStoreMsgTooLarge(fileStoreMsgSize(subject, hdr, msg)) { + return hdr, msg, 0, NewJSStreamStoreFailedError(ErrMsgTooLarge), ErrMsgTooLarge + } + // Some header checks must be checked pre proposal. if len(hdr) > 0 { // Since we encode header len as u16 make sure we do not exceed. @@ -689,7 +695,7 @@ func checkMsgHeadersPreClusteredProposal( if sources == nil { sources = map[string]map[string]string{} } - if _, ok = sources[origStream]; !ok { + if sources[origStream] == nil { sources[origStream] = map[string]string{} } prevVal := sources[origStream][origSubj] @@ -758,7 +764,7 @@ func checkMsgHeadersPreClusteredProposal( // Allow override of the subject used for the check. seqSubj := subject if optSubj := getExpectedLastSeqPerSubjectForSubject(hdr); optSubj != _EMPTY_ { - seqSubj = optSubj + seqSubj = copyString(optSubj) } // The subject is already written to in this batch, we can't allow @@ -1071,11 +1077,11 @@ func recalculateClusteredSeq(mset *stream, needStreamLock bool) (lseq uint64) { // mset.clMu lock must be held. func commitSingleMsg( diff *batchStagedDiff, mset *stream, subject string, reply string, hdr []byte, msg []byte, name string, - jsa *jsAccount, mt *msgTrace, node RaftNode, replicas int, lseq uint64, + jsa *jsAccount, mt *msgTrace, node RaftNode, term uint64, replicas int, lseq uint64, ) error { // Do proposal. esm := encodeStreamMsgAllowCompress(subject, reply, hdr, msg, mset.clseq, time.Now().UnixNano(), false) - if err := node.Propose(esm); err != nil { + if err := node.Propose(term, esm); err != nil { return err } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go index b1d3ae19de..e97b36a992 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go @@ -60,6 +60,8 @@ type jetStreamCluster 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. @@ -1038,7 +1040,7 @@ func (js *jetStream) setupMetaGroup() error { cfg.Observer = s.canExtendOtherDomain() && s.getOpts().JetStreamExtHint != jsNoExtend var bootstrap bool - if ps, err := readPeerState(storeDir); err != nil { + if ps, err := readPeerState(s.diskIOSemaphore(), storeDir); err != nil { s.Noticef("JetStream cluster bootstrapping") bootstrap = true peers := s.ActivePeers() @@ -1072,7 +1074,7 @@ func (js *jetStream) setupMetaGroup() error { // To track possible configuration changes, responsible for an altered value of cfg.Observer, // set extension state to undetermined. ps.domainExt = extUndetermined - if err := writePeerState(storeDir, ps); err != nil { + if err := writePeerState(s.diskIOSemaphore(), storeDir, ps); err != nil { return err } } @@ -1916,9 +1918,10 @@ func (js *jetStream) monitorCluster() { } aq.recycle(&ces) - case isLeader = <-lch: + case lc := <-lch: + isLeader = lc.isLeader // Process the change. - js.processLeaderChange(isLeader) + js.processLeaderChange(isLeader, lc.term) if isLeader { s.sendInternalMsgLocked(serverStatsPingReqSubj, _EMPTY_, nil, nil) // Install a snapshot as we become leader. @@ -2013,10 +2016,12 @@ type writeableStreamAssignment struct { 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, ok := js.cluster.streams[accName][streamName]; ok { + if sa := js.streamAssignmentOrInflight(accName, streamName); sa != nil { return *sa.Config, true } return StreamConfig{}, false @@ -2044,7 +2049,12 @@ func (js *jetStream) applyMetaSnapshot(buf []byte, ru *recoveryUpdates, isRecove 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) { + // Stream was recreated. + saDel = append(saDel, sa) + saAdd = append(saAdd, nsa) } else { saChk = append(saChk, nsa) } @@ -2069,10 +2079,10 @@ func (js *jetStream) applyMetaSnapshot(buf []byte, ru *recoveryUpdates, isRecove } if osa := js.streamAssignment(sa.Client.serviceAccount(), sa.Config.Name); osa != nil { for _, ca := range osa.consumers { - // Consumer was either removed, or recreated with a different raft group. + // Consumer was either removed or recreated. if nca := sa.consumers[ca.Name]; nca == nil { caDel = append(caDel, ca) - } else if nca.Group != nil && ca.Group != nil && nca.Group.Name != ca.Group.Name { + } else if !nca.Created.Equal(ca.Created) { caDel = append(caDel, ca) } } @@ -2330,6 +2340,9 @@ func (js *jetStream) collectStreamAndConsumerChanges(c RaftNodeCheckpoint, strea 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: @@ -2497,7 +2510,7 @@ func (js *jetStream) processAddPeer(peer string) { csa := sa.copyGroup() csa.Group.Peers = append(csa.Group.Peers, peer) // Send our proposal for this csa. Also use same group definition for all the consumers as well. - if err := cc.meta.Propose(encodeAddStreamAssignment(csa)); err != nil { + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(csa)); err != nil { return } cc.trackInflightStreamProposal(accName, csa, false) @@ -2509,7 +2522,7 @@ func (js *jetStream) processAddPeer(peer string) { if ca.Config.Durable != _EMPTY_ || len(ca.Group.Peers) > 1 { cca := ca.copyGroup() cca.Group.Peers = csa.Group.Peers - if err := cc.meta.Propose(encodeAddConsumerAssignment(cca)); err != nil { + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(cca)); err != nil { return } cc.trackInflightConsumerProposal(accName, csa.Config.Name, cca, false) @@ -2596,7 +2609,7 @@ func (js *jetStream) removePeerFromStreamLocked(sa *streamAssignment, peer strin } // Send our proposal for this csa. Also use same group definition for all the consumers as well. - if err := cc.meta.Propose(encodeAddStreamAssignment(csa)); err != nil { + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(csa)); err != nil { return false } cc.trackInflightStreamProposal(accName, csa, false) @@ -2609,13 +2622,13 @@ func (js *jetStream) removePeerFromStreamLocked(sa *streamAssignment, peer strin if ca.Config.Durable != _EMPTY_ { cca := ca.copyGroup() cca.Group.Peers, cca.Group.Preferred = rg.Peers, _EMPTY_ - if err := cc.meta.Propose(encodeAddConsumerAssignment(cca)); err != nil { + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(cca)); err != nil { return false } cc.trackInflightConsumerProposal(accName, csa.Config.Name, cca, false) } else if ca.Group.isMember(peer) { // These are ephemerals. Check to see if we deleted this peer. - if err := cc.meta.Propose(encodeDeleteConsumerAssignment(ca)); err != nil { + if err := cc.meta.Propose(cc.term, encodeDeleteConsumerAssignment(ca)); err != nil { return false } cc.trackInflightConsumerProposal(accName, csa.Config.Name, ca, true) @@ -2715,6 +2728,9 @@ func (js *jetStream) applyMetaEntries(entries []*Entry, ru *recoveryUpdates) (bo } } 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:]) @@ -2986,7 +3002,7 @@ retry: cfg := &RaftConfig{Name: rgName, Store: storeDir, Log: store, Track: true, Recovering: recovering, ScaleUp: rgScaleUp} - if _, err := readPeerState(storeDir); err != nil { + if _, err := readPeerState(s.diskIOSemaphore(), storeDir); err != nil { s.bootstrapRaftNode(cfg, rgPeers, true) } @@ -3500,9 +3516,10 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps doSnapshot(false) } - case isLeader = <-lch: + case lc := <-lch: + isLeader = lc.isLeader // Process our leader change. - js.processStreamLeaderChange(mset, isLeader) + js.processStreamLeaderChange(mset, isLeader, lc.term) if isLeader { if mset != nil && n != nil && sendSnapshot && !isRecovering { @@ -3677,10 +3694,12 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps 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) + 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. @@ -3695,7 +3714,12 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps } for _, o := range consumers { name, cfg := o.String(), o.config() - rg := cc.createGroupForConsumer(&cfg, sa) + rg, err := cc.createGroupForConsumer(&cfg, sa) + if err != nil { + s.Warnf("Could not create group for consumer '%s > %s > %s': %v", + sa.Client.serviceAccount(), sa.Config.Name, name, err) + continue + } // Pick a preferred leader. rg.setPreferred(s) @@ -3986,6 +4010,9 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco } 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:]) @@ -4648,7 +4675,7 @@ func (s *Server) replicas(node RaftNode) []*PeerInfo { } // Process a leader change for the clustered stream. -func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) { +func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool, term uint64) { if mset == nil { return } @@ -4719,7 +4746,7 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) { } // Tell stream to switch leader status. - mset.setLeader(isLeader) + mset.setLeader(isLeader, term) if !isLeader || hasResponded { return @@ -5303,7 +5330,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss // If the stream is scaled down, there is a chance we weren't already the leader. if isLeader && numReplicas == 1 && oldNumReplicas > 1 { - js.processStreamLeaderChange(mset, true) + js.processStreamLeaderChange(mset, true, 0) } // Check for missing syncSubject bug. @@ -5564,7 +5591,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, b) return } - js.processStreamLeaderChange(mset, true) + 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. @@ -5581,7 +5608,12 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme for _, o := range consumers { name, cfg := o.String(), o.config() - rg := cc.createGroupForConsumer(&cfg, sa) + rg, err := cc.createGroupForConsumer(&cfg, sa) + if err != nil { + s.Warnf("Could not create group for consumer '%s > %s > %s': %v", + sa.Client.serviceAccount(), sa.Config.Name, name, err) + continue + } // Place our initial state here as well for assignment distribution. ca := &consumerAssignment{ @@ -5623,7 +5655,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme } }) } else { - js.processStreamLeaderChange(mset, true) + js.processStreamLeaderChange(mset, true, 0) } } } @@ -6220,7 +6252,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s func() { defer s.grWG.Done() defer o.clearMonitorRunning() - err = o.setLeader(true) + err = o.setLeader(true, 0) var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} if err != nil { resp.Error = NewJSConsumerCreateError(err, Unless(err)) @@ -6260,7 +6292,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s func() { defer s.grWG.Done() defer o.clearMonitorRunning() - js.processConsumerLeaderChangeWithAssignment(o, cca, true) + js.processConsumerLeaderChangeWithAssignment(o, cca, true, 0) }, pprofLabels{ "type": "consumer", @@ -6695,13 +6727,14 @@ func (js *jetStream) monitorConsumer(o *consumer, ca *consumerAssignment) { } aq.recycle(&ces) - case isLeader = <-lch: + case lc := <-lch: + isLeader = lc.isLeader if recovering && !isLeader { js.setConsumerAssignmentRecovering(ca) } // Process the change. - if err := js.processConsumerLeaderChange(o, isLeader); err == nil { + 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 @@ -6900,6 +6933,9 @@ func (js *jetStream) applyConsumerEntries(o *consumer, ce *CommittedEntry, isLea // 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:]) @@ -6950,9 +6986,11 @@ func (js *jetStream) applyConsumerEntries(o *consumer, ce *CommittedEntry, isLea return err } case updateSkipOp: + sseq, err := decodeSkipUpdate(buf[1:]) + if err != nil { + return err + } o.mu.Lock() - var le = binary.LittleEndian - sseq := le.Uint64(buf[1:]) if !o.isLeader() && sseq > o.sseq { o.sseq = sseq } @@ -6964,10 +7002,11 @@ func (js *jetStream) applyConsumerEntries(o *consumer, ce *CommittedEntry, isLea } o.mu.Unlock() case resetSeqOp: + sseq, reply, err := decodeResetUpdate(buf[1:]) + if err != nil { + return err + } o.mu.Lock() - var le = binary.LittleEndian - sseq := le.Uint64(buf[1:9]) - reply := string(buf[9:]) o.resetLocalStartingSeq(sseq) if o.store != nil { o.store.Reset(sseq - 1) @@ -7086,16 +7125,19 @@ func (o *consumer) processReplicatedAck(dseq, sseq uint64) error { 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 { + if dseq, n = binary.Uvarint(buf); n <= 0 { return 0, 0, errBadAckUpdate } bi += n - if sseq, n = binary.Uvarint(buf[bi:]); n < 0 { + if sseq, n = binary.Uvarint(buf[bi:]); n <= 0 { return 0, 0, errBadAckUpdate } return dseq, sseq, nil @@ -7103,29 +7145,43 @@ func decodeAckUpdate(buf []byte) (dseq, sseq uint64, err error) { func decodeDeliveredUpdate(buf []byte) (dseq, sseq, dc uint64, ts int64, err error) { var bi, n int - if dseq, n = binary.Uvarint(buf); n < 0 { + 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 { + 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 { + 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 { + if ts, n = binary.Varint(buf[bi:]); n <= 0 { return 0, 0, 0, 0, errBadDeliveredUpdate } return dseq, sseq, dc, ts, nil } -func (js *jetStream) processConsumerLeaderChange(o *consumer, isLeader bool) error { - return js.processConsumerLeaderChangeWithAssignment(o, nil, isLeader) +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) error { +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() @@ -7173,7 +7229,7 @@ func (js *jetStream) processConsumerLeaderChangeWithAssignment(o *consumer, ca * } // Tell consumer to switch leader status. - if lerr := o.setLeader(isLeader); lerr != nil && err == nil { + if lerr := o.setLeader(isLeader, term); lerr != nil && err == nil { err = lerr } @@ -7356,20 +7412,21 @@ func (js *jetStream) processStreamAssignmentResults(sub *subscription, c *client // Pick a new preferred leader. rg.setPreferred(s) // Get rid of previous attempt. - if err := cc.meta.Propose(encodeDeleteStreamAssignment(sa)); err != nil { + if err := cc.meta.Propose(cc.term, encodeDeleteStreamAssignment(sa)); err != nil { return } cc.trackInflightStreamProposal(result.Account, sa, true) // Propose new. - sa.Group, sa.err = rg, nil - if err := cc.meta.Propose(encodeAddStreamAssignment(sa)); err != nil { + 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, sa, false) + 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. - sa.reassigning = true + nsa.reassigning = true return } } @@ -7397,7 +7454,7 @@ func (js *jetStream) processStreamAssignmentResults(sub *subscription, c *client } 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(encodeDeleteStreamAssignment(sa)); err != nil { + if err := cc.meta.Propose(cc.term, encodeDeleteStreamAssignment(sa)); err != nil { return } cc.trackInflightStreamProposal(result.Account, sa, true) @@ -7535,7 +7592,7 @@ func (s *Server) sendDomainLeaderElectAdvisory() { s.publishAdvisory(nil, JSAdvisoryDomainLeaderElected, adv) } -func (js *jetStream) processLeaderChange(isLeader bool) { +func (js *jetStream) processLeaderChange(isLeader bool, term uint64) { if js == nil { return } @@ -7543,8 +7600,6 @@ func (js *jetStream) processLeaderChange(isLeader bool) { if s == nil { return } - // Update our server atomic. - s.isMetaLeader.Store(isLeader) if isLeader { s.Noticef("Self is new JetStream cluster metadata leader") @@ -7568,6 +7623,10 @@ func (js *jetStream) processLeaderChange(isLeader bool) { 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 @@ -7601,7 +7660,7 @@ func (js *jetStream) processLeaderChange(isLeader bool) { 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(encodeUpdateStreamAssignment(nsa)); err != nil { + if err := cc.meta.Propose(cc.term, encodeUpdateStreamAssignment(nsa)); err != nil { return } cc.trackInflightStreamProposal(acc, nsa, false) @@ -8239,7 +8298,7 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject, } // Sync subject for post snapshot sync. sa := &streamAssignment{Group: rg, Sync: syncSubject, Config: cfg, Subject: subject, Reply: reply, Client: ci, Created: time.Now().UTC()} - if err := cc.meta.Propose(encodeAddStreamAssignment(sa)); err != nil { + 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 @@ -8683,14 +8742,14 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su syncSubject = syncSubjForStream() } sa := &streamAssignment{Group: rg, Sync: syncSubject, Created: osa.Created, Config: newCfg, Subject: subject, Reply: reply, Client: ci} - if err := meta.Propose(encodeUpdateStreamAssignment(sa)); err != nil { + if err := meta.Propose(cc.term, encodeUpdateStreamAssignment(sa)); err != nil { return } cc.trackInflightStreamProposal(acc.Name, sa, false) // Process any staged consumers. for _, ca := range consumers { - if err := meta.Propose(encodeAddConsumerAssignment(ca)); err != nil { + if err := meta.Propose(cc.term, encodeAddConsumerAssignment(ca)); err != nil { return } cc.trackInflightConsumerProposal(acc.Name, sa.Config.Name, ca, false) @@ -8719,7 +8778,7 @@ func (s *Server) jsClusteredStreamDeleteRequest(ci *ClientInfo, acc *Account, st } sa := &streamAssignment{Group: osa.Group, Config: osa.Config, Subject: subject, Reply: reply, Client: ci, Created: osa.Created} - if err := cc.meta.Propose(encodeDeleteStreamAssignment(sa)); err != nil { + if err := cc.meta.Propose(cc.term, encodeDeleteStreamAssignment(sa)); err != nil { return } cc.trackInflightStreamProposal(acc.Name, sa, true) @@ -8735,7 +8794,7 @@ func (s *Server) jsClusteredStreamPurgeRequest( preq *JSApiStreamPurgeRequest, ) { js, cc := s.getJetStreamCluster() - if js == nil || cc == nil { + if js == nil || cc == nil || mset == nil { return } @@ -8750,17 +8809,16 @@ func (s *Server) jsClusteredStreamPurgeRequest( } if n := sa.Group.node; n != nil { - sp := &streamPurge{Stream: stream, LastSeq: mset.state().LastSeq, Subject: subject, Reply: reply, Client: ci, Request: preq} - n.Propose(encodeStreamPurge(sp)) + 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() - if mset == nil { - return - } - var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} purged, err := mset.purge(preq) if err != nil { @@ -8824,7 +8882,7 @@ func (s *Server) jsClusteredStreamRestoreRequest( 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(encodeAddStreamAssignment(sa)); err != nil { + if err := cc.meta.Propose(cc.term, encodeAddStreamAssignment(sa)); err != nil { return } cc.trackInflightStreamProposal(ci.serviceAccount(), sa, false) @@ -9249,7 +9307,7 @@ func (s *Server) jsClusteredConsumerDeleteRequest(ci *ClientInfo, acc *Account, 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(encodeDeleteConsumerAssignment(ca)); err != nil { + if err := cc.meta.Propose(cc.term, encodeDeleteConsumerAssignment(ca)); err != nil { return } cc.trackInflightConsumerProposal(acc.Name, stream, ca, true) @@ -9284,9 +9342,12 @@ func (s *Server) jsClusteredMsgDeleteRequest(ci *ClientInfo, acc *Account, mset // Check for single replica items. if n := sa.Group.node; n != nil { - md := streamMsgDelete{Seq: req.Seq, NoErase: req.NoErase, Stream: stream, Subject: subject, Reply: reply, Client: ci} - n.Propose(encodeMsgDelete(&md)) + 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() @@ -9393,9 +9454,9 @@ func decodeDeleteRange(buf []byte) (*DeleteRange, error) { } // createGroupForConsumer will create a new group from same peer set as the stream. -func (cc *jetStreamCluster) createGroupForConsumer(cfg *ConsumerConfig, sa *streamAssignment) *raftGroup { +func (cc *jetStreamCluster) createGroupForConsumer(cfg *ConsumerConfig, sa *streamAssignment) (*raftGroup, *selectPeerError) { if len(sa.Group.Peers) == 0 || cfg.Replicas > len(sa.Group.Peers) { - return nil + return nil, &selectPeerError{misc: true} } replicas := cfg.replicas(sa.Config) @@ -9413,14 +9474,14 @@ func (cc *jetStreamCluster) createGroupForConsumer(cfg *ConsumerConfig, sa *stre } if quorum := replicas/2 + 1; quorum > len(active) { // Not enough active to satisfy the request. - return nil + 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 + 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] }) @@ -9430,7 +9491,7 @@ func (cc *jetStreamCluster) createGroupForConsumer(cfg *ConsumerConfig, sa *stre if cfg.MemoryStorage { storage = MemoryStorage } - return &raftGroup{Name: groupNameForConsumer(peers, storage), Storage: storage, Peers: peers} + return &raftGroup{Name: groupNameForConsumer(peers, storage), Storage: storage, Peers: peers}, nil } // jsClusteredConsumerRequest is first point of entry to create a consumer in clustered mode. @@ -9615,8 +9676,8 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return } - rg := cc.createGroupForConsumer(cfg, sa) - if rg == nil { + rg, err := cc.createGroupForConsumer(cfg, sa) + if err != nil { resp.Error = NewJSInsufficientResourcesError() s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return @@ -9792,7 +9853,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec } // Do formal proposal. - if err := cc.meta.Propose(encodeAddConsumerAssignment(ca)); err != nil { + if err := cc.meta.Propose(cc.term, encodeAddConsumerAssignment(ca)); err != nil { return } cc.trackInflightConsumerProposal(acc.Name, stream, ca, false) @@ -9920,7 +9981,10 @@ func decodeStreamMsg(buf []byte) (subject, reply string, hdr, msg []byte, lseq u } ml := int(le.Uint32(buf)) buf = buf[4:] - if len(buf) < ml { + // 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 { @@ -10141,7 +10205,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ canRespond := !mset.cfg.NoAck && len(reply) > 0 name, stype := mset.cfg.Name, mset.cfg.Storage discard, discardNewPer, maxMsgs, maxMsgsPer, maxBytes := mset.cfg.Discard, mset.cfg.DiscardNewPer, mset.cfg.MaxMsgs, mset.cfg.MaxMsgsPer, mset.cfg.MaxBytes - s, js, jsa, st, r, tierName, outq, node := mset.srv, mset.js, mset.jsa, mset.cfg.Storage, mset.cfg.Replicas, mset.tier, mset.outq, mset.node + 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 @@ -10266,7 +10330,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ return err } - err = commitSingleMsg(diff, mset, subject, reply, hdr, msg, name, jsa, mt, node, r, lseq) + err = commitSingleMsg(diff, mset, subject, reply, hdr, msg, name, jsa, mt, node, term, r, lseq) mset.clMu.Unlock() return err } @@ -10666,7 +10730,7 @@ RETRY: sreq = nil // Run our own select loop here. - for qch, lch := n.QuitC(), n.LeadChangeC(); ; { + for qch := n.QuitC(); ; { select { case <-msgsQ.ch: notActive.Reset(activityInterval) @@ -10754,16 +10818,16 @@ RETRY: 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 ErrServerNotRunning case <-qch: return errCatchupStreamStopped - case isLeader := <-lch: - if isLeader { - n.StepDown() - goto RETRY - } } } } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go b/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go index ee5f281d9b..6740b31e8b 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go @@ -216,6 +216,12 @@ func validateLeafNode(o *Options) error { if r.LocalAccount == _EMPTY_ { r.LocalAccount = globalAccountName } + if err := checkPermSubjectArray(r.DenyImports, false); err != nil { + return fmt.Errorf("invalid deny_imports for remote %s: %w", r.safeName(), err) + } + if err := checkPermSubjectArray(r.DenyExports, false); err != nil { + return fmt.Errorf("invalid deny_exports for remote %s: %w", r.safeName(), err) + } rn := r.name() if _, dup := names[rn]; dup { return fmt.Errorf("duplicate remote %s", r.safeName()) @@ -2049,7 +2055,7 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c meta.setObserver(false, extNotExtended) c.Debugf("Turning JetStream metadata controller Observer Mode off") // Take note that the domain was not extended to avoid this state from startup. - writePeerState(js.config.StoreDir, meta.currentPeerState()) + writePeerState(c.srv.diskIOSemaphore(), js.config.StoreDir, meta.currentPeerState()) // Meta controller can't be leader yet. // Yet it is possible that due to observer mode every server already stopped campaigning. // Therefore this server needs to be kicked into campaigning gear explicitly. @@ -2284,9 +2290,10 @@ func (c *client) processLeafNodeConnect(s *Server, arg []byte, lang string) erro if !c.isSolicitedLeafNode() && c.perms != nil { sp, pp := c.perms.sub, c.perms.pub c.perms.sub, c.perms.pub = pp, sp - if c.opts.Import != nil { - c.darray = c.opts.Import.Deny - } else { + // setPermissions populated darray from the subscribe permissions, + // which are the import permissions advertised to the spoke. Keep + // those parsed denies after reversing the live permission directions. + if c.opts.Import == nil { c.darray = nil } } @@ -2959,6 +2966,7 @@ func (c *client) processLeafSub(argo []byte) (err error) { c.Debugf(fmt.Sprintf("Permissions Violation for Subscription to %q", sub.subject)) return nil } + c.loadMsgDenyFilterIfNeeded(subj, len(sub.queue) > 0) } // Check if we have a maximum on the number of subscriptions. diff --git a/vendor/github.com/nats-io/nats-server/v2/server/memstore.go b/vendor/github.com/nats-io/nats-server/v2/server/memstore.go index ed841eafb5..d60db1d4d8 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/memstore.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/memstore.go @@ -22,6 +22,7 @@ import ( "sync" "time" + "github.com/antithesishq/antithesis-sdk-go/assert" "github.com/nats-io/nats-server/v2/server/ats" "github.com/nats-io/nats-server/v2/server/avl" "github.com/nats-io/nats-server/v2/server/gsl" @@ -2395,24 +2396,42 @@ func (ms *memStore) EncodedStreamState(failed uint64) ([]byte, error) { numDeleted = 0 } - // Encoded is Msgs, Bytes, FirstSeq, LastSeq, Failed, NumDeleted and optional DeletedBlocks - var buf [1024]byte - buf[0], buf[1] = streamStateMagic, streamStateVersion - n := hdrLen - n += binary.PutUvarint(buf[n:], ms.state.Msgs) - n += binary.PutUvarint(buf[n:], ms.state.Bytes) - n += binary.PutUvarint(buf[n:], ms.state.FirstSeq) - n += binary.PutUvarint(buf[n:], ms.state.LastSeq) - n += binary.PutUvarint(buf[n:], failed) - n += binary.PutUvarint(buf[n:], uint64(numDeleted)) + // 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(ms.state.Msgs) + uvarintLen(ms.state.Bytes) + + uvarintLen(ms.state.FirstSeq) + uvarintLen(ms.state.LastSeq) + + uvarintLen(failed) + uvarintLen(uint64(numDeleted)) - b := buf[0:n] + if numDeleted > 0 { + total += ms.dmap.EncodeLen() + } + + b := make([]byte, 0, total) + b = append(b, streamStateMagic, streamStateVersion) + b = binary.AppendUvarint(b, ms.state.Msgs) + b = binary.AppendUvarint(b, ms.state.Bytes) + b = binary.AppendUvarint(b, ms.state.FirstSeq) + b = binary.AppendUvarint(b, ms.state.LastSeq) + b = binary.AppendUvarint(b, failed) + b = binary.AppendUvarint(b, uint64(numDeleted)) if numDeleted > 0 { - buf := ms.dmap.Encode(nil) - b = append(b, buf...) + enc := ms.dmap.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...) + } } + if len(b) != total { + assert.Unreachable("Memstore EncodedStreamState size accounting mismatch", map[string]any{ + "name": ms.cfg.Name, + "total": total, + "length": len(b), + }) + } return b, nil } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/monitor.go b/vendor/github.com/nats-io/nats-server/v2/server/monitor.go index fbc79d56f6..c6412b4dc8 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/monitor.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/monitor.go @@ -21,6 +21,7 @@ import ( "crypto/x509" "encoding/hex" "encoding/json" + "errors" "expvar" "fmt" "maps" @@ -1290,6 +1291,7 @@ type Varz struct { 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 } @@ -1449,6 +1451,14 @@ type StaleConnectionStats struct { 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 @@ -1845,6 +1855,15 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) { } 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 { @@ -1969,6 +1988,19 @@ func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64 } } } + 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. @@ -3691,6 +3723,29 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus { 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 @@ -3986,6 +4041,28 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus { // 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 @@ -4245,7 +4322,7 @@ func (s *Server) Raftz(opts *RaftzOptions) *RaftzStatus { PTerm: n.pterm, PIndex: n.pindex, SystemAcc: n.IsSystemAccount(), - TrafficAcc: n.acc.GetName(), + TrafficAcc: n.t.Account().GetName(), IPQPropLen: n.prop.len(), IPQEntryLen: n.entry.len(), IPQRespLen: n.resp.len(), diff --git a/vendor/github.com/nats-io/nats-server/v2/server/mqtt.go b/vendor/github.com/nats-io/nats-server/v2/server/mqtt.go index 65004ccdaa..2175614690 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/mqtt.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/mqtt.go @@ -72,6 +72,7 @@ const ( 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) @@ -242,6 +243,7 @@ var ( 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") ) @@ -379,6 +381,12 @@ type mqttSub struct { 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). @@ -475,6 +483,13 @@ 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). @@ -493,9 +508,10 @@ const ( ) type mqttParsedPublishNATSHeader struct { - qos byte - subject []byte - mapped []byte + qos byte + retained bool + subject []byte + mapped []byte } func (s *Server) startMQTT() { @@ -1127,6 +1143,28 @@ func (s *Server) mqttStoreQoSMsgForAccountOnNewSubject(hdr int, msg []byte, acc, 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 @@ -1137,9 +1175,10 @@ func mqttParsePublishNATSHeader(headerBytes []byte) *mqttParsedPublishNATSHeader return nil } return &mqttParsedPublishNATSHeader{ - qos: pubValue[0] - '0', - subject: getHeader(mqttNatsHeaderSubject, headerBytes), - mapped: getHeader(mqttNatsHeaderMapped, headerBytes), + qos: pubValue[0] - '0', + retained: mqttIsRetained(mqttNatsHeaderDecodeFlags(pubValue)), + subject: getHeader(mqttNatsHeaderSubject, headerBytes), + mapped: getHeader(mqttNatsHeaderMapped, headerBytes), } } @@ -1467,6 +1506,10 @@ func (s *Server) mqttCreateAccountSessionManager(acc *Account, quitCh chan struc 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. @@ -1503,6 +1546,10 @@ func (s *Server) mqttCreateAccountSessionManager(acc *Account, quitCh chan struc 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. @@ -1764,7 +1811,13 @@ func (jsa *mqttJSA) createStream(cfg *StreamConfig) (*StreamInfo, bool, error) { return nil, false, err } scr := scri.(*JSApiStreamCreateResponse) - return scr.StreamInfo, scr.DidCreate, scr.ToError() + 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) { @@ -1777,7 +1830,13 @@ func (jsa *mqttJSA) updateStream(cfg *StreamConfig) (*StreamInfo, error) { return nil, err } scr := scri.(*JSApiStreamUpdateResponse) - return scr.StreamInfo, scr.ToError() + 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) { @@ -1786,7 +1845,13 @@ func (jsa *mqttJSA) lookupStream(name string) (*StreamInfo, error) { return nil, err } slr := slri.(*JSApiStreamInfoResponse) - return slr.StreamInfo, slr.ToError() + 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) { @@ -1809,7 +1874,13 @@ func (jsa *mqttJSA) loadLastMsgFor(streamName string, subject string) (*StoredMs return nil, err } lmr := lmri.(*JSApiMsgGetResponse) - return lmr.Message, lmr.ToError() + 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) { @@ -1847,7 +1918,13 @@ func (jsa *mqttJSA) loadNextMsgFor(streamName string, subject string) (*StoredMs return nil, err } lmr := lmri.(*JSApiMsgGetResponse) - return lmr.Message, lmr.ToError() + 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) { @@ -1861,7 +1938,13 @@ func (jsa *mqttJSA) loadMsg(streamName string, seq uint64) (*StoredMsg, error) { return nil, err } lmr := lmri.(*JSApiMsgGetResponse) - return lmr.Message, lmr.ToError() + 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) { @@ -1963,13 +2046,13 @@ func (as *mqttAccountSessionManager) processJSAPIReplies(_ *subscription, pc *cl out(resp) case mqttJSAStreamLookup: var resp = &JSApiStreamInfoResponse{} - if err := json.Unmarshal(msg, &resp); err != nil { + 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 { + if err := json.Unmarshal(msg, resp); err != nil { resp.Error = NewJSInvalidJSONError(err) } out(resp) @@ -1993,7 +2076,7 @@ func (as *mqttAccountSessionManager) processJSAPIReplies(_ *subscription, pc *cl out(resp) case mqttJSAMsgLoad: var resp = &JSApiMsgGetResponse{} - if err := json.Unmarshal(msg, &resp); err != nil { + if err := json.Unmarshal(msg, resp); err != nil { resp.Error = NewJSInvalidJSONError(err) } out(resp) @@ -2098,6 +2181,9 @@ func (as *mqttAccountSessionManager) processSessionPersist(_ *subscription, pc * 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) @@ -2484,6 +2570,8 @@ func (sess *mqttSession) processSub( // 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 { @@ -2538,13 +2626,10 @@ func (as *mqttAccountSessionManager) processSubs(sess *mqttSession, c *client, f.qos = 1 } - // Do not allow subscribing to our internal subjects. - // - // TODO: (levb: not sure why since one can subscribe to `#` and it'll - // include everything; I guess this would discourage? Otherwise another - // candidate for DO NOT DELIVER prefix list). - if strings.HasPrefix(f.filter, mqttSubPrefix) || - strings.HasPrefix(f.filter, mqttPubRelDeliverySubjectPrefix) { + // 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 } @@ -2703,7 +2788,7 @@ func (as *mqttAccountSessionManager) serializeRetainedMsgsForSub(rms map[string] } // A broad wildcard subscription can overlap a subscribe deny clause. c.mu.Lock() - denied := c.mperms != nil && c.checkDenySub(string(subj)) + denied := c.mperms != nil && c.checkDenySub(string(subj), bytesToString(sub.queue)) c.mu.Unlock() if denied { return @@ -2715,7 +2800,7 @@ func (as *mqttAccountSessionManager) serializeRetainedMsgsForSub(rms map[string] return } if qos > 0 { - pi = sess.trackPublishRetained() + 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? @@ -2817,6 +2902,10 @@ func (as *mqttAccountSessionManager) loadRetainedMessages(subjects map[string]ui 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. @@ -2984,6 +3073,9 @@ func mqttDecodeRetainedMessage(subject string, h, m []byte) (*mqttRetainedMsg, e if err := json.Unmarshal(m, &rm); err != nil { return nil, err } + if rm == nil { + return nil, errMQTTInvalidRetainedMessage + } } // Now check that the values are correct. // @@ -3038,6 +3130,11 @@ func (as *mqttAccountSessionManager) createOrRestoreSession(clientID string, opt 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) @@ -3291,6 +3388,10 @@ func (sess *mqttSession) save() error { 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() @@ -3326,6 +3427,9 @@ func (sess *mqttSession) clear(noWait bool) error { 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 { @@ -3410,14 +3514,23 @@ func (sess *mqttSession) bumpPI() uint16 { 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 need a new PI to be allocated, so we add it to the pendingPublish map, -// with an empty value. Since cpending (not pending) is used to serialize the PI -// mappings, we need to add this PI there as well. Make a unique key by using -// mqttRetainedMsgsStreamName for the durable name, and PI for sseq. +// 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() uint16 { +func (sess *mqttSession) trackPublishRetained(sid string) uint16 { // Make sure we initialize the tracking maps. if sess.pendingPublish == nil { sess.pendingPublish = make(map[uint16]*mqttPending) @@ -3430,7 +3543,14 @@ func (sess *mqttSession) trackPublishRetained() uint16 { if pi == 0 { return 0 } - sess.pendingPublish[pi] = &mqttPending{} + 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 } @@ -3527,9 +3647,9 @@ func (sess *mqttSession) untrackPublish(pi uint16) (jsAckSubject string) { } delete(sess.pendingPublish, pi) - if len(sess.pendingPublish) == 0 { - sess.last_pi = 0 - } + // 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 { @@ -4052,14 +4172,16 @@ func (s *Server) mqttHandleWill(c *client) { c.mu.Unlock() return } - pp := c.mqtt.pp - pp.topic = will.topic - pp.subject = will.subject - pp.mapped = will.mapped - pp.msg = will.message - pp.sz = len(will.message) - pp.pi = 0 - pp.flags = will.qos << 1 + // 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 } @@ -4197,6 +4319,7 @@ func mqttComputeNatsMsgSize(pp *mqttPublish, encodePP bool) int { 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 @@ -4228,6 +4351,9 @@ func mqttNewDeliverableMessage(pp *mqttPublish, encodePP bool) (natsMsg []byte, buf.WriteString(mqttNatsHeader) buf.WriteByte(':') buf.WriteByte(qos + '0') + if encodePP { + buf.WriteByte(mqttNatsHeaderEncodeFlags(pp.flags)) + } buf.WriteString(_CRLF_) if encodePP { @@ -4330,7 +4456,13 @@ func (s *Server) mqttProcessPub(c *client, pp *mqttPublish, trace bool) error { func (s *Server) mqttInitiateMsgDelivery(c *client, pp *mqttPublish) error { natsMsg, headerLen := mqttNewDeliverableMessage(pp, false) - // Set the client's pubarg for processing. + // 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 @@ -4339,6 +4471,7 @@ func (s *Server) mqttInitiateMsgDelivery(c *client, pp *mqttPublish) error { 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 @@ -4426,6 +4559,10 @@ func (s *Server) mqttProcessPubRel(c *client, pi uint16, trace bool) error { 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, @@ -4433,7 +4570,7 @@ func (s *Server) mqttProcessPubRel(c *client, pi uint16, trace bool) error { msg: stored.Data, sz: len(stored.Data), pi: pi, - flags: h.qos << 1, + flags: flags, } return s.mqttInitiateMsgDelivery(c, pp) @@ -5039,7 +5176,7 @@ func mqttDeliverMsgCbQoS12(sub *subscription, pc *client, _ *Account, subject, r // 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 { + if sess.c != cc || sub.mqtt == nil || sub.mqtt.closed { sess.mu.Unlock() return } @@ -5070,7 +5207,7 @@ func mqttDeliverMsgCbQoS12(sub *subscription, pc *client, _ *Account, subject, r // A broad wildcard subscription can overlap a subscribe deny clause. cc.mu.Lock() - denied := cc.mperms != nil && cc.checkDenySub(strippedSubj) + denied := cc.mperms != nil && cc.checkDenySub(strippedSubj, bytesToString(sub.queue)) cc.mu.Unlock() if denied { sess.mu.Unlock() @@ -5436,8 +5573,32 @@ func (sess *mqttSession) processJSConsumer(c *client, subject, sid string, 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) @@ -5586,15 +5747,30 @@ func (c *client) mqttProcessUnsubs(filters []*mqttFilter) error { 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() - if seqPis, ok := sess.cpending[cc.Durable]; ok { - delete(sess.cpending, cc.Durable) - for _, pi := range seqPis { - delete(sess.pendingPublish, pi) - } - if len(sess.pendingPublish) == 0 { - sess.last_pi = 0 + // 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() diff --git a/vendor/github.com/nats-io/nats-server/v2/server/opts.go b/vendor/github.com/nats-io/nats-server/v2/server/opts.go index 3ef3a60d4c..46b18a56b4 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/opts.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/opts.go @@ -462,6 +462,7 @@ type Options struct { JetStreamMetaCompact uint64 JetStreamMetaCompactSize uint64 JetStreamMetaCompactSync bool + JetStreamConcurrentIOs int StreamMaxBufferedMsgs int `json:"-"` StreamMaxBufferedSize int64 `json:"-"` StoreDir string `json:"-"` @@ -2061,6 +2062,10 @@ func parseCluster(v any, opts *Options, errors *[]error, warnings *[]error) erro 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{ @@ -2116,6 +2121,10 @@ func parseCluster(v any, opts *Options, errors *[]error, warnings *[]error) erro *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": @@ -2762,6 +2771,12 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er 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{ @@ -3127,14 +3142,14 @@ func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteL case "hub": remote.Hub = v.(bool) case "deny_imports", "deny_import": - subjects, err := parsePermSubjects(tk, errors) + 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) + subjects, err := parsePermSubjects(tk, errors, false) if err != nil { *errors = append(*errors, err) continue @@ -3346,6 +3361,29 @@ func setClusterPermissions(opts *ClusterOpts, perms *Permissions) { } } +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 { @@ -4787,14 +4825,14 @@ func parseUserPermissions(mv any, errors *[]error) (*Permissions, error) { // Import is Publish // Export is Subscribe case "pub", "publish", "import": - perms, err := parseVariablePermissions(mv, errors) + 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) + perms, err := parseVariablePermissions(mv, errors, true) if err != nil { *errors = append(*errors, err) continue @@ -4834,19 +4872,19 @@ func parseUserPermissions(mv any, errors *[]error) (*Permissions, error) { } // Top level parser for authorization configurations. -func parseVariablePermissions(v any, errors *[]error) (*SubjectPermission, error) { +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) + return parseSubjectPermission(vv, errors, allowQueue) default: // Old style - return parseOldPermissionStyle(v, errors) + return parseOldPermissionStyle(v, errors, allowQueue) } } // Helper function to parse subject singletons and/or arrays -func parsePermSubjects(v any, errors *[]error) ([]string, error) { +func parsePermSubjects(v any, errors *[]error, allowQueue bool) ([]string, error) { var lt token defer convertPanicToErrorList(<, errors) @@ -4871,7 +4909,7 @@ func parsePermSubjects(v any, errors *[]error) ([]string, error) { 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); err != nil { + if err := checkPermSubjectArray(subjects, allowQueue); err != nil { return nil, &configErr{tk, err.Error()} } return subjects, nil @@ -4936,8 +4974,8 @@ func parseAllowResponses(v any, errors *[]error) *ResponsePermission { } // Helper function to parse old style authorization configs. -func parseOldPermissionStyle(v any, errors *[]error) (*SubjectPermission, error) { - subjects, err := parsePermSubjects(v, errors) +func parseOldPermissionStyle(v any, errors *[]error, allowQueue bool) (*SubjectPermission, error) { + subjects, err := parsePermSubjects(v, errors, allowQueue) if err != nil { return nil, err } @@ -4945,7 +4983,7 @@ func parseOldPermissionStyle(v any, errors *[]error) (*SubjectPermission, error) } // Helper function to parse new style authorization into a SubjectPermission with Allow and Deny. -func parseSubjectPermission(v any, errors *[]error) (*SubjectPermission, error) { +func parseSubjectPermission(v any, errors *[]error, allowQueue bool) (*SubjectPermission, error) { var lt token defer convertPanicToErrorList(<, errors) @@ -4958,14 +4996,14 @@ func parseSubjectPermission(v any, errors *[]error) (*SubjectPermission, error) tk, _ := unwrapValue(v, <) switch strings.ToLower(k) { case "allow": - subjects, err := parsePermSubjects(tk, errors) + subjects, err := parsePermSubjects(tk, errors, allowQueue) if err != nil { *errors = append(*errors, err) continue } p.Allow = subjects case "deny": - subjects, err := parsePermSubjects(tk, errors) + subjects, err := parsePermSubjects(tk, errors, allowQueue) if err != nil { *errors = append(*errors, err) continue @@ -4982,15 +5020,20 @@ func parseSubjectPermission(v any, errors *[]error) (*SubjectPermission, error) } // Helper function to validate permissions subjects. -func checkPermSubjectArray(sa []string) error { +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]) } } } @@ -6122,6 +6165,9 @@ func setBaselineOptions(opts *Options) { if opts.JetStreamInfoQueueLimit <= 0 { opts.JetStreamInfoQueueLimit = opts.JetStreamRequestQueueLimit } + if opts.JetStreamConcurrentIOs <= 0 { + opts.JetStreamConcurrentIOs = defaultConcurrentIOs + } } func getDefaultAuthTimeout(tls *tls.Config, tlsTimeout float64) float64 { diff --git a/vendor/github.com/nats-io/nats-server/v2/server/parser.go b/vendor/github.com/nats-io/nats-server/v2/server/parser.go index c052ebb166..011bf6d173 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/parser.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/parser.go @@ -181,14 +181,24 @@ func (c *client) parse(buf []byte) error { s.mu.Lock() user, exists := s.users[noAuthUser] s.mu.Unlock() - // Enforce the same connection restrictions as CONNECT before allowing. + // Run the same authentication pipeline as CONNECT. In addition to + // the connection restrictions checked here, this delegates the + // decision to auth callouts or custom authenticators. if exists && !user.ProxyRequired && c.connectionTypeAllowed(user.AllowedConnectionTypes) { - c.RegisterUser(user) + // Mirror processConnect: clear the auth-timeout timer and + // mark CONNECT received *before* authenticating. Auth may + // install a JWT/callout expiration timer into the same c.atmr + // slot, so clearing it afterwards would drop the expiration + // and leave the client connected past expiry. Setting + // connectReceived first also lets the expiration deadline be + // recorded on c.expires. c.mu.Lock() c.clearAuthTimer() c.flags.set(connectReceived) c.mu.Unlock() - authSet, ok = false, true + if s.checkAuthentication(c) { + authSet, ok = false, true + } } } case LEAF: diff --git a/vendor/github.com/nats-io/nats-server/v2/server/raft.go b/vendor/github.com/nats-io/nats-server/v2/server/raft.go index e164093e47..c27be8011d 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/raft.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/raft.go @@ -37,8 +37,8 @@ import ( ) type RaftNode interface { - Propose(entry []byte) error - ProposeMulti(entries []*Entry) error + Propose(term uint64, entry []byte) error + ProposeMulti(term uint64, entries []*Entry) error ForwardProposal(entry []byte) error InstallSnapshot(snap []byte, force bool) error CreateSnapshotCheckpoint(force bool) (RaftNodeCheckpoint, error) @@ -78,7 +78,7 @@ type RaftNode interface { PauseApply() error ResumeApply() DrainAndReplaySnapshot() bool - LeadChangeC() <-chan bool + LeadChangeC() <-chan leadChange QuitC() <-chan struct{} Created() time.Time Stop() @@ -154,12 +154,12 @@ type raft struct { created time.Time // Time that the group was created accName string // Account name of the asset this raft group is for - acc *Account // Account that NRG traffic will be sent/received in 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) @@ -201,7 +201,6 @@ type raft struct { vote string // Our current vote state s *Server // Reference to top-level server - c *client // Internal client for subscriptions js *jetStream // JetStream, if running, to see if we are out of resources hasleader atomic.Bool // Is there a group leader right now? @@ -220,7 +219,7 @@ type raft struct { asubj string // Append entries subject areply string // Append entries responses subject - sq *sendq // Send queue for outbound RPC messages + t raftTransport // Transport that handles Raft messaging aesub *subscription // Subscription for handleAppendEntry callbacks wtv []byte // Term and vote to be written @@ -237,7 +236,7 @@ type raft struct { 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 bool // Leader changes + leadc chan leadChange // Leader changes quit chan struct{} // Raft group shutdown lxfer bool // Are we doing a leadership transfer? @@ -323,6 +322,11 @@ type RaftConfig struct { // 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 ( @@ -410,13 +414,13 @@ func (s *Server) bootstrapRaftNode(cfg *RaftConfig, knownPeers []string, allPeer tmpfile.Close() os.Remove(tmpfile.Name()) - return writePeerState(cfg.Store, &peerState{knownPeers, expected, extUndetermined}) + 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(cfg.Store) + ps, err := readPeerState(s.diskIOSemaphore(), cfg.Store) if err != nil { return err } @@ -447,6 +451,7 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel sd: cfg.Store, wal: cfg.Log, wtype: cfg.Log.Type(), + dios: s.diskIOSemaphore(), track: cfg.Track, peers: make(map[string]*lps), acks: make(map[uint64]map[string]struct{}), @@ -461,10 +466,16 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel resp: newIPQueue[*appendEntryResponse](s, qpfx+"appendEntryResponse"), apply: newIPQueue[*CommittedEntry](s, qpfx+"committedEntry"), accName: accName, - leadc: make(chan bool, 32), + 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. @@ -659,7 +670,10 @@ func (n *raft) IsSystemAccount() bool { func (n *raft) GetTrafficAccountName() string { n.RLock() defer n.RUnlock() - return n.acc.GetName() + if n.t == nil { + return (*Account)(nil).GetName() + } + return n.t.Account().GetName() } func (n *raft) RecreateInternalSubs() error { @@ -709,7 +723,7 @@ func (n *raft) recreateInternalSubsLocked() error { } } } - if n.aesub != nil && n.acc == nrgAcc { + if n.aesub != nil && n.t.Account() == nrgAcc { // Subscriptions already exist and the account NRG state // hasn't changed. return nil @@ -720,33 +734,11 @@ func (n *raft) recreateInternalSubsLocked() error { // the next step... n.cancelCatchup() - // If we have an existing client then tear down any existing - // subscriptions and close the internal client. - if c := n.c; c != nil { - c.mu.Lock() - subs := make([]*subscription, 0, len(c.subs)) - for _, sub := range c.subs { - subs = append(subs, sub) - } - c.mu.Unlock() - for _, sub := range subs { - n.unsubscribe(sub) - } - c.closeConnection(InternalClient) - } - - if n.acc != nrgAcc { + if n.t.Account() != nrgAcc { n.debug("Subscribing in '%s'", nrgAcc.GetName()) } - c := n.s.createInternalSystemClient() - c.registerWithAccount(nrgAcc) - if nrgAcc.sq == nil { - nrgAcc.sq = n.s.newSendQ(nrgAcc) - } - n.c = c - n.sq = nrgAcc.sq - n.acc = nrgAcc + n.t.Reset(nrgAcc) // Recreate any internal subscriptions for voting, append // entries etc in the new account. @@ -913,12 +905,16 @@ func (s *Server) transferRaftLeaders() bool { // Propose will propose a new entry to the group. // This should only be called on the leader. -func (n *raft) Propose(data []byte) error { +func (n *raft) Propose(term uint64, data []byte) error { 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 { - n.debug("Proposal ignored, not leader (state: %v)", state) + 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 } @@ -942,12 +938,12 @@ func (n *raft) Propose(data []byte) error { // ProposeMulti will propose multiple entries at once. // This should only be called on the leader. -func (n *raft) ProposeMulti(entries []*Entry) error { +func (n *raft) ProposeMulti(term uint64, entries []*Entry) error { n.Lock() defer n.Unlock() // Check state under lock, we might not be leader anymore. - if state := n.State(); state != Leader { - n.debug("Multi proposal ignored, not leader (state: %v)", state) + 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 } @@ -992,7 +988,12 @@ func (n *raft) isLeaderOverrun() bool { // If we are the leader this is the same as calling propose. func (n *raft) ForwardProposal(entry []byte) error { if n.State() == Leader { - return n.Propose(entry) + 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 @@ -1372,7 +1373,7 @@ func (n *raft) installSnapshot(snap *snapshot) error { sn := fmt.Sprintf(snapFileT, snap.lastTerm, snap.lastIndex) sfile := filepath.Join(snapDir, sn) - if err := writeFileWithSync(sfile, n.encodeSnapshot(snap), defaultFilePerms); err != nil { + if err := writeFileWithSync(n.dios, sfile, n.encodeSnapshot(snap), defaultFilePerms); 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 @@ -1517,8 +1518,11 @@ func (c *checkpoint) AppendEntriesSeq() iter.Seq2[*appendEntry, error] { yield(nil, err) return } - yield(ae, nil) + hasMore := yield(ae, nil) ae.returnToPool() + if !hasMore { + return + } } } } @@ -1564,7 +1568,7 @@ func (c *checkpoint) InstallSnapshot(data []byte) (uint64, error) { // Unlock while writing. n.Unlock() - err := writeFileWithSync(c.snapFile, encoded, defaultFilePerms) + err := writeFileWithSync(n.dios, c.snapFile, encoded, defaultFilePerms) 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 @@ -1742,9 +1746,9 @@ func (n *raft) loadLastSnapshot() (*snapshot, error) { return nil, errNoSnapAvailable } - <-dios + n.dios.acquire() buf, err := os.ReadFile(n.snapfile) - dios <- struct{}{} + n.dios.release() if err != nil { n.warn("Error reading snapshot: %v", err) @@ -2208,7 +2212,7 @@ 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 bool { return n.leadc } +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 } @@ -2351,17 +2355,12 @@ func (n *raft) newInbox() string { // Our internal subscribe. // Lock should be held. func (n *raft) subscribe(subject string, cb msgHandler) (*subscription, error) { - if n.c == nil { - return nil, errNoInternalClient - } - return n.s.systemSubscribe(subject, _EMPTY_, false, n.c, cb) + return n.t.Subscribe(subject, cb) } // Lock should be held. func (n *raft) unsubscribe(sub *subscription) { - if n.c != nil && sub != nil { - n.c.processUnsub(sub.sid) - } + n.t.Unsubscribe(sub) } // Lock should be held. @@ -2486,19 +2485,7 @@ runner: n.Lock() defer n.Unlock() - if c := n.c; c != nil { - var subs []*subscription - c.mu.Lock() - for _, sub := range c.subs { - subs = append(subs, sub) - } - c.mu.Unlock() - for _, sub := range subs { - n.unsubscribe(sub) - } - c.closeConnection(InternalClient) - n.c = nil - } + n.t.Close() // Unregistering ipQueues do not prevent them from push/pop // just will remove them from the central monitoring map @@ -3780,6 +3767,19 @@ func (n *raft) adjustClusterSizeAndQuorum() { } } +// 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() @@ -3851,15 +3851,18 @@ func (n *raft) runAsCandidate() { n.RLock() nterm := n.term csz := n.csz + countVote := n.shouldCountVoteFromPeer(vresp.peer) n.RUnlock() if vresp.granted && nterm == vresp.term { // only track peers that would be our followers n.trackPeer(vresp.peer) - if !vresp.empty { - votes[vresp.peer] = struct{}{} - } else { - emptyVotes[vresp.peer] = struct{}{} + if countVote { + if !vresp.empty { + 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. @@ -4970,25 +4973,25 @@ func (n *raft) writePeerState(ps *peerState) { } // Stamp latest and write the peer state file. n.wps = pse - if err := writePeerState(n.sd, ps); err != nil && !n.isClosed() { + 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(sd string, ps *peerState) error { +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(psf, encodePeerState(ps), defaultFilePerms) + return writeFileWithSync(dios, psf, encodePeerState(ps), defaultFilePerms) } -func readPeerState(sd string) (ps *peerState, err error) { - <-dios +func readPeerState(dios *diskIOSemaphore, sd string) (ps *peerState, err error) { + dios.acquire() buf, err := os.ReadFile(filepath.Join(sd, peerStateFile)) - dios <- struct{}{} + dios.release() if err != nil { return nil, err @@ -5001,20 +5004,20 @@ const termLen = 8 // uint64 const termVoteLen = idLen + termLen // Writes out our term & vote outside of a specific raft context. -func writeTermVote(sd string, wtv []byte) error { +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(psf, wtv, defaultFilePerms) + 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) { - <-dios + n.dios.acquire() buf, err := os.ReadFile(filepath.Join(n.sd, termVoteFile)) - dios <- struct{}{} + n.dios.release() if err != nil { return 0, noVote, err @@ -5118,7 +5121,7 @@ func (n *raft) writeTermVote() error { } // Stamp latest and write the term & vote file. n.wtv = b - if err := writeTermVote(n.sd, n.wtv); err != nil && !n.isClosed() { + if err := writeTermVote(n.dios, n.sd, n.wtv); err != nil && !n.isClosed() { // Clear wtv since we failed. n.wtv = nil n.setWriteErrLocked(err) @@ -5288,15 +5291,11 @@ func (n *raft) requestVote() { } func (n *raft) sendRPC(subject, reply string, msg []byte) { - if n.sq != nil { - n.sq.send(subject, reply, nil, msg) - } + n.t.Publish(subject, reply, msg) } func (n *raft) sendReply(subject string, msg []byte) { - if n.sq != nil { - n.sq.send(subject, _EMPTY_, nil, msg) - } + n.t.Publish(subject, _EMPTY_, msg) } func (n *raft) wonElection(votes int) bool { @@ -5311,13 +5310,21 @@ func (n *raft) quorumNeeded() int { return qn } +// 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 +} + // 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 <- isLeader: + case n.leadc <- lc: return default: select { @@ -5347,23 +5354,23 @@ retry: // Reset the election timer. n.resetElectionTimeout() - var leadChange bool + var leadChanged bool if pstate == Leader && state != Leader { - leadChange = true + leadChanged = true n.updateLeadChange(false) // 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. - leadChange = true + leadChanged = true if len(n.pae) > 0 { n.pae = make(map[uint64]*appendEntry) } } n.writeTermVote() - return leadChange + return leadChanged } const ( diff --git a/vendor/github.com/nats-io/nats-server/v2/server/raft_transport.go b/vendor/github.com/nats-io/nats-server/v2/server/raft_transport.go new file mode 100644 index 0000000000..f45316efdc --- /dev/null +++ b/vendor/github.com/nats-io/nats-server/v2/server/raft_transport.go @@ -0,0 +1,114 @@ +// Copyright 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 + +// raftTransport is an interface that defines the communication +// mechanism for Raft nodes. +type raftTransport interface { + // Node returns the RaftNode associated with this transport. + Node() RaftNode + + // Account returns the NATS Account this transport operates within. + Account() *Account + + // Reset reconfigures the transport for a new account. + // This involves tearing down existing client resources and + // setting up new ones for the provided account. + Reset(acc *Account) + + // Close shuts down the transport, releasing any associated resources + // like internal clients and subscriptions. + Close() + + // Publish sends a message to the specified subject. + Publish(subject string, reply string, msg []byte) + + // Subscribe creates a subscription for to the specified subject. + Subscribe(subject string, cb msgHandler) (*subscription, error) + + // Unsubscribe removes a previously established subscription. + Unsubscribe(sub *subscription) +} + +type newTransportFunc func(*Server, RaftNode) raftTransport + +// defaultTransport is the default implementation of the raftTransport interface. +// It uses an internal NATS client to allow communication between Raft nodes. +type defaultTransport struct { + n RaftNode + s *Server + c *client + sq *sendq + acc *Account +} + +func defaultRaftTransport(server *Server, raft RaftNode) raftTransport { + return &defaultTransport{s: server, n: raft} +} + +func (t *defaultTransport) Node() RaftNode { + return t.n +} + +func (t *defaultTransport) Account() *Account { + return t.acc +} + +func (t *defaultTransport) Reset(acc *Account) { + t.Close() + + t.c = t.s.createInternalSystemClient() + t.c.registerWithAccount(acc) + if acc.sq == nil { + acc.sq = t.s.newSendQ(acc) + } + t.sq = acc.sq + t.acc = acc +} + +func (t *defaultTransport) Close() { + if c := t.c; c != nil { + c.mu.Lock() + subs := make([]*subscription, 0, len(c.subs)) + for _, sub := range c.subs { + subs = append(subs, sub) + } + c.mu.Unlock() + for _, sub := range subs { + t.Unsubscribe(sub) + } + c.closeConnection(InternalClient) + t.c = nil + } +} + +func (t *defaultTransport) Publish(subject string, reply string, msg []byte) { + if t.sq == nil { + return + } + t.sq.send(subject, reply, nil, msg) +} + +func (t *defaultTransport) Subscribe(subject string, cb msgHandler) (*subscription, error) { + if t.c == nil { + return nil, errNoInternalClient + } + return t.s.systemSubscribe(subject, _EMPTY_, false, t.c, cb) +} + +func (t *defaultTransport) Unsubscribe(sub *subscription) { + if t.c != nil && sub != nil { + t.c.processUnsub(sub.sid) + } +} diff --git a/vendor/github.com/nats-io/nats-server/v2/server/reload.go b/vendor/github.com/nats-io/nats-server/v2/server/reload.go index a8909596e8..53bcdd6dcc 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/reload.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/reload.go @@ -1864,6 +1864,15 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { } case "jetstreammetacompact", "jetstreammetacompactsize", "jetstreammetacompactsync": // Allowed at runtime but monitorCluster looks at s.opts directly, so no further work needed here. + case "jetstreamconcurrentios": + // Not reloadable at runtime; preserve the current value while JetStream is disabled, + // e.g. the entire jetstream{} block was deleted. + if newOpts.JetStream { + return nil, fmt.Errorf("config reload not supported for %s: old=%v, new=%v", + field.Name, oldValue, newValue) + } else { + newOpts.JetStreamConcurrentIOs = oldValue.(int) + } case "websocket": // Similar to gateways tmpOld := oldValue.(WebsocketOpts) diff --git a/vendor/github.com/nats-io/nats-server/v2/server/server.go b/vendor/github.com/nats-io/nats-server/v2/server/server.go index 3a2d81a9d2..61dc434fe7 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/server.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/server.go @@ -190,6 +190,7 @@ type Server struct { sys *internal sysAcc atomic.Pointer[Account] js atomic.Pointer[jetStream] + dios *diskIOSemaphore isMetaLeader atomic.Bool jsClustered atomic.Bool accounts sync.Map @@ -773,6 +774,7 @@ func NewServer(opts *Options) (*Server, error) { 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 @@ -1096,6 +1098,12 @@ 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 @@ -2791,8 +2799,15 @@ func (s *Server) AcceptLoop(clr chan struct{}) { // Alert of TLS enabled. if opts.TLSConfig != nil { - s.Noticef("TLS required for client connections") - if opts.TLSHandshakeFirst && opts.TLSHandshakeFirstFallback == 0 { + // "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") } } @@ -4774,3 +4789,10 @@ func (s *Server) LDMClientByID(id uint64) error { 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/vendor/github.com/nats-io/nats-server/v2/server/signal.go b/vendor/github.com/nats-io/nats-server/v2/server/signal.go index aef50a5b4b..f8e5dc74c9 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/signal.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/signal.go @@ -46,34 +46,7 @@ func (s *Server) handleSignals() { for { select { case sig := <-c: - s.Noticef("Trapped %q signal", sig) - switch sig { - case syscall.SIGINT: - s.Shutdown() - s.WaitForShutdown() - os.Exit(0) - case syscall.SIGTERM: - // Shutdown unless graceful shutdown already in progress. - s.mu.Lock() - ldm := s.ldm - s.mu.Unlock() - - if !ldm { - s.Shutdown() - s.WaitForShutdown() - os.Exit(0) - } - case syscall.SIGUSR1: - // File log re-open for rotating file logs. - s.ReOpenLogFile() - case syscall.SIGUSR2: - go s.lameDuckMode() - case syscall.SIGHUP: - // Config reload. - if err := s.Reload(); err != nil { - s.Errorf("Failed to reload server configuration: %s", err) - } - } + s.handleSignal(sig) case <-s.quitCh: return } @@ -81,6 +54,37 @@ func (s *Server) handleSignals() { }() } +func (s *Server) handleSignal(sig os.Signal) { + s.Noticef("Trapped %q signal", sig) + switch sig { + case syscall.SIGINT: + s.Shutdown() + s.WaitForShutdown() + os.Exit(0) + case syscall.SIGTERM: + // Shutdown unless graceful shutdown already in progress. + s.mu.Lock() + ldm := s.ldm + s.mu.Unlock() + + if !ldm { + s.Shutdown() + s.WaitForShutdown() + os.Exit(0) + } + case syscall.SIGUSR1: + // File log re-open for rotating file logs. + s.ReOpenLogFile() + case syscall.SIGUSR2: + go s.lameDuckMode() + case syscall.SIGHUP: + // Config reload. + if err := s.Reload(); err != nil { + s.Errorf("Failed to reload server configuration: %s", err) + } + } +} + // ProcessSignal sends the given signal command to the given process. If pidStr // is empty, this will send the signal to the single running instance of // nats-server. If multiple instances are running, pidStr can be a globular diff --git a/vendor/github.com/nats-io/nats-server/v2/server/signal_wasm.go b/vendor/github.com/nats-io/nats-server/v2/server/signal_wasm.go index 7ee34e4aba..140d5007aa 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/signal_wasm.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/signal_wasm.go @@ -15,10 +15,16 @@ package server +import "os" + func (s *Server) handleSignals() { } +func (s *Server) handleSignal(sig os.Signal) { + +} + func ProcessSignal(command Command, service string) error { return nil } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/store.go b/vendor/github.com/nats-io/nats-server/v2/server/store.go index a7c97439db..fe04ace0e8 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/store.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/store.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "io" + "math/bits" "os" "strings" "time" @@ -307,6 +308,27 @@ func DecodeStreamState(buf []byte) (*StreamReplicatedState, error) { return ss, nil } +// uvarintLen returns the number of bytes binary.PutUvarint/binary.AppendUvarint +// write for v: ceil(bits/7), with v=0 taking one byte. +func uvarintLen(v uint64) int { + return (bits.Len64(v|1) + 6) / 7 +} + +// runLengthEncodeLen returns the encoded size of a run-length delete record, +// exactly matching what appendRunLength writes. +func runLengthEncodeLen(first, num uint64) int { + return 1 + uvarintLen(first) + uvarintLen(num) +} + +// appendRunLength appends a run-length encoded delete record for num +// deleted sequences starting at first. +func appendRunLength(b []byte, first, num uint64) []byte { + b = append(b, runLengthMagic) + b = binary.AppendUvarint(b, first) + b = binary.AppendUvarint(b, num) + return b +} + // DeleteRange is a run length encoded delete range. type DeleteRange struct { First uint64 diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stream.go b/vendor/github.com/nats-io/nats-server/v2/server/stream.go index 108056d2e0..4ff6cb4e3a 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stream.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stream.go @@ -480,6 +480,10 @@ type stream struct { // 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. @@ -487,6 +491,7 @@ type stream struct { 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. numFilter int // The number of filtered consumers. @@ -1043,7 +1048,7 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt // Call directly to set leader if not in clustered mode. // This can be called though before we actually setup clustering, so check both. if singleServerMode { - if err := mset.setLeader(true); err != nil { + if err := mset.setLeader(true, 0); err != nil { mset.stop(true, false) return nil, err } @@ -1252,8 +1257,37 @@ func (mset *stream) isLeaderNodeState() bool { } // TODO(dlc) - Check to see if we can accept being the leader or we should step down. -func (mset *stream) setLeader(isLeader bool) error { +func (mset *stream) setLeader(isLeader bool, term uint64) error { mset.mu.Lock() + 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) + // Clear catchup state + mset.clearAllCatchupPeers() + mset.store.ResetState() + } + // If we are here we have a change in leader status. if isLeader { // Make sure we are listening for sync requests. @@ -1274,31 +1308,14 @@ func (mset *stream) setLeader(isLeader bool) error { // Reset any inflight fast batches. We were likely a follower before and need // to send an ack to the publishers so they know we're still there. - if mset.batches != nil { + 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() } - } else { - // cancel timer to create the source consumers if not fired yet - if mset.sourcesConsumerSetup != nil { - mset.sourcesConsumerSetup.Stop() - mset.sourcesConsumerSetup = nil - } else { - // Stop any source consumers - mset.stopSourceConsumers() - } - - // Stop responding to sync requests. - mset.stopClusterSubs() - // Unsubscribe from direct stream. - mset.unsubscribeToStream(false, false) - // Clear catchup state - mset.clearAllCatchupPeers() } - mset.store.ResetState() mset.mu.Unlock() // If we are interest based make sure to check consumers. @@ -3274,16 +3291,18 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool { } s, js, stype := mset.srv, mset.js, mset.cfg.Storage - node := mset.node + node, term := mset.node, mset.term mset.mu.Unlock() var err error if node != nil { - if js.limitsExceeded(stype) { + 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(encodeStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts, true)) + 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) @@ -3364,7 +3383,7 @@ func (mset *stream) skipMsgs(start, end uint64) error { // 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(encodeDeleteRange(&DeleteRange{First: start, Num: end - start + 1})) + return node.Propose(mset.term, encodeDeleteRange(&DeleteRange{First: start, Num: end - start + 1})) } var entries []*Entry @@ -3372,7 +3391,7 @@ func (mset *stream) skipMsgs(start, end uint64) error { 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(entries); err != nil { + if err := node.ProposeMulti(mset.term, entries); err != nil { return err } // We need to re-create `entries` because there is a reference @@ -3382,7 +3401,7 @@ func (mset *stream) skipMsgs(start, end uint64) error { } // Send all at once. if len(entries) > 0 { - return node.ProposeMulti(entries) + return node.ProposeMulti(mset.term, entries) } return nil } @@ -3570,22 +3589,26 @@ func (mset *stream) setupMirrorConsumer() error { } } - respCh := make(chan *JSApiConsumerCreateResponse, 1) - reply := infoReplySubject() - crSub, err := mset.subscribeInternal(reply, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { - _, msg := c.msgParts(rmsg) + newReplySubscription := func() (string, chan *JSApiConsumerCreateResponse, *subscription, error) { + respCh := make(chan *JSApiConsumerCreateResponse, 1) + reply := infoReplySubject() + crSub, err := mset.subscribeInternal(reply, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + _, msg := c.msgParts(rmsg) - var ccr JSApiConsumerCreateResponse - if err := json.Unmarshal(msg, &ccr); err != nil { - c.Warnf("JetStream bad mirror consumer create response: %q", msg) - mset.setMirrorErr(ApiErrors[JSInvalidJSONErr]) - return - } - select { - case respCh <- &ccr: - default: - } - }) + 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() @@ -3677,7 +3700,7 @@ func (mset *stream) setupMirrorConsumer() error { mirror := mset.mirror mirror.err = nil - if ccr.Error != nil || ccr.ConsumerInfo == 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)) { @@ -3688,13 +3711,25 @@ func (mset *stream) setupMirrorConsumer() error { 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) - mset.srv.Warnf("JetStream error response for create mirror consumer: %+v", ccr.Error) - mirror.err = ccr.Error + 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() @@ -4004,20 +4039,24 @@ func (mset *stream) trySetupSourceConsumer(iname string, seq uint64, startTime t } req.Config.FilterSubjects = filterSubjects - respCh := make(chan *JSApiConsumerCreateResponse, 1) - reply := infoReplySubject() - crSub, err := mset.subscribeInternal(reply, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { - _, msg := c.msgParts(rmsg) - var ccr JSApiConsumerCreateResponse - if err := json.Unmarshal(msg, &ccr); err != nil { - c.Warnf("JetStream bad source consumer create response: %q", msg) - return - } - select { - case respCh <- &ccr: - default: - } - }) + newReplySubscription := func() (string, chan *JSApiConsumerCreateResponse, *subscription, error) { + respCh := make(chan *JSApiConsumerCreateResponse, 1) + reply := infoReplySubject() + crSub, err := mset.subscribeInternal(reply, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { + _, msg := c.msgParts(rmsg) + var ccr JSApiConsumerCreateResponse + if err := json.Unmarshal(msg, &ccr); err != nil { + c.Warnf("JetStream bad source consumer create response: %q", msg) + return + } + select { + case respCh <- &ccr: + default: + } + }) + return reply, respCh, crSub, err + } + reply, respCh, crSub, err := newReplySubscription() if err != nil { si.err = NewJSSourceConsumerSetupFailedError(err, Unless(err)) mset.setupSourceConsumer(iname, seq, startTime) @@ -4098,7 +4137,7 @@ func (mset *stream) trySetupSourceConsumer(iname string, seq uint64, startTime t } else { si.err = nil - if ccr.Error != nil || ccr.ConsumerInfo == 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)) { @@ -4109,6 +4148,14 @@ func (mset *stream) trySetupSourceConsumer(iname string, seq uint64, startTime t 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 @@ -4117,8 +4164,12 @@ func (mset *stream) trySetupSourceConsumer(iname string, seq uint64, startTime t // Note: this warning can happen a few times when starting up the server when sourcing streams are // defined, this is normal as the streams are re-created in no particular order and it is possible // that a stream sourcing another could come up before all of its sources have been recreated. - mset.srv.Warnf("JetStream error response for stream %s create source consumer %s: %+v", mset.cfg.Name, si.name, ccr.Error) - si.err = ccr.Error + 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() @@ -4313,7 +4364,7 @@ func (mset *stream) handleFlowControl(m *inMsg, dseq, sseq uint64) { // Append the current delivery and stream sequences, to be sent after replication. m.hdr = genHeader(m.hdr, JSLastConsumerSeq, strconv.FormatUint(dseq, 10)) m.hdr = genHeader(m.hdr, JSLastStreamSeq, strconv.FormatUint(sseq, 10)) - mset.node.Propose(encodeStreamMsg(_EMPTY_, m.rply, m.hdr, nil, 0, 0, false)) + 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) @@ -5176,12 +5227,12 @@ func (mset *stream) setupStore(fsCfg *FileStoreConfig) error { mset.store.RegisterStorageUpdates(mset.storeUpdates) mset.store.RegisterStorageRemoveMsg(func(seq uint64) { if mset.IsClustered() { - if mset.IsLeader() { - mset.mu.RLock() + mset.mu.RLock() + if mset.isLeader() { md := streamMsgDelete{Seq: seq, NoErase: true, Stream: mset.cfg.Name} - mset.node.Propose(encodeMsgDelete(&md)) - mset.mu.RUnlock() + mset.node.Propose(mset.term, encodeMsgDelete(&md)) } + mset.mu.RUnlock() } else { mset.removeMsg(seq) } @@ -6199,6 +6250,16 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg 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 @@ -6755,7 +6816,7 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg if sources == nil { sources = map[string]map[string]string{} } - if _, ok := sources[origStream]; !ok { + if sources[origStream] == nil { sources[origStream] = map[string]string{} } prevVal := sources[origStream][origSubj] @@ -7016,7 +7077,7 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg // If this proposal fails, we retry out-of-band. if isClustered && isLeader { md := streamMsgDelete{Seq: seq, NoErase: true, Stream: mset.cfg.Name} - _ = mset.node.Propose(encodeMsgDelete(&md)) + _ = mset.node.Propose(mset.term, encodeMsgDelete(&md)) } } @@ -7546,10 +7607,11 @@ func (mset *stream) processJetStreamAtomicBatchMsg(batchId, subject, reply strin } mset.mu.Unlock() } 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(entries); err == nil { + if err = node.ProposeMulti(term, entries); err == nil { diff.commit(mset) mset.trackReplicationTraffic(node, sz, r) @@ -7576,7 +7638,7 @@ func (mset *stream) processJetStreamFastBatchMsg(batch *FastBatch, subject, repl canRespond := !mset.cfg.NoAck && len(reply) > 0 name, stype := mset.cfg.Name, mset.cfg.Storage discard, discardNewPer, maxMsgs, maxMsgsPer, maxBytes := mset.cfg.Discard, mset.cfg.DiscardNewPer, mset.cfg.MaxMsgs, mset.cfg.MaxMsgsPer, mset.cfg.MaxBytes - s, js, jsa, st, r, tierName, outq, node := mset.srv, mset.js, mset.jsa, mset.cfg.Storage, mset.cfg.Replicas, mset.tier, mset.outq, mset.node + 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 @@ -7903,7 +7965,7 @@ func (mset *stream) processJetStreamFastBatchMsg(batch *FastBatch, subject, repl mset.clMu.Unlock() return mset.processJetStreamMsgWithBatch(subject, reply, hdr, msg, 0, 0, mt, false, true, batch) } - err = commitSingleMsg(diff, mset, subject, reply, hdr, msg, name, jsa, mt, node, r, lseq) + err = commitSingleMsg(diff, mset, subject, reply, hdr, msg, name, jsa, mt, node, term, r, lseq) mset.clMu.Unlock() return err } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/leaf.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/leaf.go index 119837ec26..b5281a5a3e 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/leaf.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/leaf.go @@ -13,10 +13,6 @@ package stree -import ( - "bytes" -) - // Leaf node // Order of struct fields for best memory alignment (as per govet/fieldalignment) type leaf[T any] struct { @@ -24,23 +20,25 @@ type leaf[T any] struct { // This could be the whole subject, but most likely just the suffix portion. // We will only store the suffix here and assume all prior prefix paths have // been checked once we arrive at this leafnode. - suffix []byte + suffix string } func newLeaf[T any](suffix []byte, value T) *leaf[T] { - return &leaf[T]{value, copyBytes(suffix)} + return &leaf[T]{value, string(suffix)} } -func (n *leaf[T]) isLeaf() bool { return true } -func (n *leaf[T]) base() *meta { return nil } -func (n *leaf[T]) match(subject []byte) bool { return bytes.Equal(subject, n.suffix) } -func (n *leaf[T]) setSuffix(suffix []byte) { n.suffix = copyBytes(suffix) } -func (n *leaf[T]) isFull() bool { return true } -func (n *leaf[T]) matchParts(parts [][]byte) ([][]byte, bool) { return matchParts(parts, n.suffix) } -func (n *leaf[T]) iter(f func(node) bool) {} -func (n *leaf[T]) children() []node { return nil } -func (n *leaf[T]) numChildren() uint16 { return 0 } -func (n *leaf[T]) path() []byte { return n.suffix } +func (n *leaf[T]) isLeaf() bool { return true } +func (n *leaf[T]) base() *meta { return nil } +func (n *leaf[T]) match(subject []byte) bool { return string(subject) == n.suffix } +func (n *leaf[T]) setSuffix(suffix []byte) { n.suffix = string(suffix) } +func (n *leaf[T]) isFull() bool { return true } +func (n *leaf[T]) matchParts(parts [][]byte) ([][]byte, bool) { + return matchParts(parts, n.suffix) +} +func (n *leaf[T]) iter(f func(node) bool) {} +func (n *leaf[T]) children() []node { return nil } +func (n *leaf[T]) numChildren() uint16 { return 0 } +func (n *leaf[T]) path() string { return n.suffix } // Not applicable to leafs and should not be called, so panic if we do. func (n *leaf[T]) setPrefix(pre []byte) { panic("setPrefix called on leaf") } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/node.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/node.go index c8edfe3ea9..e5a7cb432e 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/node.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/node.go @@ -29,11 +29,11 @@ type node interface { iter(f func(node) bool) children() []node numChildren() uint16 - path() []byte + path() string } type meta struct { - prefix []byte + prefix string size uint16 } @@ -41,11 +41,11 @@ func (n *meta) isLeaf() bool { return false } func (n *meta) base() *meta { return n } func (n *meta) setPrefix(pre []byte) { - n.prefix = append([]byte(nil), pre...) + n.prefix = string(pre) } func (n *meta) numChildren() uint16 { return n.size } -func (n *meta) path() []byte { return n.prefix } +func (n *meta) path() string { return n.prefix } // Will match parts against our prefix. func (n *meta) matchParts(parts [][]byte) ([][]byte, bool) { diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/node10.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/node10.go index 37cd2cc946..d359f26079 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/node10.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/node10.go @@ -52,7 +52,8 @@ func (n *node10) findChild(c byte) *node { func (n *node10) isFull() bool { return n.size >= 10 } func (n *node10) grow() node { - nn := newNode16(n.prefix) + nn := &node16{} + nn.prefix = n.prefix for i := 0; i < 10; i++ { nn.addChild(n.key[i], n.child[i]) } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/node16.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/node16.go index e2dc97908d..7950928a87 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/node16.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/node16.go @@ -50,7 +50,8 @@ func (n *node16) findChild(c byte) *node { func (n *node16) isFull() bool { return n.size >= 16 } func (n *node16) grow() node { - nn := newNode48(n.prefix) + nn := &node48{} + nn.prefix = n.prefix for i := 0; i < 16; i++ { nn.addChild(n.key[i], n.child[i]) } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/node4.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/node4.go index 4eddf11b83..9e361c0fd3 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/node4.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/node4.go @@ -49,7 +49,8 @@ func (n *node4) findChild(c byte) *node { func (n *node4) isFull() bool { return n.size >= 4 } func (n *node4) grow() node { - nn := newNode10(n.prefix) + nn := &node10{} + nn.prefix = n.prefix for i := 0; i < 4; i++ { nn.addChild(n.key[i], n.child[i]) } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/node48.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/node48.go index 7099edd58b..17a50c78a5 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/node48.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/node48.go @@ -50,7 +50,8 @@ func (n *node48) findChild(c byte) *node { func (n *node48) isFull() bool { return n.size >= 48 } func (n *node48) grow() node { - nn := newNode256(n.prefix) + nn := &node256{} + nn.prefix = n.prefix for c := 0; c < len(n.key); c++ { if i := n.key[byte(c)]; i > 0 { nn.addChild(byte(c), n.child[i-1]) diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/parts.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/parts.go index af5dd9c176..520d0f24c5 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/parts.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/parts.go @@ -14,7 +14,7 @@ package stree import ( - "bytes" + "strings" ) // genParts will break a filter subject up into parts. @@ -74,8 +74,9 @@ func genParts(filter []byte, parts [][]byte) [][]byte { return parts } -// Match our parts against a fragment, which could be prefix for nodes or a suffix for leafs. -func matchParts(parts [][]byte, frag []byte) ([][]byte, bool) { +// Match our parts against a stored fragment, which could be a prefix for nodes +// or a suffix for leaves. +func matchParts(parts [][]byte, frag string) ([][]byte, bool) { lf := len(frag) if lf == 0 { return parts, true @@ -92,7 +93,7 @@ func matchParts(parts [][]byte, frag []byte) ([][]byte, bool) { // Check for pwc or fwc place holders. if lp == 1 { if part[0] == pwc { - index := bytes.IndexByte(frag[si:], tsep) + index := strings.IndexByte(frag[si:], tsep) // We are trying to match pwc and did not find our tsep. // Will need to move to next node from caller. if index < 0 { @@ -114,7 +115,7 @@ func matchParts(parts [][]byte, frag []byte) ([][]byte, bool) { // Frag is smaller then part itself. part = part[:end-si] } - if !bytes.Equal(part, frag[si:end]) { + if string(part) != frag[si:end] { return parts, false } // If we still have a portion of the fragment left, update and continue. diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/stree.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/stree.go index 0c257435dc..a2db3219ad 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/stree.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/stree.go @@ -16,6 +16,7 @@ package stree import ( "bytes" "slices" + "strings" "unsafe" "github.com/nats-io/nats-server/v2/server/gsl" @@ -87,7 +88,7 @@ func (t *SubjectTree[T]) Find(subject []byte) (*T, bool) { // We are a node type here, grab meta portion. if bn := n.base(); len(bn.prefix) > 0 { end := min(si+len(bn.prefix), len(subject)) - if !bytes.Equal(subject[si:end], bn.prefix) { + if string(subject[si:end]) != bn.prefix { return nil, false } // Increment our subject index. @@ -181,12 +182,12 @@ func (t *SubjectTree[T]) insert(np *node, subject []byte, value T, si int) (*T, return &old, true } // Here we need to split this leaf. - cpi := commonPrefixLen(ln.suffix, subject[si:]) + cpi := commonPrefixLen(stringToBytes(ln.suffix), subject[si:]) nn := newNode4(subject[si : si+cpi]) - ln.setSuffix(ln.suffix[cpi:]) + ln.setSuffix(stringToBytes(ln.suffix[cpi:])) si += cpi // Make sure we have different pivot, normally this will be the case unless we have overflowing prefixes. - if p := pivot(ln.suffix, 0); cpi > 0 && si < len(subject) && p == subject[si] { + if p := pivot(stringToBytes(ln.suffix), 0); cpi > 0 && si < len(subject) && p == subject[si] { // We need to split the original leaf. Recursively call into insert. t.insert(np, subject, value, si) // Now add the update version of *np as a child to the new node4. @@ -194,9 +195,9 @@ func (t *SubjectTree[T]) insert(np *node, subject []byte, value T, si int) (*T, } else { // Can just add this new leaf as a sibling. nl := newLeaf(subject[si:], value) - nn.addChild(pivot(nl.suffix, 0), nl) + nn.addChild(pivot(stringToBytes(nl.suffix), 0), nl) // Add back original. - nn.addChild(pivot(ln.suffix, 0), ln) + nn.addChild(pivot(stringToBytes(ln.suffix), 0), ln) } *np = nn return nil, false @@ -205,7 +206,7 @@ func (t *SubjectTree[T]) insert(np *node, subject []byte, value T, si int) (*T, // Non-leaf nodes. bn := n.base() if len(bn.prefix) > 0 { - cpi := commonPrefixLen(bn.prefix, subject[si:]) + cpi := commonPrefixLen(stringToBytes(bn.prefix), subject[si:]) if pli := len(bn.prefix); cpi >= pli { // Move past this node. We look for an existing child node to recurse into. // If one does not exist we can create a new leaf node. @@ -227,8 +228,8 @@ func (t *SubjectTree[T]) insert(np *node, subject []byte, value T, si int) (*T, // We will insert a new node4 and attach our current node below after adjusting prefix. nn := newNode4(prefix) // Shift the prefix for our original node. - n.setPrefix(bn.prefix[cpi:]) - nn.addChild(pivot(bn.prefix[:], 0), n) + n.setPrefix(stringToBytes(bn.prefix[cpi:])) + nn.addChild(pivot(stringToBytes(bn.prefix), 0), n) // Add in our new leaf. nn.addChild(pivot(subject[si:], 0), newLeaf(subject[si:], value)) // Update our node reference. @@ -269,7 +270,7 @@ func (t *SubjectTree[T]) delete(np *node, subject []byte, si int) (*T, bool) { if len(subject) < si+len(bn.prefix) { return nil, false } - if !bytes.Equal(subject[si:si+len(bn.prefix)], bn.prefix) { + if string(subject[si:si+len(bn.prefix)]) != bn.prefix { return nil, false } // Increment our subject index. @@ -288,18 +289,16 @@ func (t *SubjectTree[T]) delete(np *node, subject []byte, si int) (*T, bool) { if sn := n.shrink(); sn != nil { bn := n.base() - // Make sure to set cap so we force an append to copy below. - pre := bn.prefix[:len(bn.prefix):len(bn.prefix)] + pre := bn.prefix // Need to fix up prefixes/suffixes. if sn.isLeaf() { ln := sn.(*leaf[T]) - // Make sure to set cap so we force an append to copy. - ln.suffix = append(pre, ln.suffix...) + ln.suffix = pre + ln.suffix } else { // We are a node here, we need to add in the old prefix. if len(pre) > 0 { bsn := sn.base() - sn.setPrefix(append(pre, bsn.prefix...)) + bsn.prefix = pre + bsn.prefix } } *np = sn @@ -367,7 +366,7 @@ func (t *SubjectTree[T]) match(n node, parts [][]byte, pre []byte, cb func(subje if !cb(append(pre, ln.suffix...), &ln.value) { return false } - } else if hasTermPWC && bytes.IndexByte(ln.suffix, tsep) < 0 { + } else if hasTermPWC && strings.IndexByte(ln.suffix, tsep) < 0 { if !cb(append(pre, ln.suffix...), &ln.value) { return false } @@ -446,7 +445,7 @@ func (t *SubjectTree[T]) iter(n node, pre []byte, ordered bool, cb func(subject } } // Now sort. - slices.SortStableFunc(nodes, func(a, b node) int { return bytes.Compare(a.path(), b.path()) }) + slices.SortStableFunc(nodes, func(a, b node) int { return strings.Compare(a.path(), b.path()) }) // Now walk the nodes in order and call into next iter. for i := range nodes { if !t.iter(nodes[i], pre, true, cb) { @@ -541,3 +540,13 @@ func bytesToString(b []byte) string { p := unsafe.SliceData(b) return unsafe.String(p, len(b)) } + +// Note this will avoid a copy of the string data, but the returned slice must +// only be used for reading since strings are immutable. +func stringToBytes(s string) []byte { + if len(s) == 0 { + return nil + } + p := unsafe.StringData(s) + return unsafe.Slice(p, len(s)) +} diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/util.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/util.go index 8cb6224fec..820b7d481d 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/stree/util.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/util.go @@ -32,16 +32,6 @@ func commonPrefixLen(s1, s2 []byte) int { return i } -// Helper to copy bytes. -func copyBytes(src []byte) []byte { - if len(src) == 0 { - return nil - } - dst := make([]byte, len(src)) - copy(dst, src) - return dst -} - type position interface{ int | uint16 } // No pivot available. diff --git a/vendor/modules.txt b/vendor/modules.txt index f6c29fa8b9..f1d1a62069 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -93,7 +93,7 @@ github.com/alexedwards/argon2id # github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 ## explicit github.com/amoghe/go-crypt -# github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op +# github.com/antithesishq/antithesis-sdk-go v0.7.2-default-no-op ## explicit; go 1.24.0 github.com/antithesishq/antithesis-sdk-go/assert github.com/antithesishq/antithesis-sdk-go/internal @@ -1146,7 +1146,7 @@ github.com/munnerz/goautoneg # github.com/nats-io/jwt/v2 v2.8.2 ## explicit; go 1.25.0 github.com/nats-io/jwt/v2 -# github.com/nats-io/nats-server/v2 v2.14.3 +# github.com/nats-io/nats-server/v2 v2.14.4 ## explicit; go 1.25.0 github.com/nats-io/nats-server/v2/conf github.com/nats-io/nats-server/v2/internal/fastrand