From 703b8dd0843a91f0b5fc118ccbc5b32ab0e41a0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:34:29 +0000 Subject: [PATCH] build(deps): bump github.com/nats-io/nats-server/v2 Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.11.9 to 2.12.0. - [Release notes](https://github.com/nats-io/nats-server/releases) - [Changelog](https://github.com/nats-io/nats-server/blob/main/.goreleaser.yml) - [Commits](https://github.com/nats-io/nats-server/compare/v2.11.9...v2.12.0) --- updated-dependencies: - dependency-name: github.com/nats-io/nats-server/v2 dependency-version: 2.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 +- go.sum | 8 +- .../github.com/nats-io/jwt/v2/user_claims.go | 1 + .../nats-io/nats-server/v2/server/auth.go | 184 +- .../nats-server/v2/server/auth_callout.go | 14 +- .../nats-server/v2/server/ciphersuites.go | 87 +- .../nats-io/nats-server/v2/server/client.go | 452 +++-- .../nats-io/nats-server/v2/server/const.go | 7 +- .../nats-io/nats-server/v2/server/consumer.go | 286 ++- .../nats-server/v2/server/elastic/elastic.go | 60 + .../nats-io/nats-server/v2/server/errors.go | 8 + .../nats-io/nats-server/v2/server/errors.json | 292 ++- .../nats-io/nats-server/v2/server/events.go | 158 +- .../nats-server/v2/server/filestore.go | 850 ++++++-- .../nats-io/nats-server/v2/server/gateway.go | 22 +- .../nats-server/v2/server/jetstream.go | 81 +- .../nats-server/v2/server/jetstream_api.go | 287 ++- .../v2/server/jetstream_batching.go | 653 ++++++ .../v2/server/jetstream_cluster.go | 1110 +++++++---- .../v2/server/jetstream_errors_generated.go | 766 ++++++-- .../nats-server/v2/server/jetstream_events.go | 23 +- .../v2/server/jetstream_versioning.go | 32 +- .../nats-io/nats-server/v2/server/leafnode.go | 51 +- .../nats-io/nats-server/v2/server/memstore.go | 106 +- .../nats-io/nats-server/v2/server/monitor.go | 135 +- .../nats-io/nats-server/v2/server/mqtt.go | 4 +- .../nats-io/nats-server/v2/server/nkey.go | 2 +- .../nats-io/nats-server/v2/server/opts.go | 288 ++- .../nats-io/nats-server/v2/server/raft.go | 282 ++- .../nats-io/nats-server/v2/server/reload.go | 133 +- .../nats-io/nats-server/v2/server/route.go | 13 +- .../nats-server/v2/server/scheduler.go | 239 +++ .../nats-io/nats-server/v2/server/server.go | 134 +- .../nats-io/nats-server/v2/server/store.go | 24 +- .../nats-io/nats-server/v2/server/stream.go | 1742 ++++++++++++++--- .../v2/server/subject_transform.go | 79 +- .../nats-io/nats-server/v2/server/sublist.go | 3 +- .../nats-io/nats-server/v2/server/thw/thw.go | 2 +- .../nats-io/nats-server/v2/server/util.go | 2 +- .../nats-server/v2/server/websocket.go | 9 +- vendor/modules.txt | 5 +- 41 files changed, 6938 insertions(+), 1700 deletions(-) create mode 100644 vendor/github.com/nats-io/nats-server/v2/server/elastic/elastic.go create mode 100644 vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go create mode 100644 vendor/github.com/nats-io/nats-server/v2/server/scheduler.go diff --git a/go.mod b/go.mod index a0ebe63db..222388537 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( github.com/mitchellh/mapstructure v1.5.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.11.9 + github.com/nats-io/nats-server/v2 v2.12.0 github.com/nats-io/nats.go v1.46.0 github.com/oklog/run v1.2.0 github.com/olekukonko/tablewriter v1.0.9 @@ -296,7 +296,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nats-io/jwt/v2 v2.7.4 // indirect + github.com/nats-io/jwt/v2 v2.8.0 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/nxadm/tail v1.4.8 // indirect diff --git a/go.sum b/go.sum index 673a63473..5035bc0f7 100644 --- a/go.sum +++ b/go.sum @@ -881,10 +881,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= -github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI= -github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= -github.com/nats-io/nats-server/v2 v2.11.9 h1:k7nzHZjUf51W1b08xiQih63Rdxh0yr5O4K892Mx5gQA= -github.com/nats-io/nats-server/v2 v2.11.9/go.mod h1:1MQgsAQX1tVjpf3Yzrk3x2pzdsZiNL/TVP3Amhp3CR8= +github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= +github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= +github.com/nats-io/nats-server/v2 v2.12.0 h1:OIwe8jZUqJFrh+hhiyKu8snNib66qsx806OslqJuo74= +github.com/nats-io/nats-server/v2 v2.12.0/go.mod h1:nr8dhzqkP5E/lDwmn+A2CvQPMd1yDKXQI7iGg3lAvww= github.com/nats-io/nats.go v1.46.0 h1:iUcX+MLT0HHXskGkz+Sg20sXrPtJLsOojMDTDzOHSb8= github.com/nats-io/nats.go v1.46.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= diff --git a/vendor/github.com/nats-io/jwt/v2/user_claims.go b/vendor/github.com/nats-io/jwt/v2/user_claims.go index 294cc4b75..2cda51c84 100644 --- a/vendor/github.com/nats-io/jwt/v2/user_claims.go +++ b/vendor/github.com/nats-io/jwt/v2/user_claims.go @@ -36,6 +36,7 @@ type UserPermissionLimits struct { Permissions Limits BearerToken bool `json:"bearer_token,omitempty"` + ProxyRequired bool `json:"proxy_required,omitempty"` AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"` } 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 ce917a06e..2d735c0e2 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 @@ -25,6 +25,7 @@ import ( "net" "net/url" "regexp" + "slices" "strings" "sync/atomic" "time" @@ -65,6 +66,7 @@ type NkeyUser struct { Account *Account `json:"account,omitempty"` SigningKey string `json:"signing_key,omitempty"` AllowedConnectionTypes map[string]struct{} `json:"connection_types,omitempty"` + ProxyRequired bool `json:"proxy_required,omitempty"` } // User is for multiple accounts/users. @@ -75,6 +77,7 @@ type User struct { Account *Account `json:"account,omitempty"` ConnectionDeadline time.Time `json:"connection_deadline,omitempty"` AllowedConnectionTypes map[string]struct{} `json:"connection_types,omitempty"` + ProxyRequired bool `json:"proxy_required,omitempty"` } // clone performs a deep copy of the User struct, returning a new clone with @@ -593,10 +596,30 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au ao bool // auth override ) + // Little helper that will log the error as a debug statement, set the auth error in + // the connection and return false to indicate authentication failure. + setProxyAuthError := func(err error) bool { + c.Debugf(err.Error()) + c.setAuthError(err) + return false + } + + // Indicate if this connection came from a trusted proxy. Note that if + // trustedProxy could be false even if the connection is proxied, but it + // means that there was no trusted proxy configured. + trustedProxy, ok := s.proxyCheck(c, opts) + if trustedProxy && !ok { + return setProxyAuthError(ErrAuthProxyNotTrusted) + } + + var proxyRequired bool // Check if we have auth callouts enabled at the server level or in the bound account. defer func() { - // Default reason - reason := AuthenticationViolation.String() + authErr := c.getAuthError() + if authErr == nil { + authErr = ErrAuthentication + } + reason := getAuthErrClosedState(authErr).String() // No-op if juc == nil && opts.AuthCallout == nil { if !authorized { @@ -656,7 +679,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au // If we are here we have an auth callout defined and we have failed auth so far // so we will callout to our auth backend for processing. if !skip { - authorized, reason = s.processClientOrLeafCallout(c, opts) + authorized, reason = s.processClientOrLeafCallout(c, opts, proxyRequired, trustedProxy) } // Check if we are authorized and in the auth callout account, and if so add in deny publish permissions for the auth subject. if authorized { @@ -697,6 +720,11 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au if !authRequired { // TODO(dlc) - If they send us credentials should we fail? s.mu.Unlock() + if c.kind == LEAF { + // Auth is not required, register the leaf node with the selected account. + // Otherwise, auth needs to match client auth. + return s.registerLeafWithAccount(c, opts.LeafNode.Account) + } return true } var ( @@ -769,6 +797,10 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au c.Debugf("User JWT not valid: %v", err) return false } + if proxyRequired = juc.ProxyRequired; proxyRequired && !trustedProxy { + s.mu.Unlock() + return setProxyAuthError(ErrAuthProxyRequired) + } vr := jwt.CreateValidationResults() juc.Validate(vr) if vr.IsBlocking(true) { @@ -1045,6 +1077,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au } if nkey != nil { + if proxyRequired = nkey.ProxyRequired; proxyRequired && !trustedProxy { + return setProxyAuthError(ErrAuthProxyRequired) + } // If we did not match noAuthUser check signature which is required. if nkey.Nkey != noAuthUser { if c.opts.Sig == _EMPTY_ { @@ -1076,6 +1111,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au return true } if user != nil { + if proxyRequired = user.ProxyRequired; proxyRequired && !trustedProxy { + return setProxyAuthError(ErrAuthProxyRequired) + } ok = comparePasswords(user.Password, c.opts.Password) // If we are authorized, register the user which will properly setup any permissions // for pub/sub authorizations. @@ -1086,6 +1124,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au } if c.kind == CLIENT { + if proxyRequired = opts.ProxyRequired; proxyRequired && !trustedProxy { + return setProxyAuthError(ErrAuthProxyRequired) + } if token != _EMPTY_ { return comparePasswords(token, c.opts.Token) } else if username != _EMPTY_ { @@ -1094,16 +1135,65 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au } return comparePasswords(password, c.opts.Password) } - } else if c.kind == LEAF { - // There is no required username/password to connect and - // there was no u/p in the CONNECT or none that matches the - // know users. Register the leaf connection with global account - // or the one specified in config (if provided). - return s.registerLeafWithAccount(c, opts.LeafNode.Account) } return false } +// If there are configured trusted proxies and this connection comes +// from a proxy whose signature can be verified by one of the known +// trusted key, this function will return `true, true`. If the signature +// cannot be verified by any, it will return `true, false`. +// If the connectio is not proxied, or there are no configured trusted +// proxies, then this function returns `false, false`. +// +// Server lock MUST NOT be held on entry since this function will grab +// the read lock to extract the list of proxy trusted keys. The signature +// verification process will be done outside of the lock. +func (s *Server) proxyCheck(c *client, opts *Options) (bool, bool) { + // If there is no signature or no configured trusted proxy, return false. + psig := c.opts.ProxySig + if psig == _EMPTY_ || opts.Proxies == nil || len(opts.Proxies.Trusted) == 0 { + return false, false + } + // Decode the signature. + sig, err := base64.RawURLEncoding.DecodeString(psig) + if err != nil { + c.Debugf("Proxy signature not valid base64") + return true, false + } + // Go through the trusted keys and verify the signature. + s.mu.RLock() + keys := slices.Clone(s.proxiesKeyPairs) + s.mu.RUnlock() + for _, kp := range keys { + // We stop at the first that is valid. + if err := kp.Verify(c.nonce, sig); err == nil { + pub, _ := kp.PublicKey() + // Track which proxy public key is used by this connection. + c.mu.Lock() + c.proxyKey = pub + cid := c.cid + c.mu.Unlock() + // Track this proxied connection so that it can be closed + // if the trusted key is removed on configuration reload. + s.mu.Lock() + if s.proxiedConns == nil { + s.proxiedConns = make(map[string]map[uint64]*client) + } + clients := s.proxiedConns[pub] + if clients == nil { + clients = make(map[uint64]*client) + } + clients[cid] = c + s.proxiedConns[pub] = clients + s.mu.Unlock() + return true, true + } + } + // We could not verify the signature, so indicate failure. + return true, false +} + func getTLSAuthDCs(rdns *pkix.RDNSequence) string { dcOID := asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25} dcs := []string{} @@ -1352,7 +1442,25 @@ func (s *Server) registerLeafWithAccount(c *client, account string) bool { func (s *Server) isLeafNodeAuthorized(c *client) bool { opts := s.getOpts() - isAuthorized := func(username, password, account string) bool { + setProxyAuthError := func(err error) bool { + c.Debugf(err.Error()) + c.setAuthError(err) + return false + } + + isAuthorized := func(username, password, account string, proxyRequired bool) bool { + trustedProxy, ok := s.proxyCheck(c, opts) + if trustedProxy && !ok { + return setProxyAuthError(ErrAuthProxyNotTrusted) + } + // A given user may not be required, but if the boolean is set at the + // authorization top-level, then override. + if !proxyRequired && opts.LeafNode.ProxyRequired { + proxyRequired = true + } + if proxyRequired && !trustedProxy { + return setProxyAuthError(ErrAuthProxyRequired) + } if username != c.opts.Username { return false } @@ -1366,8 +1474,16 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool { // The user in CONNECT must match. We will bind to the account associated // with that user (from the leafnode's authorization{} config). if opts.LeafNode.Username != _EMPTY_ { - return isAuthorized(opts.LeafNode.Username, opts.LeafNode.Password, opts.LeafNode.Account) + return isAuthorized(opts.LeafNode.Username, opts.LeafNode.Password, opts.LeafNode.Account, + opts.LeafNode.ProxyRequired) } else if opts.LeafNode.Nkey != _EMPTY_ { + trustedProxy, ok := s.proxyCheck(c, opts) + if trustedProxy && !ok { + return false + } + if opts.LeafNode.ProxyRequired && !trustedProxy { + return setProxyAuthError(ErrAuthProxyRequired) + } if c.opts.Nkey != opts.LeafNode.Nkey { return false } @@ -1421,7 +1537,7 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool { } // This will authorize since are using an existing user, // but it will also register with proper account. - return isAuthorized(user.Username, user.Password, accName) + return isAuthorized(user.Username, user.Password, accName, user.ProxyRequired) } // This is expected to be a very small array. @@ -1431,7 +1547,7 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool { if u.Account != nil { accName = u.Account.Name } - return isAuthorized(u.Username, u.Password, accName) + return isAuthorized(u.Username, u.Password, accName, u.ProxyRequired) } } return false @@ -1536,3 +1652,45 @@ func validateNoAuthUser(o *Options, noAuthUser string) error { `no_auth_user: "%s" not present as user or nkey in authorization block or account configuration`, noAuthUser) } + +func validateProxies(o *Options) error { + if o.Proxies == nil { + return nil + } + for _, p := range o.Proxies.Trusted { + if !nkeys.IsValidPublicKey(p.Key) { + return fmt.Errorf("proxy trusted key %q is invalid", p.Key) + } + } + return nil +} + +// Create a list of nkeys.KeyPair corresponding to the public keys +// of the Proxies.TrustedKeys list. +// Server lock must be held on entry. +func (s *Server) processProxiesTrustedKeys() { + // We could be here on reload. + if s.proxiesKeyPairs != nil { + s.proxiesKeyPairs = s.proxiesKeyPairs[:0] + } + if opts := s.getOpts(); opts.Proxies == nil { + return + } + for _, p := range s.getOpts().Proxies.Trusted { + // Can't fail since we have already checked that it was a valid key. + kp, _ := nkeys.FromPublicKey(p.Key) + s.proxiesKeyPairs = append(s.proxiesKeyPairs, kp) + } +} + +// Returns the connection's `ClosedState` for the given authenication error. +func getAuthErrClosedState(authErr error) ClosedState { + switch authErr { + case ErrAuthProxyNotTrusted: + return ProxyNotTrusted + case ErrAuthProxyRequired: + return ProxyRequired + default: + return AuthenticationViolation + } +} diff --git a/vendor/github.com/nats-io/nats-server/v2/server/auth_callout.go b/vendor/github.com/nats-io/nats-server/v2/server/auth_callout.go index 038d53bb3..a7ed073e9 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/auth_callout.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/auth_callout.go @@ -33,7 +33,7 @@ const ( ) // Process a callout on this client's behalf. -func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorized bool, errStr string) { +func (s *Server) processClientOrLeafCallout(c *client, opts *Options, proxyRequired, trustedProxy bool) (authorized bool, errStr string) { isOperatorMode := len(opts.TrustedKeys) > 0 // this is the account the user connected in, or the one running the callout @@ -245,6 +245,16 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize respCh <- titleCase(err.Error()) return } + // If the caller had established that the user should go through a proxy, + // or if the `arc` JWT requires it, and we don't have a trusted proxy, + // reject the connection. + if (proxyRequired || arc.ProxyRequired) && !trustedProxy { + err = ErrAuthProxyRequired + c.setAuthError(err) + c.authViolation() + respCh <- titleCase(err.Error()) + return + } vr := jwt.CreateValidationResults() arc.Validate(vr) if len(vr.Issues) > 0 { @@ -369,7 +379,7 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize conn := c.nc.(*tls.Conn) cs := conn.ConnectionState() ct.Version = tlsVersion(cs.Version) - ct.Cipher = tlsCipher(cs.CipherSuite) + ct.Cipher = tls.CipherSuiteName(cs.CipherSuite) // Check verified chains. for _, vs := range cs.VerifiedChains { var certs []string diff --git a/vendor/github.com/nats-io/nats-server/v2/server/ciphersuites.go b/vendor/github.com/nats-io/nats-server/v2/server/ciphersuites.go index 740db5128..a85b9b26b 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/ciphersuites.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/ciphersuites.go @@ -17,85 +17,44 @@ import ( "crypto/tls" ) -// Where we maintain all of the available ciphers -var cipherMap = map[string]uint16{ - "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, - "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, - "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, - "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, - "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, - "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, - "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, +func init() { + for _, cs := range tls.CipherSuites() { + cipherMap[cs.Name] = cs + cipherMapByID[cs.ID] = cs + } + for _, cs := range tls.InsecureCipherSuites() { + cipherMap[cs.Name] = cs + cipherMapByID[cs.ID] = cs + } } -var cipherMapByID = map[uint16]string{ - tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA", - tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA", - tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA", - tls.TLS_RSA_WITH_AES_128_CBC_SHA256: "TLS_RSA_WITH_AES_128_CBC_SHA256", - tls.TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA", - tls.TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384", - tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", - tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: "TLS_ECDHE_RSA_WITH_RC4_128_SHA", - tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", - tls.TLS_AES_128_GCM_SHA256: "TLS_AES_128_GCM_SHA256", - tls.TLS_AES_256_GCM_SHA384: "TLS_AES_256_GCM_SHA384", - tls.TLS_CHACHA20_POLY1305_SHA256: "TLS_CHACHA20_POLY1305_SHA256", -} +var cipherMap = map[string]*tls.CipherSuite{} +var cipherMapByID = map[uint16]*tls.CipherSuite{} func defaultCipherSuites() []uint16 { - return []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + ciphers := tls.CipherSuites() + defaults := make([]uint16, 0, len(ciphers)) + for _, cs := range ciphers { + defaults = append(defaults, cs.ID) } + return defaults } // Where we maintain available curve preferences var curvePreferenceMap = map[string]tls.CurveID{ - "X25519": tls.X25519, - "CurveP256": tls.CurveP256, - "CurveP384": tls.CurveP384, - "CurveP521": tls.CurveP521, + "X25519MLKEM768": tls.X25519MLKEM768, + "X25519": tls.X25519, + "CurveP256": tls.CurveP256, + "CurveP384": tls.CurveP384, + "CurveP521": tls.CurveP521, } // reorder to default to the highest level of security. See: // https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go func defaultCurvePreferences() []tls.CurveID { return []tls.CurveID{ - tls.X25519, // faster than P256, arguably more secure + tls.X25519MLKEM768, // post-quantum + tls.X25519, // faster than P256, arguably more secure tls.CurveP256, tls.CurveP384, tls.CurveP521, 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 bad079d34..cd1ba3e83 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 @@ -35,6 +35,8 @@ import ( "sync/atomic" "time" + "slices" + "github.com/klauspost/compress/s2" "github.com/nats-io/jwt/v2" "github.com/nats-io/nats-server/v2/internal/fastrand" @@ -222,6 +224,8 @@ const ( MinimumVersionRequired ClusterNamesIdentical Kicked + ProxyNotTrusted + ProxyRequired ) // Some flags passed to processMsgResults @@ -255,6 +259,8 @@ type client struct { pubKey string nc net.Conn ncs atomic.Value + ncsAcc atomic.Value + ncsUser atomic.Value out outbound user *NkeyUser host string @@ -270,6 +276,7 @@ type client struct { msgb [msgScratchSize]byte last time.Time lastIn time.Time + proxyKey string repliesSincePrune uint16 lastReplyPrune time.Time @@ -298,6 +305,13 @@ type client struct { nameTag string tlsTo *time.Timer + + // Authentication error override. This is used because the authentication + // stack is simply returning a boolean, and the only authentication error + // reported is the generic `ErrAuthentication`. In the authentication code, + // if we want to report a different error, we can now set this field + // and `authViolation()` will use that one. + authErr error } type rrTracking struct { @@ -494,15 +508,13 @@ func (rcf readCacheFlag) isSet(c readCacheFlag) bool { } const ( - defaultMaxPerAccountCacheSize = 8192 - defaultPrunePerAccountCacheSize = 1024 - defaultClosedSubsCheckInterval = 5 * time.Minute + defaultMaxPerAccountCacheSize = 8192 + defaultClosedSubsCheckInterval = 5 * time.Minute ) var ( - maxPerAccountCacheSize = defaultMaxPerAccountCacheSize - prunePerAccountCacheSize = defaultPrunePerAccountCacheSize - closedSubsCheckInterval = defaultClosedSubsCheckInterval + maxPerAccountCacheSize = defaultMaxPerAccountCacheSize + closedSubsCheckInterval = defaultClosedSubsCheckInterval ) // perAccountCache is for L1 semantics for inbound messages from a route or gateway to mimic the performance of clients. @@ -652,6 +664,9 @@ type ClientOpts struct { // Leafnodes RemoteAccount string `json:"remote_account,omitempty"` + + // Proxy would include its own nonce signature. + ProxySig string `json:"proxy_sig,omitempty"` } var defaultOpts = ClientOpts{Verbose: true, Pedantic: true, Echo: true} @@ -1854,6 +1869,19 @@ func (c *client) markConnAsClosed(reason ClosedState) { case ReadError, WriteError, SlowConsumerPendingBytes, SlowConsumerWriteDeadline, TLSHandshakeError: c.flags.set(skipFlushOnClose) skipFlush = true + case StaleConnection: + // Track stale connections statistics. + atomic.AddInt64(&c.srv.staleConnections, 1) + switch c.kind { + case CLIENT: + c.srv.staleStats.clients.Add(1) + case ROUTER: + c.srv.staleStats.routes.Add(1) + case GATEWAY: + c.srv.staleStats.gateways.Add(1) + case LEAF: + c.srv.staleStats.leafs.Add(1) + } } if c.flags.isSet(connMarkedClosed) { return @@ -1867,14 +1895,25 @@ func (c *client) markConnAsClosed(reason ClosedState) { // Be consistent with the creation: for routes, gateways and leaf, // we use Noticef on create, so use that too for delete. if c.srv != nil { - if c.kind == LEAF { - if c.acc != nil { - c.Noticef("%s connection closed: %s - Account: %s", c.kindString(), reason, c.acc.traceLabel()) + if c.kind == LEAF || c.kind == ROUTER || c.kind == GATEWAY { + var tags []string + var remoteName string + switch { + case c.kind == LEAF && c.leaf != nil: + remoteName = c.leaf.remoteServer + case c.kind == ROUTER && c.route != nil: + remoteName = c.route.remoteName + case c.kind == GATEWAY && c.gw != nil: + remoteName = c.gw.remoteName + } + if remoteName != _EMPTY_ { + tags = append(tags, fmt.Sprintf("Remote: %s", remoteName)) + } + if len(tags) > 0 { + c.Noticef("%s connection closed: %s - %s", c.kindString(), reason, strings.Join(tags, ", ")) } else { c.Noticef("%s connection closed: %s", c.kindString(), reason) } - } else if c.kind == ROUTER || c.kind == GATEWAY { - c.Noticef("%s connection closed: %s", c.kindString(), reason) } else { // Client, System, Jetstream, and Account connections. c.Debugf("%s connection closed: %s", c.kindString(), reason) } @@ -1917,7 +1956,7 @@ func (c *client) flushSignal() { // Traces a message. // Will NOT check if tracing is enabled, does NOT need the client lock. -func (c *client) traceMsg(msg []byte) { +func (c *client) traceMsgInternal(msg []byte, delivered bool, hdrSize int) { opts := c.srv.getOpts() maxTrace := opts.MaxTracedMsgLen headersOnly := opts.TraceHeaders @@ -1926,8 +1965,13 @@ func (c *client) traceMsg(msg []byte) { // If TraceHeaders is enabled, extract only the header portion of the msg. // If a header is present, it ends with an additional trailing CRLF. if headersOnly { - msg, _ = c.msgParts(msg) - suffix += LEN_CR_LF + if hdrSize > 0 && len(msg) >= hdrSize { + msg = msg[:hdrSize] + suffix += LEN_CR_LF + } else { + // No headers present, so nothing to trace. + return + } } // Do not emit a log line for zero-length payloads. @@ -1936,14 +1980,32 @@ func (c *client) traceMsg(msg []byte) { return } + const ( + traceInPrefix string = "<<-" + traceOutPrefix string = "->>" + ) + var prefix string + if delivered { + prefix = traceOutPrefix + } else { + prefix = traceInPrefix + } if maxTrace > 0 && l > maxTrace { tm := fmt.Sprintf("%q", msg[:maxTrace]) - c.Tracef("<<- MSG_PAYLOAD: [\"%s...\"]", tm[1:len(tm)-1]) + c.Tracef("%s MSG_PAYLOAD: [\"%s...\"]", prefix, tm[1:len(tm)-1]) } else { - c.Tracef("<<- MSG_PAYLOAD: [%q]", msg[:l]) + c.Tracef("%s MSG_PAYLOAD: [%q]", prefix, msg[:l]) } } +func (c *client) traceMsg(msg []byte) { + c.traceMsgInternal(msg, false, c.pa.hdr) +} + +func (c *client) traceMsgDelivery(msg []byte, hdrSize int) { + c.traceMsgInternal(msg, true, hdrSize) +} + // Traces an incoming operation. // Will NOT check if tracing is enabled, does NOT need the client lock. func (c *client) traceInOp(op string, arg []byte) { @@ -2094,6 +2156,7 @@ func (c *client) processConnect(arg []byte) error { } // Indicate that the CONNECT protocol has been received, and that the // server now knows which protocol this client supports. + firstConnect := !c.flags.isSet(connectReceived) c.flags.set(connectReceived) // Capture these under lock c.echo = c.opts.Echo @@ -2103,30 +2166,6 @@ func (c *client) processConnect(arg []byte) error { account := c.opts.Account accountNew := c.opts.AccountNew - if c.kind == CLIENT { - var ncs string - if c.opts.Version != _EMPTY_ { - ncs = fmt.Sprintf("v%s", c.opts.Version) - } - if c.opts.Lang != _EMPTY_ { - if c.opts.Version == _EMPTY_ { - ncs = c.opts.Lang - } else { - ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Lang) - } - } - if c.opts.Name != _EMPTY_ { - if c.opts.Version == _EMPTY_ && c.opts.Lang == _EMPTY_ { - ncs = c.opts.Name - } else { - ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Name) - } - } - if ncs != _EMPTY_ { - c.ncs.CompareAndSwap(nil, fmt.Sprintf("%s - %q", c, ncs)) - } - } - // if websocket client, maybe some options through cookies if ws := c.ws; ws != nil { // if JWT not in the CONNECT, use the cookie JWT (possibly empty). @@ -2200,6 +2239,55 @@ func (c *client) processConnect(arg []byte) error { // By default register with the global account. c.registerWithAccount(srv.globalAccount()) } + + // Initialize user info used in logs. + c.mu.Lock() + acc := c.acc + if c.getRawAuthUser() != _EMPTY_ { + c.ncsUser.Store(c.getAuthUserLabel()) + } + c.mu.Unlock() + if acc != nil { + acc.mu.RLock() + c.ncsAcc.Store(acc.traceLabel()) + acc.mu.RUnlock() + } + + // Enable logging connection details and auth info for this client. + if c.kind == CLIENT && firstConnect && c.srv != nil { + var ncs string + if c.opts.Version != _EMPTY_ { + ncs = fmt.Sprintf("v%s", c.opts.Version) + } + if c.opts.Lang != _EMPTY_ { + if c.opts.Version == _EMPTY_ { + ncs = c.opts.Lang + } else { + ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Lang) + } + } + if c.opts.Name != _EMPTY_ { + if c.opts.Version == _EMPTY_ && c.opts.Lang == _EMPTY_ { + ncs = c.opts.Name + } else { + ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Name) + } + } + var acs string + accl := c.ncsAcc.Load() + authUser := c.ncsUser.Load() + if accl != nil && authUser != nil { + acs = fmt.Sprintf("%s/%s", accl, authUser) + } + switch { + case ncs != _EMPTY_ && acs != _EMPTY_: + c.ncs.Store(fmt.Sprintf("%s - %q - %q", c, ncs, acs)) + case ncs != _EMPTY_: + c.ncs.Store(fmt.Sprintf("%s - %q", c, ncs)) + case acs != _EMPTY_: + c.ncs.Store(fmt.Sprintf("%s - %q", c, acs)) + } + } } switch kind { @@ -2238,12 +2326,12 @@ func (c *client) processConnect(arg []byte) error { func (c *client) sendErrAndErr(err string) { c.sendErr(err) - c.Errorf(err) + c.RateLimitErrorf(err) } func (c *client) sendErrAndDebug(err string) { c.sendErr(err) - c.Debugf(err) + c.RateLimitDebugf(err) } func (c *client) authTimeout() { @@ -2262,38 +2350,28 @@ func (c *client) accountAuthExpired() { } func (c *client) authViolation() { - var s *Server - var hasTrustedNkeys, hasNkeys, hasUsers bool - if s = c.srv; s != nil { - s.mu.RLock() - hasTrustedNkeys = s.trustedKeys != nil - hasNkeys = s.nkeys != nil - hasUsers = s.users != nil - s.mu.RUnlock() - defer s.sendAuthErrorEvent(c) + authErr := c.getAuthError() + if authErr == nil { + authErr = ErrAuthentication } + reason := getAuthErrClosedState(authErr) - if hasTrustedNkeys { - c.Errorf("%v", ErrAuthentication) - } else if hasNkeys { - c.Errorf("%s - Nkey %q", - ErrAuthentication.Error(), - c.opts.Nkey) - } else if hasUsers { - c.Errorf("%s - User %q", - ErrAuthentication.Error(), - c.opts.Username) - } else { - if c.srv != nil { + var s *Server + if s = c.srv; s != nil { + defer s.sendAuthErrorEvent(c, reason.String()) + if c.getRawAuthUser() != _EMPTY_ { + c.Errorf("%v - %s", ErrAuthentication, c.getAuthUser()) + } else { c.Errorf(ErrAuthentication.Error()) } } if c.isMqtt() { c.mqttEnqueueConnAck(mqttConnAckRCNotAuthorized, false) } else { + // Send this to client, regardless of the authErr override. c.sendErr("Authorization Violation") } - c.closeConnection(AuthenticationViolation) + c.closeConnection(reason) } func (c *client) maxAccountConnExceeded() { @@ -2523,36 +2601,29 @@ func (c *client) processPing() { // If we are here, the CONNECT has been received so we know // if this client supports async INFO or not. var ( - checkInfoChange bool + sendConnectInfo bool srv = c.srv ) - // For older clients, just flip the firstPongSent flag if not already - // set and we are done. - if c.opts.Protocol < ClientProtoInfo || srv == nil { - c.flags.setIfNotSet(firstPongSent) - } else { - // This is a client that supports async INFO protocols. - // If this is the first PING (so firstPongSent is not set yet), - // we will need to check if there was a change in cluster topology - // or we have a different max payload. We will send this first before - // pong since most clients do flush after connect call. - checkInfoChange = !c.flags.isSet(firstPongSent) + // For the first PING (so firstPongSet is false) and for clients + // that support async INFO protocols, we will send one with ConnectInfo=true, + // the name of the account the client is bound to, and if the + // account is the system account. + if !c.flags.isSet(firstPongSent) { + // Flip the flag. + c.flags.set(firstPongSent) + // Evaluate if we should send the INFO protocol. + sendConnectInfo = srv != nil && c.opts.Protocol >= ClientProtoInfo } c.mu.Unlock() - if checkInfoChange { - opts := srv.getOpts() + if sendConnectInfo { srv.mu.Lock() + info := srv.copyInfo() c.mu.Lock() - // Now that we are under both locks, we can flip the flag. - // This prevents sendAsyncInfoToClients() and code here to - // send a double INFO protocol. - c.flags.set(firstPongSent) - // If there was a cluster update since this client was created, - // send an updated INFO protocol now. - if srv.lastCURLsUpdate >= c.start.UnixNano() || c.mpay != int32(opts.MaxPayload) { - c.enqueueProto(c.generateClientInfoJSON(srv.copyInfo())) - } + info.RemoteAccount = c.acc.Name + info.IsSystemAccount = c.acc == srv.SystemAccount() + info.ConnectInfo = true + c.enqueueProto(c.generateClientInfoJSON(info)) c.mu.Unlock() srv.mu.Unlock() } @@ -3469,6 +3540,12 @@ func (c *client) stalledWait(producer *client) { c.mu.Unlock() defer c.mu.Lock() + // Track per client and total client stalls. + atomic.AddInt64(&c.stalls, 1) + if c.srv != nil { + atomic.AddInt64(&c.srv.stalls, 1) + } + // Now check if we are close to total allowed. if producer.in.tst+ttl > stallTotalAllowed { ttl = stallTotalAllowed - producer.in.tst @@ -3621,6 +3698,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su // support we need to strip the headers from the payload. // The actual header would have been processed correctly for us, so just // need to update payload. + hdrSize := c.pa.hdr if c.pa.hdr > 0 && !sub.client.headers { msg = msg[c.pa.hdr:] } @@ -3762,6 +3840,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su if client.trace { client.traceOutOp(bytesToString(mh[:len(mh)-LEN_CR_LF]), nil) + client.traceMsgDelivery(msg, hdrSize) } client.mu.Unlock() @@ -4197,7 +4276,8 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) { c.mu.Lock() if c.opts.NoResponders { if sub := c.subForReply(c.pa.reply); sub != nil { - proto := fmt.Sprintf("HMSG %s %s 16 16\r\nNATS/1.0 503\r\n\r\n\r\n", c.pa.reply, sub.sid) + hdrLen := 32 /* header without the subject */ + len(c.pa.subject) + proto := fmt.Sprintf("HMSG %s %s %d %d\r\nNATS/1.0 503\r\nNats-Subject: %s\r\n\r\n\r\n", c.pa.reply, sub.sid, hdrLen, hdrLen, c.pa.subject) c.queueOutbound([]byte(proto)) c.addToPCD(c) } @@ -4257,7 +4337,7 @@ func (c *client) setupResponseServiceImport(acc *Account, si *serviceImport, tra // Will remove a header if present. func removeHeaderIfPresent(hdr []byte, key string) []byte { - start := bytes.Index(hdr, []byte(key)) + start := bytes.Index(hdr, []byte(key+":")) // key can't be first and we want to check that it is preceded by a '\n' if start < 1 || hdr[start-1] != '\n' { return hdr @@ -4406,6 +4486,27 @@ func sliceHeader(key string, hdr []byte) []byte { return hdr[start:index:index] } +func setHeader(key, val string, hdr []byte) []byte { + prefix := []byte(key + ": ") + start := bytes.Index(hdr, prefix) + if start >= 0 { + valStart := start + len(prefix) + valEnd := bytes.Index(hdr[valStart:], []byte("\r")) + if valEnd < 0 { + return hdr // malformed headers + } + valEnd += valStart + suffix := slices.Clone(hdr[valEnd:]) + newHdr := append(hdr[:valStart], val...) + return append(newHdr, suffix...) + } + if len(hdr) > 0 && bytes.HasSuffix(hdr, []byte("\r\n")) { + hdr = hdr[:len(hdr)-2] + val += "\r\n" + } + return fmt.Appendf(hdr, "%s: %s\r\n", key, val) +} + // For bytes.HasPrefix below. var ( jsRequestNextPreB = []byte(jsRequestNextPre) @@ -5272,18 +5373,16 @@ func (c *client) pubPermissionViolation(subject []byte) { mt.setIngressError(errTxt) } c.sendErr(errTxt) - c.Errorf("Publish Violation - %s, Subject %q", c.getAuthUser(), subject) + c.Errorf("Publish Violation - Subject %q", subject) } func (c *client) subPermissionViolation(sub *subscription) { errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q", sub.subject) - logTxt := fmt.Sprintf("Subscription Violation - %s, Subject %q, SID %s", - c.getAuthUser(), sub.subject, sub.sid) + logTxt := fmt.Sprintf("Subscription Violation - Subject %q, SID %s", sub.subject, sub.sid) if sub.queue != nil { errTxt = fmt.Sprintf("Permissions Violation for Subscription to %q using queue %q", sub.subject, sub.queue) - logTxt = fmt.Sprintf("Subscription Violation - %s, Subject %q, Queue: %q, SID %s", - c.getAuthUser(), sub.subject, sub.queue, sub.sid) + logTxt = fmt.Sprintf("Subscription Violation - Subject %q, Queue: %q, SID %s", sub.subject, sub.queue, sub.sid) } c.sendErr(errTxt) @@ -5296,13 +5395,12 @@ func (c *client) replySubjectViolation(reply []byte) { mt.setIngressError(errTxt) } c.sendErr(errTxt) - c.Errorf("Publish Violation - %s, Reply %q", c.getAuthUser(), reply) + c.Errorf("Publish Violation - Reply %q", reply) } func (c *client) maxTokensViolation(sub *subscription) { errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q, too many tokens", sub.subject) - logTxt := fmt.Sprintf("Subscription Violation Too Many Tokens - %s, Subject %q, SID %s", - c.getAuthUser(), sub.subject, sub.sid) + logTxt := fmt.Sprintf("Subscription Violation Too Many Tokens - Subject %q, SID %s", sub.subject, sub.sid) c.sendErr(errTxt) c.Errorf(logTxt) } @@ -5621,10 +5719,8 @@ 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()) + 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()) } } @@ -5914,7 +6010,7 @@ func (c *client) getAccAndResultFromCache() (*Account, *SublistResult) { if genid := atomic.LoadUint64(&sl.genid); genid != pac.genid { ok = false - c.in.pacache = make(map[string]*perAccountCache) + clear(c.in.pacache) } else { acc = pac.acc r = pac.results @@ -5937,13 +6033,33 @@ func (c *client) getAccAndResultFromCache() (*Account, *SublistResult) { // Match against the account sublist. r = sl.MatchBytes(c.pa.subject) - // Check if we need to prune. + // Check if we need to prune. This should give us a perAccountCache struct + // to reuse instead of having to allocate a new one. + // Previously we would have removed multiple entries but now we will only + // prune the minimum number required to maintain the cache size, so that + // we reduce the amount of GC pressure and maintain cache stability as best + // as possible. if len(c.in.pacache) >= maxPerAccountCacheSize { - c.prunePerAccountCache() + for cacheKey, p := range c.in.pacache { + delete(c.in.pacache, cacheKey) + pac = p + if len(c.in.pacache) < maxPerAccountCacheSize { + break + } + } } + // If we can reuse the pac from earlier (i.e. we loaded one but it was an + // old generation or we pruned the cache) then do so. + if pac == nil { + pac = &perAccountCache{} + } + pac.acc = acc + pac.results = r + pac.genid = atomic.LoadUint64(&sl.genid) + // Store in our cache,make sure to do so after we prune. - c.in.pacache[string(c.pa.pacache)] = &perAccountCache{acc, r, atomic.LoadUint64(&sl.genid)} + c.in.pacache[string(c.pa.pacache)] = pac } return acc, r } @@ -5959,17 +6075,6 @@ func (c *client) Account() *Account { return acc } -// prunePerAccountCache will prune off a random number of cache entries. -func (c *client) prunePerAccountCache() { - n := 0 - for cacheKey := range c.in.pacache { - delete(c.in.pacache, cacheKey) - if n++; n > prunePerAccountCacheSize { - break - } - } -} - // pruneClosedSubFromPerAccountCache remove entries that contain subscriptions // that have been closed. func (c *client) pruneClosedSubFromPerAccountCache() { @@ -6225,6 +6330,22 @@ func (c *client) getAuthUser() string { } } +// getAuthUserLabel returns a label for the auth user for the client. +func (c *client) getAuthUserLabel() string { + switch { + case c.opts.Nkey != _EMPTY_: + return fmt.Sprintf("nkey:%s", c.opts.Nkey) + case c.opts.Username != _EMPTY_: + return fmt.Sprintf("user:%s", c.opts.Username) + case c.opts.JWT != _EMPTY_: + return fmt.Sprintf("jwt:%s", c.pubKey) + case c.opts.Token != _EMPTY_: + return "token" + default: + return "" + } +} + // Given an array of strings, this function converts it to a map as long // as all the content (converted to upper-case) matches some constants. @@ -6300,51 +6421,107 @@ func (c *client) isClosed() bool { return c.flags.isSet(closeConnection) || c.flags.isSet(connMarkedClosed) || c.nc == nil } +func (c *client) format(format string) string { + if s := c.String(); s != _EMPTY_ { + return fmt.Sprintf("%s - %s", s, format) + } else { + return format + } +} + +func (c *client) formatNoClientInfo(format string) string { + acc := c.ncsAcc.Load() + if acc != nil { + return fmt.Sprintf("%s - Account:%s", format, acc) + } else { + return format + } +} + +func (c *client) formatClientSuffix() string { + user := c.ncsUser.Load() + if user == nil || user.(string) == _EMPTY_ { + return _EMPTY_ + } + return fmt.Sprintf(" - %s", user) +} + // Logging functionality scoped to a client or route. func (c *client) Error(err error) { - c.srv.Errors(c, err) + c.srv.Errorf(c.format(err.Error())) } func (c *client) Errorf(format string, v ...any) { - format = fmt.Sprintf("%s - %s", c, format) - c.srv.Errorf(format, v...) + c.srv.Errorf(c.format(format), v...) } func (c *client) Debugf(format string, v ...any) { - format = fmt.Sprintf("%s - %s", c, format) - c.srv.Debugf(format, v...) + c.srv.Debugf(c.format(format), v...) } func (c *client) Noticef(format string, v ...any) { - format = fmt.Sprintf("%s - %s", c, format) - c.srv.Noticef(format, v...) + c.srv.Noticef(c.format(format), v...) } func (c *client) Tracef(format string, v ...any) { - format = fmt.Sprintf("%s - %s", c, format) - c.srv.Tracef(format, v...) + c.srv.Tracef(c.format(format), v...) } func (c *client) Warnf(format string, v ...any) { - format = fmt.Sprintf("%s - %s", c, format) - c.srv.Warnf(format, v...) + c.srv.Warnf(c.format(format), v...) +} + +func (c *client) RateLimitErrorf(format string, v ...any) { + // Do the check before adding the client info to the format... + statement := fmt.Sprintf(c.formatNoClientInfo(format), v...) + if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded { + return + } + if s := c.String(); s != _EMPTY_ { + c.srv.Errorf("%s - %s%s", c, statement, c.formatClientSuffix()) + } else { + c.srv.Errorf("%s%s", statement, c.formatClientSuffix()) + } } func (c *client) rateLimitFormatWarnf(format string, v ...any) { + // Do the check before adding the client info to the format... + format = c.formatNoClientInfo(format) if _, loaded := c.srv.rateLimitLogging.LoadOrStore(format, time.Now()); loaded { return } statement := fmt.Sprintf(format, v...) - c.Warnf("%s", statement) + if s := c.String(); s != _EMPTY_ { + c.srv.Warnf("%s - %s%s", c, statement, c.formatClientSuffix()) + } else { + c.srv.Warnf("%s%s", statement, c.formatClientSuffix()) + } } func (c *client) RateLimitWarnf(format string, v ...any) { // Do the check before adding the client info to the format... - statement := fmt.Sprintf(format, v...) + statement := fmt.Sprintf(c.formatNoClientInfo(format), v...) if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded { return } - c.Warnf("%s", statement) + if s := c.String(); s != _EMPTY_ { + c.srv.Warnf("%s - %s%s", c, statement, c.formatClientSuffix()) + } else { + c.srv.Warnf("%s%s", statement, c.formatClientSuffix()) + } +} + +func (c *client) RateLimitDebugf(format string, v ...any) { + // Do the check before adding the client info to the format... + statement := fmt.Sprintf(c.formatNoClientInfo(format), v...) + if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded { + return + } + if s := c.String(); s != _EMPTY_ { + c.srv.Debugf("%s - %s%s", c, statement, c.formatClientSuffix()) + } else { + c.srv.Debugf("%s%s", statement, c.formatClientSuffix()) + } } // Set the very first PING to a lower interval to capture the initial RTT. @@ -6383,3 +6560,18 @@ func (c *client) setFirstPingTimer() { } c.ping.tmr = time.AfterFunc(d, c.processPingTimer) } + +// Sets this error as the authentication error. To be used in authViolation() +// to report an error different of `ErrAuthentication`. +func (c *client) setAuthError(err error) { + c.mu.Lock() + c.authErr = err + c.mu.Unlock() +} + +// Returns the authentication error set in the connection, possibly nil. +func (c *client) getAuthError() error { + c.mu.Lock() + defer c.mu.Unlock() + return c.authErr +} diff --git a/vendor/github.com/nats-io/nats-server/v2/server/const.go b/vendor/github.com/nats-io/nats-server/v2/server/const.go index a5a61c26f..c72b212e7 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.11.9" + VERSION = "2.12.0" // PROTO is the currently supported protocol. // 0 was the original @@ -146,7 +146,10 @@ const ( // DEFAULT_ROUTE_CONNECT Route solicitation intervals. DEFAULT_ROUTE_CONNECT = 1 * time.Second - // DEFAULT_ROUTE_RECONNECT Route reconnect intervals. + // DEFAULT_ROUTE_CONNECT_MAX Route solicitation intervals (max). + DEFAULT_ROUTE_CONNECT_MAX = 30 * time.Second + + // DEFAULT_ROUTE_RECONNECT Route reconnect delay. DEFAULT_ROUTE_RECONNECT = 1 * time.Second // DEFAULT_ROUTE_DIAL Route dial timeout. 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 e945bb517..080ec7a1f 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 @@ -19,6 +19,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "math/rand" "os" "path/filepath" @@ -227,18 +228,22 @@ const ( PriorityOverflow // Single client takes over handling of the messages, while others are on standby. PriorityPinnedClient + // Clients with lowest priority will be selected first. + PriorityPrioritized ) const ( PriorityNoneJSONString = `"none"` PriorityOverflowJSONString = `"overflow"` PriorityPinnedClientJSONString = `"pinned_client"` + PriorityPrioritizedJSONString = `"prioritized"` ) var ( PriorityNoneJSONBytes = []byte(PriorityNoneJSONString) PriorityOverflowJSONBytes = []byte(PriorityOverflowJSONString) PriorityPinnedClientJSONBytes = []byte(PriorityPinnedClientJSONString) + PriorityPrioritizedJSONBytes = []byte(PriorityPrioritizedJSONString) ) func (pp PriorityPolicy) String() string { @@ -247,6 +252,8 @@ func (pp PriorityPolicy) String() string { return PriorityOverflowJSONString case PriorityPinnedClient: return PriorityPinnedClientJSONString + case PriorityPrioritized: + return PriorityPrioritizedJSONString default: return PriorityNoneJSONString } @@ -258,6 +265,8 @@ func (pp PriorityPolicy) MarshalJSON() ([]byte, error) { return PriorityOverflowJSONBytes, nil case PriorityPinnedClient: return PriorityPinnedClientJSONBytes, nil + case PriorityPrioritized: + return PriorityPrioritizedJSONBytes, nil case PriorityNone: return PriorityNoneJSONBytes, nil default: @@ -271,6 +280,8 @@ func (pp *PriorityPolicy) UnmarshalJSON(data []byte) error { *pp = PriorityOverflow case PriorityPinnedClientJSONString: *pp = PriorityPinnedClient + case PriorityPrioritizedJSONString: + *pp = PriorityPrioritized case PriorityNoneJSONString: *pp = PriorityNone default: @@ -554,6 +565,63 @@ const ( // Helper function to set consumer config defaults from above. func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig, lim *JSLimitOpts, accLim *JetStreamAccountLimits, pedantic bool) *ApiError { + // Setup default of -1, meaning no limit for MaxDeliver. + if config.MaxDeliver == 0 || config.MaxDeliver < -1 { + if pedantic && config.MaxDeliver < -1 { + return NewJSPedanticError(errors.New("max_deliver must be set to -1")) + } + config.MaxDeliver = -1 + } + // Setup zero defaults. + if config.MaxWaiting < 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_waiting must not be negative")) + } + config.MaxWaiting = 0 + } + if config.MaxAckPending < -1 { + if pedantic { + return NewJSPedanticError(errors.New("max_ack_pending must be set to -1")) + } + config.MaxAckPending = -1 + } + if config.MaxRequestBatch < 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_batch must not be negative")) + } + config.MaxRequestBatch = 0 + } + if config.MaxRequestExpires < 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_expires must not be negative")) + } + config.MaxRequestExpires = 0 + } + if config.MaxRequestMaxBytes < 0 { + if pedantic { + return NewJSPedanticError(errors.New("max_bytes must not be negative")) + } + config.MaxRequestMaxBytes = 0 + } + if config.Heartbeat < 0 { + if pedantic { + return NewJSPedanticError(errors.New("idle_heartbeat must not be negative")) + } + config.Heartbeat = 0 + } + if config.InactiveThreshold < 0 { + if pedantic { + return NewJSPedanticError(errors.New("inactive_threshold must not be negative")) + } + config.InactiveThreshold = 0 + } + if config.PinnedTTL < 0 { + if pedantic { + return NewJSPedanticError(errors.New("priority_timeout must not be negative")) + } + config.PinnedTTL = 0 + } + // Set to default if not specified. if config.DeliverSubject == _EMPTY_ && config.MaxWaiting == 0 { config.MaxWaiting = JSWaitQueueDefaultMax @@ -562,10 +630,6 @@ func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig, if config.AckWait == 0 && (config.AckPolicy == AckExplicit || config.AckPolicy == AckAll) { config.AckWait = JsAckWaitDefault } - // Setup default of -1, meaning no limit for MaxDeliver. - if config.MaxDeliver == 0 { - config.MaxDeliver = -1 - } // If BackOff was specified that will override the AckWait and the MaxDeliver. if len(config.BackOff) > 0 { if pedantic && config.AckWait != config.BackOff[0] { @@ -587,14 +651,14 @@ func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig, } // Set proper default for max ack pending if we are ack explicit and none has been set. if (config.AckPolicy == AckExplicit || config.AckPolicy == AckAll) && config.MaxAckPending == 0 { - accPending := JsDefaultMaxAckPending - if lim.MaxAckPending > 0 && lim.MaxAckPending < accPending { - accPending = lim.MaxAckPending + ackPending := JsDefaultMaxAckPending + if lim.MaxAckPending > 0 && lim.MaxAckPending < ackPending { + ackPending = lim.MaxAckPending } - if accLim.MaxAckPending > 0 && accLim.MaxAckPending < accPending { - accPending = accLim.MaxAckPending + if accLim.MaxAckPending > 0 && accLim.MaxAckPending < ackPending { + ackPending = accLim.MaxAckPending } - config.MaxAckPending = accPending + config.MaxAckPending = ackPending } // if applicable set max request batch size if config.DeliverSubject == _EMPTY_ && config.MaxRequestBatch == 0 && lim.MaxRequestBatch > 0 { @@ -640,6 +704,23 @@ func checkConsumerCfg( } } + if _, err := config.AckPolicy.MarshalJSON(); err != nil { + return NewJSConsumerAckPolicyInvalidError() + } + if _, err := config.ReplayPolicy.MarshalJSON(); err != nil { + return NewJSConsumerReplayPolicyInvalidError() + } + + // Check not negative AckWait/BackOff + for _, backoff := range config.BackOff { + if backoff < 0 { + return NewJSConsumerBackOffNegativeError() + } + } + if config.AckWait < 0 { + return NewJSConsumerAckWaitNegativeError() + } + // Check if we have a BackOff defined that MaxDeliver is within range etc. if lbo := len(config.BackOff); lbo > 0 && config.MaxDeliver != -1 && lbo > config.MaxDeliver { return NewJSConsumerMaxDeliverBackoffError() @@ -690,7 +771,7 @@ func checkConsumerCfg( return NewJSConsumerMaxRequestBatchNegativeError() } if config.MaxRequestExpires != 0 && config.MaxRequestExpires < time.Millisecond { - return NewJSConsumerMaxRequestExpiresToSmallError() + return NewJSConsumerMaxRequestExpiresTooSmallError() } if srvLim.MaxRequestBatch > 0 && config.MaxRequestBatch > srvLim.MaxRequestBatch { return NewJSConsumerMaxRequestBatchExceededError(srvLim.MaxRequestBatch) @@ -849,6 +930,14 @@ func checkConsumerCfg( return NewJSConsumerInvalidGroupNameError() } } + } else { + // If PriorityPolicy is None or not set, reject if PriorityGroups or PinnedTTL are set + if len(config.PriorityGroups) > 0 { + return NewJSConsumerPriorityGroupWithPolicyNoneError() + } + if config.PinnedTTL > 0 { + return NewJSConsumerPinnedTTLWithoutPriorityPolicyNoneError() + } } // For now don't allow preferred server in placement. @@ -3445,6 +3534,7 @@ type PriorityGroup struct { MinPending int64 `json:"min_pending,omitempty"` MinAckPending int64 `json:"min_ack_pending,omitempty"` Id string `json:"id,omitempty"` + Priority int `json:"priority,omitempty"` } // Used in nextReqFromMsg, since the json.Unmarshal causes the request @@ -3574,6 +3664,33 @@ var ( errWaitQueueNil = errors.New("wait queue is nil") ) +// insertSorted inserts wr at the correct position based on priority +func (wq *waitQueue) insertSorted(wr *waitingRequest) { + + // Handle empty queue + if wq.head == nil { + wq.head = wr + wq.tail = wr + wr.next = nil + return + } + insertAtPosition(wr, wq) +} + +func (wq *waitQueue) addPrioritized(wr *waitingRequest) error { + if wq == nil { + return errWaitQueueNil + } + if wq.isFull() { + return errWaitQueueFull + } + + wq.insertSorted(wr) + wq.n++ + wq.last = wr.received + return nil +} + // Adds in a new request. func (wq *waitQueue) add(wr *waitingRequest) error { if wq == nil { @@ -3637,6 +3754,17 @@ func (wq *waitQueue) cycle() { } } +func (wq *waitQueue) popOrPopAndRequeue(priority PriorityPolicy) *waitingRequest { + if wq == nil || wq.head == nil { + return nil + } + + if priority == PriorityPrioritized { + return wq.popAndRequeue() + } + return wq.pop() +} + // pop will return the next request and move the read cursor. // This will now place a request that still has pending items at the ends of the list. func (wq *waitQueue) pop() *waitingRequest { @@ -3656,6 +3784,74 @@ func (wq *waitQueue) pop() *waitingRequest { return wr } +// popAndRequeue pops the head element and requeues it at the end of its priority group. +// This maintains FIFO order within the same priority level. +func (wq *waitQueue) popAndRequeue() *waitingRequest { + if wq == nil || wq.head == nil { + return nil + } + + // Save the head + wr := wq.head + + if wr == nil { + return wr + } + + wr.d++ + wr.n-- + + if wr.n > 0 && wq.n > 1 { + if wr.next == nil { + return wr + } + wq.head = wq.head.next + wr.next = nil + + insertAtPosition(wr, wq) + + } else if wr.n <= 0 { + wq.removeCurrent() + return wr + + } + + return wr +} + +func insertAtPosition(wr *waitingRequest, wq *waitQueue) { + priority := math.MaxInt32 + if wr.priorityGroup != nil { + priority = wr.priorityGroup.Priority + } + + var prev *waitingRequest + current := wq.head + for current != nil { + currentPriority := math.MaxInt32 + if current.priorityGroup != nil { + currentPriority = current.priorityGroup.Priority + } + if currentPriority > priority { + break + } + prev = current + current = current.next + } + + if prev == nil { + // All remaining elements have higher priority + wr.next = wq.head + wq.head = wr + } else { + wr.next = prev.next + prev.next = wr + if wr.next == nil { + wq.tail = wr + } + } +} + // Removes the current read pointer (head FIFO) entry. func (wq *waitQueue) removeCurrent() { wq.remove(nil, wq.head) @@ -3817,17 +4013,17 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest { if needNewPin { o.sendPinnedAdvisoryLocked(priorityGroup) } - return o.waiting.pop() + return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy) } else if time.Since(wr.received) < defaultGatewayRecentSubExpiration && (o.srv.leafNodeEnabled || o.srv.gateway.enabled) { if needNewPin { o.sendPinnedAdvisoryLocked(priorityGroup) } - return o.waiting.pop() + return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy) } else if o.srv.gateway.enabled && o.srv.hasGatewayInterest(wr.acc.Name, wr.interest) { if needNewPin { o.sendPinnedAdvisoryLocked(priorityGroup) } - return o.waiting.pop() + return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy) } } else { // We do check for expiration in `processWaiting`, but it is possible to hit the expiry here, and not there. @@ -3902,7 +4098,7 @@ func (nmr *nextMsgReq) returnToPool() { // processNextMsgReq will process a request for the next message available. A nil message payload means deliver // a single message. If the payload is a formal request or a number parseable with Atoi(), then we will send a // batch of messages without requiring another request to this endpoint, or an ACK. -func (o *consumer) processNextMsgReq(_ *subscription, c *client, _ *Account, _, reply string, msg []byte) { +func (o *consumer) processNextMsgReq(_ *subscription, c *client, _ *Account, _, reply string, rmsg []byte) { if reply == _EMPTY_ { return } @@ -3914,7 +4110,12 @@ func (o *consumer) processNextMsgReq(_ *subscription, c *client, _ *Account, _, return } - _, msg = c.msgParts(msg) + hdr, msg := c.msgParts(rmsg) + if errorOnRequiredApiLevel(hdr) { + hdr = []byte("NATS/1.0 412 Required Api Level\r\n\r\n") + o.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + return + } o.nextMsgReqs.push(newNextMsgReq(reply, copyBytes(msg))) } @@ -3968,6 +4169,10 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) { if priorityGroup.Id != _EMPTY_ && o.cfg.PriorityPolicy != PriorityPinnedClient { sendErr(400, "Bad Request - Not a Pinned Client Priority consumer") } + if priorityGroup.Priority < 0 || priorityGroup.Priority > 9 { + sendErr(400, "Bad Request - Priority must be between 0 and 9") + return + } } if priorityGroup != nil && o.cfg.PriorityPolicy != PriorityNone { @@ -4038,15 +4243,25 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) { wr.b = maxBytes wr.received = time.Now() - if err := o.waiting.add(wr); err != nil { - // If the client has a heartbeat interval set, don't bother responding with a 409, - // otherwise we can end up in a hot loop with the client re-requesting instead of - // waiting for the missing heartbeats instead and retrying. - if hb == 0 { - sendErr(409, "Exceeded MaxWaiting") + if o.cfg.PriorityPolicy == PriorityPrioritized { + if err := o.waiting.addPrioritized(wr); err != nil { + if hb == 0 { + sendErr(409, "Exceeded MaxWaiting") + } + wr.recycle() + return + } + } else { + if err := o.waiting.add(wr); err != nil { + // If the client has a heartbeat interval set, don't bother responding with a 409, + // otherwise we can end up in a hot loop with the client re-requesting instead of + // waiting for the missing heartbeats instead and retrying. + if hb == 0 { + sendErr(409, "Exceeded MaxWaiting") + } + wr.recycle() + return } - wr.recycle() - return } o.signalNewMessages() // If we are clustered update our followers about this request. @@ -4243,9 +4458,6 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) { return pmsg, 1, err } - // Hold onto this since we release the lock. - store := o.mset.store - var sseq uint64 var err error var sm *StoreMsg @@ -4255,13 +4467,13 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) { filters, subjf, fseq := o.filters, o.subjf, o.sseq // Check if we are multi-filtered or not. if filters != nil { - sm, sseq, err = store.LoadNextMsgMulti(filters, fseq, &pmsg.StoreMsg) + sm, sseq, err = o.mset.store.LoadNextMsgMulti(filters, fseq, &pmsg.StoreMsg) } else if len(subjf) > 0 { // Means single filtered subject since o.filters means > 1. filter, wc := subjf[0].subject, subjf[0].hasWildcard - sm, sseq, err = store.LoadNextMsg(filter, wc, fseq, &pmsg.StoreMsg) + sm, sseq, err = o.mset.store.LoadNextMsg(filter, wc, fseq, &pmsg.StoreMsg) } else { // No filter here. - sm, sseq, err = store.LoadNextMsg(_EMPTY_, false, fseq, &pmsg.StoreMsg) + sm, sseq, err = o.mset.store.LoadNextMsg(_EMPTY_, false, fseq, &pmsg.StoreMsg) } if sm == nil { pmsg.returnToPool() @@ -4670,14 +4882,14 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) { } if err == ErrStoreMsgNotFound || err == errDeletedMsg || err == ErrStoreEOF || err == errMaxAckPending { goto waitForMsgs - } else if err == errPartialCache { - s.Warnf("Unexpected partial cache error looking up message for consumer '%s > %s > %s'", - o.mset.acc, stream, o.cfg.Name) - goto waitForMsgs - } else { - s.Errorf("Received an error looking up message for consumer '%s > %s > %s': %v", - o.mset.acc, stream, o.cfg.Name, err) + if pmsg != nil { + s.Errorf("Received an error looking up message with sequence %d for consumer '%s > %s > %s': %v", + pmsg.seq, o.mset.acc, stream, o.cfg.Name, err) + } else { + s.Errorf("Received an error looking up message for consumer '%s > %s > %s': %v", + o.mset.acc, stream, o.cfg.Name, err) + } goto waitForMsgs } } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/elastic/elastic.go b/vendor/github.com/nats-io/nats-server/v2/server/elastic/elastic.go new file mode 100644 index 000000000..d764cf197 --- /dev/null +++ b/vendor/github.com/nats-io/nats-server/v2/server/elastic/elastic.go @@ -0,0 +1,60 @@ +// Copyright 2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package elastic + +import ( + "weak" +) + +func Make[T any](ptr *T) *Pointer[T] { + return &Pointer[T]{ + weak: weak.Make(ptr), + } +} + +type Pointer[T any] struct { + weak weak.Pointer[T] + strong *T +} + +func (e *Pointer[T]) Set(ptr *T) { + e.weak = weak.Make(ptr) + if e.strong != nil { + e.strong = ptr + } +} + +func (e *Pointer[T]) Strengthen() { + if e == nil || e.strong != nil { + return + } + e.strong = e.weak.Value() +} + +func (e *Pointer[T]) Weaken() { + if e == nil || e.strong == nil { + return + } + e.strong = nil +} + +func (e *Pointer[T]) Value() *T { + if e == nil { + return nil + } + if e.strong != nil { + return e.strong + } + return e.weak.Value() +} diff --git a/vendor/github.com/nats-io/nats-server/v2/server/errors.go b/vendor/github.com/nats-io/nats-server/v2/server/errors.go index 1a83abc46..07a3ca696 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/errors.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/errors.go @@ -31,6 +31,14 @@ var ( // ErrAuthExpired represents an expired authorization due to timeout. ErrAuthExpired = errors.New("authentication expired") + // ErrAuthProxyNotTrusted represents an error condition on failed authentication + // due to a connection from a proxy not in the list of trusted proxies. + ErrAuthProxyNotTrusted = errors.New("proxy is not trusted") + + // ErrAuthProxyRequired represents an error condition on failed authentication + // due to a connection not coming from a proxy. + ErrAuthProxyRequired = errors.New("proxy connection required") + // ErrMaxPayload represents an error condition when the payload is too big. ErrMaxPayload = errors.New("maximum payload exceeded") diff --git a/vendor/github.com/nats-io/nats-server/v2/server/errors.json b/vendor/github.com/nats-io/nats-server/v2/server/errors.json index a9eaf3be6..9293361db 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/errors.json +++ b/vendor/github.com/nats-io/nats-server/v2/server/errors.json @@ -1140,7 +1140,7 @@ "deprecates": "" }, { - "constant": "JSConsumerMaxRequestExpiresToSmall", + "constant": "JSConsumerMaxRequestExpiresTooSmall", "code": 400, "error_code": 10115, "description": "consumer max request expires needs to be \u003e= 1ms", @@ -1659,6 +1659,106 @@ "url": "", "deprecates": "" }, + { + "constant": "JSMessageIncrDisabledErr", + "code": 400, + "error_code": 10168, + "description": "message counters is disabled", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageIncrMissingErr", + "code": 400, + "error_code": 10169, + "description": "message counter increment is missing", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageIncrPayloadErr", + "code": 400, + "error_code": 10170, + "description": "message counter has payload", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageIncrInvalidErr", + "code": 400, + "error_code": 10171, + "description": "message counter increment is invalid", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageCounterBrokenErr", + "code": 400, + "error_code": 10172, + "description": "message counter is broken", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMirrorWithCountersErr", + "code": 400, + "error_code": 10173, + "description": "stream mirrors can not also calculate counters", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSAtomicPublishDisabledErr", + "code": 400, + "error_code": 10174, + "description": "atomic publish is disabled", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSAtomicPublishMissingSeqErr", + "code": 400, + "error_code": 10175, + "description": "atomic publish sequence is missing", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSAtomicPublishIncompleteBatchErr", + "code": 400, + "error_code": 10176, + "description": "atomic publish batch is incomplete", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSAtomicPublishUnsupportedHeaderBatchErr", + "code": 400, + "error_code": 10177, + "description": "atomic publish unsupported header used: {header}", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, { "constant": "JSConsumerPushWithPriorityGroupErr", "code": 400, @@ -1669,6 +1769,156 @@ "url": "", "deprecates": "" }, + { + "constant": "JSAtomicPublishInvalidBatchIDErr", + "code": 400, + "error_code": 10179, + "description": "atomic publish batch ID is invalid", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSStreamMinLastSeqErr", + "code": 412, + "error_code": 10180, + "description": "min last sequence", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSConsumerAckPolicyInvalidErr", + "code": 400, + "error_code": 10181, + "description": "consumer ack policy invalid", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSConsumerReplayPolicyInvalidErr", + "code": 400, + "error_code": 10182, + "description": "consumer replay policy invalid", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSConsumerAckWaitNegativeErr", + "code": 400, + "error_code": 10183, + "description": "consumer ack wait needs to be positive", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSConsumerBackOffNegativeErr", + "code": 400, + "error_code": 10184, + "description": "consumer backoff needs to be positive", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSRequiredApiLevelErr", + "code": 412, + "error_code": 10185, + "description": "JetStream minimum api level required", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMirrorWithMsgSchedulesErr", + "code": 400, + "error_code": 10186, + "description": "stream mirrors can not also schedule messages", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSSourceWithMsgSchedulesErr", + "code": 400, + "error_code": 10187, + "description": "stream source can not also schedule messages", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageSchedulesDisabledErr", + "code": 400, + "error_code": 10188, + "description": "message schedules is disabled", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageSchedulesPatternInvalidErr", + "code": 400, + "error_code": 10189, + "description": "message schedules pattern is invalid", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageSchedulesTargetInvalidErr", + "code": 400, + "error_code": 10190, + "description": "message schedules target is invalid", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageSchedulesTTLInvalidErr", + "code": 400, + "error_code": 10191, + "description": "message schedules invalid per-message TTL", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMessageSchedulesRollupInvalidErr", + "code": 400, + "error_code": 10192, + "description": "message schedules invalid rollup", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSStreamExpectedLastSeqPerSubjectInvalid", + "code": 400, + "error_code": 10193, + "description": "missing sequence for expected last sequence per subject", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, { "constant": "JSStreamOfflineReasonErrF", "code": 500, @@ -1688,5 +1938,45 @@ "help": "", "url": "", "deprecates": "" + }, + { + "constant": "JSConsumerPriorityGroupWithPolicyNone", + "code": 400, + "error_code": 10196, + "description": "consumer can not have priority groups when policy is none", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSConsumerPinnedTTLWithoutPriorityPolicyNone", + "code": 400, + "error_code": 10197, + "description": "PinnedTTL cannot be set when PriorityPolicy is none", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSMirrorWithAtomicPublishErr", + "code": 400, + "error_code": 10198, + "description": "stream mirrors can not also use atomic publishing", + "comment": "", + "help": "", + "url": "", + "deprecates": "" + }, + { + "constant": "JSAtomicPublishTooLargeBatchErrF", + "code": 400, + "error_code": 10199, + "description": "atomic publish batch is too large: {size}", + "comment": "", + "help": "", + "url": "", + "deprecates": "" } ] diff --git a/vendor/github.com/nats-io/nats-server/v2/server/events.go b/vendor/github.com/nats-io/nats-server/v2/server/events.go index c22cbe3a5..ff2ee4636 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/events.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/events.go @@ -247,13 +247,14 @@ type ServerCapability uint64 // ServerInfo identifies remote servers. type ServerInfo struct { - Name string `json:"name"` - Host string `json:"host"` - ID string `json:"id"` - Cluster string `json:"cluster,omitempty"` - Domain string `json:"domain,omitempty"` - Version string `json:"ver"` - Tags []string `json:"tags,omitempty"` + Name string `json:"name"` + Host string `json:"host"` + ID string `json:"id"` + Cluster string `json:"cluster,omitempty"` + Domain string `json:"domain,omitempty"` + Version string `json:"ver"` + Tags []string `json:"tags,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` // Whether JetStream is enabled (deprecated in favor of the `ServerCapability`). JetStream bool `json:"jetstream"` // Generic capability flags @@ -362,24 +363,27 @@ func (ci *ClientInfo) forAdvisory() *ClientInfo { // ServerStats hold various statistics that we will periodically send out. type ServerStats struct { - Start time.Time `json:"start"` - Mem int64 `json:"mem"` - Cores int `json:"cores"` - CPU float64 `json:"cpu"` - Connections int `json:"connections"` - TotalConnections uint64 `json:"total_connections"` - ActiveAccounts int `json:"active_accounts"` - NumSubs uint32 `json:"subscriptions"` - Sent DataStats `json:"sent"` - Received DataStats `json:"received"` - SlowConsumers int64 `json:"slow_consumers"` - SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"` - Routes []*RouteStat `json:"routes,omitempty"` - Gateways []*GatewayStat `json:"gateways,omitempty"` - ActiveServers int `json:"active_servers,omitempty"` - JetStream *JetStreamVarz `json:"jetstream,omitempty"` - MemLimit int64 `json:"gomemlimit,omitempty"` - MaxProcs int `json:"gomaxprocs,omitempty"` + Start time.Time `json:"start"` + Mem int64 `json:"mem"` + Cores int `json:"cores"` + CPU float64 `json:"cpu"` + Connections int `json:"connections"` + TotalConnections uint64 `json:"total_connections"` + ActiveAccounts int `json:"active_accounts"` + NumSubs uint32 `json:"subscriptions"` + Sent DataStats `json:"sent"` + Received DataStats `json:"received"` + SlowConsumers int64 `json:"slow_consumers"` + SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"` + StaleConnections int64 `json:"stale_connections,omitempty"` + StaleConnectionStats *StaleConnectionStats `json:"stale_connection_stats,omitempty"` + StalledClients int64 `json:"stalled_clients,omitempty"` + Routes []*RouteStat `json:"routes,omitempty"` + Gateways []*GatewayStat `json:"gateways,omitempty"` + ActiveServers int `json:"active_servers,omitempty"` + JetStream *JetStreamVarz `json:"jetstream,omitempty"` + MemLimit int64 `json:"gomemlimit,omitempty"` + MaxProcs int `json:"gomaxprocs,omitempty"` } // RouteStat holds route statistics. @@ -400,17 +404,17 @@ type GatewayStat struct { NumInbound int `json:"inbound_connections"` } -type dataStats struct { +type MsgBytes struct { Msgs int64 `json:"msgs"` Bytes int64 `json:"bytes"` } // DataStats reports how may msg and bytes. Applicable for both sent and received. type DataStats struct { - dataStats - Gateways dataStats `json:"gateways,omitempty"` - Routes dataStats `json:"routes,omitempty"` - Leafs dataStats `json:"leafs,omitempty"` + MsgBytes + Gateways *MsgBytes `json:"gateways,omitempty"` + Routes *MsgBytes `json:"routes,omitempty"` + Leafs *MsgBytes `json:"leafs,omitempty"` } // Used for internally queueing up messages that the server wants to send. @@ -512,8 +516,9 @@ RESET: } s.mu.RUnlock() - // Grab tags. - tags := s.getOpts().Tags + // Grab tags and metadata. + opts := s.getOpts() + tags, metadata := opts.Tags, opts.Metadata for s.eventsRunning() { select { @@ -530,6 +535,7 @@ RESET: si.Version = VERSION si.Time = time.Now().UTC() si.Tags = tags + si.Metadata = metadata si.Flags = 0 if js { // New capability based flags. @@ -860,13 +866,13 @@ func routeStat(r *client) *RouteStat { rs := &RouteStat{ ID: r.cid, Sent: DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: r.outMsgs, Bytes: r.outBytes, }, }, Received: DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: atomic.LoadInt64(&r.inMsgs), Bytes: atomic.LoadInt64(&r.inBytes), }, @@ -952,6 +958,17 @@ func (s *Server) sendStatsz(subj string) { if scs.Clients != 0 || scs.Routes != 0 || scs.Gateways != 0 || scs.Leafs != 0 { m.Stats.SlowConsumersStats = scs } + m.Stats.StaleConnections = atomic.LoadInt64(&s.staleConnections) + m.Stats.StalledClients = atomic.LoadInt64(&s.stalls) + stcs := &StaleConnectionStats{ + Clients: s.NumStaleConnectionsClients(), + Routes: s.NumStaleConnectionsRoutes(), + Gateways: s.NumStaleConnectionsGateways(), + Leafs: s.NumStaleConnectionsLeafs(), + } + if stcs.Clients != 0 || stcs.Routes != 0 || stcs.Gateways != 0 || stcs.Leafs != 0 { + m.Stats.StaleConnectionStats = stcs + } m.Stats.NumSubs = s.numSubscriptions() // Routes s.forEachRoute(func(r *client) { @@ -968,7 +985,7 @@ func (s *Server) sendStatsz(subj string) { // Note that *client.out[Msgs|Bytes] are not set using atomic, // unlike the in[Msgs|bytes]. gs.Sent = DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: c.outMsgs, Bytes: c.outBytes, }, @@ -1927,11 +1944,12 @@ func (s *Server) leafNodeConnected(sub *subscription, _ *client, _ *Account, sub // Common filter options for system requests STATSZ VARZ SUBSZ CONNZ ROUTEZ GATEWAYZ LEAFZ type EventFilterOptions struct { - Name string `json:"server_name,omitempty"` // filter by server name - Cluster string `json:"cluster,omitempty"` // filter by cluster name - Host string `json:"host,omitempty"` // filter by host name - Tags []string `json:"tags,omitempty"` // filter by tags (must match all tags) - Domain string `json:"domain,omitempty"` // filter by JS domain + Name string `json:"server_name,omitempty"` // filter by server name + Cluster string `json:"cluster,omitempty"` // filter by cluster name + Host string `json:"host,omitempty"` // filter by host name + ExactMatch bool `json:"exact_match,omitempty"` // if the above filters should use exact matching or only "contains" + Tags []string `json:"tags,omitempty"` // filter by tags (must match all tags) + Domain string `json:"domain,omitempty"` // filter by JS domain } // StatszEventOptions are options passed to Statsz @@ -2030,18 +2048,21 @@ type RaftzEventOptions struct { } // returns true if the request does NOT apply to this server and can be ignored. -// DO NOT hold the server lock when +// DO NOT hold the server lock when calling this. func (s *Server) filterRequest(fOpts *EventFilterOptions) bool { - if fOpts.Name != _EMPTY_ && !strings.Contains(s.info.Name, fOpts.Name) { - return true + if fOpts == nil { + return false } - if fOpts.Host != _EMPTY_ && !strings.Contains(s.info.Host, fOpts.Host) { - return true - } - if fOpts.Cluster != _EMPTY_ { - if !strings.Contains(s.ClusterName(), fOpts.Cluster) { + if fOpts.ExactMatch { + if (fOpts.Name != _EMPTY_ && fOpts.Name != s.info.Name) || + (fOpts.Host != _EMPTY_ && fOpts.Host != s.info.Host) || + (fOpts.Cluster != _EMPTY_ && fOpts.Cluster != s.ClusterName()) { return true } + } else if (fOpts.Name != _EMPTY_ && !strings.Contains(s.info.Name, fOpts.Name)) || + (fOpts.Host != _EMPTY_ && !strings.Contains(s.info.Host, fOpts.Host)) || + (fOpts.Cluster != _EMPTY_ && !strings.Contains(s.ClusterName(), fOpts.Cluster)) { + return true } if len(fOpts.Tags) > 0 { opts := s.getOpts() @@ -2428,37 +2449,37 @@ func (a *Account) statz() *AccountStat { a.stats.Lock() received := DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: a.stats.inMsgs, Bytes: a.stats.inBytes, }, - Gateways: dataStats{ + Gateways: &MsgBytes{ Msgs: a.stats.gw.inMsgs, Bytes: a.stats.gw.inBytes, }, - Routes: dataStats{ + Routes: &MsgBytes{ Msgs: a.stats.rt.inMsgs, Bytes: a.stats.rt.inBytes, }, - Leafs: dataStats{ + Leafs: &MsgBytes{ Msgs: a.stats.ln.inMsgs, Bytes: a.stats.ln.inBytes, }, } sent := DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: a.stats.outMsgs, Bytes: a.stats.outBytes, }, - Gateways: dataStats{ + Gateways: &MsgBytes{ Msgs: a.stats.gw.outMsgs, Bytes: a.stats.gw.outBytes, }, - Routes: dataStats{ + Routes: &MsgBytes{ Msgs: a.stats.rt.outMsgs, Bytes: a.stats.rt.outBytes, }, - Leafs: dataStats{ + Leafs: &MsgBytes{ Msgs: a.stats.ln.outMsgs, Bytes: a.stats.ln.outBytes, }, @@ -2504,13 +2525,11 @@ func (s *Server) accountConnectEvent(c *client) { s.mu.Unlock() return } - gacc := s.gacc eid := s.nextEventID() s.mu.Unlock() c.mu.Lock() - // Ignore global account activity - if c.acc == nil || c.acc == gacc { + if c.acc == nil { c.mu.Unlock() return } @@ -2553,18 +2572,15 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string) s.mu.Unlock() return } - gacc := s.gacc eid := s.nextEventID() s.mu.Unlock() c.mu.Lock() - // Ignore global account activity - if c.acc == nil || c.acc == gacc { + if c.acc == nil { c.mu.Unlock() return } - m := DisconnectEventMsg{ TypedEvent: TypedEvent{ Type: DisconnectEventMsgType, @@ -2591,13 +2607,13 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string) MQTTClient: c.getMQTTClientID(), }, Sent: DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: atomic.LoadInt64(&c.inMsgs), Bytes: atomic.LoadInt64(&c.inBytes), }, }, Received: DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: c.outMsgs, Bytes: c.outBytes, }, @@ -2612,7 +2628,7 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string) } // This is the system level event sent to the system account for operators. -func (s *Server) sendAuthErrorEvent(c *client) { +func (s *Server) sendAuthErrorEvent(c *client, reason string) { s.mu.Lock() if !s.eventsEnabled() { s.mu.Unlock() @@ -2649,18 +2665,18 @@ func (s *Server) sendAuthErrorEvent(c *client) { MQTTClient: c.getMQTTClientID(), }, Sent: DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: c.inMsgs, Bytes: c.inBytes, }, }, Received: DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: c.outMsgs, Bytes: c.outBytes, }, }, - Reason: AuthenticationViolation.String(), + Reason: reason, } c.mu.Unlock() @@ -2711,13 +2727,13 @@ func (s *Server) sendAccountAuthErrorEvent(c *client, acc *Account, reason strin MQTTClient: c.getMQTTClientID(), }, Sent: DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: c.inMsgs, Bytes: c.inBytes, }, }, Received: DataStats{ - dataStats: dataStats{ + MsgBytes: MsgBytes{ Msgs: c.outMsgs, Bytes: c.outBytes, }, 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 d97647f5e..572e4e33d 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 @@ -45,6 +45,7 @@ import ( "github.com/minio/highwayhash" "github.com/nats-io/nats-server/v2/server/ats" "github.com/nats-io/nats-server/v2/server/avl" + "github.com/nats-io/nats-server/v2/server/elastic" "github.com/nats-io/nats-server/v2/server/gsl" "github.com/nats-io/nats-server/v2/server/stree" "github.com/nats-io/nats-server/v2/server/thw" @@ -177,7 +178,7 @@ type fileStore struct { ld *LostStreamData scb StorageUpdateHandler rmcb StorageRemoveMsgHandler - sdmcb SubjectDeleteMarkerUpdateHandler + pmsgcb ProcessJetStreamMsgHandler ageChk *time.Timer syncTmr *time.Timer cfg FileStreamInfo @@ -204,6 +205,7 @@ type fileStore struct { receivedAny bool firstMoved bool ttls *thw.HashWheel + scheduling *MsgScheduling sdm *SDMMeta lpex time.Time // Last PurgeEx call. } @@ -236,6 +238,7 @@ type msgBlock struct { lsts int64 llseq uint64 hh hash.Hash64 + ecache elastic.Pointer[cache] cache *cache cloads uint64 cexp time.Duration @@ -254,6 +257,7 @@ type msgBlock struct { 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 @@ -262,7 +266,6 @@ type msgBlock struct { // Write through caching layer that is also used on loading messages. type cache struct { buf []byte - off int wp int idx []uint32 lrl uint32 @@ -286,6 +289,8 @@ const ( hdrLen = 2 // This is where we keep the streams. streamsDir = "streams" + // This is where we keep inflight batches for streams. + batchesDir = "batches" // This is where we keep the message store blocks. msgDir = "msgs" // This is where we temporarily move the messages dir. @@ -309,9 +314,8 @@ const ( // The suffix that will be given to a new temporary block for compression or when rewriting the full file. blkTmpSuffix = ".tmp" // This is where we keep state on templates. + // Deprecated: stream templates are deprecated and will be removed in a future version. tmplsDir = "templates" - // Maximum size of a write buffer we may consider for re-use. - maxBufReuse = 2 * 1024 * 1024 // default cache buffer expiration defaultCacheBufferExpiration = 10 * time.Second // default sync interval @@ -336,6 +340,9 @@ const ( // This is the encoded time hash wheel for TTLs. ttlStreamStateFile = "thw.db" + // This is the encoded message scheduling file. + msgSchedulingStreamStateFile = "sched.db" + // AEK key sizes minMetaKeySize = 64 minBlkKeySize = 64 @@ -437,6 +444,10 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim if cfg.AllowMsgTTL { fs.ttls = thw.NewHashWheel() } + // Only create scheduling data structure if we're going to allow message schedules. + if cfg.AllowMsgSchedules { + fs.scheduling = newMsgScheduling(fs.runMsgScheduling) + } // Set flush in place to AsyncFlush which by default is false. fs.fip = !fcfg.AsyncFlush @@ -487,7 +498,8 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim } // Check if our prior state remembers a last sequence past where we can see. - if prior.LastSeq > fs.state.LastSeq { + // Unless we're async flushing, in which case this can happen if some blocks weren't flushed. + if prior.LastSeq > fs.state.LastSeq && !fs.fcfg.AsyncFlush { fs.state.LastSeq, fs.state.LastTime = prior.LastSeq, prior.LastTime if fs.state.Msgs == 0 { fs.state.FirstSeq = fs.state.LastSeq + 1 @@ -514,6 +526,13 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim } } + // See if we can bring back our message scheduling state from disk. + if cfg.AllowMsgSchedules { + if err = fs.recoverMsgSchedulingState(); err != nil && !os.IsNotExist(err) { + fs.warn("Recovering message scheduling state from index errored: %v", err) + } + } + // Also make sure we get rid of old idx and fss files on return. // Do this in separate go routine vs inline and at end of processing. defer func() { @@ -655,6 +674,12 @@ func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error { } else if !cfg.AllowMsgTTL && fs.ttls != nil { fs.ttls = nil } + // Create or delete the message scheduling state if needed. + if cfg.AllowMsgSchedules && fs.scheduling == nil { + fs.recoverMsgSchedulingState() + } else if !cfg.AllowMsgSchedules && fs.scheduling != nil { + fs.scheduling = nil + } // Limits checks and enforcement. fs.enforceMsgLimit() @@ -672,11 +697,44 @@ func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error { if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer == 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) { fs.enforceMsgPerSubjectLimit(true) } + + if lmb := fs.lmb; lmb != nil { + // Enable/disable async flush depending on if it's supported and already initialized. + supportsAsyncFlush := !fs.fcfg.SyncAlways && cfg.Replicas > 1 + + // Async persist mode opts in to async flushing, + // sync always would also be disabled if it was configured. + if cfg.PersistMode == AsyncPersistMode { + supportsAsyncFlush = true + fs.fcfg.SyncAlways = false + lmb.syncAlways = false + } + + if supportsAsyncFlush && !fs.fcfg.AsyncFlush { + fs.fcfg.AsyncFlush = true + lmb.spinUpFlushLoop() + } else if !supportsAsyncFlush && fs.fcfg.AsyncFlush { + fs.fcfg.AsyncFlush = false + lmb.mu.Lock() + // Quit the flush loop. + if lmb.qch != nil { + close(lmb.qch) + lmb.qch = nil + } + lmb.flushPendingMsgsLocked() + lmb.mu.Unlock() + } + // Set flush in place to AsyncFlush which by default is false. + fs.fip = !fs.fcfg.AsyncFlush + } fs.mu.Unlock() if cfg.MaxAge != 0 || cfg.AllowMsgTTL { fs.expireMsgs() } + if cfg.AllowMsgSchedules { + fs.runMsgScheduling() + } return nil } @@ -886,53 +944,56 @@ func (fs *fileStore) writeStreamMeta() error { } // Pools to recycle the blocks to help with memory pressure. -var blkPoolBig sync.Pool // 16MB -var blkPoolMedium sync.Pool // 8MB -var blkPoolSmall sync.Pool // 2MB +var blkPoolSmall = &sync.Pool{ + New: func() any { + b := [defaultSmallBlockSize]byte{} + return &b + }, +} +var blkPoolMedium = &sync.Pool{ + New: func() any { + b := [defaultMediumBlockSize]byte{} + return &b + }, +} +var blkPoolBig = &sync.Pool{ + New: func() any { + b := [defaultLargeBlockSize]byte{} + return &b + }, +} // Get a new msg block based on sz estimate. func getMsgBlockBuf(sz int) (buf []byte) { - var pb any - if sz <= defaultSmallBlockSize { - pb = blkPoolSmall.Get() - } else if sz <= defaultMediumBlockSize { - pb = blkPoolMedium.Get() - } else { - pb = blkPoolBig.Get() + switch { + case sz <= defaultSmallBlockSize: + return blkPoolSmall.Get().(*[defaultSmallBlockSize]byte)[:0] + case sz <= defaultMediumBlockSize: + return blkPoolMedium.Get().(*[defaultMediumBlockSize]byte)[:0] + case sz <= defaultLargeBlockSize: + return blkPoolBig.Get().(*[defaultLargeBlockSize]byte)[:0] + default: + // Ideally this should not happen, once we return a buffer that's + // larger than defaultLargeBlockSize then we will refuse to recycle + // it to stop the pools from bloating excessively. + return make([]byte, 0, sz) } - if pb != nil { - buf = *(pb.(*[]byte)) - } else { - // Here we need to make a new blk. - // If small leave as is.. - if sz > defaultSmallBlockSize && sz <= defaultMediumBlockSize { - sz = defaultMediumBlockSize - } else if sz > defaultMediumBlockSize { - sz = defaultLargeBlockSize - } - buf = make([]byte, sz) - } - return buf[:0] } // Recycle the msg block. func recycleMsgBlockBuf(buf []byte) { - if buf == nil || cap(buf) < defaultSmallBlockSize { - return - } - // Make sure to reset before placing back into pool. - buf = buf[:0] - - // We need to make sure the load code gets a block that can fit the maximum for a size block. - // E.g. 8, 16 etc. otherwise we thrash and actually make things worse by pulling it out, and putting - // it right back in and making a new []byte. - // From above we know its already >= defaultSmallBlockSize - if sz := cap(buf); sz < defaultMediumBlockSize { - blkPoolSmall.Put(&buf) - } else if sz < defaultLargeBlockSize { - blkPoolMedium.Put(&buf) - } else { - blkPoolBig.Put(&buf) + switch cap(buf) { + case defaultSmallBlockSize: + b := (*[defaultSmallBlockSize]byte)(buf[0:defaultSmallBlockSize]) + blkPoolSmall.Put(b) + case defaultMediumBlockSize: + b := (*[defaultMediumBlockSize]byte)(buf[0:defaultMediumBlockSize]) + blkPoolMedium.Put(b) + case defaultLargeBlockSize: + b := (*[defaultLargeBlockSize]byte)(buf[0:defaultLargeBlockSize]) + blkPoolBig.Put(b) + default: + // Too large, let the GC collect it instead. } } @@ -1446,7 +1507,7 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, []uint64, error) { minTombstoneTs int64 ) - // To detect gaps from compaction. + // To detect gaps from compaction, and to ensure the sequence keeps moving up. var last uint64 for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; { @@ -1469,15 +1530,16 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, []uint64, error) { // Do some quick sanity checks here. if dlen < 0 || slen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh { truncate(index) - return gatherLost(lbuf - index), tombstones, errBadMsg + return gatherLost(lbuf - index), tombstones, errBadMsg{mb.mfn, fmt.Sprintf("sanity check failed (dlen %d slen %d rl %d index %d lbuf %d)", dlen, slen, rl, index, lbuf)} } // Check for checksum failures before additional processing. data := buf[index+msgHdrSize : index+rl] + subj := data[:slen] if hh := mb.hh; hh != nil { hh.Reset() hh.Write(hdr[4:20]) - hh.Write(data[:slen]) + hh.Write(subj) if hasHeaders { hh.Write(data[slen+4 : dlen-recordHashSize]) } else { @@ -1486,7 +1548,7 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, []uint64, error) { checksum := hh.Sum(nil) if !bytes.Equal(checksum, data[len(data)-recordHashSize:]) { truncate(index) - return gatherLost(lbuf - index), tombstones, errBadMsg + return gatherLost(lbuf - index), tombstones, errBadMsg{mb.mfn, "invalid checksum"} } copy(mb.lchk[0:], checksum) } @@ -1534,14 +1596,31 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, []uint64, error) { firstNeedsSet, mb.first.ts = false, ts } + // The sequence needs to only ever move up. + if seq <= last { + // Advance to next record. + // We've already accounted for this sequence and marked it as deleted. + index += rl + continue + } if !mb.dmap.Exists(seq) { mb.msgs++ mb.bytes += uint64(rl) - if mb.fs.ttls != nil && ttl > 0 { - expires := time.Duration(ts) + (time.Second * time.Duration(ttl)) - mb.fs.ttls.Add(seq, int64(expires)) + if ttl > 0 { + if mb.fs.ttls != nil { + expires := time.Duration(ts) + (time.Second * time.Duration(ttl)) + mb.fs.ttls.Add(seq, int64(expires)) + } + // Need to count these regardless as we might want to enable TTLs + // later via UpdateConfig. mb.ttls++ } + if mb.fs.scheduling != nil { + if schedule, ok := getMessageSchedule(hdr); ok && !schedule.IsZero() { + mb.fs.scheduling.add(seq, string(subj), schedule.UnixNano()) + mb.schedules++ + } + } } // Check for any gaps from compaction, meaning no ebit entry. @@ -1777,6 +1856,10 @@ func (fs *fileStore) recoverFullState() (rerr error) { if version >= 2 { ttls = readU64() } + var schedules uint64 + if version >= 3 { + schedules = readU64() + } if bi < 0 { os.Remove(fn) return errCorruptState @@ -1787,6 +1870,7 @@ func (fs *fileStore) recoverFullState() (rerr error) { mb.msgs, mb.bytes = lseq-fseq+1, nbytes mb.first.ts, mb.last.ts = fts+baseTime, lts+baseTime mb.ttls = ttls + mb.schedules = schedules if numDeleted > 0 { dmap, n, err := avl.Decode(buf[bi:]) if err != nil { @@ -1993,6 +2077,86 @@ func (fs *fileStore) recoverTTLState() error { return nil } +// Lock should be held. +func (fs *fileStore) recoverMsgSchedulingState() error { + // See if we have a timed hash wheel for TTLs. + <-dios + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, msgSchedulingStreamStateFile) + buf, err := os.ReadFile(fn) + dios <- struct{}{} + + if err != nil && !os.IsNotExist(err) { + return err + } + + fs.scheduling = newMsgScheduling(fs.runMsgScheduling) + + var schedSeq uint64 + if err == nil { + schedSeq, err = fs.scheduling.decode(buf) + if err != nil { + fs.warn("Error decoding message scheduling state: %s", err) + os.Remove(fn) + } + } + + if schedSeq < fs.state.FirstSeq { + schedSeq = fs.state.FirstSeq + } + + defer fs.scheduling.resetTimer() + if fs.state.Msgs > 0 && schedSeq <= fs.state.LastSeq { + fs.warn("Message scheduling state is outdated; attempting to recover using linear scan (seq %d to %d)", schedSeq, fs.state.LastSeq) + var ( + mb *msgBlock + sm StoreMsg + mblseq uint64 + ) + for seq := schedSeq; seq <= fs.state.LastSeq; seq++ { + retry: + if mb == nil { + if mb = fs.selectMsgBlock(seq); mb == nil { + // Selecting the message block should return a block that contains this sequence, + // or a later block if it can't be found. + // It's an error if we can't find any block within the bounds of first and last seq. + fs.warn("Error loading msg block with seq %d for recovering message schedules", seq) + continue + } + seq = atomic.LoadUint64(&mb.first.seq) + mblseq = atomic.LoadUint64(&mb.last.seq) + } + if mb.schedules == 0 { + // None of the messages in the block have message schedules, so don't + // bother doing anything further with this block, skip to the end. + seq = atomic.LoadUint64(&mb.last.seq) + 1 + } + if seq > mblseq { + // We've reached the end of the loaded block, so let's go back to the + // beginning and process the next block. + mb.tryForceExpireCache() + mb = nil + if seq <= fs.state.LastSeq { + goto retry + } + // Done. + break + } + msg, _, err := mb.fetchMsgNoCopy(seq, &sm) + if err != nil { + fs.warn("Error loading msg seq %d for recovering message schedules: %s", seq, err) + continue + } + if len(msg.hdr) == 0 { + continue + } + if schedule, ok := getMessageSchedule(sm.hdr); ok && !schedule.IsZero() { + fs.scheduling.init(seq, sm.subj, schedule.UnixNano()) + } + } + } + return nil +} + // Grabs last checksum for the named block file. // Takes into account encryption etc. func (mb *msgBlock) lastChecksum() []byte { @@ -2311,6 +2475,7 @@ func (fs *fileStore) expireMsgsOnRecover() error { if mb.msgs == 0 { deleteEmptyBlock(mb) } + mb.finishedWithCache() mb.mu.Unlock() break } @@ -2417,6 +2582,7 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm * if updateLLTS { mb.llts = ats.AccessTime() } + mb.finishedWithCache() mb.mu.Unlock() }() @@ -2529,6 +2695,7 @@ func (mb *msgBlock) firstMatching(filter string, wc bool, start uint64, sm *Stor if updateLLTS { mb.llts = ats.AccessTime() } + mb.finishedWithCache() mb.mu.Unlock() }() @@ -2741,6 +2908,7 @@ func (mb *msgBlock) filteredPendingLocked(filter string, wc bool, sseq uint64) ( mb.loadMsgsWithLock() shouldExpire = true } + defer mb.finishedWithCache() _tsa, _fsa := [32]string{}, [32]string{} tsa, fsa := _tsa[:0], _fsa[:0] @@ -3048,6 +3216,8 @@ func (fs *fileStore) SubjectsState(subject string) map[string]SimpleState { if shouldExpire { // Expire this cache before moving on. mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } mb.mu.Unlock() @@ -3104,6 +3274,7 @@ func (fs *fileStore) allLastSeqsLocked() ([]uint64, error) { // Expire this cache before moving on. mb.tryForceExpireCacheLocked() } + mb.finishedWithCache() mb.mu.Unlock() } @@ -3219,8 +3390,10 @@ 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 if mb.cacheNotLoaded() { mb.loadMsgsWithLock() + didLoad = true } var smv StoreMsg fseq := atomic.LoadUint64(&mb.first.seq) @@ -3235,6 +3408,9 @@ func (fs *fileStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed i delete(subs, ssubj) break } + if didLoad { + mb.finishedWithCache() + } } return true }) @@ -3389,6 +3565,8 @@ func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool) // If we loaded the block try to force expire. if shouldExpire { mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } if updateLLTS { mb.llts = ats.AccessTime() @@ -3461,6 +3639,8 @@ func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool) // If we loaded this block for this operation go ahead and expire it here. if shouldExpire { mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } mb.mu.Unlock() total += t @@ -3555,6 +3735,8 @@ func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool) // If we loaded the block try to force expire. if shouldExpire { mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } if updateLLTS { mb.llts = ats.AccessTime() @@ -3694,6 +3876,8 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer // If we loaded the block try to force expire. if shouldExpire { mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } if updateLLTS { mb.llts = ats.AccessTime() @@ -3772,6 +3956,8 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer // If we loaded this block for this operation go ahead and expire it here. if shouldExpire { mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } if updateLLTS { mb.llts = ats.AccessTime() @@ -3868,6 +4054,8 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer // If we loaded the block try to force expire. if shouldExpire { mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } if updateLLTS { mb.llts = ats.AccessTime() @@ -3924,10 +4112,10 @@ func (fs *fileStore) RegisterStorageRemoveMsg(cb StorageRemoveMsgHandler) { fs.mu.Unlock() } -// RegisterSubjectDeleteMarkerUpdates registers a callback for updates to new tombstones. -func (fs *fileStore) RegisterSubjectDeleteMarkerUpdates(cb SubjectDeleteMarkerUpdateHandler) { +// RegisterProcessJetStreamMsg registers a callback to process new JetStream messages. +func (fs *fileStore) RegisterProcessJetStreamMsg(cb ProcessJetStreamMsgHandler) { fs.mu.Lock() - fs.sdmcb = cb + fs.pmsgcb = cb fs.mu.Unlock() } @@ -3937,15 +4125,17 @@ func (fs *fileStore) hashKeyForBlock(index uint32) []byte { return []byte(fmt.Sprintf("%s-%d", fs.cfg.Name, index)) } -func (mb *msgBlock) setupWriteCache(buf []byte) { - // Make sure we have a cache setup. +func (mb *msgBlock) setupWriteCache(buf []byte) error { + // Not safe to use mb.cacheAlreadyLoaded() here as the heuristic + // is wrong for writing to the beginning of an empty block cache. if mb.cache != nil { - return + return nil + } + if mb.cache = mb.ecache.Value(); mb.cache != nil { + return nil } - // Setup simple cache. - mb.cache = &cache{buf: buf} - // Make sure we set the proper cache offset if we have existing data. + // If there's an existing block on disk then we'll load it in. var fi os.FileInfo if mb.mfd != nil { fi, _ = mb.mfd.Stat() @@ -3953,10 +4143,26 @@ func (mb *msgBlock) setupWriteCache(buf []byte) { fi, _ = os.Stat(mb.mfn) } if fi != nil { - mb.cache.off = int(fi.Size()) + return mb.loadMsgsWithLock() } + + // Looks like there isn't an existing file on disk, mint a new cache. + mb.cache = &cache{buf: buf} + mb.ecache.Set(mb.cache) mb.llts = ats.AccessTime() mb.startCacheExpireTimer() + return nil +} + +// finishedWithCache removes the strong reference to the cache, instead +// returning it to the elastic pointer. Note that the elastic pointer can +// be either weak or strong, they are strengthened when there are pending +// writes and weak otherwise. +// Lock must be held. +func (mb *msgBlock) finishedWithCache() { + if mb.cache != nil && mb.pendingWriteSizeLocked() == 0 { + mb.cache = nil + } } // This rolls to a new append msg block. @@ -3969,6 +4175,11 @@ func (fs *fileStore) newMsgBlockForWrite() (*msgBlock, error) { lmb.mu.Lock() index = lmb.index + 1 + // Quit our loops. + if lmb.qch != nil { + close(lmb.qch) + lmb.qch = nil + } // Flush any pending messages. lmb.flushPendingMsgsLocked() // Determine if we can reclaim any resources here. @@ -3993,7 +4204,10 @@ func (fs *fileStore) newMsgBlockForWrite() (*msgBlock, error) { mb := fs.initMsgBlock(index) // Lock should be held to quiet race detector. mb.mu.Lock() - mb.setupWriteCache(rbuf) + if err := mb.setupWriteCache(rbuf); err != nil { + mb.mu.Unlock() + return nil, err + } mb.fss = stree.NewSubjectTree[SimpleState]() // Set cache time to creation time to start. @@ -4189,9 +4403,13 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t fs.enforceBytesLimit() // Per-message TTL. - if fs.ttls != nil && ttl > 0 { - expires := time.Duration(ts) + (time.Second * time.Duration(ttl)) - fs.ttls.Add(seq, int64(expires)) + if ttl > 0 { + if fs.ttls != nil { + expires := time.Duration(ts) + (time.Second * time.Duration(ttl)) + fs.ttls.Add(seq, int64(expires)) + } + // Need to count these regardless as we might want to enable TTLs + // later via UpdateConfig. fs.lmb.ttls++ } @@ -4203,6 +4421,14 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t fs.startAgeChk() } + // Message scheduling. + if fs.scheduling != nil { + if schedule, ok := getMessageSchedule(hdr); ok && !schedule.IsZero() { + fs.scheduling.add(seq, subj, schedule.UnixNano()) + fs.lmb.schedules++ + } + } + return nil } @@ -4268,6 +4494,7 @@ func (mb *msgBlock) skipMsg(seq uint64, now int64) { // We want to make sure since we have no messages // that we write to the beginning since we only need last one. mb.rbytes, mb.cache = 0, &cache{} + mb.ecache.Set(mb.cache) // If encrypted we need to reset counter since we just keep one. if mb.bek != nil { // Recreate to reset counter. @@ -4380,6 +4607,13 @@ func (fs *fileStore) SkipMsgs(seq uint64, num uint64) error { return nil } +// FlushAllPending flushes all data that was still pending to be written. +func (fs *fileStore) FlushAllPending() { + fs.mu.Lock() + defer fs.mu.Unlock() + fs.checkAndFlushLastBlock() +} + // Lock should be held. func (fs *fileStore) rebuildFirst() { if len(fs.blks) == 0 { @@ -4462,6 +4696,8 @@ func (fs *fileStore) firstSeqForSubj(subj string) (uint64, error) { if shouldExpire { // Expire this cache before moving on. mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } mb.mu.Unlock() // Re-acquire fs lock @@ -4747,6 +4983,7 @@ func (fs *fileStore) removeMsg(seq uint64, secure, viaLimits, needFSLock bool) ( var smv StoreMsg sm, err := mb.cacheLookupNoCopy(seq, &smv) if err != nil { + mb.finishedWithCache() mb.mu.Unlock() fsUnlock() // Mimic err behavior from above check to dmap. No error returned if already removed. @@ -4803,6 +5040,7 @@ func (fs *fileStore) removeMsg(seq uint64, secure, viaLimits, needFSLock bool) ( // Grab record info. ri, rl, _, _ := mb.slotInfo(int(seq - mb.cache.fseq)) if err := mb.eraseMsg(seq, int(ri), int(rl), isLastBlock); err != nil { + mb.finishedWithCache() return false, err } } @@ -4847,6 +5085,7 @@ func (fs *fileStore) removeMsg(seq uint64, secure, viaLimits, needFSLock bool) ( fs.removeMsgBlock(mb) firstSeqNeedsUpdate = seq == fs.state.FirstSeq } + mb.finishedWithCache() mb.mu.Unlock() // If we emptied the current message block and the seq was state.FirstSeq @@ -4917,12 +5156,13 @@ func (mb *msgBlock) compact() { // if fseq > 0 we will attempt to cleanup stale tombstones. // Write lock needs to be held. func (mb *msgBlock) compactWithFloor(floor uint64) { - wasLoaded := mb.cacheAlreadyLoaded() + wasLoaded := mb.cache != nil && mb.cacheAlreadyLoaded() if !wasLoaded { if err := mb.loadMsgsWithLock(); err != nil { return } } + defer mb.finishedWithCache() buf := mb.cache.buf nbuf := getMsgBlockBuf(len(buf)) @@ -5077,7 +5317,7 @@ func (mb *msgBlock) slotInfo(slot int) (uint32, uint32, bool, error) { } } if rl < msgHdrSize { - return 0, 0, false, errBadMsg + return 0, 0, false, errBadMsg{mb.mfn, fmt.Sprintf("length too short for slot %d", slot)} } return uint32(ri), rl, hashChecked, nil } @@ -5137,6 +5377,14 @@ func (mb *msgBlock) setInFlusher() { func (mb *msgBlock) clearInFlusher() { mb.mu.Lock() mb.flusher = false + if mb.qch != nil { + close(mb.qch) + mb.qch = nil + } + if mb.fch != nil { + close(mb.fch) + mb.fch = nil + } mb.mu.Unlock() } @@ -5167,16 +5415,19 @@ func (mb *msgBlock) flushLoop(fch, qch chan struct{}) { waiting = newWaiting ts *= 2 } + mb.flushPendingMsgs() - // Check if we are no longer the last message block. If we are - // not we can close FDs and exit. - mb.fs.mu.RLock() - notLast := mb != mb.fs.lmb - mb.fs.mu.RUnlock() - if notLast { - if err := mb.closeFDs(); err == nil { - return - } + } + + // Check if we are no longer the last message block. If we are + // not we can close FDs and exit. + mb.fs.mu.RLock() + notLast := mb != mb.fs.lmb + mb.fs.mu.RUnlock() + + if notLast { + if err := mb.closeFDs(); err == nil { + return } } case <-qch: @@ -5220,14 +5471,12 @@ func (mb *msgBlock) eraseMsg(seq uint64, ri, rl int, isLastBlock bool) error { nbytes := b.Bytes() // Cache - if ri >= mb.cache.off { - li := ri - mb.cache.off - buf := mb.cache.buf[li : li+rl] - copy(buf, nbytes) + if ri+rl <= len(mb.cache.buf) { + copy(mb.cache.buf[ri:ri+rl], nbytes) } // Disk - if mb.cache.off+mb.cache.wp > ri { + if mb.cache.wp > ri { if err := mb.atomicOverwriteFile(mb.cache.buf, !isLastBlock); err != nil { return err } @@ -5242,6 +5491,7 @@ func (mb *msgBlock) truncate(tseq uint64, ts int64) (nmsgs, nbytes uint64, err e if err := mb.loadMsgsWithLock(); err != nil { return 0, 0, err } + defer mb.finishedWithCache() // Calculate new eof using slot info from our new last sm. ri, rl, _, err := mb.slotInfo(int(tseq - mb.cache.fseq)) @@ -5251,6 +5501,19 @@ func (mb *msgBlock) truncate(tseq uint64, ts int64) (nmsgs, nbytes uint64, err e // Calculate new eof. eof := int64(ri + rl) + // FIXME(dlc) - We could be smarter here. + if buf, _ := mb.bytesPending(); len(buf) > 0 { + ld, err := mb.flushPendingMsgsLocked() + if ld != nil && mb.fs != nil { + // We do not know if fs is locked or not at this point. + // This should be an exceptional condition so do so in Go routine. + go mb.fs.rebuildState(ld) + } + if err != nil { + return 0, 0, err + } + } + var purged, bytes uint64 checkDmap := mb.dmap.Size() > 0 @@ -5417,10 +5680,6 @@ func (mb *msgBlock) startCacheExpireTimer() { func (mb *msgBlock) clearCacheAndOffset() { // Reset linear scan tracker. mb.llseq = 0 - if mb.cache != nil { - mb.cache.off = 0 - mb.cache.wp = 0 - } mb.clearCache() } @@ -5438,19 +5697,17 @@ func (mb *msgBlock) clearCache() { } } - if mb.cache == nil { + mbcache := mb.cache + if mbcache == nil { + mbcache = mb.ecache.Value() + } + if mbcache == nil { return } - buf := mb.cache.buf - if mb.cache.off == 0 { - mb.cache = nil - } else { - // Clear msgs and index. - mb.cache.buf = nil - mb.cache.idx = nil - mb.cache.wp = 0 - } + buf := mbcache.buf + mb.cache = nil + mb.ecache.Set(nil) recycleMsgBlockBuf(buf) } @@ -5500,6 +5757,11 @@ func (mb *msgBlock) tryExpireWriteCache() []byte { // Lock should be held. func (mb *msgBlock) expireCacheLocked() { + var strengthened bool + if mb.cache == nil { + mb.cache = mb.ecache.Value() + strengthened = true + } if mb.cache == nil && mb.fss == nil { if mb.ctmr != nil { mb.ctmr.Stop() @@ -5511,6 +5773,9 @@ func (mb *msgBlock) expireCacheLocked() { // Can't expire if we still have pending. if mb.cache != nil && len(mb.cache.buf)-int(mb.cache.wp) > 0 { mb.resetCacheExpireTimer(mb.cexp) + if strengthened { + mb.finishedWithCache() + } return } @@ -5526,13 +5791,15 @@ func (mb *msgBlock) expireCacheLocked() { // Check for activity on the cache that would prevent us from expiring. if tns-bufts <= int64(mb.cexp) { mb.resetCacheExpireTimer(mb.cexp - time.Duration(tns-bufts)) + if strengthened { + mb.finishedWithCache() + } return } // If we are here we will at least expire the core msg buffer. // We need to capture offset in case we do a write next before a full load. if mb.cache != nil { - mb.cache.off += len(mb.cache.buf) if !mb.cache.nra { recycleMsgBlockBuf(mb.cache.buf) } @@ -5627,12 +5894,12 @@ func (fs *fileStore) expireMsgs() { maxAge := int64(fs.cfg.MaxAge) minAge := ats.AccessTime() - maxAge rmcb := fs.rmcb - sdmcb := fs.sdmcb + pmsgcb := fs.pmsgcb sdmTTL := int64(fs.cfg.SubjectDeleteMarkerTTL.Seconds()) sdmEnabled := sdmTTL > 0 fs.mu.RUnlock() - if sdmEnabled && (rmcb == nil || sdmcb == nil) { + if sdmEnabled && (rmcb == nil || pmsgcb == nil) { return } @@ -5793,23 +6060,52 @@ func (fs *fileStore) handleRemovalOrSdm(seq uint64, subj string, sdm bool, sdmTT subj: subj, hdr: hdr, } - fs.sdmcb(msg) + fs.pmsgcb(msg) } else { fs.rmcb(seq) } } +// Will run through scheduled messages. +func (fs *fileStore) runMsgScheduling() { + // TODO: Not great that we're holding the lock here, but the timed hash wheel and message scheduling isn't thread-safe. + fs.mu.Lock() + defer fs.mu.Unlock() + + if fs.scheduling == nil || fs.pmsgcb == nil { + return + } + + scheduledMsgs := fs.scheduling.getScheduledMessages(func(seq uint64, smv *StoreMsg) *StoreMsg { + sm, _ := fs.msgForSeqLocked(seq, smv, false) + return sm + }) + if len(scheduledMsgs) > 0 { + fs.mu.Unlock() + for _, msg := range scheduledMsgs { + fs.pmsgcb(msg) + } + fs.mu.Lock() + } + + if fs.scheduling != nil { + fs.scheduling.resetTimer() + } +} + // Lock should be held. -func (fs *fileStore) checkAndFlushAllBlocks() { - for _, mb := range fs.blks { - if mb.pendingWriteSize() > 0 { - // Since fs lock is held need to pull this apart in case we need to rebuild state. - mb.mu.Lock() - ld, _ := mb.flushPendingMsgsLocked() - mb.mu.Unlock() - if ld != nil { - fs.rebuildStateLocked(ld) - } +func (fs *fileStore) checkAndFlushLastBlock() { + lmb := fs.lmb + if lmb == nil { + return + } + if lmb.pendingWriteSize() > 0 { + // Since fs lock is held need to pull this apart in case we need to rebuild state. + lmb.mu.Lock() + ld, _ := lmb.flushPendingMsgsLocked() + lmb.mu.Unlock() + if ld != nil { + fs.rebuildStateLocked(ld) } } } @@ -5819,7 +6115,7 @@ func (fs *fileStore) checkMsgs() *LostStreamData { fs.mu.Lock() defer fs.mu.Unlock() - fs.checkAndFlushAllBlocks() + fs.checkAndFlushLastBlock() // Clear any global subject state. fs.psim, fs.tsl = fs.psim.Empty(), 0 @@ -5894,11 +6190,6 @@ func (mb *msgBlock) writeMsgRecordLocked(rl, seq uint64, subj string, mhdr, msg } } - // Make sure we have a cache setup. - if mb.cache == nil { - mb.setupWriteCache(nil) - } - // Check if we are tracking per subject for our simple state. // Do this before changing the cache that would trigger a flush pending msgs call // if we needed to regenerate the per subject info. @@ -5918,8 +6209,18 @@ func (mb *msgBlock) writeMsgRecordLocked(rl, seq uint64, subj string, mhdr, msg } } + // Make sure we have a cache setup. Do so after ensurePerSubjectInfoLoaded as it may + // have already brought in a cache for us. + if err := mb.setupWriteCache(nil); err != nil { + return err + } + + // Make sure that the GC can't take away our writes by strengthening the elastic + // reference. It will now stay strong until the flusher decides it is time to weaken. + mb.ecache.Strengthen() + // Indexing - index := len(mb.cache.buf) + int(mb.cache.off) + index := len(mb.cache.buf) // Formats // Format with no header @@ -5992,9 +6293,9 @@ func (mb *msgBlock) writeMsgRecordLocked(rl, seq uint64, subj string, mhdr, msg fch, werr := mb.fch, mb.werr // If we should be flushing, or had a write error, do so here. - if flush || werr != nil { + if (flush && mb.fs.fip) || werr != nil { ld, err := mb.flushPendingMsgsLocked() - if ld != nil && mb.fs != nil { + if ld != nil { // We have the mb lock here, this needs the mb locks so do in its own go routine. go mb.fs.rebuildState(ld) } @@ -6005,7 +6306,6 @@ func (mb *msgBlock) writeMsgRecordLocked(rl, seq uint64, subj string, mhdr, msg // Kick the flusher here. kickFlusher(fch) } - return nil } @@ -6552,16 +6852,17 @@ func (fs *fileStore) selectMsgBlockForStart(minTime time.Time) *msgBlock { func (mb *msgBlock) indexCacheBuf(buf []byte) error { var le = binary.LittleEndian - var fseq uint64 + var alloc bool var idx []uint32 var index uint32 mbFirstSeq := atomic.LoadUint64(&mb.first.seq) mbLastSeq := atomic.LoadUint64(&mb.last.seq) + fseq := mbFirstSeq // Sanity check here since we calculate size to allocate based on this. if mbFirstSeq > (mbLastSeq + 1) { // Purged state first == last + 1 - mb.fs.warn("indexCacheBuf corrupt state: mb.first %d mb.last %d", mbFirstSeq, mbLastSeq) + mb.fs.warn("indexCacheBuf corrupt state in %s: mb.first %d mb.last %d", mb.mfn, mbFirstSeq, mbLastSeq) // This would cause idxSz to wrap. return errCorruptState } @@ -6571,18 +6872,26 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error { idxSz := mbLastSeq - mbFirstSeq + 1 if mb.cache == nil { - // Approximation, may adjust below. - fseq = mbFirstSeq - idx = make([]uint32, 0, idxSz) + mb.cache = mb.ecache.Value() + } + if mb.cache == nil { mb.cache = &cache{} + alloc = true } else { - fseq = mb.cache.fseq - idx = mb.cache.idx - if len(idx) == 0 { - idx = make([]uint32, 0, idxSz) - } - index = uint32(len(mb.cache.buf)) - buf = append(mb.cache.buf, buf...) + // 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. + recycleMsgBlockBuf(mb.cache.buf) + } + if idx = mb.cache.idx; uint64(cap(idx)) >= idxSz { + idx = idx[:0] + } else { + idx = make([]uint32, 0, idxSz) + } + if !alloc { + // We didn't allocate a new *cache so instead wipe the current + // one, we already have reused idx if possible. + *mb.cache = cache{} } // Create FSS if we should track. @@ -6594,11 +6903,15 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error { // Mark fss activity. mb.lsts = ats.AccessTime() mb.ttls = 0 + mb.schedules = 0 lbuf := uint32(len(buf)) - var seq, ttls uint64 + var seq, ttls, schedules uint64 var sm StoreMsg // Used for finding TTL headers + // To ensure the sequence keeps moving up. + var last uint64 + for index < lbuf { if index+msgHdrSize > lbuf { return errCorruptState @@ -6614,7 +6927,7 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error { // Do some quick sanity checks here. if dlen < 0 || slen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh { - mb.fs.warn("indexCacheBuf corrupt record state: dlen %d slen %d index %d rl %d lbuf %d", dlen, slen, index, rl, lbuf) + mb.fs.warn("indexCacheBuf corrupt record state in %s: dlen %d slen %d index %d rl %d lbuf %d", mb.mfn, dlen, slen, index, rl, lbuf) // This means something is off. // TODO(dlc) - Add into bad list? return errCorruptState @@ -6630,6 +6943,15 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error { erased := seq&ebit != 0 seq = seq &^ ebit + // The sequence needs to only ever move up. + if seq <= last { + // Advance to next record. + // We've already accounted for this sequence and marked it as deleted. + index += rl + continue + } + last = seq + // We defer checksum checks to individual msg cache lookups to amortorize costs and // not introduce latency for first message from a newly loaded block. if seq >= mbFirstSeq { @@ -6672,13 +6994,16 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error { } } - // Count how many TTLs we think are in this message block. + // Count how many TTLs/schedules we think are in this message block. // TODO(nat): Not terribly optimal... if hasHeaders { if fsm, err := mb.msgFromBufNoCopy(buf[index:], &sm, nil); err == nil && fsm != nil { if ttl := sliceHeader(JSMessageTTL, fsm.hdr); len(ttl) > 0 { ttls++ } + if sched := sliceHeader(JSSchedulePattern, fsm.hdr); len(sched) > 0 { + schedules++ + } } } } @@ -6701,8 +7026,9 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error { mb.cache.buf = buf mb.cache.idx = idx mb.cache.fseq = fseq - mb.cache.wp += int(lbuf) + mb.cache.wp = int(lbuf) mb.ttls = ttls + mb.schedules = schedules return nil } @@ -6744,8 +7070,11 @@ func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) { // Signals us that we need to rebuild filestore state. var fsLostData *LostStreamData + if mb.cache == nil { + mb.cache = mb.ecache.Value() + } if mb.cache == nil || mb.mfd == nil { - return nil, nil + return nil, errNoCache } buf, err := mb.bytesPending() @@ -6758,7 +7087,7 @@ func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) { return nil, err } - woff := int64(mb.cache.off + mb.cache.wp) + wp := int64(mb.cache.wp) lob := len(buf) // TODO(dlc) - Normally we would not hold the lock across I/O so we can improve performance. @@ -6774,6 +7103,7 @@ func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) { var dst []byte if lob <= defaultLargeBlockSize { dst = getMsgBlockBuf(lob)[:lob] + defer recycleMsgBlockBuf(dst) } else { dst = make([]byte, lob) } @@ -6783,7 +7113,7 @@ func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) { // Append new data to the message block file. for lbb := lob; lbb > 0; lbb = len(buf) { - n, err := mb.writeAt(buf, woff) + n, err := mb.writeAt(buf, wp) if err != nil { mb.dirtyCloseWithRemove(false) ld, _, _ := mb.rebuildStateLocked() @@ -6791,14 +7121,8 @@ func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) { return ld, err } // Update our write offset. - woff += int64(n) - // Partial write. - if n != lbb { - buf = buf[n:] - } else { - // Done. - break - } + wp += int64(n) + buf = buf[n:] } // Clear any error. @@ -6809,6 +7133,9 @@ func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) { return fsLostData, mb.werr } + // Update write pointer. + mb.cache.wp = int(wp) + // Check if we are in sync always mode. if mb.syncAlways { mb.mfd.Sync() @@ -6817,37 +7144,25 @@ func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) { } // Check for additional writes while we were writing to the disk. - moreBytes := len(mb.cache.buf) - mb.cache.wp - lob - - // Decide what we want to do with the buffer in hand. If we have load interest - // we will hold onto the whole thing, otherwise empty the buffer, possibly reusing it. - if ts := ats.AccessTime(); ts < mb.llts || (ts-mb.llts) <= int64(mb.cexp) { - mb.cache.wp += lob - } else { - if cap(mb.cache.buf) <= maxBufReuse { - buf = mb.cache.buf[:0] - } else { - recycleMsgBlockBuf(mb.cache.buf) - buf = nil - } - if moreBytes > 0 { - nbuf := mb.cache.buf[len(mb.cache.buf)-moreBytes:] - if moreBytes > (len(mb.cache.buf)/4*3) && cap(nbuf) <= maxBufReuse { - buf = nbuf - } else { - buf = append(buf, nbuf...) - } - } - // Update our cache offset. - mb.cache.off = int(woff) - // Reset write pointer. - mb.cache.wp = 0 - // Place buffer back in the cache structure. - mb.cache.buf = buf - // Mark fseq to 0 - mb.cache.fseq = 0 + // TODO(nat): This should not be possible at present since, as above, we are + // not releasing the lock during I/O operation. Therefore this will always + // return zero. + if mb.pendingWriteSizeLocked() > 0 { + return fsLostData, mb.werr } + // Check last access time. If we think the block still has read interest + // then we will weaken the pointer but otherwise try to hold onto it. + if ts := ats.AccessTime(); ts < mb.llts || (ts-mb.llts) <= int64(mb.cexp) { + mb.cache = nil + mb.ecache.Weaken() + mb.resetCacheExpireTimer(0) + return fsLostData, mb.werr + } + + // If not, we'll just drop the cache altogether & recycle the buffer. + mb.cache.nra = false + mb.expireCacheLocked() return fsLostData, mb.werr } @@ -6866,7 +7181,10 @@ func (mb *msgBlock) loadMsgs() error { // Lock should be held. func (mb *msgBlock) cacheAlreadyLoaded() bool { - if mb.cache == nil || mb.cache.off != 0 || mb.cache.fseq == 0 || len(mb.cache.buf) == 0 { + if mb.cache == nil { + mb.cache = mb.ecache.Value() + } + if mb.cache == nil || mb.cache.fseq == 0 || len(mb.cache.buf) == 0 { return false } numEntries := mb.msgs + uint64(mb.dmap.Size()) + (atomic.LoadUint64(&mb.first.seq) - mb.cache.fseq) @@ -6928,23 +7246,15 @@ func (mb *msgBlock) loadBlock(buf []byte) ([]byte, error) { } } - if buf == nil { + if buf == nil || cap(buf) < sz { + // getMsgBlockBuf will try to return a slice that fits from the + // buffer pool, but if by chance `sz` is greater than the maximum + // large block size, it'll return a new slice to fit regardless. buf = getMsgBlockBuf(sz) - if sz > cap(buf) { - // We know we will make a new one so just recycle for now. - recycleMsgBlockBuf(buf) - buf = nil - } - } - - if sz > cap(buf) { - buf = make([]byte, sz) - } else { - buf = buf[:sz] } <-dios - n, err := io.ReadFull(f, buf) + n, err := io.ReadFull(f, buf[:sz]) dios <- struct{}{} // On success capture raw bytes size. if err == nil { @@ -6973,7 +7283,7 @@ func (mb *msgBlock) loadMsgsWithLock() error { checkCache: nchecks++ if nchecks > 8 { - return errCorruptState + return errNoCache } // Check to see if we have a full cache. @@ -7011,10 +7321,6 @@ checkCache: return err } - // Reset the cache since we just read everything in. - // Make sure this is cleared in case we had a partial when we started. - mb.clearCacheAndOffset() - // Check if we need to decrypt. if err = mb.encryptOrDecryptIfNeeded(buf); err != nil { return err @@ -7044,6 +7350,9 @@ checkCache: mb.startCacheExpireTimer() } + // Update the cache pointer since we cleared & rebuilt. + mb.ecache.Set(mb.cache) + return nil } @@ -7091,6 +7400,7 @@ func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg return nil, false, err } } + defer mb.finishedWithCache() llseq := mb.llseq fsm, err := mb.cacheLookupEx(seq, sm, doCopy) @@ -7103,7 +7413,6 @@ func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg var ( errNoCache = errors.New("no message cache") - errBadMsg = errors.New("malformed or corrupt message") errDeletedMsg = errors.New("deleted message") errPartialCache = errors.New("partial cache") errNoPending = errors.New("message block does not have pending data") @@ -7121,6 +7430,17 @@ var ( errStateTooBig = errors.New("store state too big for optional write") ) +type ( + errBadMsg struct{ fn, detail string } +) + +func (e errBadMsg) Error() string { + if e.detail != _EMPTY_ { + return fmt.Sprintf("malformed or corrupt message in %s: %s", filepath.Base(e.fn), e.detail) + } + return fmt.Sprintf("malformed or corrupt message in %s", filepath.Base(e.fn)) +} + const ( // "Checksum bit" is used in "mb.cache.idx" for marking messages that have had their checksums checked. cbit = 1 << 31 @@ -7169,6 +7489,9 @@ func (mb *msgBlock) cacheLookupEx(seq uint64, sm *StoreMsg, doCopy bool) (*Store } // Detect no cache loaded. + if mb.cache == nil { + mb.cache = mb.ecache.Value() + } if mb.cache == nil || mb.cache.fseq == 0 || len(mb.cache.idx) == 0 || len(mb.cache.buf) == 0 { var reason string if mb.cache == nil { @@ -7197,7 +7520,7 @@ func (mb *msgBlock) cacheLookupEx(seq uint64, sm *StoreMsg, doCopy bool) (*Store // Update cache activity. mb.llts = ats.AccessTime() - li := int(bi) - mb.cache.off + li := int(bi) if li >= len(mb.cache.buf) { return nil, errPartialCache } @@ -7220,7 +7543,7 @@ func (mb *msgBlock) cacheLookupEx(seq uint64, sm *StoreMsg, doCopy bool) (*Store return nil, errDeletedMsg } - if seq != fsm.seq { + if seq != fsm.seq { // See TestFileStoreInvalidIndexesRebuilt. recycleMsgBlockBuf(mb.cache.buf) mb.cache.buf = nil return nil, fmt.Errorf("sequence numbers for cache load did not match, %d vs %d", seq, fsm.seq) @@ -7321,7 +7644,7 @@ func (mb *msgBlock) msgFromBufNoCopy(buf []byte, sm *StoreMsg, hh hash.Hash64) ( // Lock should be held. func (mb *msgBlock) msgFromBufEx(buf []byte, sm *StoreMsg, hh hash.Hash64, doCopy bool) (*StoreMsg, error) { if len(buf) < emptyRecordLen { - return nil, errBadMsg + return nil, errBadMsg{mb.mfn, "record too short"} } var le = binary.LittleEndian @@ -7333,7 +7656,7 @@ func (mb *msgBlock) msgFromBufEx(buf []byte, sm *StoreMsg, hh hash.Hash64, doCop slen := int(le.Uint16(hdr[20:])) // Simple sanity check. if dlen < 0 || slen > (dlen-recordHashSize) || dlen > int(rl) || int(rl) > len(buf) || rl > rlBadThresh { - return nil, errBadMsg + return nil, errBadMsg{mb.mfn, fmt.Sprintf("sanity check failed (dlen %d slen %d rl %d buf %d)", dlen, slen, rl, buf)} } data := buf[msgHdrSize : msgHdrSize+dlen] // Do checksum tests here if requested. @@ -7347,7 +7670,7 @@ func (mb *msgBlock) msgFromBufEx(buf []byte, sm *StoreMsg, hh hash.Hash64, doCop hh.Write(data[slen : dlen-recordHashSize]) } if !bytes.Equal(hh.Sum(nil), data[len(data)-8:]) { - return nil, errBadMsg + return nil, errBadMsg{mb.mfn, "invalid checksum"} } } seq := le.Uint64(hdr[4:]) @@ -7365,29 +7688,51 @@ func (mb *msgBlock) msgFromBufEx(buf []byte, sm *StoreMsg, hh hash.Hash64, doCop // To recycle the large blocks we can never pass back a reference, so need to copy for the upper // layers and for us to be safe to expire, and recycle, the large msgBlocks. end := dlen - 8 + if len(data) < end { + return nil, errBadMsg{mb.mfn, "invalid data length"} + } if hasHeaders { + if slen+4 > len(data) { + return nil, errBadMsg{mb.mfn, "invalid subject length greataer than data length"} + } hl := le.Uint32(data[slen:]) bi := slen + 4 li := bi + int(hl) + if bi > end { + return nil, errBadMsg{mb.mfn, "invalid buffer index"} + } if doCopy { sm.buf = append(sm.buf, data[bi:end]...) } else { sm.buf = data[bi:end] } li, end = li-bi, end-bi + if li > len(sm.buf) || end > len(sm.buf) { + return nil, errBadMsg{mb.mfn, "invalid message length or end greater than buffer length"} + } sm.hdr = sm.buf[0:li:li] sm.msg = sm.buf[li:end] } else { + if slen > end { + return nil, errBadMsg{mb.mfn, "invalid subject length greater than end"} + } if doCopy { sm.buf = append(sm.buf, data[slen:end]...) } else { sm.buf = data[slen:end] } - sm.msg = sm.buf[0 : end-slen] + mlen := end - slen + if mlen > len(sm.buf) { + return nil, errBadMsg{mb.mfn, "invalid message length greater than buffer length"} + } + sm.msg = sm.buf[0:mlen] } sm.seq, sm.ts = seq, ts if slen > 0 { + if slen > len(data) { + return nil, errBadMsg{mb.mfn, "invalid subject length greater than data length"} + } if doCopy { // Make a copy since sm.subj lifetime may last longer. sm.subj = string(data[:slen]) @@ -7486,15 +7831,20 @@ func (fs *fileStore) loadLast(subj string, sm *StoreMsg) (lsm *StoreMsg, err err if l == 0 { _, _, l = mb.filteredPendingLocked(subj, wc, atomic.LoadUint64(&mb.first.seq)) } + var didLoad bool if l > 0 { if mb.cacheNotLoaded() { if err := mb.loadMsgsWithLock(); err != nil { mb.mu.Unlock() return nil, err } + didLoad = true } lsm, err = mb.cacheLookup(l, sm) } + if didLoad { + mb.finishedWithCache() + } mb.mu.Unlock() if l > 0 { break @@ -7665,10 +8015,12 @@ func (fs *fileStore) LoadPrevMsg(start uint64, smp *StoreMsg) (sm *StoreMsg, err continue } if sm, err := mb.cacheLookup(seq, smp); err == nil { + mb.finishedWithCache() mb.mu.Unlock() return sm, nil } } + mb.finishedWithCache() mb.mu.Unlock() } } @@ -7774,19 +8126,32 @@ func (fs *fileStore) Utilization() (total, reported uint64, err error) { return total, reported, nil } -func fileStoreMsgSize(subj string, hdr, msg []byte) uint64 { - if len(hdr) == 0 { +func fileStoreMsgSizeRaw(slen, hlen, mlen int) uint64 { + if hlen == 0 { // length of the message record (4bytes) + seq(8) + ts(8) + subj_len(2) + subj + msg + hash(8) - return uint64(22 + len(subj) + len(msg) + 8) + return uint64(22 + slen + mlen + 8) } // length of the message record (4bytes) + seq(8) + ts(8) + subj_len(2) + subj + hdr_len(4) + hdr + msg + hash(8) - return uint64(22 + len(subj) + 4 + len(hdr) + len(msg) + 8) + return uint64(22 + slen + 4 + hlen + mlen + 8) +} + +func fileStoreMsgSize(subj string, hdr, msg []byte) uint64 { + return fileStoreMsgSizeRaw(len(subj), len(hdr), len(msg)) } func fileStoreMsgSizeEstimate(slen, maxPayload int) uint64 { return uint64(emptyRecordLen + slen + 4 + maxPayload) } +// ResetState resets any state that's temporary. For example when changing leaders. +func (fs *fileStore) ResetState() { + fs.mu.Lock() + defer fs.mu.Unlock() + if fs.scheduling != nil { + fs.scheduling.clearInflight() + } +} + // Determine time since any last activity, read/load, write or remove. func (mb *msgBlock) sinceLastActivity() time.Duration { if mb.closed { @@ -7945,9 +8310,17 @@ func (fs *fileStore) cacheSize() uint64 { fs.mu.RLock() for _, mb := range fs.blks { mb.mu.RLock() + var needsCleanup bool + if mb.cache == nil { + mb.cache = mb.ecache.Value() + needsCleanup = mb.cache != nil + } if mb.cache != nil { sz += uint64(len(mb.cache.buf)) } + if needsCleanup { + mb.finishedWithCache() + } mb.mu.RUnlock() } fs.mu.RUnlock() @@ -8121,6 +8494,8 @@ func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint if shouldExpire && time.Since(fs.lpex) > time.Second { // Expire this cache before moving on. mb.tryForceExpireCacheLocked() + } else { + mb.finishedWithCache() } mb.mu.Unlock() @@ -8337,6 +8712,7 @@ func (fs *fileStore) compact(seq uint64) (uint64, error) { if err = smb.loadMsgsWithLock(); err != nil { goto SKIP } + defer smb.finishedWithCache() } for mseq := atomic.LoadUint64(&smb.first.seq); mseq < seq; mseq++ { sm, err := smb.cacheLookupNoCopy(mseq, &smv) @@ -8572,6 +8948,7 @@ func (mb *msgBlock) tombsLocked() []msgId { return nil } } + defer mb.finishedWithCache() var tombs []msgId var le = binary.LittleEndian @@ -8946,6 +9323,7 @@ func (mb *msgBlock) recalculateForSubj(subj string, ss *SimpleState) { return } } + defer mb.finishedWithCache() startSlot := int(ss.First - mb.cache.fseq) if startSlot < 0 { @@ -8981,7 +9359,7 @@ func (mb *msgBlock) recalculateForSubj(subj string, ss *SimpleState) { // delete marker so skip. continue } - li := int(bi) - mb.cache.off + li := int(bi) if li >= len(mb.cache.buf) { ss.First = ss.Last // Only need to reset ss.lastNeedsUpdate, ss.firstNeedsUpdate is already reset above. @@ -9022,7 +9400,7 @@ func (mb *msgBlock) recalculateForSubj(subj string, ss *SimpleState) { // delete marker so skip. continue } - li := int(bi) - mb.cache.off + li := int(bi) if li >= len(mb.cache.buf) { // Can't overwrite ss.Last, just skip. return @@ -9082,6 +9460,7 @@ func (mb *msgBlock) generatePerSubjectInfo() error { return nil } } + defer mb.finishedWithCache() // Create new one regardless. mb.fss = mb.fss.Empty() @@ -9191,6 +9570,8 @@ func (mb *msgBlock) close(sync bool) { // Clear fss. mb.fss = nil + // Flush pending writes. + mb.flushPendingMsgsLocked() // Close cache mb.clearCacheAndOffset() // Quit our loops. @@ -9215,7 +9596,7 @@ func (fs *fileStore) closeAllMsgBlocks(sync bool) { } } -func (fs *fileStore) Delete() error { +func (fs *fileStore) Delete(inline bool) error { if fs.isClosed() { // Always attempt to remove since we could have been closed beforehand. os.RemoveAll(fs.fcfg.StoreDir) @@ -9270,7 +9651,7 @@ func (fs *fileStore) Delete() error { } // Do this in separate Go routine in case lots of blocks. // Purge above protects us as does the removal of meta artifacts above. - go func() { + removeDir := func() { <-dios err := os.RemoveAll(ndir) dios <- struct{}{} @@ -9287,8 +9668,12 @@ func (fs *fileStore) Delete() error { return } } - }() - + } + if inline { + removeDir() + } else { + go removeDir() + } return nil } @@ -9319,7 +9704,7 @@ func (fs *fileStore) cancelSyncTimer() { const ( fullStateMagic = uint8(11) fullStateMinVersion = uint8(1) // What is the minimum version we know how to parse? - fullStateVersion = uint8(2) // What is the current version written out to index.db? + fullStateVersion = uint8(3) // What is the current version written out to index.db? ) // This go routine periodically writes out our full stream state index. @@ -9481,7 +9866,8 @@ func (fs *fileStore) _writeFullState(force bool) error { numDeleted := mb.dmap.Size() buf = binary.AppendUvarint(buf, uint64(numDeleted)) - buf = binary.AppendUvarint(buf, mb.ttls) // Field is new in version 2 + buf = binary.AppendUvarint(buf, mb.ttls) // Field is new in version 2 + buf = binary.AppendUvarint(buf, mb.schedules) // Field is new in version 3 if numDeleted > 0 { dmap, _ := mb.dmap.Encode(scratch[:0]) dmapTotalLen += len(dmap) @@ -9567,15 +9953,23 @@ func (fs *fileStore) _writeFullState(force bool) error { fs.mu.Unlock() } - return fs.writeTTLState() + // Attempt to write both files, an error in one should not prevent the other from being written. + ttlErr := fs.writeTTLState() + schedErr := fs.writeMsgSchedulingState() + if ttlErr != nil { + return ttlErr + } else if schedErr != nil { + return schedErr + } + return nil } func (fs *fileStore) writeTTLState() error { + fs.mu.RLock() if fs.ttls == nil { + fs.mu.RUnlock() return nil } - - fs.mu.RLock() fn := filepath.Join(fs.fcfg.StoreDir, msgDir, ttlStreamStateFile) // Must be lseq+1 to identify up to which sequence the TTLs are valid. buf := fs.ttls.Encode(fs.state.LastSeq + 1) @@ -9588,6 +9982,24 @@ func (fs *fileStore) writeTTLState() error { return err } +func (fs *fileStore) writeMsgSchedulingState() error { + fs.mu.RLock() + if fs.scheduling == nil { + fs.mu.RUnlock() + return nil + } + fn := filepath.Join(fs.fcfg.StoreDir, msgDir, msgSchedulingStreamStateFile) + // Must be lseq+1 to identify up to which sequence the schedules are valid. + buf := fs.scheduling.encode(fs.state.LastSeq + 1) + fs.mu.RUnlock() + + <-dios + err := os.WriteFile(fn, buf, defaultFilePerms) + dios <- struct{}{} + + return err +} + // Stop the current filestore. func (fs *fileStore) Stop() error { return fs.stop(false, true) @@ -9606,7 +10018,7 @@ func (fs *fileStore) stop(delete, writeState bool) error { fs.closing = true if writeState { - fs.checkAndFlushAllBlocks() + fs.checkAndFlushLastBlock() } fs.closeAllMsgBlocks(false) @@ -11099,11 +11511,13 @@ func (fs *fileStore) RemoveConsumer(o ConsumerStore) error { // Templates //////////////////////////////////////////////////////////////////////////////// +// Deprecated: stream templates are deprecated and will be removed in a future version. type templateFileStore struct { dir string hh hash.Hash64 } +// Deprecated: stream templates are deprecated and will be removed in a future version. func newTemplateFileStore(storeDir string) *templateFileStore { tdir := filepath.Join(storeDir, tmplsDir) key := sha256.Sum256([]byte("templates")) @@ -11114,6 +11528,7 @@ func newTemplateFileStore(storeDir string) *templateFileStore { return &templateFileStore{dir: tdir, hh: hh} } +// Deprecated: stream templates are deprecated and will be removed in a future version. func (ts *templateFileStore) Store(t *streamTemplate) error { dir := filepath.Join(ts.dir, t.Name) if err := os.MkdirAll(dir, defaultDirPerms); err != nil { @@ -11143,6 +11558,7 @@ func (ts *templateFileStore) Store(t *streamTemplate) error { return nil } +// Deprecated: stream templates are deprecated and will be removed in a future version. func (ts *templateFileStore) Delete(t *streamTemplate) error { return os.RemoveAll(filepath.Join(ts.dir, t.Name)) } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/gateway.go b/vendor/github.com/nats-io/nats-server/v2/server/gateway.go index 962858f99..085f0e18f 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/gateway.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/gateway.go @@ -35,6 +35,7 @@ import ( const ( defaultSolicitGatewaysDelay = time.Second defaultGatewayConnectDelay = time.Second + defaultGatewayConnectMaxDelay = 30 * time.Second defaultGatewayReconnectDelay = time.Second defaultGatewayRecentSubExpiration = 2 * time.Second defaultGatewayMaxRUnsubBeforeSwitch = 1000 @@ -59,6 +60,7 @@ const ( var ( gatewayConnectDelay = defaultGatewayConnectDelay + gatewayConnectMaxDelay = defaultGatewayConnectMaxDelay gatewayReconnectDelay = defaultGatewayReconnectDelay gatewayMaxRUnsubBeforeSwitch = defaultGatewayMaxRUnsubBeforeSwitch gatewaySolicitDelay = int64(defaultSolicitGatewaysDelay) @@ -703,10 +705,11 @@ func (s *Server) reconnectGateway(cfg *gatewayCfg) { // to the given Gateway. It will return once a connection has been created. func (s *Server) solicitGateway(cfg *gatewayCfg, firstConnect bool) { var ( - opts = s.getOpts() - isImplicit = cfg.isImplicit() - attempts int - typeStr string + opts = s.getOpts() + isImplicit = cfg.isImplicit() + attemptDelay = gatewayConnectDelay + attempts int + typeStr string ) if isImplicit { typeStr = "implicit" @@ -769,7 +772,14 @@ func (s *Server) solicitGateway(cfg *gatewayCfg, firstConnect bool) { select { case <-s.quitCh: return - case <-time.After(gatewayConnectDelay): + case <-time.After(attemptDelay): + if opts.Gateway.ConnectBackoff { + // Use exponential backoff for connection attempts. + attemptDelay *= 2 + if attemptDelay > gatewayConnectMaxDelay { + attemptDelay = gatewayConnectMaxDelay + } + } continue } } @@ -923,7 +933,7 @@ func (s *Server) createGateway(cfg *gatewayCfg, url *url.URL, conn net.Conn) { if tlsRequired { c.Debugf("TLS handshake complete") cs := c.nc.(*tls.Conn).ConnectionState() - c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite)) + c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite)) } // For outbound, we can't set the normal ping timer yet since the other 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 d676f6402..05dac6ef4 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 @@ -156,8 +156,8 @@ type jsAccount struct { storeDir string inflight sync.Map streams map[string]*stream - templates map[string]*streamTemplate - store TemplateStore + templates map[string]*streamTemplate // Deprecated: stream templates are deprecated and will be removed in a future version. + store TemplateStore // Deprecated: stream templates are deprecated and will be removed in a future version. // From server sendq *ipQueue[*pubMsg] @@ -560,7 +560,7 @@ func (s *Server) restartJetStream() error { MaxMemory: opts.JetStreamMaxMemory, MaxStore: opts.JetStreamMaxStore, Domain: opts.JetStreamDomain, - Strict: opts.JetStreamStrict, + Strict: !opts.NoJetStreamStrict, } s.Noticef("Restarting JetStream") err := s.EnableJetStream(&cfg) @@ -1451,6 +1451,61 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro mset.setCreatedTime(cfg.Created) } + // Might need to recover from a partial batch write, but only if a single replica stream. + if cfg.AllowAtomicPublish && cfg.Replicas == 1 { + var ( + ok bool + smv StoreMsg + batchId string + batchSeq uint64 + commit bool + batchStoreDir string + store StreamStore + state StreamState + ) + // Check if the last message was part of a batch. + sm, err := mset.store.LoadLastMsg(fwcs, &smv) + if err != nil || sm == nil { + goto SKIP + } + batchId = getBatchId(sm.hdr) + batchSeq, ok = getBatchSequence(sm.hdr) + commit = len(sliceHeader(JSBatchCommit, sm.hdr)) != 0 + if batchId == _EMPTY_ || !ok || commit { + goto SKIP + } + // We've observed a partial batch write. Write the remainder of the batch. + batchSeq++ + _, batchStoreDir = getBatchStoreDir(mset, batchId) + if _, err = os.Stat(batchStoreDir); err != nil { + s.Errorf(" Failed restoring partial batch write for stream '%s > %s' at sequence %d: %v", + mset.accName(), mset.name(), batchSeq, err) + goto SKIP + } + store, err = newBatchStore(mset, batchId) + if err != nil { + s.Errorf(" Failed restoring partial batch write for stream '%s > %s' at sequence %d: %v", + mset.accName(), mset.name(), batchSeq, err) + goto SKIP + } + store.FastState(&state) + s.Noticef(" Restoring partial batch write for stream '%s > %s' (seq %d to %d)", + mset.accName(), mset.name(), batchSeq, state.LastSeq) + // Loop through items that weren't persisted yet. + for seq := batchSeq; seq <= state.LastSeq; seq++ { + sm, err = store.LoadMsg(seq, &smv) + if err != nil || sm == nil { + s.Errorf(" Failed restoring partial batch write for stream '%s > %s' at sequence %d: %v", + mset.accName(), mset.name(), seq, err) + break + } + mset.processJetStreamMsg(sm.subj, _EMPTY_, sm.hdr, sm.msg, 0, 0, nil, false, true) + } + store.Delete(true) + SKIP: + os.RemoveAll(filepath.Join(sdir, fi.Name(), batchesDir)) + } + state := mset.state() s.Noticef(" Restored %s messages for stream '%s > %s' in %v", comma(int64(state.Msgs)), mset.accName(), mset.name(), time.Since(rt).Round(time.Millisecond)) @@ -2640,7 +2695,7 @@ func (s *Server) dynJetStreamConfig(storeDir string, maxStore, maxMem int64) *Je opts := s.getOpts() // Strict mode. - jsc.Strict = opts.JetStreamStrict + jsc.Strict = !opts.NoJetStreamStrict // Sync options. jsc.SyncInterval = opts.SyncInterval @@ -2687,6 +2742,7 @@ func (a *Account) checkForJetStream() (*Server, *jsAccount, error) { // StreamTemplateConfig allows a configuration to auto-create streams based on this template when a message // is received that matches. Each new stream will use the config as the template config to create them. +// Deprecated: stream templates are deprecated and will be removed in a future version. type StreamTemplateConfig struct { Name string `json:"name"` Config *StreamConfig `json:"config"` @@ -2694,12 +2750,14 @@ type StreamTemplateConfig struct { } // StreamTemplateInfo +// Deprecated: stream templates are deprecated and will be removed in a future version. type StreamTemplateInfo struct { Config *StreamTemplateConfig `json:"config"` Streams []string `json:"streams"` } // streamTemplate +// Deprecated: stream templates are deprecated and will be removed in a future version. type streamTemplate struct { mu sync.Mutex tc *client @@ -2708,6 +2766,7 @@ type streamTemplate struct { streams []string } +// Deprecated: stream templates are deprecated and will be removed in a future version. func (t *StreamTemplateConfig) deepCopy() *StreamTemplateConfig { copy := *t cfg := *t.Config @@ -2716,6 +2775,7 @@ func (t *StreamTemplateConfig) deepCopy() *StreamTemplateConfig { } // addStreamTemplate will add a stream template to this account that allows auto-creation of streams. +// Deprecated: stream templates are deprecated and will be removed in a future version. func (a *Account) addStreamTemplate(tc *StreamTemplateConfig) (*streamTemplate, error) { s, jsa, err := a.checkForJetStream() if err != nil { @@ -2772,6 +2832,7 @@ func (a *Account) addStreamTemplate(tc *StreamTemplateConfig) (*streamTemplate, return t, nil } +// Deprecated: stream templates are deprecated and will be removed in a future version. func (t *streamTemplate) createTemplateSubscriptions() error { if t == nil { return fmt.Errorf("no template") @@ -2795,6 +2856,7 @@ func (t *streamTemplate) createTemplateSubscriptions() error { return nil } +// Deprecated: stream templates are deprecated and will be removed in a future version. func (t *streamTemplate) processInboundTemplateMsg(_ *subscription, pc *client, acc *Account, subject, reply string, msg []byte) { if t == nil || t.jsa == nil { return @@ -2842,6 +2904,7 @@ func (t *streamTemplate) processInboundTemplateMsg(_ *subscription, pc *client, } // lookupStreamTemplate looks up the names stream template. +// Deprecated: stream templates are deprecated and will be removed in a future version. func (a *Account) lookupStreamTemplate(name string) (*streamTemplate, error) { _, jsa, err := a.checkForJetStream() if err != nil { @@ -2860,6 +2923,7 @@ func (a *Account) lookupStreamTemplate(name string) (*streamTemplate, error) { } // This function will check all named streams and make sure they are valid. +// Deprecated: stream templates are deprecated and will be removed in a future version. func (a *Account) validateStreams(t *streamTemplate) { t.mu.Lock() var vstreams []string @@ -2872,6 +2936,7 @@ func (a *Account) validateStreams(t *streamTemplate) { t.mu.Unlock() } +// Deprecated: stream templates are deprecated and will be removed in a future version. func (t *streamTemplate) delete() error { if t == nil { return fmt.Errorf("nil stream template") @@ -2930,6 +2995,7 @@ func (t *streamTemplate) delete() error { return lastErr } +// Deprecated: stream templates are deprecated and will be removed in a future version. func (a *Account) deleteStreamTemplate(name string) error { t, err := a.lookupStreamTemplate(name) if err != nil { @@ -2938,6 +3004,7 @@ func (a *Account) deleteStreamTemplate(name string) error { return t.delete() } +// Deprecated: stream templates are deprecated and will be removed in a future version. func (a *Account) templates() []*streamTemplate { var ts []*streamTemplate _, jsa, err := a.checkForJetStream() @@ -2956,6 +3023,7 @@ func (a *Account) templates() []*streamTemplate { } // Will add a stream to a template, this is for recovery. +// Deprecated: stream templates are deprecated and will be removed in a future version. func (jsa *jsAccount) addStreamNameToTemplate(tname, mname string) error { if jsa.templates == nil { return fmt.Errorf("template not found") @@ -2973,6 +3041,7 @@ func (jsa *jsAccount) addStreamNameToTemplate(tname, mname string) error { // This will check if a template owns this stream. // jsAccount lock should be held +// Deprecated: stream templates are deprecated and will be removed in a future version. func (jsa *jsAccount) checkTemplateOwnership(tname, sname string) bool { if jsa.templates == nil { return false @@ -3022,12 +3091,12 @@ func canonicalName(name string) string { } // To throttle the out of resources errors. -func (s *Server) resourcesExceededError() { +func (s *Server) resourcesExceededError(storeType StorageType) { var didAlert bool s.rerrMu.Lock() if now := time.Now(); now.Sub(s.rerrLast) > 10*time.Second { - s.Errorf("JetStream resource limits exceeded for server") + s.Errorf("JetStream %s resource limits exceeded for server", strings.ToLower(storeType.String())) s.rerrLast = now didAlert = true } 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 66145181d..7e21bc1c4 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 @@ -49,20 +49,24 @@ const ( // JSApiTemplateCreate is the endpoint to create new stream templates. // Will return JSON response. + // Deprecated: stream templates are deprecated and will be removed in a future version. JSApiTemplateCreate = "$JS.API.STREAM.TEMPLATE.CREATE.*" JSApiTemplateCreateT = "$JS.API.STREAM.TEMPLATE.CREATE.%s" // JSApiTemplates is the endpoint to list all stream template names for this account. // Will return JSON response. + // Deprecated: stream templates are deprecated and will be removed in a future version. JSApiTemplates = "$JS.API.STREAM.TEMPLATE.NAMES" // JSApiTemplateInfo is for obtaining general information about a named stream template. // Will return JSON response. + // Deprecated: stream templates are deprecated and will be removed in a future version. JSApiTemplateInfo = "$JS.API.STREAM.TEMPLATE.INFO.*" JSApiTemplateInfoT = "$JS.API.STREAM.TEMPLATE.INFO.%s" // JSApiTemplateDelete is the endpoint to delete stream templates. // Will return JSON response. + // Deprecated: stream templates are deprecated and will be removed in a future version. JSApiTemplateDelete = "$JS.API.STREAM.TEMPLATE.DELETE.*" JSApiTemplateDeleteT = "$JS.API.STREAM.TEMPLATE.DELETE.%s" @@ -305,6 +309,9 @@ const ( // JSAdvisoryStreamQuorumLostPre notification that a stream and its consumers are stalled. JSAdvisoryStreamQuorumLostPre = "$JS.EVENT.ADVISORY.STREAM.QUORUM_LOST" + // JSAdvisoryStreamBatchAbandonedPre notification that a stream's batch was abandoned. + JSAdvisoryStreamBatchAbandonedPre = "$JS.EVENT.ADVISORY.STREAM.BATCH_ABANDONED" + // JSAdvisoryConsumerLeaderElectedPre notification that a replicated consumer has elected a leader. JSAdvisoryConsumerLeaderElectedPre = "$JS.EVENT.ADVISORY.CONSUMER.LEADER_ELECTED" @@ -325,6 +332,12 @@ const ( JSAuditAdvisory = "$JS.EVENT.ADVISORY.API" ) +// Headers used in $JS.API.> requests. +const ( + // JSRequiredApiLevel requires the API level of the responding server to have the specified minimum value. + JSRequiredApiLevel = "Nats-Required-Api-Level" +) + var denyAllClientJs = []string{jsAllAPI, "$KV.>", "$OBJ.>"} var denyAllJs = []string{jscAllSubj, raftAllSubj, jsAllAPI, "$KV.>", "$OBJ.>"} @@ -672,7 +685,7 @@ type JSApiMsgGetRequest struct { LastFor string `json:"last_by_subj,omitempty"` NextFor string `json:"next_by_subj,omitempty"` - // Batch support. Used to request more then one msg at a time. + // Batch support. Used to request more than one msg at a time. // Can be used with simple starting seq, but also NextFor with wildcards. Batch int `json:"batch,omitempty"` // This will make sure we limit how much data we blast out. If not set we will @@ -687,6 +700,8 @@ type JSApiMsgGetRequest struct { UpToSeq uint64 `json:"up_to_seq,omitempty"` // Only return messages up to this time. UpToTime *time.Time `json:"up_to_time,omitempty"` + // Only return the message payload, excluding headers if present. + NoHeaders bool `json:"no_hdr,omitempty"` } type JSApiMsgGetResponse struct { @@ -766,44 +781,52 @@ type JSApiConsumerGetNextRequest struct { } // JSApiStreamTemplateCreateResponse for creating templates. +// Deprecated: stream templates are deprecated and will be removed in a future version. type JSApiStreamTemplateCreateResponse struct { ApiResponse *StreamTemplateInfo } +// Deprecated: stream templates are deprecated and will be removed in a future version. const JSApiStreamTemplateCreateResponseType = "io.nats.jetstream.api.v1.stream_template_create_response" +// Deprecated: stream templates are deprecated and will be removed in a future version. type JSApiStreamTemplateDeleteResponse struct { ApiResponse Success bool `json:"success,omitempty"` } +// Deprecated: stream templates are deprecated and will be removed in a future version. const JSApiStreamTemplateDeleteResponseType = "io.nats.jetstream.api.v1.stream_template_delete_response" // JSApiStreamTemplateInfoResponse for information about stream templates. +// Deprecated: stream templates are deprecated and will be removed in a future version. type JSApiStreamTemplateInfoResponse struct { ApiResponse *StreamTemplateInfo } +// Deprecated: stream templates are deprecated and will be removed in a future version. const JSApiStreamTemplateInfoResponseType = "io.nats.jetstream.api.v1.stream_template_info_response" +// Deprecated: stream templates are deprecated and will be removed in a future version. type JSApiStreamTemplatesRequest struct { ApiPagedRequest } // JSApiStreamTemplateNamesResponse list of templates +// Deprecated: stream templates are deprecated and will be removed in a future version. type JSApiStreamTemplateNamesResponse struct { ApiResponse ApiPaged Templates []string `json:"streams"` } +// Deprecated: stream templates are deprecated and will be removed in a future version. const JSApiStreamTemplateNamesResponseType = "io.nats.jetstream.api.v1.stream_template_names_response" // Structure that holds state for a JetStream API request that is processed -// in a separate long-lived go routine. This is to avoid possibly blocking -// ROUTE and GATEWAY connections. +// in a separate long-lived go routine. This is to avoid blocking connections. type jsAPIRoutedReq struct { jsub *subscription sub *subscription @@ -872,17 +895,6 @@ func (js *jetStream) apiDispatch(sub *subscription, c *client, acc *Account, sub } jsub := rr.psubs[0] - // If this is directly from a client connection ok to do in place. - if c.kind != ROUTER && c.kind != GATEWAY && c.kind != LEAF { - start := time.Now() - jsub.icb(sub, c, acc, subject, reply, rmsg) - if dur := time.Since(start); dur >= readLoopReportThreshold { - s.Warnf("Internal subscription on %q took too long: %v", subject, dur) - } - return - } - - // If we are here we have received this request over a non-client connection. // We need to make sure not to block. We will send the request to a long-lived // pool of go routines. @@ -1277,13 +1289,18 @@ func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, _ *Account, return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiAccountInfoResponse{ApiResponse: ApiResponse{Type: JSApiAccountInfoResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -1333,17 +1350,23 @@ func consumerNameFromSubject(subject string) string { } // Request to create a new template. +// Deprecated: stream templates are deprecated and will be removed in a future version. func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { if c == nil { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamTemplateCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateCreateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if !acc.JetStreamEnabled() { resp.Error = NewJSNotEnabledForAccountError() s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) @@ -1388,17 +1411,23 @@ func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, _ *Accoun } // Request for the list of all template names. +// Deprecated: stream templates are deprecated and will be removed in a future version. func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { if c == nil { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamTemplateNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateNamesResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if !acc.JetStreamEnabled() { resp.Error = NewJSNotEnabledForAccountError() s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) @@ -1452,17 +1481,23 @@ func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, _ *Account } // Request for information about a stream template. +// Deprecated: stream templates are deprecated and will be removed in a future version. func (s *Server) jsTemplateInfoRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { if c == nil { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamTemplateInfoResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateInfoResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if !acc.JetStreamEnabled() { resp.Error = NewJSNotEnabledForAccountError() s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) @@ -1493,17 +1528,23 @@ func (s *Server) jsTemplateInfoRequest(sub *subscription, c *client, _ *Account, } // Request to delete a stream template. +// Deprecated: stream templates are deprecated and will be removed in a future version. func (s *Server) jsTemplateDeleteRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) { if c == nil { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamTemplateDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateDeleteResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if !acc.JetStreamEnabled() { resp.Error = NewJSNotEnabledForAccountError() s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) @@ -1564,13 +1605,18 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, _ *Account, if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -1676,13 +1722,18 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account, return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -1731,8 +1782,7 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account, // Handle clustered version here. if s.JetStreamIsClustered() { - // Always do in separate Go routine. - go s.jsClusteredStreamUpdateRequest(ci, acc, subject, reply, copyBytes(rmsg), &cfg, nil, ncfg.Pedantic) + s.jsClusteredStreamUpdateRequest(ci, acc, subject, reply, copyBytes(rmsg), &cfg, nil, ncfg.Pedantic) return } @@ -1775,13 +1825,18 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, _ *Account, if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamNamesResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -1902,7 +1957,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -1912,6 +1967,11 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s ApiResponse: ApiResponse{Type: JSApiStreamListResponseType}, Streams: []*StreamInfo{}, } + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -2030,6 +2090,11 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, a *Account, s if rt := getHeader(JSResponseType, hdr); len(rt) > 0 && string(rt) == jsCreateResponse { resp.ApiResponse.Type = JSApiStreamCreateResponseType } + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } var clusterWideConsCount int @@ -2234,7 +2299,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, _ * if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -2244,6 +2309,11 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, _ * name := tokenAt(subject, 6) var resp = JSApiStreamLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiStreamLeaderStepDownResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // If we are not in clustered mode this is a failed request. if !s.JetStreamIsClustered() { @@ -2344,13 +2414,18 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, _ if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiConsumerLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiConsumerLeaderStepDownResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // If we are not in clustered mode this is a failed request. if !s.JetStreamIsClustered() { @@ -2462,7 +2537,7 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, _ *Acco if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -2472,6 +2547,11 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, _ *Acco name := tokenAt(subject, 6) var resp = JSApiStreamRemovePeerResponse{ApiResponse: ApiResponse{Type: JSApiStreamRemovePeerResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // If we are not in clustered mode this is a failed request. if !s.JetStreamIsClustered() { @@ -2565,7 +2645,7 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, _ *Ac return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -2589,6 +2669,11 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, _ *Ac } var resp = JSApiMetaServerRemoveResponse{ApiResponse: ApiResponse{Type: JSApiMetaServerRemoveResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if isEmptyRequest(msg) { resp.Error = NewJSBadRequestError() @@ -2674,7 +2759,7 @@ func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _ return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -2702,6 +2787,11 @@ func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _ } var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } var req JSApiMetaServerStreamMoveRequest if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { @@ -2833,7 +2923,7 @@ func (s *Server) jsLeaderServerStreamCancelMoveRequest(sub *subscription, c *cli return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -2854,6 +2944,11 @@ func (s *Server) jsLeaderServerStreamCancelMoveRequest(sub *subscription, c *cli } var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } accName := tokenAt(subject, 6) streamName := tokenAt(subject, 7) @@ -2943,7 +3038,7 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -2960,6 +3055,11 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac accName := tokenAt(subject, 5) var resp = JSApiAccountPurgeResponse{ApiResponse: ApiResponse{Type: JSApiAccountPurgeResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if !s.JetStreamIsClustered() { var streams []*stream @@ -3030,7 +3130,7 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Accoun return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -3058,6 +3158,11 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Accoun var preferredLeader string var resp = JSApiLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiLeaderStepDownResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if isJSONObjectOrArray(msg) { var req JSApiLeaderStepdownRequest @@ -3203,13 +3308,18 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, _ *Account, if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamDeleteResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -3271,7 +3381,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -3280,6 +3390,11 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su stream := tokenAt(subject, 6) var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // If we are in clustered mode we need to be the stream leader to proceed. if s.JetStreamIsClustered() { @@ -3390,7 +3505,7 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -3399,6 +3514,11 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje stream := tokenAt(subject, 6) var resp = JSApiMsgGetResponse{ApiResponse: ApiResponse{Type: JSApiMsgGetResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // If we are in clustered mode we need to be the stream leader to proceed. if s.JetStreamIsClustered() { @@ -3499,6 +3619,10 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje var svp StoreMsg var sm *StoreMsg + // Ensure this read request is isolated and doesn't interleave with writes. + mset.mu.RLock() + defer mset.mu.RUnlock() + // If AsOfTime is set, perform this first to get the sequence. var seq uint64 if req.StartTime != nil { @@ -3522,10 +3646,12 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje resp.Message = &StoredMsg{ Subject: sm.subj, Sequence: sm.seq, - Header: sm.hdr, Data: sm.msg, Time: time.Unix(0, sm.ts).UTC(), } + if !req.NoHeaders { + resp.Message.Header = sm.hdr + } // Don't send response through API layer for this call. s.sendInternalAccountMsg(nil, reply, s.jsonResponse(resp)) @@ -3536,7 +3662,7 @@ func (s *Server) jsConsumerUnpinRequest(sub *subscription, c *client, _ *Account return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -3547,6 +3673,11 @@ func (s *Server) jsConsumerUnpinRequest(sub *subscription, c *client, _ *Account var req JSApiConsumerUnpinRequest var resp = JSApiConsumerUnpinResponse{ApiResponse: ApiResponse{Type: JSApiConsumerUnpinResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if err := json.Unmarshal(msg, &req); err != nil { resp.Error = NewJSInvalidJSONError(err) @@ -3670,7 +3801,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account, if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -3679,6 +3810,11 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account, stream := streamNameFromSubject(subject) var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // If we are in clustered mode we need to be the stream leader to proceed. if s.JetStreamIsClustered() { @@ -3812,13 +3948,18 @@ func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, _ *Account if c == nil || !s.JetStreamIsLeader() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if !acc.JetStreamEnabled() { resp.Error = NewJSNotEnabledForAccountError() s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) @@ -3964,7 +4105,7 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC // TODO(dlc) - We could check apriori and cancel initial request if we know it won't fit. total += len(msg) if js.wouldExceedLimits(FileStorage, total) { - s.resourcesExceededError() + s.resourcesExceededError(FileStorage) resultCh <- result{NewJSInsufficientResourcesError(), reply} return } @@ -4092,7 +4233,7 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -4107,6 +4248,11 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun } var resp = JSApiStreamSnapshotResponse{ApiResponse: ApiResponse{Type: JSApiStreamSnapshotResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if !acc.JetStreamEnabled() { resp.Error = NewJSNotEnabledForAccountError() s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) @@ -4328,13 +4474,18 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } var req CreateConsumerRequest if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { @@ -4479,14 +4630,7 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun } if isClustered && !req.Config.Direct { - // If we are inline with client, we still may need to do a callout for consumer info - // during this call, so place in Go routine to not block client. - // Router and Gateway API calls already in separate context. - if c.kind != ROUTER && c.kind != GATEWAY { - go s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action, req.Pedantic) - } else { - s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action, req.Pedantic) - } + s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action, req.Pedantic) return } @@ -4552,7 +4696,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, _ *Account if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -4562,6 +4706,11 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, _ *Account ApiResponse: ApiResponse{Type: JSApiConsumerNamesResponseType}, Consumers: []string{}, } + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -4674,7 +4823,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account, return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -4684,6 +4833,11 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account, ApiResponse: ApiResponse{Type: JSApiConsumerListResponseType}, Consumers: []*ConsumerInfo{}, } + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -4702,6 +4856,12 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account, } } + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSClusterNotAvailError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } + if hasJS, doErr := acc.checkJetStream(); !hasJS { if doErr { resp.Error = NewJSNotEnabledForAccountError() @@ -4777,7 +4937,7 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, _ *Account, if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -4787,6 +4947,11 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, _ *Account, consumerName := consumerNameFromSubject(subject) var resp = JSApiConsumerInfoResponse{ApiResponse: ApiResponse{Type: JSApiConsumerInfoResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if !isEmptyRequest(msg) { resp.Error = NewJSNotEmptyRequestError() @@ -4973,13 +5138,18 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, _ *Accoun if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return } var resp = JSApiConsumerDeleteResponse{ApiResponse: ApiResponse{Type: JSApiConsumerDeleteResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } // Determine if we should proceed here when we are in clustered mode. if s.JetStreamIsClustered() { @@ -5045,7 +5215,7 @@ func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account if c == nil || !s.JetStreamEnabled() { return } - ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) + ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg) if err != nil { s.Warnf(badAPIRequestT, msg) return @@ -5053,6 +5223,11 @@ func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account var req JSApiConsumerPauseRequest var resp = JSApiConsumerPauseResponse{ApiResponse: ApiResponse{Type: JSApiConsumerPauseResponseType}} + if errorOnRequiredApiLevel(hdr) { + resp.Error = NewJSRequiredApiLevelError() + s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) + return + } if isJSONObjectOrArray(msg) { if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { 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 new file mode 100644 index 000000000..16daf3064 --- /dev/null +++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go @@ -0,0 +1,653 @@ +// Copyright 2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "math/big" + "path/filepath" + "slices" + "strings" + "sync" + "sync/atomic" + "time" +) + +var ( + // Tracks the total inflight batches, across all streams and accounts that enable batching. + globalInflightBatches atomic.Int32 +) + +type batching struct { + mu sync.Mutex + group map[string]*batchGroup +} + +type batchGroup struct { + lseq uint64 + store StreamStore + timer *time.Timer +} + +// Lock should be held. +func (batches *batching) newBatchGroup(mset *stream, batchId string) (*batchGroup, error) { + store, err := newBatchStore(mset, batchId) + if err != nil { + return nil, err + } + b := &batchGroup{store: store} + + // Create a timer to clean up after timeout. + timeout := streamMaxBatchTimeout + if maxBatchTimeout := mset.srv.getOpts().JetStreamLimits.MaxBatchTimeout; maxBatchTimeout > 0 { + timeout = maxBatchTimeout + } + b.timer = time.AfterFunc(timeout, func() { + b.cleanup(batchId, batches) + mset.sendStreamBatchAbandonedAdvisory(batchId, BatchTimeout) + }) + return b, nil +} + +func getBatchStoreDir(mset *stream, batchId string) (string, string) { + mset.mu.RLock() + jsa, name := mset.jsa, mset.cfg.Name + mset.mu.RUnlock() + + jsa.mu.RLock() + sd := jsa.storeDir + jsa.mu.RUnlock() + + bname := getHash(batchId) + return bname, filepath.Join(sd, streamsDir, name, batchesDir, bname) +} + +func newBatchStore(mset *stream, batchId string) (StreamStore, error) { + mset.mu.RLock() + replicas, storage := mset.cfg.Replicas, mset.cfg.Storage + mset.mu.RUnlock() + + if replicas == 1 && storage == FileStorage { + bname, storeDir := getBatchStoreDir(mset, batchId) + fcfg := FileStoreConfig{AsyncFlush: true, BlockSize: defaultLargeBlockSize, StoreDir: storeDir} + s := mset.srv + prf := s.jsKeyGen(s.getOpts().JetStreamKey, mset.acc.Name) + if prf != nil { + // We are encrypted here, fill in correct cipher selection. + fcfg.Cipher = s.getOpts().JetStreamCipher + } + oldprf := s.jsKeyGen(s.getOpts().JetStreamOldKey, mset.acc.Name) + cfg := StreamConfig{Name: bname, Storage: FileStorage} + return newFileStoreWithCreated(fcfg, cfg, time.Time{}, prf, oldprf) + } + return newMemStore(&StreamConfig{Name: _EMPTY_, Storage: MemoryStorage}) +} + +// readyForCommit indicates the batch is ready to be committed. +// If the timer has already cleaned up the batch, we can't commit. +// Otherwise, we ensure the timer does not clean up the batch in the meantime. +// Lock should be held. +func (b *batchGroup) readyForCommit() bool { + if !b.timer.Stop() { + return false + } + b.store.FlushAllPending() + return true +} + +// cleanup deletes underlying resources associated with the batch and unregisters it from the stream's batches. +func (b *batchGroup) cleanup(batchId string, batches *batching) { + batches.mu.Lock() + defer batches.mu.Unlock() + b.cleanupLocked(batchId, batches) +} + +// Lock should be held. +func (b *batchGroup) cleanupLocked(batchId string, batches *batching) { + globalInflightBatches.Add(-1) + b.timer.Stop() + b.store.Delete(true) + delete(batches.group, batchId) +} + +// Lock should be held. +func (b *batchGroup) stopLocked() { + globalInflightBatches.Add(-1) + b.timer.Stop() + b.store.Stop() +} + +// batchStagedDiff stages all changes for consistency checks until commit. +type batchStagedDiff struct { + msgIds map[string]struct{} + counter map[string]*msgCounterRunningTotal + inflight map[string]*inflightSubjectRunningTotal + expectedPerSubject map[string]*batchExpectedPerSubject +} + +type batchExpectedPerSubject struct { + sseq uint64 // Stream sequence. + clseq uint64 // Clustered proposal sequence. +} + +func (diff *batchStagedDiff) commit(mset *stream) { + if len(diff.msgIds) > 0 { + ts := time.Now().UnixNano() + mset.ddMu.Lock() + for msgId := range diff.msgIds { + // We stage with zero, and will update in processJetStreamMsg once we know the sequence. + mset.storeMsgIdLocked(&ddentry{msgId, 0, ts}) + } + mset.ddMu.Unlock() + } + + // Store running totals for counters, we could have multiple counter increments proposed, but not applied yet. + if len(diff.counter) > 0 { + if mset.clusteredCounterTotal == nil { + mset.clusteredCounterTotal = make(map[string]*msgCounterRunningTotal, len(diff.counter)) + } + for k, c := range diff.counter { + mset.clusteredCounterTotal[k] = c + } + } + + // Track inflight. + if len(diff.inflight) > 0 { + if mset.inflight == nil { + mset.inflight = make(map[string]*inflightSubjectRunningTotal, len(diff.inflight)) + } + for subj, i := range diff.inflight { + if c, ok := mset.inflight[subj]; ok { + c.bytes += i.bytes + c.ops += i.ops + } else { + mset.inflight[subj] = i + } + } + } + + // Track sequence and subject. + if len(diff.expectedPerSubject) > 0 { + if mset.expectedPerSubjectSequence == nil { + mset.expectedPerSubjectSequence = make(map[uint64]string, len(diff.expectedPerSubject)) + } + if mset.expectedPerSubjectInProcess == nil { + mset.expectedPerSubjectInProcess = make(map[string]struct{}, len(diff.expectedPerSubject)) + } + for subj, e := range diff.expectedPerSubject { + mset.expectedPerSubjectSequence[e.clseq] = subj + mset.expectedPerSubjectInProcess[subj] = struct{}{} + } + } +} + +type batchApply struct { + mu sync.Mutex + id string // ID of the current batch. + count uint64 // Number of entries in the batch, for consistency checks. + entries []*CommittedEntry // Previous entries that are part of this batch. + entryStart int // The index into an entry indicating the first message of the batch. + maxApplied uint64 // Applied value before the entry containing the first message of the batch. +} + +// clearBatchStateLocked clears in-memory apply-batch-related state. +// batch.mu lock should be held. +func (batch *batchApply) clearBatchStateLocked() { + batch.id = _EMPTY_ + batch.count = 0 + batch.entries = nil + batch.entryStart = 0 + batch.maxApplied = 0 +} + +// rejectBatchStateLocked rejects the batch and clears in-memory apply-batch-related state. +// Corrects mset.clfs to take the failed batch into account. +// batch.mu lock should be held. +func (batch *batchApply) rejectBatchStateLocked(mset *stream) { + mset.clMu.Lock() + mset.clfs += batch.count + mset.clMu.Unlock() + // We're rejecting the batch, so all entries need to be returned to the pool. + for _, bce := range batch.entries { + bce.ReturnToPool() + } + batch.clearBatchStateLocked() +} + +func (batch *batchApply) rejectBatchState(mset *stream) { + batch.mu.Lock() + defer batch.mu.Unlock() + batch.rejectBatchStateLocked(mset) +} + +// checkMsgHeadersPreClusteredProposal checks the message for expected/consistency headers. +// mset.mu lock must NOT be held or used. +// mset.clMu lock must be held. +func checkMsgHeadersPreClusteredProposal( + diff *batchStagedDiff, mset *stream, subject string, hdr []byte, msg []byte, sourced bool, name string, + jsa *jsAccount, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules bool, + discard DiscardPolicy, discardNewPer bool, maxMsgSize int, maxMsgs int64, maxMsgsPer int64, maxBytes int64, +) ([]byte, []byte, uint64, *ApiError, error) { + var incr *big.Int + + // Some header checks must be checked pre proposal. + if len(hdr) > 0 { + // Since we encode header len as u16 make sure we do not exceed. + // Again this works if it goes through but better to be pre-emptive. + if len(hdr) > math.MaxUint16 { + err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name) + return hdr, msg, 0, NewJSStreamHeaderExceedsMaximumError(), err + } + // Counter increments. + // Only supported on counter streams, and payload must be empty (if not coming from a source). + var ok bool + if incr, ok = getMessageIncr(hdr); !ok { + apiErr := NewJSMessageIncrInvalidError() + return hdr, msg, 0, apiErr, apiErr + } else if incr != nil && !sourced { + // Only do checks if the message isn't sourced. Otherwise, we need to store verbatim. + if !allowMsgCounter { + apiErr := NewJSMessageIncrDisabledError() + return hdr, msg, 0, apiErr, apiErr + } else if len(msg) > 0 { + apiErr := NewJSMessageIncrPayloadError() + return hdr, msg, 0, apiErr, apiErr + } else { + // Check for incompatible headers. + var doErr bool + if getRollup(hdr) != _EMPTY_ || + getExpectedStream(hdr) != _EMPTY_ || + getExpectedLastMsgId(hdr) != _EMPTY_ || + getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ { + doErr = true + } else if _, ok = getExpectedLastSeq(hdr); ok { + doErr = true + } else if _, ok = getExpectedLastSeqPerSubject(hdr); ok { + doErr = true + } + + if doErr { + apiErr := NewJSMessageIncrInvalidError() + return hdr, msg, 0, apiErr, apiErr + } + } + } + // Expected stream name can also be pre-checked. + if sname := getExpectedStream(hdr); sname != _EMPTY_ && sname != name { + return hdr, msg, 0, NewJSStreamNotMatchError(), errStreamMismatch + } + // TTL'd messages are rejected entirely if TTLs are not enabled on the stream, or if the TTL is invalid. + if ttl, err := getMessageTTL(hdr); !sourced && (ttl != 0 || err != nil) { + if !allowTTL { + return hdr, msg, 0, NewJSMessageTTLDisabledError(), errMsgTTLDisabled + } else if err != nil { + return hdr, msg, 0, NewJSMessageTTLInvalidError(), err + } + } + // Check for MsgIds here at the cluster level to avoid excessive CLFS accounting. + // Will help during restarts. + if msgId := getMsgId(hdr); msgId != _EMPTY_ { + // Dedupe if staged. + if _, ok = diff.msgIds[msgId]; ok { + return hdr, msg, 0, nil, errMsgIdDuplicate + } + mset.ddMu.Lock() + if dde := mset.checkMsgId(msgId); dde != nil { + seq := dde.seq + mset.ddMu.Unlock() + // Should not return an invalid sequence, in that case error. + if seq > 0 { + return hdr, msg, seq, nil, errMsgIdDuplicate + } else { + return hdr, msg, 0, NewJSStreamDuplicateMessageConflictError(), errMsgIdDuplicate + } + } + if diff.msgIds == nil { + diff.msgIds = map[string]struct{}{msgId: {}} + } else { + diff.msgIds[msgId] = struct{}{} + } + mset.ddMu.Unlock() + } + } + + // Apply increment for counter. + // But only if it's allowed for this stream. This can happen when we store verbatim for a sourced stream. + if incr == nil && allowMsgCounter { + apiErr := NewJSMessageIncrMissingError() + return hdr, msg, 0, apiErr, apiErr + } + if incr != nil && allowMsgCounter { + var initial big.Int + var sources CounterSources + + // If we've got a running total, update that, since we have inflight proposals updating the same counter. + var ok bool + var counter *msgCounterRunningTotal + if counter, ok = diff.counter[subject]; ok { + initial = *counter.total + sources = counter.sources + } else if counter, ok = mset.clusteredCounterTotal[subject]; ok { + initial = *counter.total + sources = counter.sources + // Make an explicit copy to separate the staged data from what's committed. + // Don't need to initialize all values, they'll be overwritten later. + counter = &msgCounterRunningTotal{ops: counter.ops} + } else { + // Load last message, and store as inflight running total. + var smv StoreMsg + sm, err := mset.store.LoadLastMsg(subject, &smv) + if err == nil && sm != nil { + var val CounterValue + // Return an error if the counter is broken somehow. + if json.Unmarshal(sm.msg, &val) != nil { + apiErr := NewJSMessageCounterBrokenError() + return hdr, msg, 0, apiErr, apiErr + } + if ncs := sliceHeader(JSMessageCounterSources, sm.hdr); len(ncs) > 0 { + if err := json.Unmarshal(ncs, &sources); err != nil { + apiErr := NewJSMessageCounterBrokenError() + return hdr, msg, 0, apiErr, apiErr + } + } + initial.SetString(val.Value, 10) + } + } + srchdr := sliceHeader(JSStreamSource, hdr) + if len(srchdr) > 0 { + // This is a sourced message, so we can't apply Nats-Incr but + // instead should just update the source count header. + fields := strings.Split(string(srchdr), " ") + origStream := fields[0] + origSubj := subject + if len(fields) >= 5 { + origSubj = fields[4] + } + var val CounterValue + if json.Unmarshal(msg, &val) != nil { + apiErr := NewJSMessageCounterBrokenError() + return hdr, msg, 0, apiErr, apiErr + } + var sourced big.Int + sourced.SetString(val.Value, 10) + if sources == nil { + sources = map[string]map[string]string{} + } + if _, ok = sources[origStream]; !ok { + sources[origStream] = map[string]string{} + } + prevVal := sources[origStream][origSubj] + sources[origStream][origSubj] = sourced.String() + // We will also replace the Nats-Incr header with the diff + // between our last value from this source and this one, so + // that the arithmetic is always correct. + var previous big.Int + previous.SetString(prevVal, 10) + incr.Sub(&sourced, &previous) + hdr = setHeader(JSMessageIncr, incr.String(), hdr) + } + // Now make the change. + initial.Add(&initial, incr) + // Generate the new payload. + var _msg [128]byte + msg = fmt.Appendf(_msg[:0], "{%q:%q}", "val", initial.String()) + // Write the updated source count headers. + if len(sources) > 0 { + nhdr, err := json.Marshal(sources) + if err != nil { + return hdr, msg, 0, NewJSMessageCounterBrokenError(), err + } + hdr = setHeader(JSMessageCounterSources, string(nhdr), hdr) + } + + // Check to see if we are over the max msg size. + maxSize := int64(mset.srv.getOpts().MaxPayload) + if maxMsgSize >= 0 && int64(maxMsgSize) < maxSize { + maxSize = int64(maxMsgSize) + } + hdrLen, msgLen := int64(len(hdr)), int64(len(msg)) + // Subtract to prevent against overflows. + if hdrLen > maxSize || msgLen > maxSize-hdrLen { + return hdr, msg, 0, NewJSStreamMessageExceedsMaximumError(), ErrMaxPayload + } + + // Keep the in-memory counters up-to-date. + if counter == nil { + counter = &msgCounterRunningTotal{} + } + counter.total = &initial + counter.sources = sources + counter.ops++ + if diff.counter == nil { + diff.counter = map[string]*msgCounterRunningTotal{subject: counter} + } else { + diff.counter[subject] = counter + } + } + + if len(hdr) > 0 { + // Expected last sequence. + if seq, exists := getExpectedLastSeq(hdr); exists && seq != mset.clseq-mset.clfs { + mlseq := mset.clseq - mset.clfs + err := fmt.Errorf("last sequence mismatch: %d vs %d", seq, mlseq) + return hdr, msg, 0, NewJSStreamWrongLastSequenceError(mlseq), err + } else if exists && len(diff.inflight) > 0 { + // Only the first message in a batch can contain an expected last sequence. + err := fmt.Errorf("last sequence mismatch") + return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err + } + + // Expected last sequence per subject. + if seq, exists := getExpectedLastSeqPerSubject(hdr); exists { + // Allow override of the subject used for the check. + seqSubj := subject + if optSubj := getExpectedLastSeqPerSubjectForSubject(hdr); optSubj != _EMPTY_ { + seqSubj = optSubj + } + + // The subject is already written to in this batch, we can't allow + // expected checks since they would be incorrect. + if _, ok := diff.inflight[seqSubj]; ok { + err := errors.New("last sequence by subject mismatch") + return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err + } + + // If the subject is already in process, block as otherwise we could have + // multiple messages inflight with the same subject. + if _, found := mset.expectedPerSubjectInProcess[seqSubj]; found { + err := errors.New("last sequence by subject mismatch") + return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err + } + + // If the subject is already in process but without expected headers, block as we would have + // multiple messages inflight with the same subject. + if _, ok := mset.inflight[seqSubj]; ok { + err := errors.New("last sequence by subject mismatch") + return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err + } + + // If we've already done an expected-check on this subject, use the cached result. + if e, ok := diff.expectedPerSubject[seqSubj]; ok { + if e.sseq != seq { + err := fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, e.sseq) + return hdr, msg, 0, NewJSStreamWrongLastSequenceError(e.sseq), err + } + e.clseq = mset.clseq + } else { + var smv StoreMsg + var fseq uint64 + sm, err := mset.store.LoadLastMsg(seqSubj, &smv) + if sm != nil { + fseq = sm.seq + } + if err == ErrStoreMsgNotFound && seq == 0 { + fseq, err = 0, nil + } + if err != nil || fseq != seq { + err = fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, fseq) + return hdr, msg, 0, NewJSStreamWrongLastSequenceError(fseq), err + } + + e = &batchExpectedPerSubject{sseq: fseq, clseq: mset.clseq} + if diff.expectedPerSubject == nil { + diff.expectedPerSubject = map[string]*batchExpectedPerSubject{seqSubj: e} + } else { + diff.expectedPerSubject[seqSubj] = e + } + } + } else if getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ { + apiErr := NewJSStreamExpectedLastSeqPerSubjectInvalidError() + return hdr, msg, 0, apiErr, apiErr + } + + // Message scheduling. + if schedule, ok := getMessageSchedule(hdr); !ok { + apiErr := NewJSMessageSchedulesPatternInvalidError() + if !allowMsgSchedules { + apiErr = NewJSMessageSchedulesDisabledError() + } + return hdr, msg, 0, apiErr, apiErr + } else if !schedule.IsZero() { + if !allowMsgSchedules { + apiErr := NewJSMessageSchedulesDisabledError() + return hdr, msg, 0, apiErr, apiErr + } else if scheduleTtl, ok := getMessageScheduleTTL(hdr); !ok { + apiErr := NewJSMessageSchedulesTTLInvalidError() + return hdr, msg, 0, apiErr, apiErr + } else if scheduleTtl != _EMPTY_ && !allowTTL { + return hdr, msg, 0, NewJSMessageTTLDisabledError(), errMsgTTLDisabled + } else if scheduleTarget := getMessageScheduleTarget(hdr); scheduleTarget == _EMPTY_ || + !IsValidPublishSubject(scheduleTarget) || SubjectsCollide(scheduleTarget, subject) { + apiErr := NewJSMessageSchedulesTargetInvalidError() + return hdr, msg, 0, apiErr, apiErr + } else { + mset.cfgMu.RLock() + match := slices.ContainsFunc(mset.cfg.Subjects, func(subj string) bool { + return SubjectsCollide(subj, scheduleTarget) + }) + mset.cfgMu.RUnlock() + if !match { + apiErr := NewJSMessageSchedulesTargetInvalidError() + return hdr, msg, 0, apiErr, apiErr + } + + // Add a rollup sub header if it doesn't already exist. + // Otherwise, it must exist already as a rollup on the subject. + if rollup := getRollup(hdr); rollup == _EMPTY_ { + hdr = genHeader(hdr, JSMsgRollup, JSMsgRollupSubject) + } else if rollup != JSMsgRollupSubject { + apiErr := NewJSMessageSchedulesRollupInvalidError() + return hdr, msg, 0, apiErr, apiErr + } + } + } + + // Check for any rollups. + if rollup := getRollup(hdr); rollup != _EMPTY_ { + if !allowRollup || denyPurge { + err := errors.New("rollup not permitted") + return hdr, msg, 0, NewJSStreamRollupFailedError(err), err + } + switch rollup { + case JSMsgRollupSubject: + // Rolling up the subject is only allowed if the first occurrence of this subject in the batch. + if _, ok := diff.inflight[subject]; ok { + err := errors.New("batch rollup sub invalid") + return hdr, msg, 0, NewJSStreamRollupFailedError(err), err + } + case JSMsgRollupAll: + // Rolling up the whole stream is only allowed if this is the first message of the batch. + if len(diff.inflight) > 0 { + err := errors.New("batch rollup all invalid") + return hdr, msg, 0, NewJSStreamRollupFailedError(err), err + } + default: + err := fmt.Errorf("rollup value invalid: %q", rollup) + return hdr, msg, 0, NewJSStreamRollupFailedError(err), err + } + } + } + + // Track inflight. + // Store the subject to ensure other messages in this batch using + // an expected check or rollup on the same subject fail. + if diff.inflight == nil { + diff.inflight = make(map[string]*inflightSubjectRunningTotal, 1) + } + var sz uint64 + if mset.store.Type() == FileStorage { + sz = fileStoreMsgSizeRaw(len(subject), len(hdr), len(msg)) + } else { + sz = memStoreMsgSizeRaw(len(subject), len(hdr), len(msg)) + } + var ( + i *inflightSubjectRunningTotal + ok bool + err error + ) + if i, ok = diff.inflight[subject]; ok { + i.bytes += sz + i.ops++ + } else { + i = &inflightSubjectRunningTotal{bytes: sz, ops: 1} + diff.inflight[subject] = i + } + + // Check if we have discard new with max msgs or bytes. + // We need to deny here otherwise we'd need to bump CLFS, and it could succeed on some + // peers and not others depending on consumer ack state (if interest policy). + // So we deny here, if we allow that means we know it would succeed on every peer. + if discard == DiscardNew && (maxMsgs > 0 || maxBytes > 0) { + // Error if over DiscardNew per subject threshold. + if discardNewPer { + totalMsgsForSubject := i.ops + if i, ok = mset.inflight[subject]; ok { + totalMsgsForSubject += i.ops + } + if maxMsgsPer > 0 && totalMsgsForSubject > uint64(maxMsgsPer) { + err = ErrMaxMsgsPerSubject + return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err + } + } + + // Track usual max msgs/bytes thresholds for DiscardNew. + var state StreamState + mset.store.FastState(&state) + + totalMsgs := state.Msgs + totalBytes := state.Bytes + for _, i = range mset.inflight { + totalMsgs += i.ops + totalBytes += i.bytes + } + for _, i = range diff.inflight { + totalMsgs += i.ops + totalBytes += i.bytes + } + + if maxMsgs > 0 && totalMsgs > uint64(maxMsgs) { + err = ErrMaxMsgs + } else if maxBytes > 0 && totalBytes > uint64(maxBytes) { + err = ErrMaxBytes + } + if err != nil { + return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err + } + } + + return hdr, msg, 0, nil, nil +} 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 7d1c20639..c1a542acf 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 @@ -116,6 +116,9 @@ const ( compressedStreamMsgOp // For sending deleted gaps on catchups for replicas. deleteRangeOp + // Batch stream ops. + batchMsgOp + batchCommitMsgOp ) // raftGroups are controlled by the metagroup controller. @@ -126,6 +129,7 @@ type raftGroup struct { Storage StorageType `json:"store"` Cluster string `json:"cluster,omitempty"` Preferred string `json:"preferred,omitempty"` + ScaleUp bool `json:"scale_up,omitempty"` // Internal node RaftNode } @@ -887,7 +891,7 @@ func (js *jetStream) setupMetaGroup() error { return err } - cfg := &RaftConfig{Name: defaultMetaGroupName, Store: storeDir, Log: fs} + cfg := &RaftConfig{Name: defaultMetaGroupName, Store: storeDir, Log: fs, Recovering: true} // If we are soliciting leafnode connections and we are sharing a system account and do not disable it with a hint, // we want to move to observer mode so that we extend the solicited cluster or supercluster but do not form our own. @@ -1097,18 +1101,7 @@ func (cc *jetStreamCluster) isStreamAssigned(a *Account, stream string) bool { if sa == nil { return false } - rg := sa.Group - if rg == nil { - return false - } - // Check if we are the leader of this raftGroup assigned to the stream. - ourID := cc.meta.ID() - for _, peer := range rg.Peers { - if peer == ourID { - return true - } - } - return false + return sa.Group.isMember(cc.meta.ID()) } // Read lock should be held. @@ -1849,6 +1842,7 @@ func (js *jetStream) setStreamAssignmentRecovering(sa *streamAssignment) { sa.Restore = nil if sa.Group != nil { sa.Group.Preferred = _EMPTY_ + sa.Group.ScaleUp = false } } @@ -1860,6 +1854,7 @@ func (js *jetStream) setConsumerAssignmentRecovering(ca *consumerAssignment) { ca.recovering = true if ca.Group != nil { ca.Group.Preferred = _EMPTY_ + ca.Group.ScaleUp = false } } @@ -2220,21 +2215,43 @@ func (rg *raftGroup) isMember(id string) bool { return false } -func (rg *raftGroup) setPreferred() { +func (rg *raftGroup) setPreferred(s *Server) { if rg == nil || len(rg.Peers) == 0 { return } if len(rg.Peers) == 1 { rg.Preferred = rg.Peers[0] } else { - // For now just randomly select a peer for the preferred. - pi := rand.Int31n(int32(len(rg.Peers))) - rg.Preferred = rg.Peers[pi] + var online []string + for _, p := range rg.Peers { + si, ok := s.nodeToInfo.Load(p) + if !ok || si == nil { + continue + } + ni := si.(nodeInfo) + if ni.offline { + continue + } + online = append(online, p) + } + + if len(online) == 0 { + // No online servers, just randomly select a peer for the preferred. + pi := rand.Int31n(int32(len(rg.Peers))) + rg.Preferred = rg.Peers[pi] + } else if len(online) == 1 { + // Only one online server. + rg.Preferred = online[0] + } else { + // Randomly select an online peer. + pi := rand.Int31n(int32(len(online))) + rg.Preferred = online[pi] + } } } // createRaftGroup is called to spin up this raft group if needed. -func (js *jetStream) createRaftGroup(accName string, rg *raftGroup, storage StorageType, labels pprofLabels) (RaftNode, error) { +func (js *jetStream) createRaftGroup(accName string, rg *raftGroup, recovering bool, storage StorageType, labels pprofLabels) (RaftNode, error) { // Must hold JS lock throughout, otherwise two parallel calls for the same raft group could result // in duplicate instances for the same identifier, if the current Raft node is shutting down. // We can release the lock temporarily while waiting for the Raft node to shut down. @@ -2347,7 +2364,7 @@ retry: store = ms } - cfg := &RaftConfig{Name: rg.Name, Store: storeDir, Log: store, Track: true} + cfg := &RaftConfig{Name: rg.Name, Store: storeDir, Log: store, Track: true, Recovering: recovering, ScaleUp: rg.ScaleUp} if _, err := readPeerState(storeDir); err != nil { s.bootstrapRaftNode(cfg, rg.Peers, true) @@ -2571,6 +2588,8 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps return } + // Make sure all pending data is flushed before allowing snapshots. + mset.flushAllPending() if err := n.InstallSnapshot(mset.stateSnapshot()); err == nil { lastState = curState } else if err != errNoSnapAvailable && err != errNodeClosed && err != errCatchupsRunning { @@ -2684,13 +2703,35 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps sendSnapshot = false } continue + } else if len(ce.Entries) == 0 { + // If we have a partial batch, it needs to be rejected to ensure CLFS is correct. + if mset != nil { + mset.mu.RLock() + batch := mset.batchApply + mset.mu.RUnlock() + if batch != nil { + mset.srv.Debugf("[batch] reject %s - empty entry", batch.id) + batch.rejectBatchState(mset) + } + } + + // Entry could be empty on a restore when mset is nil. + ne, nb = n.Applied(ce.Index) + ce.ReturnToPool() + continue } // Apply our entries. - if err := js.applyStreamEntries(mset, ce, isRecovering); err == nil { + if maxApplied, err := js.applyStreamEntries(mset, ce, isRecovering); err == nil { // Update our applied. - ne, nb = n.Applied(ce.Index) - ce.ReturnToPool() + if maxApplied > 0 { + // Indicate we've processed (but not applied) everything up to this point. + ne, nb = n.Processed(ce.Index, min(maxApplied, ce.Index)) + // Don't return entry to the pool, this is handled by the in-progress batch. + } else { + ne, nb = n.Applied(ce.Index) + ce.ReturnToPool() + } } else { // Make sure to clean up. ce.ReturnToPool() @@ -2983,7 +3024,7 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps name, cfg := o.String(), o.config() rg := cc.createGroupForConsumer(&cfg, sa) // Pick a preferred leader. - rg.setPreferred() + rg.setPreferred(s) // Place our initial state here as well for assignment distribution. state, _ := o.store.State() @@ -3157,128 +3198,204 @@ func isControlHdr(hdr []byte) bool { } // Apply our stream entries. -func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isRecovering bool) error { - for _, e := range ce.Entries { +// Return maximum allowed applied value, if currently inside a batch, zero otherwise. +func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isRecovering bool) (uint64, error) { + mset.mu.RLock() + batch := mset.batchApply + mset.mu.RUnlock() + + for i, e := range ce.Entries { + // Check if a batch is abandoned. + if e.Type != EntryNormal && batch != nil && batch.id != _EMPTY_ { + batch.rejectBatchState(mset) + } + if e.Type == EntryNormal { buf, op := e.Data, entryOp(e.Data[0]) - switch op { - case streamMsgOp, compressedStreamMsgOp: - if mset == nil { + if op == batchMsgOp { + batchId, batchSeq, _, _, err := decodeBatchMsg(buf[1:]) + if err != nil { + panic(err.Error()) + } + // Initialize if unset. + if batch == nil { + batch = &batchApply{} + mset.mu.Lock() + mset.batchApply = batch + mset.mu.Unlock() + } + + // Need to grab the stream lock before the batch lock. + if isRecovering { + mset.mu.Lock() + } + batch.mu.Lock() + + // Previous batch (if any) was abandoned. + if batch.id != _EMPTY_ && batchId != batch.id { + batch.rejectBatchStateLocked(mset) + } + if batchSeq == 1 { + // If this is the first message in the batch, need to mark the start index. + // We'll continue to check batch-completeness and try to find the commit. + // At that point we'll commit the whole batch. + batch.rejectBatchStateLocked(mset) + batch.entryStart = i + batch.maxApplied = ce.Index - 1 + } + batch.id = batchId + + // While recovering, we could come up in the middle of a compacted batch that has already been applied. + // This is possible if two batches are part of the same append entry, and the first batch was fully + // applied but the second wasn't. + // If we still see the first message of the batch, we don't skip any messages of the batch here. + if isRecovering { + if batchSeq > 1 && batch.count == 0 { + if skip, err := mset.skipBatchIfRecovering(batch, buf); err != nil || skip { + batch.mu.Unlock() + mset.mu.Unlock() + if err != nil { + panic(err.Error()) + } + continue + } + } + mset.mu.Unlock() + } + + batch.count++ + // If the sequence is not monotonically increasing/we identify gaps, the batch can't be accepted. + if batchSeq != batch.count { + batch.rejectBatchStateLocked(mset) + batch.mu.Unlock() continue } - s := js.srv - - mbuf := buf[1:] - if op == compressedStreamMsgOp { - var err error - mbuf, err = s2.Decode(nil, mbuf) - if err != nil { - panic(err.Error()) - } - } - - subject, reply, hdr, msg, lseq, ts, sourced, err := decodeStreamMsg(mbuf) + batch.mu.Unlock() + continue + } else if op == batchCommitMsgOp { + batchId, batchSeq, _, _, err := decodeBatchMsg(buf[1:]) if err != nil { - if node := mset.raftNode(); node != nil { - s.Errorf("JetStream cluster could not decode stream msg for '%s > %s' [%s]", - mset.account(), mset.name(), node.Group()) - } panic(err.Error()) } - // Check for flowcontrol here. - if len(msg) == 0 && len(hdr) > 0 && reply != _EMPTY_ && isControlHdr(hdr) { - if !isRecovering { - mset.sendFlowControlReply(reply) - } - continue + // Ensure the whole batch is fully isolated, and reads + // can only happen after the full batch is committed. + mset.mu.Lock() + + // Initialize if unset. + if batch == nil { + batch = &batchApply{} + mset.batchApply = batch } + batch.mu.Lock() - // Grab last sequence and CLFS. - last, clfs := mset.lastSeqAndCLFS() - - // We can skip if we know this is less than what we already have. - if lseq-clfs < last { - s.Debugf("Apply stream entries for '%s > %s' skipping message with sequence %d with last of %d", - mset.account(), mset.name(), lseq+1-clfs, last) - mset.mu.Lock() - // Check for any preAcks in case we are interest based. - mset.clearAllPreAcks(lseq + 1 - clfs) - mset.mu.Unlock() - continue + // Previous batch (if any) was abandoned. + if batch.id != _EMPTY_ && batchId != batch.id { + batch.rejectBatchStateLocked(mset) } - - // Skip by hand here since first msg special case. - // Reason is sequence is unsigned and for lseq being 0 - // the lseq under stream would have to be -1. - if lseq == 0 && last != 0 { - continue + if batchSeq == 1 { + // If this is the first message in the batch, need to mark the start index. + // This is a batch of size one that immediately commits. + batch.rejectBatchStateLocked(mset) + batch.entryStart = i + batch.maxApplied = ce.Index - 1 } + batch.id = batchId - // Messages to be skipped have no subject or timestamp or msg or hdr. - if subject == _EMPTY_ && ts == 0 && len(msg) == 0 && len(hdr) == 0 { - // Skip and update our lseq. - last := mset.store.SkipMsg() - mset.mu.Lock() - mset.setLastSeq(last) - mset.clearAllPreAcks(last) - mset.mu.Unlock() - continue - } - - var mt *msgTrace - // If not recovering, see if we find a message trace object for this - // sequence. Only the leader that has proposed this entry will have - // stored the trace info. - if !isRecovering { - mt = mset.getAndDeleteMsgTrace(lseq) - } - // Process the actual message here. - err = mset.processJetStreamMsg(subject, reply, hdr, msg, lseq, ts, mt, sourced) - - // If we have inflight make sure to clear after processing. - // TODO(dlc) - technically check on inflight != nil could cause datarace. - // But do not want to acquire lock since tracking this will be rare. - if mset.inflight != nil { - mset.clMu.Lock() - delete(mset.inflight, lseq) - mset.clMu.Unlock() - } - - // Clear expected per subject state after processing. - if mset.expectedPerSubjectSequence != nil { - mset.clMu.Lock() - if subj, found := mset.expectedPerSubjectSequence[lseq]; found { - delete(mset.expectedPerSubjectSequence, lseq) - delete(mset.expectedPerSubjectInProcess, subj) - } - mset.clMu.Unlock() - } - - if err != nil { - if err == errLastSeqMismatch { - - var state StreamState - mset.store.FastState(&state) - - // If we have no msgs and the other side is delivering us a sequence past where we - // should be reset. This is possible if the other side has a stale snapshot and no longer - // has those messages. So compact and retry to reset. - if state.Msgs == 0 { - mset.store.Compact(lseq + 1) - // Retry - err = mset.processJetStreamMsg(subject, reply, hdr, msg, lseq, ts, mt, sourced) + // While recovering, we could come up in the middle of a compacted batch that has already been applied. + // This is possible if two batches are part of the same append entry, and the first batch was fully + // applied but the second wasn't. + // If we still see the first message of the batch, we don't skip any messages of the batch here. + if isRecovering && batchSeq > 1 && batch.count == 0 { + if skip, err := mset.skipBatchIfRecovering(batch, buf); err != nil || skip { + batch.mu.Unlock() + mset.mu.Unlock() + if err != nil { + panic(err.Error()) } - // FIXME(dlc) - We could just run a catchup with a request defining the span between what we expected - // and what we got. + continue } + } - // Only return in place if we are going to reset our stream or we are out of space, or we are closed. - if isClusterResetErr(err) || isOutOfSpaceErr(err) || err == errStreamClosed { - return err + batch.count++ + // Detected a gap, reject the batch. + if batchSeq != batch.count { + batch.rejectBatchStateLocked(mset) + batch.mu.Unlock() + mset.mu.Unlock() + continue + } + + // Process any entries that are part of this batch but prior to the current one. + var entries []*Entry + for j, bce := range batch.entries { + if j == 0 { + // The first needs only the entries when the batch is started. + entries = bce.Entries[batch.entryStart:] + } else { + // Otherwise, all entries are used. + entries = bce.Entries } - s.Debugf("Apply stream entries for '%s > %s' got error processing message: %v", - mset.account(), mset.name(), err) + for _, entry := range entries { + _, _, op, buf, err = decodeBatchMsg(entry.Data[1:]) + if err != nil { + batch.mu.Unlock() + mset.mu.Unlock() + panic(err.Error()) + } + if err = js.applyStreamMsgOp(mset, op, buf, isRecovering, false); err != nil { + // Make sure to return remaining entries to the pool on an error. + for _, nce := range batch.entries[j:] { + nce.ReturnToPool() + } + // Important to clear, otherwise we could return the entries to the pool multiple times. + batch.clearBatchStateLocked() + batch.mu.Unlock() + mset.mu.Unlock() + return 0, err + } + } + // Return the entry to the pool now. + bce.ReturnToPool() + } + if len(batch.entries) == 0 { + // Get within the same entry, but within the range of this batch. + entries = ce.Entries[batch.entryStart : i+1] + } else { + // Get all entries up to and including the current one. + entries = ce.Entries[:i+1] + } + // Process remaining entries in the current entry. + for _, entry := range entries { + _, _, op, buf, err = decodeBatchMsg(entry.Data[1:]) + if err != nil { + batch.mu.Unlock() + mset.mu.Unlock() + panic(err.Error()) + } + if err = js.applyStreamMsgOp(mset, op, buf, isRecovering, false); err != nil { + // Important to clear, otherwise we could return the entries to the pool multiple times. + batch.clearBatchStateLocked() + batch.mu.Unlock() + mset.mu.Unlock() + return 0, err + } + } + // Clear state, batch was successful. + batch.clearBatchStateLocked() + batch.mu.Unlock() + mset.mu.Unlock() + continue + } else if batch != nil && batch.id != _EMPTY_ { + // If a batch is abandoned without a commit, reject it. + batch.rejectBatchState(mset) + } + + switch op { + case streamMsgOp, compressedStreamMsgOp: + mbuf := buf[1:] + if err := js.applyStreamMsgOp(mset, op, mbuf, isRecovering, true); err != nil { + return 0, err } case deleteMsgOp: @@ -3302,7 +3419,7 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco // Cluster reset error. if err == ErrStoreEOF { - return err + return 0, err } if err != nil && !isRecovering { @@ -3374,10 +3491,6 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco panic(fmt.Sprintf("JetStream Cluster Unknown group entry op type: %v", op)) } } else if e.Type == EntrySnapshot { - if mset == nil { - continue - } - // Everything operates on new replicated state. Will convert legacy snapshots to this for processing. var ss *StreamReplicatedState @@ -3399,13 +3512,13 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco ss, err = DecodeStreamState(e.Data) if err != nil { onBadState(err) - return err + return 0, err } } else { var snap streamSnapshot if err := json.Unmarshal(e.Data, &snap); err != nil { onBadState(err) - return err + return 0, err } // Convert over to StreamReplicatedState ss = &StreamReplicatedState{ @@ -3422,7 +3535,7 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco if isRecovering || !mset.IsLeader() { if err := mset.processSnapshot(ss, ce.Index); err != nil { - return err + return 0, err } } } else if e.Type == EntryRemovePeer { @@ -3447,6 +3560,230 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco } } } + + // If we're still actively processing a batch, must store the entry in-memory + // to come back to it later once we find the commit. + if batch != nil && batch.id != _EMPTY_ { + batch.mu.Lock() + if batch.entries == nil { + batch.entries = []*CommittedEntry{ce} + } else { + batch.entries = append(batch.entries, ce) + } + maxApplied := batch.maxApplied + batch.mu.Unlock() + return maxApplied, nil + } + return 0, nil +} + +// skipBatchIfRecovering returns whether the batched message can be skipped because the batch was already fully applied. +// Stream and batch.mu locks should be held. +func (mset *stream) skipBatchIfRecovering(batch *batchApply, buf []byte) (bool, error) { + _, _, op, mbuf, err := decodeBatchMsg(buf[1:]) + if err != nil { + return false, err + } + + if op == compressedStreamMsgOp { + if mbuf, err = s2.Decode(nil, mbuf); err != nil { + return false, err + } + } + + _, _, _, _, lseq, _, _, err := decodeStreamMsg(mbuf) + if err != nil { + return false, err + } + + // Grab last sequence and CLFS. + last, clfs := mset.lastSeqAndCLFS() + + // We can skip if we know this is less than what we already have. + if lseq-clfs < last { + mset.srv.Debugf("[batch] Apply stream entries for '%s > %s' skipping message with sequence %d with last of %d", + mset.accountLocked(false), mset.nameLocked(false), lseq+1-clfs, last) + // Check for any preAcks in case we are interest based. + mset.clearAllPreAcks(lseq + 1 - clfs) + batch.clearBatchStateLocked() + return true, nil + } + return false, nil +} + +func (js *jetStream) applyStreamMsgOp(mset *stream, op entryOp, mbuf []byte, isRecovering bool, needLock bool) error { + s := js.srv + + if op == compressedStreamMsgOp { + var err error + mbuf, err = s2.Decode(nil, mbuf) + if err != nil { + panic(err.Error()) + } + } + + subject, reply, hdr, msg, lseq, ts, sourced, err := decodeStreamMsg(mbuf) + if err != nil { + // We're going to panic below, but if we're already holding the stream lock, we should let go now. + // Otherwise we'll deadlock when trying to get the raft node. + if !needLock { + mset.mu.Unlock() + } + if node := mset.raftNode(); node != nil { + s.Errorf("JetStream cluster could not decode stream msg for '%s > %s' [%s]", + mset.account(), mset.name(), node.Group()) + } + panic(err.Error()) + } + + // Check for flowcontrol here. + if len(msg) == 0 && len(hdr) > 0 && reply != _EMPTY_ && isControlHdr(hdr) { + if !isRecovering { + if needLock { + mset.mu.RLock() + } + mset.sendFlowControlReply(reply) + if needLock { + mset.mu.RUnlock() + } + } + return nil + } + + if needLock { + mset.mu.RLock() + } + // Grab last sequence and CLFS. + last, clfs := mset.lastSeqAndCLFS() + if needLock { + mset.mu.RUnlock() + } + + // We can skip if we know this is less than what we already have. + if lseq-clfs < last { + s.Debugf("Apply stream entries for '%s > %s' skipping message with sequence %d with last of %d", + mset.accountLocked(needLock), mset.nameLocked(needLock), lseq+1-clfs, last) + if needLock { + mset.mu.Lock() + } + // Check for any preAcks in case we are interest based. + mset.clearAllPreAcks(lseq + 1 - clfs) + if needLock { + mset.mu.Unlock() + } + return nil + } + + // Skip by hand here since first msg special case. + // Reason is sequence is unsigned and for lseq being 0 + // the lseq under stream would have to be -1. + if lseq == 0 && last != 0 { + return nil + } + + // Messages to be skipped have no subject or timestamp or msg or hdr. + if subject == _EMPTY_ && ts == 0 && len(msg) == 0 && len(hdr) == 0 { + // Skip and update our lseq. + last := mset.store.SkipMsg() + if needLock { + mset.mu.Lock() + } + mset.setLastSeq(last) + mset.clearAllPreAcks(last) + if needLock { + mset.mu.Unlock() + } + return nil + } + + var mt *msgTrace + // If not recovering, see if we find a message trace object for this + // sequence. Only the leader that has proposed this entry will have + // stored the trace info. + if !isRecovering { + mt = mset.getAndDeleteMsgTrace(lseq) + } + // Process the actual message here. + err = mset.processJetStreamMsg(subject, reply, hdr, msg, lseq, ts, mt, sourced, needLock) + + // If we have inflight make sure to clear after processing. + // TODO(dlc) - technically check on inflight != nil could cause datarace. + // But do not want to acquire lock since tracking this will be rare. + if mset.inflight != nil { + mset.clMu.Lock() + if i, found := mset.inflight[subject]; found { + // Decrement from pending operations. Once it reaches zero, it can be deleted. + if i.ops > 0 { + var sz uint64 + if mset.store.Type() == FileStorage { + sz = fileStoreMsgSizeRaw(len(subject), len(hdr), len(msg)) + } else { + sz = memStoreMsgSizeRaw(len(subject), len(hdr), len(msg)) + } + if i.bytes >= sz { + i.bytes -= sz + } else { + i.bytes = 0 + } + i.ops-- + } + if i.ops == 0 { + delete(mset.inflight, subject) + } + } + mset.clMu.Unlock() + } + + // Update running total for counter. + if mset.clusteredCounterTotal != nil { + mset.clMu.Lock() + if counter, found := mset.clusteredCounterTotal[subject]; found { + // Decrement from pending operations. Once it reaches zero, it can be deleted. + if counter.ops > 0 { + counter.ops-- + } + if counter.ops == 0 { + delete(mset.clusteredCounterTotal, subject) + } + } + mset.clMu.Unlock() + } + + // Clear expected per subject state after processing. + if mset.expectedPerSubjectSequence != nil { + mset.clMu.Lock() + if subj, found := mset.expectedPerSubjectSequence[lseq]; found { + delete(mset.expectedPerSubjectSequence, lseq) + delete(mset.expectedPerSubjectInProcess, subj) + } + mset.clMu.Unlock() + } + + if err != nil { + if err == errLastSeqMismatch { + + var state StreamState + mset.store.FastState(&state) + + // If we have no msgs and the other side is delivering us a sequence past where we + // should be reset. This is possible if the other side has a stale snapshot and no longer + // has those messages. So compact and retry to reset. + if state.Msgs == 0 { + mset.store.Compact(lseq + 1) + // Retry + err = mset.processJetStreamMsg(subject, reply, hdr, msg, lseq, ts, mt, sourced, needLock) + } + // FIXME(dlc) - We could just run a catchup with a request defining the span between what we expected + // and what we got. + } + + // Only return in place if we are going to reset our stream or we are out of space, or we are closed. + if isClusterResetErr(err) || isOutOfSpaceErr(err) || err == errStreamClosed { + return err + } + s.Debugf("Apply stream entries for '%s > %s' got error processing message: %v", + mset.accountLocked(needLock), mset.nameLocked(needLock), err) + } return nil } @@ -3478,7 +3815,7 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) { } // Clear inflight dedupe IDs, where seq=0. - mset.mu.Lock() + mset.ddMu.Lock() var removed int for i := len(mset.ddarr) - 1; i >= mset.ddindex; i-- { dde := mset.ddarr[i] @@ -3497,11 +3834,13 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) { mset.ddindex = 0 } } - mset.mu.Unlock() + mset.ddMu.Unlock() mset.clMu.Lock() // Clear inflight if we have it. mset.inflight = nil + // Clear running counter totals. + mset.clusteredCounterTotal = nil // Clear expected per subject state. mset.expectedPerSubjectSequence = nil mset.expectedPerSubjectInProcess = nil @@ -3527,7 +3866,7 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) { } // Clear clseq. If we become leader again, it will be fixed up - // automatically on the next processClusteredInboundMsg call. + // automatically on the next mset.setLeader call. mset.clMu.Lock() if mset.clseq > 0 { mset.clseq = 0 @@ -3953,7 +4292,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss mset.startClusterSubs() mset.mu.Unlock() - js.createRaftGroup(acc.GetName(), rg, storage, pprofLabels{ + js.createRaftGroup(acc.GetName(), rg, recovering, storage, pprofLabels{ "type": "stream", "account": mset.accName(), "stream": mset.name(), @@ -4067,10 +4406,11 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme alreadyRunning := rg.node != nil storage := sa.Config.Storage restore := sa.Restore + recovering := sa.recovering js.mu.RUnlock() // Process the raft group and make sure it's running if needed. - _, err := js.createRaftGroup(acc.GetName(), rg, storage, pprofLabels{ + _, err := js.createRaftGroup(acc.GetName(), rg, recovering, storage, pprofLabels{ "type": "stream", "account": acc.Name, "stream": sa.Config.Name, @@ -4143,7 +4483,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme s.Warnf("JetStream cluster error updating stream %q for account %q: %v", sa.Config.Name, acc.Name, err) if osa != nil { // Process the raft group and make sure it's running if needed. - js.createRaftGroup(acc.GetName(), osa.Group, storage, pprofLabels{ + js.createRaftGroup(acc.GetName(), osa.Group, osa.recovering, storage, pprofLabels{ "type": "stream", "account": mset.accName(), "stream": mset.name(), @@ -4693,6 +5033,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state rg := ca.Group alreadyRunning := rg != nil && rg.node != nil accName, stream, consumer := ca.Client.serviceAccount(), ca.Stream, ca.Name + recovering := ca.recovering js.mu.RUnlock() acc, err := s.LookupAccount(accName) @@ -4730,7 +5071,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state storage = MemoryStorage } // No-op if R1. - js.createRaftGroup(accName, rg, storage, pprofLabels{ + js.createRaftGroup(accName, rg, recovering, storage, pprofLabels{ "type": "consumer", "account": mset.accName(), "stream": ca.Stream, @@ -5052,15 +5393,7 @@ func (cc *jetStreamCluster) isConsumerAssigned(a *Account, stream, consumer stri if ca == nil { return false } - rg := ca.Group - // Check if we are the leader of this raftGroup assigned to the stream. - ourID := cc.meta.ID() - for _, peer := range rg.Peers { - if peer == ourID { - return true - } - } - return false + return ca.Group.isMember(cc.meta.ID()) } // Returns our stream and underlying raft node. @@ -5811,7 +6144,7 @@ func (js *jetStream) processStreamAssignmentResults(sub *subscription, c *client s.Warnf("Retrying cluster placement for stream '%s > %s' due to insufficient resources", result.Account, result.Stream) } // Pick a new preferred leader. - rg.setPreferred() + rg.setPreferred(s) // Get rid of previous attempt. cc.meta.Propose(encodeDeleteStreamAssignment(sa)) // Propose new. @@ -5873,13 +6206,10 @@ func (js *jetStream) processConsumerAssignmentResults(sub *subscription, c *clie // Check if this failed. // TODO(dlc) - Could have mixed results, should track per peer. - // Make sure this is recent response, do not delete existing consumers. + // Make sure this is recent response. if result.Response.Error != nil && result.Response.Error != NewJSConsumerNameExistError() && time.Since(ca.Created) < 2*time.Second { - // So while we are deleting we will not respond to list/names requests. + // Do not list in consumer names/lists. ca.err = NewJSClusterNotAssignedError() - cc.meta.Propose(encodeDeleteConsumerAssignment(ca)) - s.Warnf("Proposing to delete consumer `%s > %s > %s' due to assignment response error: %v", - result.Account, result.Stream, result.Consumer, result.Response.Error) } } } @@ -6092,6 +6422,7 @@ type selectPeerError struct { misc bool noJsClust bool noMatchTags map[string]struct{} + excludeTags map[string]struct{} } func (e *selectPeerError) Error() string { @@ -6124,6 +6455,21 @@ func (e *selectPeerError) Error() string { } b.WriteString("]") } + if len(e.excludeTags) != 0 { + b.WriteString(", tags excluded [") + var firstTagWritten bool + for tag := range e.excludeTags { + if firstTagWritten { + b.WriteString(", ") + } + firstTagWritten = true + b.WriteRune('\'') + b.WriteString(tag) + b.WriteRune('\'') + } + b.WriteString("]") + } + return b.String() } @@ -6134,6 +6480,13 @@ func (e *selectPeerError) addMissingTag(t string) { e.noMatchTags[t] = struct{}{} } +func (e *selectPeerError) addExcludeTag(t string) { + if e.excludeTags == nil { + e.excludeTags = map[string]struct{}{} + } + e.excludeTags[t] = struct{}{} +} + func (e *selectPeerError) accumulate(eAdd *selectPeerError) { if eAdd == nil { return @@ -6152,6 +6505,9 @@ func (e *selectPeerError) accumulate(eAdd *selectPeerError) { for tag := range eAdd.noMatchTags { e.addMissingTag(tag) } + for tag := range eAdd.excludeTags { + e.addExcludeTag(tag) + } } // selectPeerGroup will select a group of peers to start a raft group. @@ -6168,15 +6524,26 @@ func (cc *jetStreamCluster) selectPeerGroup(r int, cluster string, cfg *StreamCo } // Check for tags. - var tags []string - if cfg.Placement != nil && len(cfg.Placement.Tags) > 0 { - tags = cfg.Placement.Tags + type tagInfo struct { + tag string + exclude bool + } + var ti []tagInfo + if cfg.Placement != nil { + ti = make([]tagInfo, 0, len(cfg.Placement.Tags)) + for _, t := range cfg.Placement.Tags { + ti = append(ti, tagInfo{ + tag: strings.TrimPrefix(t, "!"), + exclude: strings.HasPrefix(t, "!"), + }) + } } // Used for weighted sorting based on availability. type wn struct { id string avail uint64 + off bool ha int ns int } @@ -6187,8 +6554,8 @@ func (cc *jetStreamCluster) selectPeerGroup(r int, cluster string, cfg *StreamCo uniqueTagPrefix := s.getOpts().JetStreamUniqueTag if uniqueTagPrefix != _EMPTY_ { - for _, tag := range tags { - if strings.HasPrefix(tag, uniqueTagPrefix) { + for _, t := range ti { + if strings.HasPrefix(t.tag, uniqueTagPrefix) { // disable uniqueness check if explicitly listed in tags uniqueTagPrefix = _EMPTY_ break @@ -6268,6 +6635,8 @@ func (cc *jetStreamCluster) selectPeerGroup(r int, cluster string, cfg *StreamCo // Which is why we keep taps on how often which one happened. err := selectPeerError{} + var onlinePeers int + // Shuffle them up. rand.Shuffle(len(peers), func(i, j int) { peers[i], peers[j] = peers[j], peers[i] }) for _, p := range peers { @@ -6283,8 +6652,8 @@ func (cc *jetStreamCluster) selectPeerGroup(r int, cluster string, cfg *StreamCo continue } - // If we know its offline or we do not have config or err don't consider. - if ni.offline || ni.cfg == nil || ni.stats == nil { + // If we've never heard from a server, don't consider. + if ni.cfg == nil || ni.stats == nil { s.Debugf("Peer selection: discard %s@%s reason: offline", ni.name, ni.cluster) err.offline = true continue @@ -6307,14 +6676,21 @@ func (cc *jetStreamCluster) selectPeerGroup(r int, cluster string, cfg *StreamCo continue } - if len(tags) > 0 { + if len(ti) > 0 { matched := true - for _, t := range tags { - if !ni.tags.Contains(t) { + for _, t := range ti { + contains := ni.tags.Contains(t.tag) + if t.exclude && contains { + matched = false + s.Debugf("Peer selection: discard %s@%s tags: %v reason: excluded tag %s present", + ni.name, ni.cluster, ni.tags, t) + err.addExcludeTag(t.tag) + break + } else if !t.exclude && !contains { matched = false s.Debugf("Peer selection: discard %s@%s tags: %v reason: mandatory tag %s not present", ni.name, ni.cluster, ni.tags, t) - err.addMissingTag(t) + err.addMissingTag(t.tag) break } } @@ -6374,20 +6750,35 @@ func (cc *jetStreamCluster) selectPeerGroup(r int, cluster string, cfg *StreamCo } } // Add to our list of potential nodes. - nodes = append(nodes, wn{p.ID, available, peerHA[p.ID], peerStreams[p.ID]}) + nodes = append(nodes, wn{p.ID, available, ni.offline, peerHA[p.ID], peerStreams[p.ID]}) + if !ni.offline { + onlinePeers++ + } } // If we could not select enough peers, fail. - if len(nodes) < (r - len(existing)) { - s.Debugf("Peer selection: required %d nodes but found %d (cluster: %s replica: %d existing: %v/%d peers: %d result-peers: %d err: %+v)", - (r - len(existing)), len(nodes), cluster, r, existing, replaceFirstExisting, len(peers), len(nodes), err) + quorum := r/2 + 1 + missingQuorum := onlinePeers+len(existing) < quorum + if missingNodes := len(nodes) < (r - len(existing)); missingNodes || missingQuorum { if len(peers) == 0 { err.noJsClust = true + } else if !missingNodes && missingQuorum { + err.offline = true } + s.Debugf("Peer selection: required %d nodes but found %d (cluster: %s replica: %d existing: %v/%d peers: %d result-peers: %d err: %+v)", + r-len(existing), len(nodes), cluster, r, existing, replaceFirstExisting, len(peers), len(nodes), err) return nil, &err } // Sort based on available from most to least, breaking ties by number of total streams assigned to the peer. slices.SortFunc(nodes, func(i, j wn) int { + // Prefer online servers to offline ones. + if i.off != j.off { + if i.off { + return 1 + } else { + return -1 + } + } if i.avail == j.avail { return cmp.Compare(i.ns, j.ns) } @@ -6395,7 +6786,17 @@ func (cc *jetStreamCluster) selectPeerGroup(r int, cluster string, cfg *StreamCo }) // If we are placing a replicated stream, let's sort based on HAAssets, as that is more important to balance. if cfg.Replicas > 1 { - slices.SortStableFunc(nodes, func(i, j wn) int { return cmp.Compare(i.ha, j.ha) }) + slices.SortStableFunc(nodes, func(i, j wn) int { + // Prefer online servers to offline ones. + if i.off != j.off { + if i.off { + return 1 + } else { + return -1 + } + } + return cmp.Compare(i.ha, j.ha) + }) } var results []string @@ -6622,7 +7023,7 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject, } rg = nrg // Pick a preferred leader. - rg.setPreferred() + rg.setPreferred(s) } if syncSubject == _EMPTY_ { @@ -6745,7 +7146,8 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su return } // Check for mirror changes which are not allowed. - if !reflect.DeepEqual(newCfg.Mirror, osa.Config.Mirror) { + // We will allow removing the mirror config to "promote" the mirror to a normal stream. + if newCfg.Mirror != nil && !reflect.DeepEqual(newCfg.Mirror, osa.Config.Mirror) { resp.Error = NewJSStreamMirrorNotUpdatableError() s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp)) return @@ -6827,6 +7229,8 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su return } + // Reset notion of scaling up, if this was done in a previous update. + rg.ScaleUp = false if isReplicaChange { isScaleUp := newCfg.Replicas > len(rg.Peers) // We are adding new peers here. @@ -6862,6 +7266,7 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su // This is scale up from being a singelton, set preferred to that singelton. rg.Preferred = rg.Peers[0] } + rg.ScaleUp = true rg.Peers = peers } else { // We are deleting nodes here. We want to do our best to preserve the current leader. @@ -6992,14 +7397,7 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su } // filter peers present in both sets for _, peer := range rg.Peers { - found := false - for _, newPeer := range nrg.Peers { - if peer == newPeer { - found = true - break - } - } - if !found { + if !slices.Contains(nrg.Peers, peer) { peerSet = append(peerSet, peer) } } @@ -7041,14 +7439,7 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su } // make sure it overlaps with peers and remove if not if cca.Group.Preferred != _EMPTY_ { - found := false - for _, p := range cca.Group.Peers { - if p == cca.Group.Preferred { - found = true - break - } - } - if !found { + if !slices.Contains(cca.Group.Peers, cca.Group.Preferred) { cca.Group.Preferred = _EMPTY_ } } @@ -7182,7 +7573,7 @@ func (s *Server) jsClusteredStreamRestoreRequest( return } // Pick a preferred leader. - rg.setPreferred() + rg.setPreferred(s) sa := &streamAssignment{Group: rg, Sync: syncSubjForStream(), Config: cfg, Subject: subject, Reply: reply, Client: ci, Created: time.Now().UTC()} // Now add in our restore state and pre-select a peer to handle the actual receipt of the snapshot. sa.Restore = &req.State @@ -7904,7 +8295,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec return } // Pick a preferred leader. - rg.setPreferred() + rg.setPreferred(s) // Inherit cluster from stream. rg.Cluster = sa.Group.Cluster @@ -7994,6 +8385,9 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec nca := ca.copyGroup() + // Reset notion of scaling up, if this was done in a previous update. + nca.Group.ScaleUp = false + rBefore := nca.Config.replicas(sa.Config) rAfter := cfg.replicas(sa.Config) @@ -8046,6 +8440,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec } nca.Group.Peers = newPeerSet nca.Group.Preferred = curLeader + nca.Group.ScaleUp = true } else if rBefore > rAfter { newPeerSet := nca.Group.Peers // mark leader preferred and move it to end @@ -8219,6 +8614,29 @@ func decodeStreamMsg(buf []byte) (subject, reply string, hdr, msg []byte, lseq u return subject, reply, hdr, msg, lseq, ts, sourced, nil } +func decodeBatchMsg(buf []byte) (batchId string, batchSeq uint64, op entryOp, mbuf []byte, err error) { + var le = binary.LittleEndian + if len(buf) < 2 { + return _EMPTY_, 0, 0, nil, errBadStreamMsg + } + bl := int(le.Uint16(buf)) + buf = buf[2:] + if len(buf) < bl { + return _EMPTY_, 0, 0, nil, errBadStreamMsg + } + batchId = string(buf[:bl]) + buf = buf[bl:] + var n int + batchSeq, n = binary.Uvarint(buf) + if n <= 0 { + return _EMPTY_, 0, 0, nil, errBadStreamMsg + } + buf = buf[n:] + op = entryOp(buf[0]) + mbuf = buf[1:] + return batchId, batchSeq, op, mbuf, nil +} + // Flags for encodeStreamMsg/decodeStreamMsg. const ( msgFlagFromSourceOrMirror uint64 = 1 << iota @@ -8228,12 +8646,16 @@ func encodeStreamMsg(subject, reply string, hdr, msg []byte, lseq uint64, ts int return encodeStreamMsgAllowCompress(subject, reply, hdr, msg, lseq, ts, sourced) } +func encodeStreamMsgAllowCompress(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, sourced bool) []byte { + return encodeStreamMsgAllowCompressAndBatch(subject, reply, hdr, msg, lseq, ts, sourced, _EMPTY_, 0, false) +} + // Threshold for compression. // TODO(dlc) - Eventually make configurable. const compressThreshold = 8192 // 8k // If allowed and contents over the threshold we will compress. -func encodeStreamMsgAllowCompress(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, sourced bool) []byte { +func encodeStreamMsgAllowCompressAndBatch(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, sourced bool, batchId string, batchSeq uint64, batchCommit bool) []byte { // Clip the subject, reply, header and msgs down. Operate on // uint64 lengths to avoid overflowing. slen := min(uint64(len(subject)), math.MaxUint16) @@ -8246,15 +8668,34 @@ func encodeStreamMsgAllowCompress(subject, reply string, hdr, msg []byte, lseq u elen := int(1 + 8 + 8 + total) elen += (2 + 2 + 2 + 4 + 8) // Encoded lengths, 4bytes, flags are up to 8 bytes + blen := min(uint64(len(batchId)), math.MaxUint16) + if batchId != _EMPTY_ { + elen += int(2 + blen + 8) // length of batchId, batchId itself, batchSeq (up to 8 bytes) + } + var flags uint64 if sourced { flags |= msgFlagFromSourceOrMirror } - buf := make([]byte, 1, elen) - buf[0] = byte(streamMsgOp) - var le = binary.LittleEndian + var opIndex int + buf := make([]byte, 1, elen) + if batchId != _EMPTY_ { + if batchCommit { + buf[0] = byte(batchCommitMsgOp) + } else { + buf[0] = byte(batchMsgOp) + } + buf = le.AppendUint16(buf, uint16(blen)) + buf = append(buf, batchId[:blen]...) + buf = binary.AppendUvarint(buf, batchSeq) + opIndex = len(buf) + buf = append(buf, byte(streamMsgOp)) + } else { + buf[opIndex] = byte(streamMsgOp) + } + buf = le.AppendUint64(buf, lseq) buf = le.AppendUint64(buf, uint64(ts)) buf = le.AppendUint16(buf, uint16(slen)) @@ -8270,12 +8711,15 @@ func encodeStreamMsgAllowCompress(subject, reply string, hdr, msg []byte, lseq u // Check if we should compress. if shouldCompress { nbuf := make([]byte, s2.MaxEncodedLen(elen)) - nbuf[0] = byte(compressedStreamMsgOp) - ebuf := s2.Encode(nbuf[1:], buf[1:]) + if opIndex > 0 { + copy(nbuf[:opIndex], buf[:opIndex]) + } + nbuf[opIndex] = byte(compressedStreamMsgOp) + ebuf := s2.Encode(nbuf[opIndex+1:], buf[opIndex+1:]) // Only pay the cost of decode on the other side if we compressed. // S2 will allow us to try without major penalty for non-compressable data. if len(ebuf) < len(buf) { - buf = nbuf[:len(ebuf)+1] + buf = nbuf[:len(ebuf)+opIndex+1] } } @@ -8365,11 +8809,11 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ mset.mu.RLock() canRespond := !mset.cfg.NoAck && len(reply) > 0 - name, stype, store := mset.cfg.Name, mset.cfg.Storage, mset.store + name, stype := mset.cfg.Name, mset.cfg.Storage + discard, discardNewPer, maxMsgs, maxMsgsPer, maxBytes := mset.cfg.Discard, mset.cfg.DiscardNewPer, mset.cfg.MaxMsgs, mset.cfg.MaxMsgsPer, mset.cfg.MaxBytes s, js, jsa, st, r, tierName, outq, node := mset.srv, mset.js, mset.jsa, mset.cfg.Storage, mset.cfg.Replicas, mset.tier, mset.outq, mset.node maxMsgSize, lseq := int(mset.cfg.MaxMsgSize), mset.lseq - interestPolicy, discard, maxMsgs, maxBytes := mset.cfg.Retention != LimitsPolicy, mset.cfg.Discard, mset.cfg.MaxMsgs, mset.cfg.MaxBytes - isLeader, isSealed, allowTTL := mset.isLeader(), mset.cfg.Sealed, mset.cfg.AllowMsgTTL + 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 mset.mu.RUnlock() // This should not happen but possible now that we allow scale up, and scale down where this could trigger. @@ -8377,7 +8821,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ // We also invoke this in clustering mode for message tracing when not // performing message delivery. if node == nil || mt.traceOnly() { - return mset.processJetStreamMsg(subject, reply, hdr, msg, 0, 0, mt, sourced) + return mset.processJetStreamMsg(subject, reply, hdr, msg, 0, 0, mt, sourced, true) } // If message tracing (with message delivery), we will need to send the @@ -8407,7 +8851,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ // Check here pre-emptively if we have exceeded this server limits. if js.limitsExceeded(stype) { - s.resourcesExceededError() + s.resourcesExceededError(stype) if canRespond { b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: NewJSInsufficientResourcesError()}) outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, b, nil, 0)) @@ -8435,7 +8879,8 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ } // Check msgSize if we have a limit set there. Again this works if it goes through but better to be pre-emptive. - if maxMsgSize >= 0 && (len(hdr)+len(msg)) > maxMsgSize { + // Subtract to prevent against overflows. + if maxMsgSize >= 0 && (len(hdr) > maxMsgSize || len(msg) > maxMsgSize-len(hdr)) { err := fmt.Errorf("JetStream message size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name) s.RateLimitWarnf("%s", err.Error()) if canRespond { @@ -8447,198 +8892,60 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ return err } - // Some header checks can be checked pre proposal. Most can not. - var msgId string - if len(hdr) > 0 { - // Since we encode header len as u16 make sure we do not exceed. - // Again this works if it goes through but better to be pre-emptive. - if len(hdr) > math.MaxUint16 { - err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name) - s.RateLimitWarnf("%s", err.Error()) - if canRespond { - var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.Error = NewJSStreamHeaderExceedsMaximumError() - response, _ = json.Marshal(resp) - outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) - } - return err - } - // Expected stream name can also be pre-checked. - if sname := getExpectedStream(hdr); sname != _EMPTY_ && sname != name { - if canRespond { - var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.PubAck = &PubAck{Stream: name} - resp.Error = NewJSStreamNotMatchError() - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - return errStreamMismatch - } - // TTL'd messages are rejected entirely if TTLs are not enabled on the stream, or if the TTL is invalid. - if ttl, err := getMessageTTL(hdr); !sourced && (ttl != 0 || err != nil) { - if !allowTTL { - if canRespond { - var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.Error = NewJSMessageTTLDisabledError() - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - return errMsgTTLDisabled - } else if err != nil { - if canRespond { - var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.Error = NewJSMessageTTLInvalidError() - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - return err - } - } - // Check for MsgIds here at the cluster level to avoid excessive CLFS accounting. - // Will help during restarts. - if msgId = getMsgId(hdr); msgId != _EMPTY_ { - mset.mu.Lock() - if dde := mset.checkMsgId(msgId); dde != nil { - var buf [256]byte - pubAck := append(buf[:0], mset.pubAck...) - seq := dde.seq - mset.mu.Unlock() - // Should not return an invalid sequence, in that case error. - if canRespond { - if seq > 0 { - response := append(pubAck, strconv.FormatUint(seq, 10)...) - response = append(response, ",\"duplicate\": true}"...) - outq.sendMsg(reply, response) - } else { - var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.Error = ApiErrors[JSStreamDuplicateMessageConflict] - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - } - return errMsgIdDuplicate - } - // FIXME(dlc) - locking conflict with accessing mset.clseq - // For now we stage with zero, and will update in processStreamMsg. - mset.storeMsgIdLocked(&ddentry{msgId, 0, time.Now().UnixNano()}) - mset.mu.Unlock() - } - } - // Proceed with proposing this message. // We only use mset.clseq for clustering and in case we run ahead of actual commits. // Check if we need to set initial value here mset.clMu.Lock() if mset.clseq == 0 || mset.clseq < lseq+mset.clfs { + // Need to unlock and re-acquire the locks in the proper order. + mset.clMu.Unlock() + // Locking order is stream -> batchMu -> clMu + mset.mu.RLock() + batch := mset.batchApply + var batchCount uint64 + if batch != nil { + batch.mu.Lock() + batchCount = batch.count + } + mset.clMu.Lock() // Re-capture - lseq = mset.lastSeq() - mset.clseq = lseq + mset.clfs + lseq = mset.lseq + mset.clseq = lseq + mset.clfs + batchCount + // Keep hold of the mset.clMu, but unlock the others. + if batch != nil { + batch.mu.Unlock() + } + mset.mu.RUnlock() } - // Check if we have an interest policy and discard new with max msgs or bytes. - // We need to deny here otherwise it could succeed on some peers and not others - // depending on consumer ack state. So we deny here, if we allow that means we know - // it would succeed on every peer. - if interestPolicy && discard == DiscardNew && (maxMsgs > 0 || maxBytes > 0) { - // Track inflight. - if mset.inflight == nil { - mset.inflight = make(map[uint64]uint64) - } - if stype == FileStorage { - mset.inflight[mset.clseq] = fileStoreMsgSize(subject, hdr, msg) - } else { - mset.inflight[mset.clseq] = memStoreMsgSize(subject, hdr, msg) - } - - var state StreamState - mset.store.FastState(&state) - - var err error - if maxMsgs > 0 && state.Msgs+uint64(len(mset.inflight)) > uint64(maxMsgs) { - err = ErrMaxMsgs - } else if maxBytes > 0 { - // TODO(dlc) - Could track this rollup independently. - var bytesPending uint64 - for _, nb := range mset.inflight { - bytesPending += nb - } - if state.Bytes+bytesPending > uint64(maxBytes) { - err = ErrMaxBytes - } - } - if err != nil { - delete(mset.inflight, mset.clseq) - mset.clMu.Unlock() - if canRespond { - var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.Error = NewJSStreamStoreFailedError(err, Unless(err)) - response, _ = json.Marshal(resp) - outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) - } + var ( + dseq uint64 + apiErr *ApiError + err error + ) + diff := &batchStagedDiff{} + if hdr, msg, dseq, apiErr, err = checkMsgHeadersPreClusteredProposal(diff, mset, subject, hdr, msg, sourced, name, jsa, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules, discard, discardNewPer, maxMsgSize, maxMsgs, maxMsgsPer, maxBytes); err != nil { + // TODO(mvv): reset in-memory expected header maps + mset.clMu.Unlock() + if err == errMsgIdDuplicate && dseq > 0 { + var buf [256]byte + pubAck := append(buf[:0], mset.pubAck...) + response = append(pubAck, strconv.FormatUint(dseq, 10)...) + response = append(response, ",\"duplicate\": true}"...) + outq.sendMsg(reply, response) return err } - } - - if len(hdr) > 0 { - // Expected last sequence per subject. - if seq, exists := getExpectedLastSeqPerSubject(hdr); exists && store != nil { - // Allow override of the subject used for the check. - seqSubj := subject - if optSubj := getExpectedLastSeqPerSubjectForSubject(hdr); optSubj != _EMPTY_ { - seqSubj = optSubj - } - - // If subject is already in process, block as otherwise we could have multiple messages inflight with same subject. - if _, found := mset.expectedPerSubjectInProcess[seqSubj]; found { - // Could have set inflight above, cleanup here. - delete(mset.inflight, mset.clseq) - mset.clMu.Unlock() - if canRespond { - var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.PubAck = &PubAck{Stream: name} - resp.Error = NewJSStreamWrongLastSequenceConstantError() - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - return fmt.Errorf("last sequence by subject mismatch") - } - - var smv StoreMsg - var fseq uint64 - sm, err := store.LoadLastMsg(seqSubj, &smv) - if sm != nil { - fseq = sm.seq - } - if err == ErrStoreMsgNotFound && seq == 0 { - fseq, err = 0, nil - } - if err != nil || fseq != seq { - // Could have set inflight above, cleanup here. - delete(mset.inflight, mset.clseq) - mset.clMu.Unlock() - if canRespond { - var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} - resp.PubAck = &PubAck{Stream: name} - resp.Error = NewJSStreamWrongLastSequenceError(fseq) - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - return fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, fseq) - } - - // Track sequence and subject. - if mset.expectedPerSubjectSequence == nil { - mset.expectedPerSubjectSequence = make(map[uint64]string) - } - if mset.expectedPerSubjectInProcess == nil { - mset.expectedPerSubjectInProcess = make(map[string]struct{}) - } - mset.expectedPerSubjectSequence[mset.clseq] = seqSubj - mset.expectedPerSubjectInProcess[seqSubj] = struct{}{} + if canRespond { + var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} + resp.Error = apiErr + response, _ = json.Marshal(resp) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) } + return err } + diff.commit(mset) esm := encodeStreamMsgAllowCompress(subject, reply, hdr, msg, mset.clseq, time.Now().UnixNano(), sourced) var mtKey uint64 if mt != nil { @@ -8650,22 +8957,11 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [ } // Do proposal. - err := node.Propose(esm) + err = node.Propose(esm) + // TODO(mvv): reset in-memory expected header maps, if err!=nil if err == nil { mset.clseq++ - // If we are using the system account for NRG, add in the extra sent msgs and bytes to our account - // so that the end user / account owner has visibility. - if node.IsSystemAccount() && mset.acc != nil && r > 1 { - outMsgs := int64(r - 1) - outBytes := int64(len(esm) * (r - 1)) - - mset.acc.stats.Lock() - mset.acc.stats.outMsgs += outMsgs - mset.acc.stats.outBytes += outBytes - mset.acc.stats.rt.outMsgs += outMsgs - mset.acc.stats.rt.outBytes += outBytes - mset.acc.stats.Unlock() - } + mset.trackReplicationTraffic(node, len(esm), r) } // Check to see if we are being overrun. @@ -9075,6 +9371,9 @@ RETRY: lseq := mset.lseq mset.mu.RUnlock() if lseq >= snap.LastSeq { + // We MUST ensure all data is flushed up to this point, if the store hadn't already. + // Because the snapshot needs to represent what has been persisted. + mset.flushAllPending() return nil } @@ -9103,7 +9402,7 @@ RETRY: } else if err == NewJSInsufficientResourcesError() { notifyLeaderStopCatchup(mrec, err) if mset.js.limitsExceeded(mset.cfg.Storage) { - s.resourcesExceededError() + s.resourcesExceededError(mset.cfg.Storage) } else { s.Warnf("Catchup for stream '%s > %s' errored, account resources exceeded: %v", mset.account(), mset.name(), err) } @@ -9201,7 +9500,6 @@ func (mset *stream) processCatchupMsg(msg []byte) (uint64, error) { mset.mu.Lock() st := mset.cfg.Storage - ddloaded := mset.ddloaded if mset.hasAllPreAcks(seq, subj) { mset.clearAllPreAcks(seq) // Mark this to be skipped @@ -9238,16 +9536,20 @@ func (mset *stream) processCatchupMsg(msg []byte) (uint64, error) { // Check for MsgId and if we have one here make sure to update our internal map. if len(hdr) > 0 { if msgId := getMsgId(hdr); msgId != _EMPTY_ { - if !ddloaded { - mset.rebuildDedupe() - } + mset.ddMu.Lock() mset.storeMsgIdLocked(&ddentry{msgId, seq, ts}) + mset.ddMu.Unlock() } } return seq, nil } +// flushAllPending will flush any pending writes as a result of installing a snapshot or performing catchup. +func (mset *stream) flushAllPending() { + mset.store.FlushAllPending() +} + func (mset *stream) handleClusterSyncRequest(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { var sreq streamSyncRequest if err := json.Unmarshal(msg, &sreq); err != nil { diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_errors_generated.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_errors_generated.go index 5cebda1f3..a1ca08cfd 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_errors_generated.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_errors_generated.go @@ -8,6 +8,24 @@ const ( // JSAccountResourcesExceededErr resource limits exceeded for account JSAccountResourcesExceededErr ErrorIdentifier = 10002 + // JSAtomicPublishDisabledErr atomic publish is disabled + JSAtomicPublishDisabledErr ErrorIdentifier = 10174 + + // JSAtomicPublishIncompleteBatchErr atomic publish batch is incomplete + JSAtomicPublishIncompleteBatchErr ErrorIdentifier = 10176 + + // JSAtomicPublishInvalidBatchIDErr atomic publish batch ID is invalid + JSAtomicPublishInvalidBatchIDErr ErrorIdentifier = 10179 + + // JSAtomicPublishMissingSeqErr atomic publish sequence is missing + JSAtomicPublishMissingSeqErr ErrorIdentifier = 10175 + + // JSAtomicPublishTooLargeBatchErrF atomic publish batch is too large: {size} + JSAtomicPublishTooLargeBatchErrF ErrorIdentifier = 10199 + + // JSAtomicPublishUnsupportedHeaderBatchErr atomic publish unsupported header used: {header} + JSAtomicPublishUnsupportedHeaderBatchErr ErrorIdentifier = 10177 + // JSBadRequestErr bad request JSBadRequestErr ErrorIdentifier = 10003 @@ -44,9 +62,18 @@ const ( // JSClusterUnSupportFeatureErr not currently supported in clustered mode JSClusterUnSupportFeatureErr ErrorIdentifier = 10036 + // JSConsumerAckPolicyInvalidErr consumer ack policy invalid + JSConsumerAckPolicyInvalidErr ErrorIdentifier = 10181 + + // JSConsumerAckWaitNegativeErr consumer ack wait needs to be positive + JSConsumerAckWaitNegativeErr ErrorIdentifier = 10183 + // JSConsumerAlreadyExists action CREATE is used for a existing consumer with a different config (consumer already exists) JSConsumerAlreadyExists ErrorIdentifier = 10148 + // JSConsumerBackOffNegativeErr consumer backoff needs to be positive + JSConsumerBackOffNegativeErr ErrorIdentifier = 10184 + // JSConsumerBadDurableNameErr durable name can not contain '.', '*', '>' JSConsumerBadDurableNameErr ErrorIdentifier = 10103 @@ -149,8 +176,8 @@ const ( // JSConsumerMaxRequestBatchNegativeErr consumer max request batch needs to be > 0 JSConsumerMaxRequestBatchNegativeErr ErrorIdentifier = 10114 - // JSConsumerMaxRequestExpiresToSmall consumer max request expires needs to be >= 1ms - JSConsumerMaxRequestExpiresToSmall ErrorIdentifier = 10115 + // JSConsumerMaxRequestExpiresTooSmall consumer max request expires needs to be >= 1ms + JSConsumerMaxRequestExpiresTooSmall ErrorIdentifier = 10115 // JSConsumerMaxWaitingNegativeErr consumer max waiting needs to be positive JSConsumerMaxWaitingNegativeErr ErrorIdentifier = 10087 @@ -185,6 +212,12 @@ const ( // JSConsumerOverlappingSubjectFilters consumer subject filters cannot overlap JSConsumerOverlappingSubjectFilters ErrorIdentifier = 10138 + // JSConsumerPinnedTTLWithoutPriorityPolicyNone PinnedTTL cannot be set when PriorityPolicy is none + JSConsumerPinnedTTLWithoutPriorityPolicyNone ErrorIdentifier = 10197 + + // JSConsumerPriorityGroupWithPolicyNone consumer can not have priority groups when policy is none + JSConsumerPriorityGroupWithPolicyNone ErrorIdentifier = 10196 + // JSConsumerPriorityPolicyWithoutGroup Setting PriorityPolicy requires at least one PriorityGroup to be set JSConsumerPriorityPolicyWithoutGroup ErrorIdentifier = 10159 @@ -206,6 +239,9 @@ const ( // JSConsumerReplacementWithDifferentNameErr consumer replacement durable config not the same JSConsumerReplacementWithDifferentNameErr ErrorIdentifier = 10106 + // JSConsumerReplayPolicyInvalidErr consumer replay policy invalid + JSConsumerReplayPolicyInvalidErr ErrorIdentifier = 10182 + // JSConsumerReplicasExceedsStream consumer config replica count exceeds parent stream JSConsumerReplicasExceedsStream ErrorIdentifier = 10126 @@ -248,6 +284,36 @@ const ( // JSMemoryResourcesExceededErr insufficient memory resources available JSMemoryResourcesExceededErr ErrorIdentifier = 10028 + // JSMessageCounterBrokenErr message counter is broken + JSMessageCounterBrokenErr ErrorIdentifier = 10172 + + // JSMessageIncrDisabledErr message counters is disabled + JSMessageIncrDisabledErr ErrorIdentifier = 10168 + + // JSMessageIncrInvalidErr message counter increment is invalid + JSMessageIncrInvalidErr ErrorIdentifier = 10171 + + // JSMessageIncrMissingErr message counter increment is missing + JSMessageIncrMissingErr ErrorIdentifier = 10169 + + // JSMessageIncrPayloadErr message counter has payload + JSMessageIncrPayloadErr ErrorIdentifier = 10170 + + // JSMessageSchedulesDisabledErr message schedules is disabled + JSMessageSchedulesDisabledErr ErrorIdentifier = 10188 + + // JSMessageSchedulesPatternInvalidErr message schedules pattern is invalid + JSMessageSchedulesPatternInvalidErr ErrorIdentifier = 10189 + + // JSMessageSchedulesRollupInvalidErr message schedules invalid rollup + JSMessageSchedulesRollupInvalidErr ErrorIdentifier = 10192 + + // JSMessageSchedulesTTLInvalidErr message schedules invalid per-message TTL + JSMessageSchedulesTTLInvalidErr ErrorIdentifier = 10191 + + // JSMessageSchedulesTargetInvalidErr message schedules target is invalid + JSMessageSchedulesTargetInvalidErr ErrorIdentifier = 10190 + // JSMessageTTLDisabledErr per-message TTL is disabled JSMessageTTLDisabledErr ErrorIdentifier = 10166 @@ -275,9 +341,18 @@ const ( // JSMirrorOverlappingSubjectFilters mirror subject filters can not overlap JSMirrorOverlappingSubjectFilters ErrorIdentifier = 10152 + // JSMirrorWithAtomicPublishErr stream mirrors can not also use atomic publishing + JSMirrorWithAtomicPublishErr ErrorIdentifier = 10198 + + // JSMirrorWithCountersErr stream mirrors can not also calculate counters + JSMirrorWithCountersErr ErrorIdentifier = 10173 + // JSMirrorWithFirstSeqErr stream mirrors can not have first sequence configured JSMirrorWithFirstSeqErr ErrorIdentifier = 10143 + // JSMirrorWithMsgSchedulesErr stream mirrors can not also schedule messages + JSMirrorWithMsgSchedulesErr ErrorIdentifier = 10186 + // JSMirrorWithSourcesErr stream mirrors can not also contain other sources JSMirrorWithSourcesErr ErrorIdentifier = 10031 @@ -320,6 +395,9 @@ const ( // JSReplicasCountCannotBeNegative replicas count cannot be negative JSReplicasCountCannotBeNegative ErrorIdentifier = 10133 + // JSRequiredApiLevelErr JetStream minimum api level required + JSRequiredApiLevelErr ErrorIdentifier = 10185 + // JSRestoreSubscribeFailedErrF JetStream unable to subscribe to restore snapshot {subject}: {err} JSRestoreSubscribeFailedErrF ErrorIdentifier = 10042 @@ -353,6 +431,9 @@ const ( // JSSourceOverlappingSubjectFilters source filters can not overlap JSSourceOverlappingSubjectFilters ErrorIdentifier = 10147 + // JSSourceWithMsgSchedulesErr stream source can not also schedule messages + JSSourceWithMsgSchedulesErr ErrorIdentifier = 10187 + // JSStorageResourcesExceededErr insufficient storage resources available JSStorageResourcesExceededErr ErrorIdentifier = 10047 @@ -368,6 +449,9 @@ const ( // JSStreamDuplicateMessageConflict duplicate message id is in process JSStreamDuplicateMessageConflict ErrorIdentifier = 10158 + // JSStreamExpectedLastSeqPerSubjectInvalid missing sequence for expected last sequence per subject + JSStreamExpectedLastSeqPerSubjectInvalid ErrorIdentifier = 10193 + // JSStreamExpectedLastSeqPerSubjectNotReady expected last sequence per subject temporarily unavailable JSStreamExpectedLastSeqPerSubjectNotReady ErrorIdentifier = 10163 @@ -407,6 +491,9 @@ const ( // JSStreamMessageExceedsMaximumErr message size exceeds maximum allowed JSStreamMessageExceedsMaximumErr ErrorIdentifier = 10054 + // JSStreamMinLastSeqErr min last sequence + JSStreamMinLastSeqErr ErrorIdentifier = 10180 + // JSStreamMirrorNotUpdatableErr stream mirror configuration can not be updated JSStreamMirrorNotUpdatableErr ErrorIdentifier = 10055 @@ -515,175 +602,204 @@ const ( var ( ApiErrors = map[ErrorIdentifier]*ApiError{ - JSAccountResourcesExceededErr: {Code: 400, ErrCode: 10002, Description: "resource limits exceeded for account"}, - JSBadRequestErr: {Code: 400, ErrCode: 10003, Description: "bad request"}, - JSClusterIncompleteErr: {Code: 503, ErrCode: 10004, Description: "incomplete results"}, - JSClusterNoPeersErrF: {Code: 400, ErrCode: 10005, Description: "{err}"}, - JSClusterNotActiveErr: {Code: 500, ErrCode: 10006, Description: "JetStream not in clustered mode"}, - JSClusterNotAssignedErr: {Code: 500, ErrCode: 10007, Description: "JetStream cluster not assigned to this server"}, - JSClusterNotAvailErr: {Code: 503, ErrCode: 10008, Description: "JetStream system temporarily unavailable"}, - JSClusterNotLeaderErr: {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"}, - JSClusterPeerNotMemberErr: {Code: 400, ErrCode: 10040, Description: "peer not a member"}, - JSClusterRequiredErr: {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"}, - JSClusterServerNotMemberErr: {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"}, - JSClusterTagsErr: {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"}, - JSClusterUnSupportFeatureErr: {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"}, - JSConsumerAlreadyExists: {Code: 400, ErrCode: 10148, Description: "consumer already exists"}, - JSConsumerBadDurableNameErr: {Code: 400, ErrCode: 10103, Description: "durable name can not contain '.', '*', '>'"}, - JSConsumerConfigRequiredErr: {Code: 400, ErrCode: 10078, Description: "consumer config required"}, - JSConsumerCreateDurableAndNameMismatch: {Code: 400, ErrCode: 10132, Description: "Consumer Durable and Name have to be equal if both are provided"}, - JSConsumerCreateErrF: {Code: 500, ErrCode: 10012, Description: "{err}"}, - JSConsumerCreateFilterSubjectMismatchErr: {Code: 400, ErrCode: 10131, Description: "Consumer create request did not match filtered subject from create subject"}, - JSConsumerDeliverCycleErr: {Code: 400, ErrCode: 10081, Description: "consumer deliver subject forms a cycle"}, - JSConsumerDeliverToWildcardsErr: {Code: 400, ErrCode: 10079, Description: "consumer deliver subject has wildcards"}, - JSConsumerDescriptionTooLongErrF: {Code: 400, ErrCode: 10107, Description: "consumer description is too long, maximum allowed is {max}"}, - JSConsumerDirectRequiresEphemeralErr: {Code: 400, ErrCode: 10091, Description: "consumer direct requires an ephemeral consumer"}, - JSConsumerDirectRequiresPushErr: {Code: 400, ErrCode: 10090, Description: "consumer direct requires a push based consumer"}, - JSConsumerDoesNotExist: {Code: 400, ErrCode: 10149, Description: "consumer does not exist"}, - JSConsumerDuplicateFilterSubjects: {Code: 400, ErrCode: 10136, Description: "consumer cannot have both FilterSubject and FilterSubjects specified"}, - JSConsumerDurableNameNotInSubjectErr: {Code: 400, ErrCode: 10016, Description: "consumer expected to be durable but no durable name set in subject"}, - JSConsumerDurableNameNotMatchSubjectErr: {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"}, - JSConsumerDurableNameNotSetErr: {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"}, - JSConsumerEmptyFilter: {Code: 400, ErrCode: 10139, Description: "consumer filter in FilterSubjects cannot be empty"}, - JSConsumerEmptyGroupName: {Code: 400, ErrCode: 10161, Description: "Group name cannot be an empty string"}, - JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"}, - JSConsumerEphemeralWithDurableNameErr: {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"}, - JSConsumerExistingActiveErr: {Code: 400, ErrCode: 10105, Description: "consumer already exists and is still active"}, - JSConsumerFCRequiresPushErr: {Code: 400, ErrCode: 10089, Description: "consumer flow control requires a push based consumer"}, - JSConsumerFilterNotSubsetErr: {Code: 400, ErrCode: 10093, Description: "consumer filter subject is not a valid subset of the interest subjects"}, - JSConsumerHBRequiresPushErr: {Code: 400, ErrCode: 10088, Description: "consumer idle heartbeat requires a push based consumer"}, - JSConsumerInactiveThresholdExcess: {Code: 400, ErrCode: 10153, Description: "consumer inactive threshold exceeds system limit of {limit}"}, - JSConsumerInvalidDeliverSubject: {Code: 400, ErrCode: 10112, Description: "invalid push consumer deliver subject"}, - JSConsumerInvalidGroupNameErr: {Code: 400, ErrCode: 10162, Description: "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters"}, - JSConsumerInvalidPolicyErrF: {Code: 400, ErrCode: 10094, Description: "{err}"}, - JSConsumerInvalidPriorityGroupErr: {Code: 400, ErrCode: 10160, Description: "Provided priority group does not exist for this consumer"}, - JSConsumerInvalidSamplingErrF: {Code: 400, ErrCode: 10095, Description: "failed to parse consumer sampling configuration: {err}"}, - JSConsumerMaxDeliverBackoffErr: {Code: 400, ErrCode: 10116, Description: "max deliver is required to be > length of backoff values"}, - JSConsumerMaxPendingAckExcessErrF: {Code: 400, ErrCode: 10121, Description: "consumer max ack pending exceeds system limit of {limit}"}, - JSConsumerMaxPendingAckPolicyRequiredErr: {Code: 400, ErrCode: 10082, Description: "consumer requires ack policy for max ack pending"}, - JSConsumerMaxRequestBatchExceededF: {Code: 400, ErrCode: 10125, Description: "consumer max request batch exceeds server limit of {limit}"}, - JSConsumerMaxRequestBatchNegativeErr: {Code: 400, ErrCode: 10114, Description: "consumer max request batch needs to be > 0"}, - JSConsumerMaxRequestExpiresToSmall: {Code: 400, ErrCode: 10115, Description: "consumer max request expires needs to be >= 1ms"}, - JSConsumerMaxWaitingNegativeErr: {Code: 400, ErrCode: 10087, Description: "consumer max waiting needs to be positive"}, - JSConsumerMetadataLengthErrF: {Code: 400, ErrCode: 10135, Description: "consumer metadata exceeds maximum size of {limit}"}, - JSConsumerMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10137, Description: "consumer with multiple subject filters cannot use subject based API"}, - JSConsumerNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10127, Description: "Consumer name can not contain path separators"}, - JSConsumerNameExistErr: {Code: 400, ErrCode: 10013, Description: "consumer name already in use"}, - JSConsumerNameTooLongErrF: {Code: 400, ErrCode: 10102, Description: "consumer name is too long, maximum allowed is {max}"}, - JSConsumerNotFoundErr: {Code: 404, ErrCode: 10014, Description: "consumer not found"}, - JSConsumerOfflineErr: {Code: 500, ErrCode: 10119, Description: "consumer is offline"}, - JSConsumerOfflineReasonErrF: {Code: 500, ErrCode: 10195, Description: "consumer is offline: {err}"}, - JSConsumerOnMappedErr: {Code: 400, ErrCode: 10092, Description: "consumer direct on a mapped consumer"}, - JSConsumerOverlappingSubjectFilters: {Code: 400, ErrCode: 10138, Description: "consumer subject filters cannot overlap"}, - JSConsumerPriorityPolicyWithoutGroup: {Code: 400, ErrCode: 10159, Description: "Setting PriorityPolicy requires at least one PriorityGroup to be set"}, - JSConsumerPullNotDurableErr: {Code: 400, ErrCode: 10085, Description: "consumer in pull mode requires a durable name"}, - JSConsumerPullRequiresAckErr: {Code: 400, ErrCode: 10084, Description: "consumer in pull mode requires explicit ack policy on workqueue stream"}, - JSConsumerPullWithRateLimitErr: {Code: 400, ErrCode: 10086, Description: "consumer in pull mode can not have rate limit set"}, - JSConsumerPushMaxWaitingErr: {Code: 400, ErrCode: 10080, Description: "consumer in push mode can not set max waiting"}, - JSConsumerPushWithPriorityGroupErr: {Code: 400, ErrCode: 10178, Description: "priority groups can not be used with push consumers"}, - JSConsumerReplacementWithDifferentNameErr: {Code: 400, ErrCode: 10106, Description: "consumer replacement durable config not the same"}, - JSConsumerReplicasExceedsStream: {Code: 400, ErrCode: 10126, Description: "consumer config replica count exceeds parent stream"}, - JSConsumerReplicasShouldMatchStream: {Code: 400, ErrCode: 10134, Description: "consumer config replicas must match interest retention stream's replicas"}, - JSConsumerSmallHeartbeatErr: {Code: 400, ErrCode: 10083, Description: "consumer idle heartbeat needs to be >= 100ms"}, - JSConsumerStoreFailedErrF: {Code: 500, ErrCode: 10104, Description: "error creating store for consumer: {err}"}, - JSConsumerWQConsumerNotDeliverAllErr: {Code: 400, ErrCode: 10101, Description: "consumer must be deliver all on workqueue stream"}, - JSConsumerWQConsumerNotUniqueErr: {Code: 400, ErrCode: 10100, Description: "filtered consumer not unique on workqueue stream"}, - JSConsumerWQMultipleUnfilteredErr: {Code: 400, ErrCode: 10099, Description: "multiple non-filtered consumers not allowed on workqueue stream"}, - JSConsumerWQRequiresExplicitAckErr: {Code: 400, ErrCode: 10098, Description: "workqueue stream requires explicit ack"}, - JSConsumerWithFlowControlNeedsHeartbeats: {Code: 400, ErrCode: 10108, Description: "consumer with flow control also needs heartbeats"}, - JSInsufficientResourcesErr: {Code: 503, ErrCode: 10023, Description: "insufficient resources"}, - JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON: {err}"}, - JSMaximumConsumersLimitErr: {Code: 400, ErrCode: 10026, Description: "maximum consumers limit reached"}, - JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"}, - JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"}, - JSMessageTTLDisabledErr: {Code: 400, ErrCode: 10166, Description: "per-message TTL is disabled"}, - JSMessageTTLInvalidErr: {Code: 400, ErrCode: 10165, Description: "invalid per-message TTL"}, - JSMirrorConsumerSetupFailedErrF: {Code: 500, ErrCode: 10029, Description: "{err}"}, - JSMirrorInvalidStreamName: {Code: 400, ErrCode: 10142, Description: "mirrored stream name is invalid"}, - JSMirrorInvalidSubjectFilter: {Code: 400, ErrCode: 10151, Description: "mirror transform source: {err}"}, - JSMirrorInvalidTransformDestination: {Code: 400, ErrCode: 10154, Description: "mirror transform: {err}"}, - JSMirrorMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"}, - JSMirrorMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10150, Description: "mirror with multiple subject transforms cannot also have a single subject filter"}, - JSMirrorOverlappingSubjectFilters: {Code: 400, ErrCode: 10152, Description: "mirror subject filters can not overlap"}, - JSMirrorWithFirstSeqErr: {Code: 400, ErrCode: 10143, Description: "stream mirrors can not have first sequence configured"}, - JSMirrorWithSourcesErr: {Code: 400, ErrCode: 10031, Description: "stream mirrors can not also contain other sources"}, - JSMirrorWithStartSeqAndTimeErr: {Code: 400, ErrCode: 10032, Description: "stream mirrors can not have both start seq and start time configured"}, - JSMirrorWithSubjectFiltersErr: {Code: 400, ErrCode: 10033, Description: "stream mirrors can not contain filtered subjects"}, - JSMirrorWithSubjectsErr: {Code: 400, ErrCode: 10034, Description: "stream mirrors can not contain subjects"}, - JSNoAccountErr: {Code: 503, ErrCode: 10035, Description: "account not found"}, - JSNoLimitsErr: {Code: 400, ErrCode: 10120, Description: "no JetStream default or applicable tiered limit present"}, - JSNoMessageFoundErr: {Code: 404, ErrCode: 10037, Description: "no message found"}, - JSNotEmptyRequestErr: {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"}, - JSNotEnabledErr: {Code: 503, ErrCode: 10076, Description: "JetStream not enabled"}, - JSNotEnabledForAccountErr: {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account"}, - JSPedanticErrF: {Code: 400, ErrCode: 10157, Description: "pedantic mode: {err}"}, - JSPeerRemapErr: {Code: 503, ErrCode: 10075, Description: "peer remap failed"}, - JSRaftGeneralErrF: {Code: 500, ErrCode: 10041, Description: "{err}"}, - JSReplicasCountCannotBeNegative: {Code: 400, ErrCode: 10133, Description: "replicas count cannot be negative"}, - JSRestoreSubscribeFailedErrF: {Code: 500, ErrCode: 10042, Description: "JetStream unable to subscribe to restore snapshot {subject}: {err}"}, - JSSequenceNotFoundErrF: {Code: 400, ErrCode: 10043, Description: "sequence {seq} not found"}, - JSSnapshotDeliverSubjectInvalidErr: {Code: 400, ErrCode: 10015, Description: "deliver subject not valid"}, - JSSourceConsumerSetupFailedErrF: {Code: 500, ErrCode: 10045, Description: "{err}"}, - JSSourceDuplicateDetected: {Code: 400, ErrCode: 10140, Description: "duplicate source configuration detected"}, - JSSourceInvalidStreamName: {Code: 400, ErrCode: 10141, Description: "sourced stream name is invalid"}, - JSSourceInvalidSubjectFilter: {Code: 400, ErrCode: 10145, Description: "source transform source: {err}"}, - JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform: {err}"}, - JSSourceMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"}, - JSSourceMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10144, Description: "source with multiple subject transforms cannot also have a single subject filter"}, - JSSourceOverlappingSubjectFilters: {Code: 400, ErrCode: 10147, Description: "source filters can not overlap"}, - JSStorageResourcesExceededErr: {Code: 500, ErrCode: 10047, Description: "insufficient storage resources available"}, - JSStreamAssignmentErrF: {Code: 500, ErrCode: 10048, Description: "{err}"}, - JSStreamCreateErrF: {Code: 500, ErrCode: 10049, Description: "{err}"}, - JSStreamDeleteErrF: {Code: 500, ErrCode: 10050, Description: "{err}"}, - JSStreamDuplicateMessageConflict: {Code: 409, ErrCode: 10158, Description: "duplicate message id is in process"}, - JSStreamExpectedLastSeqPerSubjectNotReady: {Code: 503, ErrCode: 10163, Description: "expected last sequence per subject temporarily unavailable"}, - JSStreamExternalApiOverlapErrF: {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"}, - JSStreamExternalDelPrefixOverlapsErrF: {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"}, - JSStreamGeneralErrorF: {Code: 500, ErrCode: 10051, Description: "{err}"}, - JSStreamHeaderExceedsMaximumErr: {Code: 400, ErrCode: 10097, Description: "header size exceeds maximum allowed of 64k"}, - JSStreamInfoMaxSubjectsErr: {Code: 500, ErrCode: 10117, Description: "subject details would exceed maximum allowed"}, - JSStreamInvalidConfigF: {Code: 500, ErrCode: 10052, Description: "{err}"}, - JSStreamInvalidErr: {Code: 500, ErrCode: 10096, Description: "stream not valid"}, - JSStreamInvalidExternalDeliverySubjErrF: {Code: 400, ErrCode: 10024, Description: "stream external delivery prefix {prefix} must not contain wildcards"}, - JSStreamLimitsErrF: {Code: 500, ErrCode: 10053, Description: "{err}"}, - JSStreamMaxBytesRequired: {Code: 400, ErrCode: 10113, Description: "account requires a stream config to have max bytes set"}, - JSStreamMaxStreamBytesExceeded: {Code: 400, ErrCode: 10122, Description: "stream max bytes exceeds account limit max stream bytes"}, - JSStreamMessageExceedsMaximumErr: {Code: 400, ErrCode: 10054, Description: "message size exceeds maximum allowed"}, - JSStreamMirrorNotUpdatableErr: {Code: 400, ErrCode: 10055, Description: "stream mirror configuration can not be updated"}, - JSStreamMismatchErr: {Code: 400, ErrCode: 10056, Description: "stream name in subject does not match request"}, - JSStreamMoveAndScaleErr: {Code: 400, ErrCode: 10123, Description: "can not move and scale a stream in a single update"}, - JSStreamMoveInProgressF: {Code: 400, ErrCode: 10124, Description: "stream move already in progress: {msg}"}, - JSStreamMoveNotInProgress: {Code: 400, ErrCode: 10129, Description: "stream move not in progress"}, - JSStreamMsgDeleteFailedF: {Code: 500, ErrCode: 10057, Description: "{err}"}, - JSStreamNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10128, Description: "Stream name can not contain path separators"}, - JSStreamNameExistErr: {Code: 400, ErrCode: 10058, Description: "stream name already in use with a different configuration"}, - JSStreamNameExistRestoreFailedErr: {Code: 400, ErrCode: 10130, Description: "stream name already in use, cannot restore"}, - JSStreamNotFoundErr: {Code: 404, ErrCode: 10059, Description: "stream not found"}, - JSStreamNotMatchErr: {Code: 400, ErrCode: 10060, Description: "expected stream does not match"}, - JSStreamOfflineErr: {Code: 500, ErrCode: 10118, Description: "stream is offline"}, - JSStreamOfflineReasonErrF: {Code: 500, ErrCode: 10194, Description: "stream is offline: {err}"}, - JSStreamPurgeFailedF: {Code: 500, ErrCode: 10110, Description: "{err}"}, - JSStreamReplicasNotSupportedErr: {Code: 500, ErrCode: 10074, Description: "replicas > 1 not supported in non-clustered mode"}, - JSStreamReplicasNotUpdatableErr: {Code: 400, ErrCode: 10061, Description: "Replicas configuration can not be updated"}, - JSStreamRestoreErrF: {Code: 500, ErrCode: 10062, Description: "restore failed: {err}"}, - JSStreamRollupFailedF: {Code: 500, ErrCode: 10111, Description: "{err}"}, - JSStreamSealedErr: {Code: 400, ErrCode: 10109, Description: "invalid operation on sealed stream"}, - JSStreamSequenceNotMatchErr: {Code: 503, ErrCode: 10063, Description: "expected stream sequence does not match"}, - JSStreamSnapshotErrF: {Code: 500, ErrCode: 10064, Description: "snapshot failed: {err}"}, - JSStreamStoreFailedF: {Code: 503, ErrCode: 10077, Description: "{err}"}, - JSStreamSubjectOverlapErr: {Code: 400, ErrCode: 10065, Description: "subjects overlap with an existing stream"}, - JSStreamTemplateCreateErrF: {Code: 500, ErrCode: 10066, Description: "{err}"}, - JSStreamTemplateDeleteErrF: {Code: 500, ErrCode: 10067, Description: "{err}"}, - JSStreamTemplateNotFoundErr: {Code: 404, ErrCode: 10068, Description: "template not found"}, - JSStreamTooManyRequests: {Code: 429, ErrCode: 10167, Description: "too many requests"}, - JSStreamTransformInvalidDestination: {Code: 400, ErrCode: 10156, Description: "stream transform: {err}"}, - JSStreamTransformInvalidSource: {Code: 400, ErrCode: 10155, Description: "stream transform source: {err}"}, - JSStreamUpdateErrF: {Code: 500, ErrCode: 10069, Description: "{err}"}, - JSStreamWrongLastMsgIDErrF: {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"}, - JSStreamWrongLastSequenceConstantErr: {Code: 400, ErrCode: 10164, Description: "wrong last sequence"}, - JSStreamWrongLastSequenceErrF: {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"}, - JSTempStorageFailedErr: {Code: 500, ErrCode: 10072, Description: "JetStream unable to open temp storage for restore"}, - JSTemplateNameNotMatchSubjectErr: {Code: 400, ErrCode: 10073, Description: "template name in subject does not match request"}, + JSAccountResourcesExceededErr: {Code: 400, ErrCode: 10002, Description: "resource limits exceeded for account"}, + JSAtomicPublishDisabledErr: {Code: 400, ErrCode: 10174, Description: "atomic publish is disabled"}, + JSAtomicPublishIncompleteBatchErr: {Code: 400, ErrCode: 10176, Description: "atomic publish batch is incomplete"}, + JSAtomicPublishInvalidBatchIDErr: {Code: 400, ErrCode: 10179, Description: "atomic publish batch ID is invalid"}, + JSAtomicPublishMissingSeqErr: {Code: 400, ErrCode: 10175, Description: "atomic publish sequence is missing"}, + JSAtomicPublishTooLargeBatchErrF: {Code: 400, ErrCode: 10199, Description: "atomic publish batch is too large: {size}"}, + JSAtomicPublishUnsupportedHeaderBatchErr: {Code: 400, ErrCode: 10177, Description: "atomic publish unsupported header used: {header}"}, + JSBadRequestErr: {Code: 400, ErrCode: 10003, Description: "bad request"}, + JSClusterIncompleteErr: {Code: 503, ErrCode: 10004, Description: "incomplete results"}, + JSClusterNoPeersErrF: {Code: 400, ErrCode: 10005, Description: "{err}"}, + JSClusterNotActiveErr: {Code: 500, ErrCode: 10006, Description: "JetStream not in clustered mode"}, + JSClusterNotAssignedErr: {Code: 500, ErrCode: 10007, Description: "JetStream cluster not assigned to this server"}, + JSClusterNotAvailErr: {Code: 503, ErrCode: 10008, Description: "JetStream system temporarily unavailable"}, + JSClusterNotLeaderErr: {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"}, + JSClusterPeerNotMemberErr: {Code: 400, ErrCode: 10040, Description: "peer not a member"}, + JSClusterRequiredErr: {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"}, + JSClusterServerNotMemberErr: {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"}, + JSClusterTagsErr: {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"}, + JSClusterUnSupportFeatureErr: {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"}, + JSConsumerAckPolicyInvalidErr: {Code: 400, ErrCode: 10181, Description: "consumer ack policy invalid"}, + JSConsumerAckWaitNegativeErr: {Code: 400, ErrCode: 10183, Description: "consumer ack wait needs to be positive"}, + JSConsumerAlreadyExists: {Code: 400, ErrCode: 10148, Description: "consumer already exists"}, + JSConsumerBackOffNegativeErr: {Code: 400, ErrCode: 10184, Description: "consumer backoff needs to be positive"}, + JSConsumerBadDurableNameErr: {Code: 400, ErrCode: 10103, Description: "durable name can not contain '.', '*', '>'"}, + JSConsumerConfigRequiredErr: {Code: 400, ErrCode: 10078, Description: "consumer config required"}, + JSConsumerCreateDurableAndNameMismatch: {Code: 400, ErrCode: 10132, Description: "Consumer Durable and Name have to be equal if both are provided"}, + JSConsumerCreateErrF: {Code: 500, ErrCode: 10012, Description: "{err}"}, + JSConsumerCreateFilterSubjectMismatchErr: {Code: 400, ErrCode: 10131, Description: "Consumer create request did not match filtered subject from create subject"}, + JSConsumerDeliverCycleErr: {Code: 400, ErrCode: 10081, Description: "consumer deliver subject forms a cycle"}, + JSConsumerDeliverToWildcardsErr: {Code: 400, ErrCode: 10079, Description: "consumer deliver subject has wildcards"}, + JSConsumerDescriptionTooLongErrF: {Code: 400, ErrCode: 10107, Description: "consumer description is too long, maximum allowed is {max}"}, + JSConsumerDirectRequiresEphemeralErr: {Code: 400, ErrCode: 10091, Description: "consumer direct requires an ephemeral consumer"}, + JSConsumerDirectRequiresPushErr: {Code: 400, ErrCode: 10090, Description: "consumer direct requires a push based consumer"}, + JSConsumerDoesNotExist: {Code: 400, ErrCode: 10149, Description: "consumer does not exist"}, + JSConsumerDuplicateFilterSubjects: {Code: 400, ErrCode: 10136, Description: "consumer cannot have both FilterSubject and FilterSubjects specified"}, + JSConsumerDurableNameNotInSubjectErr: {Code: 400, ErrCode: 10016, Description: "consumer expected to be durable but no durable name set in subject"}, + JSConsumerDurableNameNotMatchSubjectErr: {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"}, + JSConsumerDurableNameNotSetErr: {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"}, + JSConsumerEmptyFilter: {Code: 400, ErrCode: 10139, Description: "consumer filter in FilterSubjects cannot be empty"}, + JSConsumerEmptyGroupName: {Code: 400, ErrCode: 10161, Description: "Group name cannot be an empty string"}, + JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"}, + JSConsumerEphemeralWithDurableNameErr: {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"}, + JSConsumerExistingActiveErr: {Code: 400, ErrCode: 10105, Description: "consumer already exists and is still active"}, + JSConsumerFCRequiresPushErr: {Code: 400, ErrCode: 10089, Description: "consumer flow control requires a push based consumer"}, + JSConsumerFilterNotSubsetErr: {Code: 400, ErrCode: 10093, Description: "consumer filter subject is not a valid subset of the interest subjects"}, + JSConsumerHBRequiresPushErr: {Code: 400, ErrCode: 10088, Description: "consumer idle heartbeat requires a push based consumer"}, + JSConsumerInactiveThresholdExcess: {Code: 400, ErrCode: 10153, Description: "consumer inactive threshold exceeds system limit of {limit}"}, + JSConsumerInvalidDeliverSubject: {Code: 400, ErrCode: 10112, Description: "invalid push consumer deliver subject"}, + JSConsumerInvalidGroupNameErr: {Code: 400, ErrCode: 10162, Description: "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters"}, + JSConsumerInvalidPolicyErrF: {Code: 400, ErrCode: 10094, Description: "{err}"}, + JSConsumerInvalidPriorityGroupErr: {Code: 400, ErrCode: 10160, Description: "Provided priority group does not exist for this consumer"}, + JSConsumerInvalidSamplingErrF: {Code: 400, ErrCode: 10095, Description: "failed to parse consumer sampling configuration: {err}"}, + JSConsumerMaxDeliverBackoffErr: {Code: 400, ErrCode: 10116, Description: "max deliver is required to be > length of backoff values"}, + JSConsumerMaxPendingAckExcessErrF: {Code: 400, ErrCode: 10121, Description: "consumer max ack pending exceeds system limit of {limit}"}, + JSConsumerMaxPendingAckPolicyRequiredErr: {Code: 400, ErrCode: 10082, Description: "consumer requires ack policy for max ack pending"}, + JSConsumerMaxRequestBatchExceededF: {Code: 400, ErrCode: 10125, Description: "consumer max request batch exceeds server limit of {limit}"}, + JSConsumerMaxRequestBatchNegativeErr: {Code: 400, ErrCode: 10114, Description: "consumer max request batch needs to be > 0"}, + JSConsumerMaxRequestExpiresTooSmall: {Code: 400, ErrCode: 10115, Description: "consumer max request expires needs to be >= 1ms"}, + JSConsumerMaxWaitingNegativeErr: {Code: 400, ErrCode: 10087, Description: "consumer max waiting needs to be positive"}, + JSConsumerMetadataLengthErrF: {Code: 400, ErrCode: 10135, Description: "consumer metadata exceeds maximum size of {limit}"}, + JSConsumerMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10137, Description: "consumer with multiple subject filters cannot use subject based API"}, + JSConsumerNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10127, Description: "Consumer name can not contain path separators"}, + JSConsumerNameExistErr: {Code: 400, ErrCode: 10013, Description: "consumer name already in use"}, + JSConsumerNameTooLongErrF: {Code: 400, ErrCode: 10102, Description: "consumer name is too long, maximum allowed is {max}"}, + JSConsumerNotFoundErr: {Code: 404, ErrCode: 10014, Description: "consumer not found"}, + JSConsumerOfflineErr: {Code: 500, ErrCode: 10119, Description: "consumer is offline"}, + JSConsumerOfflineReasonErrF: {Code: 500, ErrCode: 10195, Description: "consumer is offline: {err}"}, + JSConsumerOnMappedErr: {Code: 400, ErrCode: 10092, Description: "consumer direct on a mapped consumer"}, + JSConsumerOverlappingSubjectFilters: {Code: 400, ErrCode: 10138, Description: "consumer subject filters cannot overlap"}, + JSConsumerPinnedTTLWithoutPriorityPolicyNone: {Code: 400, ErrCode: 10197, Description: "PinnedTTL cannot be set when PriorityPolicy is none"}, + JSConsumerPriorityGroupWithPolicyNone: {Code: 400, ErrCode: 10196, Description: "consumer can not have priority groups when policy is none"}, + JSConsumerPriorityPolicyWithoutGroup: {Code: 400, ErrCode: 10159, Description: "Setting PriorityPolicy requires at least one PriorityGroup to be set"}, + JSConsumerPullNotDurableErr: {Code: 400, ErrCode: 10085, Description: "consumer in pull mode requires a durable name"}, + JSConsumerPullRequiresAckErr: {Code: 400, ErrCode: 10084, Description: "consumer in pull mode requires explicit ack policy on workqueue stream"}, + JSConsumerPullWithRateLimitErr: {Code: 400, ErrCode: 10086, Description: "consumer in pull mode can not have rate limit set"}, + JSConsumerPushMaxWaitingErr: {Code: 400, ErrCode: 10080, Description: "consumer in push mode can not set max waiting"}, + JSConsumerPushWithPriorityGroupErr: {Code: 400, ErrCode: 10178, Description: "priority groups can not be used with push consumers"}, + JSConsumerReplacementWithDifferentNameErr: {Code: 400, ErrCode: 10106, Description: "consumer replacement durable config not the same"}, + JSConsumerReplayPolicyInvalidErr: {Code: 400, ErrCode: 10182, Description: "consumer replay policy invalid"}, + JSConsumerReplicasExceedsStream: {Code: 400, ErrCode: 10126, Description: "consumer config replica count exceeds parent stream"}, + JSConsumerReplicasShouldMatchStream: {Code: 400, ErrCode: 10134, Description: "consumer config replicas must match interest retention stream's replicas"}, + JSConsumerSmallHeartbeatErr: {Code: 400, ErrCode: 10083, Description: "consumer idle heartbeat needs to be >= 100ms"}, + JSConsumerStoreFailedErrF: {Code: 500, ErrCode: 10104, Description: "error creating store for consumer: {err}"}, + JSConsumerWQConsumerNotDeliverAllErr: {Code: 400, ErrCode: 10101, Description: "consumer must be deliver all on workqueue stream"}, + JSConsumerWQConsumerNotUniqueErr: {Code: 400, ErrCode: 10100, Description: "filtered consumer not unique on workqueue stream"}, + JSConsumerWQMultipleUnfilteredErr: {Code: 400, ErrCode: 10099, Description: "multiple non-filtered consumers not allowed on workqueue stream"}, + JSConsumerWQRequiresExplicitAckErr: {Code: 400, ErrCode: 10098, Description: "workqueue stream requires explicit ack"}, + JSConsumerWithFlowControlNeedsHeartbeats: {Code: 400, ErrCode: 10108, Description: "consumer with flow control also needs heartbeats"}, + JSInsufficientResourcesErr: {Code: 503, ErrCode: 10023, Description: "insufficient resources"}, + JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON: {err}"}, + JSMaximumConsumersLimitErr: {Code: 400, ErrCode: 10026, Description: "maximum consumers limit reached"}, + JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"}, + JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"}, + JSMessageCounterBrokenErr: {Code: 400, ErrCode: 10172, Description: "message counter is broken"}, + JSMessageIncrDisabledErr: {Code: 400, ErrCode: 10168, Description: "message counters is disabled"}, + JSMessageIncrInvalidErr: {Code: 400, ErrCode: 10171, Description: "message counter increment is invalid"}, + JSMessageIncrMissingErr: {Code: 400, ErrCode: 10169, Description: "message counter increment is missing"}, + JSMessageIncrPayloadErr: {Code: 400, ErrCode: 10170, Description: "message counter has payload"}, + JSMessageSchedulesDisabledErr: {Code: 400, ErrCode: 10188, Description: "message schedules is disabled"}, + JSMessageSchedulesPatternInvalidErr: {Code: 400, ErrCode: 10189, Description: "message schedules pattern is invalid"}, + JSMessageSchedulesRollupInvalidErr: {Code: 400, ErrCode: 10192, Description: "message schedules invalid rollup"}, + JSMessageSchedulesTTLInvalidErr: {Code: 400, ErrCode: 10191, Description: "message schedules invalid per-message TTL"}, + JSMessageSchedulesTargetInvalidErr: {Code: 400, ErrCode: 10190, Description: "message schedules target is invalid"}, + JSMessageTTLDisabledErr: {Code: 400, ErrCode: 10166, Description: "per-message TTL is disabled"}, + JSMessageTTLInvalidErr: {Code: 400, ErrCode: 10165, Description: "invalid per-message TTL"}, + JSMirrorConsumerSetupFailedErrF: {Code: 500, ErrCode: 10029, Description: "{err}"}, + JSMirrorInvalidStreamName: {Code: 400, ErrCode: 10142, Description: "mirrored stream name is invalid"}, + JSMirrorInvalidSubjectFilter: {Code: 400, ErrCode: 10151, Description: "mirror transform source: {err}"}, + JSMirrorInvalidTransformDestination: {Code: 400, ErrCode: 10154, Description: "mirror transform: {err}"}, + JSMirrorMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"}, + JSMirrorMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10150, Description: "mirror with multiple subject transforms cannot also have a single subject filter"}, + JSMirrorOverlappingSubjectFilters: {Code: 400, ErrCode: 10152, Description: "mirror subject filters can not overlap"}, + JSMirrorWithAtomicPublishErr: {Code: 400, ErrCode: 10198, Description: "stream mirrors can not also use atomic publishing"}, + JSMirrorWithCountersErr: {Code: 400, ErrCode: 10173, Description: "stream mirrors can not also calculate counters"}, + JSMirrorWithFirstSeqErr: {Code: 400, ErrCode: 10143, Description: "stream mirrors can not have first sequence configured"}, + JSMirrorWithMsgSchedulesErr: {Code: 400, ErrCode: 10186, Description: "stream mirrors can not also schedule messages"}, + JSMirrorWithSourcesErr: {Code: 400, ErrCode: 10031, Description: "stream mirrors can not also contain other sources"}, + JSMirrorWithStartSeqAndTimeErr: {Code: 400, ErrCode: 10032, Description: "stream mirrors can not have both start seq and start time configured"}, + JSMirrorWithSubjectFiltersErr: {Code: 400, ErrCode: 10033, Description: "stream mirrors can not contain filtered subjects"}, + JSMirrorWithSubjectsErr: {Code: 400, ErrCode: 10034, Description: "stream mirrors can not contain subjects"}, + JSNoAccountErr: {Code: 503, ErrCode: 10035, Description: "account not found"}, + JSNoLimitsErr: {Code: 400, ErrCode: 10120, Description: "no JetStream default or applicable tiered limit present"}, + JSNoMessageFoundErr: {Code: 404, ErrCode: 10037, Description: "no message found"}, + JSNotEmptyRequestErr: {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"}, + JSNotEnabledErr: {Code: 503, ErrCode: 10076, Description: "JetStream not enabled"}, + JSNotEnabledForAccountErr: {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account"}, + JSPedanticErrF: {Code: 400, ErrCode: 10157, Description: "pedantic mode: {err}"}, + JSPeerRemapErr: {Code: 503, ErrCode: 10075, Description: "peer remap failed"}, + JSRaftGeneralErrF: {Code: 500, ErrCode: 10041, Description: "{err}"}, + JSReplicasCountCannotBeNegative: {Code: 400, ErrCode: 10133, Description: "replicas count cannot be negative"}, + JSRequiredApiLevelErr: {Code: 412, ErrCode: 10185, Description: "JetStream minimum api level required"}, + JSRestoreSubscribeFailedErrF: {Code: 500, ErrCode: 10042, Description: "JetStream unable to subscribe to restore snapshot {subject}: {err}"}, + JSSequenceNotFoundErrF: {Code: 400, ErrCode: 10043, Description: "sequence {seq} not found"}, + JSSnapshotDeliverSubjectInvalidErr: {Code: 400, ErrCode: 10015, Description: "deliver subject not valid"}, + JSSourceConsumerSetupFailedErrF: {Code: 500, ErrCode: 10045, Description: "{err}"}, + JSSourceDuplicateDetected: {Code: 400, ErrCode: 10140, Description: "duplicate source configuration detected"}, + JSSourceInvalidStreamName: {Code: 400, ErrCode: 10141, Description: "sourced stream name is invalid"}, + JSSourceInvalidSubjectFilter: {Code: 400, ErrCode: 10145, Description: "source transform source: {err}"}, + JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform: {err}"}, + JSSourceMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"}, + JSSourceMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10144, Description: "source with multiple subject transforms cannot also have a single subject filter"}, + JSSourceOverlappingSubjectFilters: {Code: 400, ErrCode: 10147, Description: "source filters can not overlap"}, + JSSourceWithMsgSchedulesErr: {Code: 400, ErrCode: 10187, Description: "stream source can not also schedule messages"}, + JSStorageResourcesExceededErr: {Code: 500, ErrCode: 10047, Description: "insufficient storage resources available"}, + JSStreamAssignmentErrF: {Code: 500, ErrCode: 10048, Description: "{err}"}, + JSStreamCreateErrF: {Code: 500, ErrCode: 10049, Description: "{err}"}, + JSStreamDeleteErrF: {Code: 500, ErrCode: 10050, Description: "{err}"}, + JSStreamDuplicateMessageConflict: {Code: 409, ErrCode: 10158, Description: "duplicate message id is in process"}, + JSStreamExpectedLastSeqPerSubjectInvalid: {Code: 400, ErrCode: 10193, Description: "missing sequence for expected last sequence per subject"}, + JSStreamExpectedLastSeqPerSubjectNotReady: {Code: 503, ErrCode: 10163, Description: "expected last sequence per subject temporarily unavailable"}, + JSStreamExternalApiOverlapErrF: {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"}, + JSStreamExternalDelPrefixOverlapsErrF: {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"}, + JSStreamGeneralErrorF: {Code: 500, ErrCode: 10051, Description: "{err}"}, + JSStreamHeaderExceedsMaximumErr: {Code: 400, ErrCode: 10097, Description: "header size exceeds maximum allowed of 64k"}, + JSStreamInfoMaxSubjectsErr: {Code: 500, ErrCode: 10117, Description: "subject details would exceed maximum allowed"}, + JSStreamInvalidConfigF: {Code: 500, ErrCode: 10052, Description: "{err}"}, + JSStreamInvalidErr: {Code: 500, ErrCode: 10096, Description: "stream not valid"}, + JSStreamInvalidExternalDeliverySubjErrF: {Code: 400, ErrCode: 10024, Description: "stream external delivery prefix {prefix} must not contain wildcards"}, + JSStreamLimitsErrF: {Code: 500, ErrCode: 10053, Description: "{err}"}, + JSStreamMaxBytesRequired: {Code: 400, ErrCode: 10113, Description: "account requires a stream config to have max bytes set"}, + JSStreamMaxStreamBytesExceeded: {Code: 400, ErrCode: 10122, Description: "stream max bytes exceeds account limit max stream bytes"}, + JSStreamMessageExceedsMaximumErr: {Code: 400, ErrCode: 10054, Description: "message size exceeds maximum allowed"}, + JSStreamMinLastSeqErr: {Code: 412, ErrCode: 10180, Description: "min last sequence"}, + JSStreamMirrorNotUpdatableErr: {Code: 400, ErrCode: 10055, Description: "stream mirror configuration can not be updated"}, + JSStreamMismatchErr: {Code: 400, ErrCode: 10056, Description: "stream name in subject does not match request"}, + JSStreamMoveAndScaleErr: {Code: 400, ErrCode: 10123, Description: "can not move and scale a stream in a single update"}, + JSStreamMoveInProgressF: {Code: 400, ErrCode: 10124, Description: "stream move already in progress: {msg}"}, + JSStreamMoveNotInProgress: {Code: 400, ErrCode: 10129, Description: "stream move not in progress"}, + JSStreamMsgDeleteFailedF: {Code: 500, ErrCode: 10057, Description: "{err}"}, + JSStreamNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10128, Description: "Stream name can not contain path separators"}, + JSStreamNameExistErr: {Code: 400, ErrCode: 10058, Description: "stream name already in use with a different configuration"}, + JSStreamNameExistRestoreFailedErr: {Code: 400, ErrCode: 10130, Description: "stream name already in use, cannot restore"}, + JSStreamNotFoundErr: {Code: 404, ErrCode: 10059, Description: "stream not found"}, + JSStreamNotMatchErr: {Code: 400, ErrCode: 10060, Description: "expected stream does not match"}, + JSStreamOfflineErr: {Code: 500, ErrCode: 10118, Description: "stream is offline"}, + JSStreamOfflineReasonErrF: {Code: 500, ErrCode: 10194, Description: "stream is offline: {err}"}, + JSStreamPurgeFailedF: {Code: 500, ErrCode: 10110, Description: "{err}"}, + JSStreamReplicasNotSupportedErr: {Code: 500, ErrCode: 10074, Description: "replicas > 1 not supported in non-clustered mode"}, + JSStreamReplicasNotUpdatableErr: {Code: 400, ErrCode: 10061, Description: "Replicas configuration can not be updated"}, + JSStreamRestoreErrF: {Code: 500, ErrCode: 10062, Description: "restore failed: {err}"}, + JSStreamRollupFailedF: {Code: 500, ErrCode: 10111, Description: "{err}"}, + JSStreamSealedErr: {Code: 400, ErrCode: 10109, Description: "invalid operation on sealed stream"}, + JSStreamSequenceNotMatchErr: {Code: 503, ErrCode: 10063, Description: "expected stream sequence does not match"}, + JSStreamSnapshotErrF: {Code: 500, ErrCode: 10064, Description: "snapshot failed: {err}"}, + JSStreamStoreFailedF: {Code: 503, ErrCode: 10077, Description: "{err}"}, + JSStreamSubjectOverlapErr: {Code: 400, ErrCode: 10065, Description: "subjects overlap with an existing stream"}, + JSStreamTemplateCreateErrF: {Code: 500, ErrCode: 10066, Description: "{err}"}, + JSStreamTemplateDeleteErrF: {Code: 500, ErrCode: 10067, Description: "{err}"}, + JSStreamTemplateNotFoundErr: {Code: 404, ErrCode: 10068, Description: "template not found"}, + JSStreamTooManyRequests: {Code: 429, ErrCode: 10167, Description: "too many requests"}, + JSStreamTransformInvalidDestination: {Code: 400, ErrCode: 10156, Description: "stream transform: {err}"}, + JSStreamTransformInvalidSource: {Code: 400, ErrCode: 10155, Description: "stream transform source: {err}"}, + JSStreamUpdateErrF: {Code: 500, ErrCode: 10069, Description: "{err}"}, + JSStreamWrongLastMsgIDErrF: {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"}, + JSStreamWrongLastSequenceConstantErr: {Code: 400, ErrCode: 10164, Description: "wrong last sequence"}, + JSStreamWrongLastSequenceErrF: {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"}, + JSTempStorageFailedErr: {Code: 500, ErrCode: 10072, Description: "JetStream unable to open temp storage for restore"}, + JSTemplateNameNotMatchSubjectErr: {Code: 400, ErrCode: 10073, Description: "template name in subject does not match request"}, } // ErrJetStreamNotClustered Deprecated by JSClusterNotActiveErr ApiError, use IsNatsError() for comparisons ErrJetStreamNotClustered = ApiErrors[JSClusterNotActiveErr] @@ -719,6 +835,78 @@ func NewJSAccountResourcesExceededError(opts ...ErrorOption) *ApiError { return ApiErrors[JSAccountResourcesExceededErr] } +// NewJSAtomicPublishDisabledError creates a new JSAtomicPublishDisabledErr error: "atomic publish is disabled" +func NewJSAtomicPublishDisabledError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSAtomicPublishDisabledErr] +} + +// NewJSAtomicPublishIncompleteBatchError creates a new JSAtomicPublishIncompleteBatchErr error: "atomic publish batch is incomplete" +func NewJSAtomicPublishIncompleteBatchError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSAtomicPublishIncompleteBatchErr] +} + +// NewJSAtomicPublishInvalidBatchIDError creates a new JSAtomicPublishInvalidBatchIDErr error: "atomic publish batch ID is invalid" +func NewJSAtomicPublishInvalidBatchIDError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSAtomicPublishInvalidBatchIDErr] +} + +// NewJSAtomicPublishMissingSeqError creates a new JSAtomicPublishMissingSeqErr error: "atomic publish sequence is missing" +func NewJSAtomicPublishMissingSeqError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSAtomicPublishMissingSeqErr] +} + +// NewJSAtomicPublishTooLargeBatchError creates a new JSAtomicPublishTooLargeBatchErrF error: "atomic publish batch is too large: {size}" +func NewJSAtomicPublishTooLargeBatchError(size interface{}, opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + e := ApiErrors[JSAtomicPublishTooLargeBatchErrF] + args := e.toReplacerArgs([]interface{}{"{size}", size}) + return &ApiError{ + Code: e.Code, + ErrCode: e.ErrCode, + Description: strings.NewReplacer(args...).Replace(e.Description), + } +} + +// NewJSAtomicPublishUnsupportedHeaderBatchError creates a new JSAtomicPublishUnsupportedHeaderBatchErr error: "atomic publish unsupported header used: {header}" +func NewJSAtomicPublishUnsupportedHeaderBatchError(header interface{}, opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + e := ApiErrors[JSAtomicPublishUnsupportedHeaderBatchErr] + args := e.toReplacerArgs([]interface{}{"{header}", header}) + return &ApiError{ + Code: e.Code, + ErrCode: e.ErrCode, + Description: strings.NewReplacer(args...).Replace(e.Description), + } +} + // NewJSBadRequestError creates a new JSBadRequestErr error: "bad request" func NewJSBadRequestError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -845,6 +1033,26 @@ func NewJSClusterUnSupportFeatureError(opts ...ErrorOption) *ApiError { return ApiErrors[JSClusterUnSupportFeatureErr] } +// NewJSConsumerAckPolicyInvalidError creates a new JSConsumerAckPolicyInvalidErr error: "consumer ack policy invalid" +func NewJSConsumerAckPolicyInvalidError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSConsumerAckPolicyInvalidErr] +} + +// NewJSConsumerAckWaitNegativeError creates a new JSConsumerAckWaitNegativeErr error: "consumer ack wait needs to be positive" +func NewJSConsumerAckWaitNegativeError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSConsumerAckWaitNegativeErr] +} + // NewJSConsumerAlreadyExistsError creates a new JSConsumerAlreadyExists error: "consumer already exists" func NewJSConsumerAlreadyExistsError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -855,6 +1063,16 @@ func NewJSConsumerAlreadyExistsError(opts ...ErrorOption) *ApiError { return ApiErrors[JSConsumerAlreadyExists] } +// NewJSConsumerBackOffNegativeError creates a new JSConsumerBackOffNegativeErr error: "consumer backoff needs to be positive" +func NewJSConsumerBackOffNegativeError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSConsumerBackOffNegativeErr] +} + // NewJSConsumerBadDurableNameError creates a new JSConsumerBadDurableNameErr error: "durable name can not contain '.', '*', '>'" func NewJSConsumerBadDurableNameError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -1237,14 +1455,14 @@ func NewJSConsumerMaxRequestBatchNegativeError(opts ...ErrorOption) *ApiError { return ApiErrors[JSConsumerMaxRequestBatchNegativeErr] } -// NewJSConsumerMaxRequestExpiresToSmallError creates a new JSConsumerMaxRequestExpiresToSmall error: "consumer max request expires needs to be >= 1ms" -func NewJSConsumerMaxRequestExpiresToSmallError(opts ...ErrorOption) *ApiError { +// NewJSConsumerMaxRequestExpiresTooSmallError creates a new JSConsumerMaxRequestExpiresTooSmall error: "consumer max request expires needs to be >= 1ms" +func NewJSConsumerMaxRequestExpiresTooSmallError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) if ae, ok := eopts.err.(*ApiError); ok { return ae } - return ApiErrors[JSConsumerMaxRequestExpiresToSmall] + return ApiErrors[JSConsumerMaxRequestExpiresTooSmall] } // NewJSConsumerMaxWaitingNegativeError creates a new JSConsumerMaxWaitingNegativeErr error: "consumer max waiting needs to be positive" @@ -1375,6 +1593,26 @@ func NewJSConsumerOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError return ApiErrors[JSConsumerOverlappingSubjectFilters] } +// NewJSConsumerPinnedTTLWithoutPriorityPolicyNoneError creates a new JSConsumerPinnedTTLWithoutPriorityPolicyNone error: "PinnedTTL cannot be set when PriorityPolicy is none" +func NewJSConsumerPinnedTTLWithoutPriorityPolicyNoneError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSConsumerPinnedTTLWithoutPriorityPolicyNone] +} + +// NewJSConsumerPriorityGroupWithPolicyNoneError creates a new JSConsumerPriorityGroupWithPolicyNone error: "consumer can not have priority groups when policy is none" +func NewJSConsumerPriorityGroupWithPolicyNoneError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSConsumerPriorityGroupWithPolicyNone] +} + // NewJSConsumerPriorityPolicyWithoutGroupError creates a new JSConsumerPriorityPolicyWithoutGroup error: "Setting PriorityPolicy requires at least one PriorityGroup to be set" func NewJSConsumerPriorityPolicyWithoutGroupError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -1445,6 +1683,16 @@ func NewJSConsumerReplacementWithDifferentNameError(opts ...ErrorOption) *ApiErr return ApiErrors[JSConsumerReplacementWithDifferentNameErr] } +// NewJSConsumerReplayPolicyInvalidError creates a new JSConsumerReplayPolicyInvalidErr error: "consumer replay policy invalid" +func NewJSConsumerReplayPolicyInvalidError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSConsumerReplayPolicyInvalidErr] +} + // NewJSConsumerReplicasExceedsStreamError creates a new JSConsumerReplicasExceedsStream error: "consumer config replica count exceeds parent stream" func NewJSConsumerReplicasExceedsStreamError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -1597,6 +1845,106 @@ func NewJSMemoryResourcesExceededError(opts ...ErrorOption) *ApiError { return ApiErrors[JSMemoryResourcesExceededErr] } +// NewJSMessageCounterBrokenError creates a new JSMessageCounterBrokenErr error: "message counter is broken" +func NewJSMessageCounterBrokenError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageCounterBrokenErr] +} + +// NewJSMessageIncrDisabledError creates a new JSMessageIncrDisabledErr error: "message counters is disabled" +func NewJSMessageIncrDisabledError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageIncrDisabledErr] +} + +// NewJSMessageIncrInvalidError creates a new JSMessageIncrInvalidErr error: "message counter increment is invalid" +func NewJSMessageIncrInvalidError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageIncrInvalidErr] +} + +// NewJSMessageIncrMissingError creates a new JSMessageIncrMissingErr error: "message counter increment is missing" +func NewJSMessageIncrMissingError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageIncrMissingErr] +} + +// NewJSMessageIncrPayloadError creates a new JSMessageIncrPayloadErr error: "message counter has payload" +func NewJSMessageIncrPayloadError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageIncrPayloadErr] +} + +// NewJSMessageSchedulesDisabledError creates a new JSMessageSchedulesDisabledErr error: "message schedules is disabled" +func NewJSMessageSchedulesDisabledError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageSchedulesDisabledErr] +} + +// NewJSMessageSchedulesPatternInvalidError creates a new JSMessageSchedulesPatternInvalidErr error: "message schedules pattern is invalid" +func NewJSMessageSchedulesPatternInvalidError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageSchedulesPatternInvalidErr] +} + +// NewJSMessageSchedulesRollupInvalidError creates a new JSMessageSchedulesRollupInvalidErr error: "message schedules invalid rollup" +func NewJSMessageSchedulesRollupInvalidError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageSchedulesRollupInvalidErr] +} + +// NewJSMessageSchedulesTTLInvalidError creates a new JSMessageSchedulesTTLInvalidErr error: "message schedules invalid per-message TTL" +func NewJSMessageSchedulesTTLInvalidError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageSchedulesTTLInvalidErr] +} + +// NewJSMessageSchedulesTargetInvalidError creates a new JSMessageSchedulesTargetInvalidErr error: "message schedules target is invalid" +func NewJSMessageSchedulesTargetInvalidError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMessageSchedulesTargetInvalidErr] +} + // NewJSMessageTTLDisabledError creates a new JSMessageTTLDisabledErr error: "per-message TTL is disabled" func NewJSMessageTTLDisabledError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -1705,6 +2053,26 @@ func NewJSMirrorOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError { return ApiErrors[JSMirrorOverlappingSubjectFilters] } +// NewJSMirrorWithAtomicPublishError creates a new JSMirrorWithAtomicPublishErr error: "stream mirrors can not also use atomic publishing" +func NewJSMirrorWithAtomicPublishError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMirrorWithAtomicPublishErr] +} + +// NewJSMirrorWithCountersError creates a new JSMirrorWithCountersErr error: "stream mirrors can not also calculate counters" +func NewJSMirrorWithCountersError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMirrorWithCountersErr] +} + // NewJSMirrorWithFirstSeqError creates a new JSMirrorWithFirstSeqErr error: "stream mirrors can not have first sequence configured" func NewJSMirrorWithFirstSeqError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -1715,6 +2083,16 @@ func NewJSMirrorWithFirstSeqError(opts ...ErrorOption) *ApiError { return ApiErrors[JSMirrorWithFirstSeqErr] } +// NewJSMirrorWithMsgSchedulesError creates a new JSMirrorWithMsgSchedulesErr error: "stream mirrors can not also schedule messages" +func NewJSMirrorWithMsgSchedulesError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSMirrorWithMsgSchedulesErr] +} + // NewJSMirrorWithSourcesError creates a new JSMirrorWithSourcesErr error: "stream mirrors can not also contain other sources" func NewJSMirrorWithSourcesError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -1867,6 +2245,16 @@ func NewJSReplicasCountCannotBeNegativeError(opts ...ErrorOption) *ApiError { return ApiErrors[JSReplicasCountCannotBeNegative] } +// NewJSRequiredApiLevelError creates a new JSRequiredApiLevelErr error: "JetStream minimum api level required" +func NewJSRequiredApiLevelError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSRequiredApiLevelErr] +} + // NewJSRestoreSubscribeFailedError creates a new JSRestoreSubscribeFailedErrF error: "JetStream unable to subscribe to restore snapshot {subject}: {err}" func NewJSRestoreSubscribeFailedError(err error, subject interface{}, opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -2007,6 +2395,16 @@ func NewJSSourceOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError { return ApiErrors[JSSourceOverlappingSubjectFilters] } +// NewJSSourceWithMsgSchedulesError creates a new JSSourceWithMsgSchedulesErr error: "stream source can not also schedule messages" +func NewJSSourceWithMsgSchedulesError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSSourceWithMsgSchedulesErr] +} + // NewJSStorageResourcesExceededError creates a new JSStorageResourcesExceededErr error: "insufficient storage resources available" func NewJSStorageResourcesExceededError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -2075,6 +2473,16 @@ func NewJSStreamDuplicateMessageConflictError(opts ...ErrorOption) *ApiError { return ApiErrors[JSStreamDuplicateMessageConflict] } +// NewJSStreamExpectedLastSeqPerSubjectInvalidError creates a new JSStreamExpectedLastSeqPerSubjectInvalid error: "missing sequence for expected last sequence per subject" +func NewJSStreamExpectedLastSeqPerSubjectInvalidError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSStreamExpectedLastSeqPerSubjectInvalid] +} + // NewJSStreamExpectedLastSeqPerSubjectNotReadyError creates a new JSStreamExpectedLastSeqPerSubjectNotReady error: "expected last sequence per subject temporarily unavailable" func NewJSStreamExpectedLastSeqPerSubjectNotReadyError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) @@ -2241,6 +2649,16 @@ func NewJSStreamMessageExceedsMaximumError(opts ...ErrorOption) *ApiError { return ApiErrors[JSStreamMessageExceedsMaximumErr] } +// NewJSStreamMinLastSeqError creates a new JSStreamMinLastSeqErr error: "min last sequence" +func NewJSStreamMinLastSeqError(opts ...ErrorOption) *ApiError { + eopts := parseOpts(opts) + if ae, ok := eopts.err.(*ApiError); ok { + return ae + } + + return ApiErrors[JSStreamMinLastSeqErr] +} + // NewJSStreamMirrorNotUpdatableError creates a new JSStreamMirrorNotUpdatableErr error: "stream mirror configuration can not be updated" func NewJSStreamMirrorNotUpdatableError(opts ...ErrorOption) *ApiError { eopts := parseOpts(opts) diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_events.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_events.go index 6b4ac9de8..c7042f89d 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_events.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_events.go @@ -73,7 +73,7 @@ type JSStreamActionAdvisory struct { TypedEvent Stream string `json:"stream"` Action ActionAdvisoryType `json:"action"` - Template string `json:"template,omitempty"` + Template string `json:"template,omitempty"` // Deprecated: stream templates are deprecated and will be removed in a future version. Domain string `json:"domain,omitempty"` } @@ -253,6 +253,27 @@ type JSStreamQuorumLostAdvisory struct { Domain string `json:"domain,omitempty"` } +// JSStreamBatchAbandonedAdvisoryType is sent when a stream's atomic batch is abandoned. +const JSStreamBatchAbandonedAdvisoryType = "io.nats.jetstream.advisory.v1.stream_batch_abandoned" + +// JSStreamBatchAbandonedAdvisory indicates that a stream's batch was abandoned. +type JSStreamBatchAbandonedAdvisory struct { + TypedEvent + Account string `json:"account,omitempty"` + Stream string `json:"stream"` + Domain string `json:"domain,omitempty"` + BatchId string `json:"batch"` + Reason BatchAbandonReason `json:"reason"` +} + +type BatchAbandonReason string + +var ( + BatchTimeout BatchAbandonReason = "timeout" + BatchLarge BatchAbandonReason = "large" + BatchIncomplete BatchAbandonReason = "incomplete" +) + // JSConsumerLeaderElectedAdvisoryType is sent when the system elects a leader for a consumer. const JSConsumerLeaderElectedAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_leader_elected" diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_versioning.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_versioning.go index 41ecf04c7..8d6e9e7ca 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_versioning.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_versioning.go @@ -17,7 +17,7 @@ import "strconv" const ( // JSApiLevel is the maximum supported JetStream API level for this server. - JSApiLevel int = 1 + JSApiLevel int = 2 JSRequiredLevelMetadataKey = "_nats.req.level" JSServerVersionMetadataKey = "_nats.ver" @@ -62,6 +62,26 @@ func setStaticStreamMetadata(cfg *StreamConfig) { requires(1) } + // Counter CRDTs were added in v2.12 and require API level 2. + if cfg.AllowMsgCounter { + requires(2) + } + + // Atomic batch publishing was added in v2.12 and require API level 2. + if cfg.AllowAtomicPublish { + requires(2) + } + + // Message scheduling was added in v2.12 and require API level 2. + if cfg.AllowMsgSchedules { + requires(2) + } + + // Async persist mode was added in v2.12 and requires API level 2. + if cfg.PersistMode == AsyncPersistMode { + requires(2) + } + cfg.Metadata[JSRequiredLevelMetadataKey] = strconv.Itoa(requiredApiLevel) } @@ -204,3 +224,13 @@ func deleteDynamicMetadata(metadata map[string]string) { delete(metadata, JSServerVersionMetadataKey) delete(metadata, JSServerLevelMetadataKey) } + +// errorOnRequiredApiLevel returns whether a request should be rejected based on the JSRequiredApiLevel header. +func errorOnRequiredApiLevel(hdr []byte) bool { + reqApiLevel := sliceHeader(JSRequiredApiLevel, hdr) + if len(reqApiLevel) == 0 { + return false + } + minLevel, err := strconv.Atoi(string(reqApiLevel)) + return err != nil || JSApiLevel < minLevel +} 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 8b64c0800..c85bdfe17 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 @@ -82,6 +82,8 @@ type leaf struct { remoteDomain string // account name of remote server remoteAccName string + // Whether or not we want to propagate east-west interest from other LNs. + isolated bool // Used to suppress sub and unsub interest. Same as routes but our audience // here is tied to this leaf node. This will hold all subscriptions except this // leaf nodes. This represents all the interest we want to send to the other side. @@ -130,6 +132,14 @@ func (c *client) isHubLeafNode() bool { return c.kind == LEAF && !c.leaf.isSpoke } +func (c *client) isIsolatedLeafNode() bool { + // TODO(nat): In future we may want to pass in and consider an isolation + // group name here, which the hub and/or leaf could provide, so that we + // can isolate away certain LNs but not others on an opt-in basis. For + // now we will just isolate all LN interest until then. + return c.kind == LEAF && c.leaf.isolated +} + // This will spin up go routines to solicit the remote leaf node connections. func (s *Server) solicitLeafNodeRemotes(remotes []*RemoteLeafOpts) { sysAccName := _EMPTY_ @@ -177,12 +187,20 @@ func (s *Server) solicitLeafNodeRemotes(remotes []*RemoteLeafOpts) { return remote } for _, r := range remotes { + // We need to call this, even if the leaf is disabled. This is so that + // the number of internal configuration matches the options' remote leaf + // configuration required for configuration reload. remote := addRemote(r, r.LocalAccount == sysAccName) - s.startGoRoutine(func() { s.connectToRemoteLeafNode(remote, true) }) + if !r.Disabled { + s.startGoRoutine(func() { s.connectToRemoteLeafNode(remote, true) }) + } } } func (s *Server) remoteLeafNodeStillValid(remote *leafNodeCfg) bool { + if remote.Disabled { + return false + } for _, ri := range s.getOpts().LeafNode.Remotes { // FIXME(dlc) - What about auth changes? if reflect.DeepEqual(ri.URLs, remote.URLs) { @@ -748,6 +766,7 @@ func (s *Server) startLeafNodeAcceptLoop() { Domain: opts.JetStreamDomain, Proto: s.getServerProto(), InfoOnConnect: true, + JSApiLevel: JSApiLevel, } // If we have selected a random port... if port == 0 { @@ -811,6 +830,7 @@ func (c *client) sendLeafConnect(clusterName string, headers bool) error { Compression: c.leaf.compression, RemoteAccount: c.acc.GetName(), Proto: c.srv.getServerProto(), + Isolate: c.leaf.remote.RequestIsolation, } // If a signature callback is specified, this takes precedence over anything else. @@ -977,6 +997,13 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf // Do not update the smap here, we need to do it in initLeafNodeSmapAndSendSubs c.leaf = &leaf{} + // If the leafnode subject interest should be isolated, flag it here. + s.optsMu.RLock() + if c.leaf.isolated = s.opts.LeafNode.IsolateLeafnodeInterest; !c.leaf.isolated && remote != nil { + c.leaf.isolated = remote.LocalIsolation + } + s.optsMu.RUnlock() + // For accepted LN connections, ws will be != nil if it was accepted // through the Websocket port. c.ws = ws @@ -1057,6 +1084,8 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf if cm := opts.LeafNode.Compression.Mode; cm != CompressionNotSupported { info.Compression = cm } + // We always send a nonce for LEAF connections. Do not change that without + // taking into account presence of proxy trusted keys. s.generateNonce(nonce[:]) s.mu.Unlock() } @@ -1794,9 +1823,13 @@ func (s *Server) removeLeafNodeConnection(c *client) { c.leaf.gwSub = nil } } + proxyKey := c.proxyKey c.mu.Unlock() s.mu.Lock() delete(s.leafs, cid) + if proxyKey != _EMPTY_ { + s.removeProxiedConn(proxyKey, cid) + } s.mu.Unlock() s.removeFromTempClients(cid) } @@ -1818,6 +1851,7 @@ type leafConnectInfo struct { Headers bool `json:"headers,omitempty"` JetStream bool `json:"jetstream,omitempty"` DenyPub []string `json:"deny_pub,omitempty"` + Isolate bool `json:"isolate,omitempty"` // There was an existing field called: // >> Comp bool `json:"compression,omitempty"` @@ -1928,6 +1962,8 @@ func (c *client) processLeafNodeConnect(s *Server, arg []byte, lang string) erro c.leaf.remoteServer = proto.Name // Remember the remote account name c.leaf.remoteAccName = proto.RemoteAccount + // Remember if the leafnode requested isolation. + c.leaf.isolated = c.leaf.isolated || proto.Isolate // If the other side has declared itself a hub, so we will take on the spoke role. if proto.Hub { @@ -2044,12 +2080,16 @@ func (c *client) remoteCluster() string { // its permission settings for local enforcement. func (s *Server) sendPermsAndAccountInfo(c *client) { // Copy + s.mu.Lock() info := s.copyLeafNodeInfo() + s.mu.Unlock() c.mu.Lock() info.CID = c.cid info.Import = c.opts.Import info.Export = c.opts.Export info.RemoteAccount = c.acc.Name + // s.SystemAccount() uses an atomic operation and does not get the server lock, so this is safe. + info.IsSystemAccount = c.acc == s.SystemAccount() info.ConnectInfo = true c.enqueueProto(generateInfoJSON(info)) c.mu.Unlock() @@ -2159,6 +2199,10 @@ func (s *Server) initLeafNodeSmapAndSendSubs(c *client) { c.Debugf("Not permitted to subscribe to %q on behalf of %s%s", sub.subject, accName, accNTag) continue } + // Don't advertise interest from leafnodes to other isolated leafnodes. + if sub.client.kind == LEAF && c.isIsolatedLeafNode() { + continue + } // We ignore ourselves here. // Also don't add the subscription if it has a origin cluster and the // cluster name matches the one of the client we are sending to. @@ -2280,6 +2324,11 @@ func (acc *Account) updateLeafNodesEx(sub *subscription, delta int32, hubOnly bo continue } ln.mu.Lock() + // Don't advertise interest from leafnodes to other isolated leafnodes. + if sub.client.kind == LEAF && ln.isIsolatedLeafNode() { + ln.mu.Unlock() + continue + } // If `hubOnly` is true, it means that we want to update only leafnodes // that connect to this server (so isHubLeafNode() would return `true`). if hubOnly && !ln.isHubLeafNode() { 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 133b6408f..f75146c6a 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 @@ -24,7 +24,6 @@ import ( "time" "github.com/nats-io/nats-server/v2/server/ats" - "github.com/nats-io/nats-server/v2/server/avl" "github.com/nats-io/nats-server/v2/server/gsl" "github.com/nats-io/nats-server/v2/server/stree" @@ -42,11 +41,12 @@ type memStore struct { maxp int64 scb StorageUpdateHandler rmcb StorageRemoveMsgHandler - sdmcb SubjectDeleteMarkerUpdateHandler + pmsgcb ProcessJetStreamMsgHandler ageChk *time.Timer consumers int receivedAny bool ttls *thw.HashWheel + scheduling *MsgScheduling sdm *SDMMeta } @@ -67,6 +67,9 @@ func newMemStore(cfg *StreamConfig) (*memStore, error) { if cfg.AllowMsgTTL { ms.ttls = thw.NewHashWheel() } + if cfg.AllowMsgSchedules { + ms.scheduling = newMsgScheduling(ms.runMsgScheduling) + } if cfg.FirstSeq > 0 { if _, err := ms.purge(cfg.FirstSeq); err != nil { return nil, err @@ -95,6 +98,11 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error { } else if !cfg.AllowMsgTTL && ms.ttls != nil { ms.ttls = nil } + if cfg.AllowMsgSchedules && ms.scheduling == nil { + ms.recoverMsgSchedulingState() + } else if !cfg.AllowMsgSchedules && ms.scheduling != nil { + ms.scheduling = nil + } // Limits checks and enforcement. ms.enforceMsgLimit() ms.enforceBytesLimit() @@ -127,6 +135,9 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error { if cfg.MaxAge != 0 || cfg.AllowMsgTTL { ms.expireMsgs() } + if cfg.AllowMsgSchedules { + ms.runMsgScheduling() + } return nil } @@ -154,6 +165,29 @@ func (ms *memStore) recoverTTLState() { } } +// Lock should be held. +func (ms *memStore) recoverMsgSchedulingState() { + ms.scheduling = newMsgScheduling(ms.runMsgScheduling) + if ms.state.Msgs == 0 { + return + } + + var ( + seq uint64 + smv StoreMsg + sm *StoreMsg + ) + defer ms.scheduling.resetTimer() + for sm, seq, _ = ms.loadNextMsgLocked(fwcs, true, 0, &smv); sm != nil; sm, seq, _ = ms.loadNextMsgLocked(fwcs, true, seq+1, &smv) { + if len(sm.hdr) == 0 { + continue + } + if schedule, ok := getMessageSchedule(sm.hdr); ok && !schedule.IsZero() { + ms.scheduling.init(seq, sm.subj, schedule.UnixNano()) + } + } +} + // Stores a raw message with expected sequence number and timestamp. // Lock should be held. func (ms *memStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) error { @@ -271,6 +305,13 @@ func (ms *memStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, tt ms.startAgeChk() } + // Message scheduling. + if ms.scheduling != nil { + if schedule, ok := getMessageSchedule(hdr); ok && !schedule.IsZero() { + ms.scheduling.add(seq, subj, schedule.UnixNano()) + } + } + return nil } @@ -360,6 +401,11 @@ func (ms *memStore) SkipMsgs(seq uint64, num uint64) error { return nil } +// FlushAllPending flushes all data that was still pending to be written. +func (ms *memStore) FlushAllPending() { + // Noop, in-memory store doesn't use async applying. +} + // RegisterStorageUpdates registers a callback for updates to storage changes. // It will present number of messages and bytes as a signed integer and an // optional sequence number of the message if a single. @@ -377,10 +423,10 @@ func (ms *memStore) RegisterStorageRemoveMsg(cb StorageRemoveMsgHandler) { ms.mu.Unlock() } -// RegisterSubjectDeleteMarkerUpdates registers a callback for updates to new subject delete markers. -func (ms *memStore) RegisterSubjectDeleteMarkerUpdates(cb SubjectDeleteMarkerUpdateHandler) { +// RegisterProcessJetStreamMsg registers a callback to process new JetStream messages. +func (ms *memStore) RegisterProcessJetStreamMsg(cb ProcessJetStreamMsgHandler) { ms.mu.Lock() - ms.sdmcb = cb + ms.pmsgcb = cb ms.mu.Unlock() } @@ -1084,11 +1130,11 @@ func (ms *memStore) expireMsgs() { maxAge := int64(ms.cfg.MaxAge) minAge := time.Now().UnixNano() - maxAge rmcb := ms.rmcb - sdmcb := ms.sdmcb + pmsgcb := ms.pmsgcb sdmTTL := int64(ms.cfg.SubjectDeleteMarkerTTL.Seconds()) sdmEnabled := sdmTTL > 0 ms.mu.RUnlock() - if sdmEnabled && (rmcb == nil || sdmcb == nil) { + if sdmEnabled && (rmcb == nil || pmsgcb == nil) { return } @@ -1244,12 +1290,39 @@ func (ms *memStore) handleRemovalOrSdm(seq uint64, subj string, sdm bool, sdmTTL subj: subj, hdr: hdr, } - ms.sdmcb(msg) + ms.pmsgcb(msg) } else { ms.rmcb(seq) } } +// Will run through scheduled messages. +func (ms *memStore) runMsgScheduling() { + // TODO: Not great that we're holding the lock here, but the timed hash wheel and message scheduling isn't thread-safe. + ms.mu.Lock() + defer ms.mu.Unlock() + + if ms.scheduling == nil || ms.pmsgcb == nil { + return + } + + scheduledMsgs := ms.scheduling.getScheduledMessages(func(seq uint64, smv *StoreMsg) *StoreMsg { + sm, _ := ms.loadMsgLocked(seq, smv, false) + return sm + }) + if len(scheduledMsgs) > 0 { + ms.mu.Unlock() + for _, msg := range scheduledMsgs { + ms.pmsgcb(msg) + } + ms.mu.Lock() + } + + if ms.scheduling != nil { + ms.scheduling.resetTimer() + } +} + // PurgeEx will remove messages based on subject filters, sequence and number of messages to keep. // Will return the number of purged messages. func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) { @@ -1952,12 +2025,25 @@ func (ms *memStore) Utilization() (total, reported uint64, err error) { return ms.state.Bytes, ms.state.Bytes, nil } +func memStoreMsgSizeRaw(slen, hlen, mlen int) uint64 { + return uint64(slen + hlen + mlen + 16) // 8*2 for seq + age +} + func memStoreMsgSize(subj string, hdr, msg []byte) uint64 { - return uint64(len(subj) + len(hdr) + len(msg) + 16) // 8*2 for seq + age + return memStoreMsgSizeRaw(len(subj), len(hdr), len(msg)) +} + +// ResetState resets any state that's temporary. For example when changing leaders. +func (ms *memStore) ResetState() { + ms.mu.Lock() + defer ms.mu.Unlock() + if ms.scheduling != nil { + ms.scheduling.clearInflight() + } } // Delete is same as Stop for memory store. -func (ms *memStore) Delete() error { +func (ms *memStore) Delete(_ bool) error { return ms.Stop() } 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 352d0b7cb..c30e51ecd 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 @@ -129,6 +129,7 @@ type ConnInfo struct { OutMsgs int64 `json:"out_msgs"` InBytes int64 `json:"in_bytes"` OutBytes int64 `json:"out_bytes"` + Stalls int64 `json:"stalls,omitempty"` NumSubs uint32 `json:"subscriptions"` Name string `json:"name,omitempty"` Lang string `json:"lang,omitempty"` @@ -146,11 +147,17 @@ type ConnInfo struct { NameTag string `json:"name_tag,omitempty"` Tags jwt.TagList `json:"tags,omitempty"` MQTTClient string `json:"mqtt_client,omitempty"` // This is the MQTT client id + Proxy *ProxyInfo `json:"proxy,omitempty"` // Internal rtt int64 // For fast sorting } +// ProxyInfo represents the information about this proxied connection. +type ProxyInfo struct { + Key string `json:"key"` +} + // TLSPeerCert contains basic information about a TLS peer certificate type TLSPeerCert struct { Subject string `json:"subject,omitempty"` @@ -571,6 +578,8 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool) // we need to use atomic here. ci.InMsgs = atomic.LoadInt64(&client.inMsgs) ci.InBytes = atomic.LoadInt64(&client.inBytes) + ci.Stalls = atomic.LoadInt64(&client.stalls) + ci.Proxy = createProxyInfo(client) // If the connection is gone, too bad, we won't set TLSVersion and TLSCipher. // Exclude clients that are still doing handshake so we don't block in @@ -579,7 +588,7 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool) if conn, ok := nc.(*tls.Conn); ok { cs := conn.ConnectionState() ci.TLSVersion = tlsVersion(cs.Version) - ci.TLSCipher = tlsCipher(cs.CipherSuite) + ci.TLSCipher = tls.CipherSuiteName(cs.CipherSuite) if auth && len(cs.PeerCertificates) > 0 { ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates) } @@ -593,6 +602,17 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool) } } +// If this client came from a trusted proxy, this will return a ProxyInfo +// to be used in ConnInfo or LeafInfo. +// +// Client lock must be held on entry. +func createProxyInfo(c *client) *ProxyInfo { + if c.proxyKey == _EMPTY_ { + return nil + } + return &ProxyInfo{Key: c.proxyKey} +} + func makePeerCerts(pc []*x509.Certificate) []*TLSPeerCert { res := make([]*TLSPeerCert, len(pc)) for i, c := range pc { @@ -808,8 +828,10 @@ type RouteInfo struct { // Routez returns a Routez struct containing information about routes. func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) { - rs := &Routez{Routes: []*RouteInfo{}} - rs.Now = time.Now().UTC() + rs := &Routez{ + Now: time.Now().UTC(), + Routes: []*RouteInfo{}, + } if routezOpts == nil { routezOpts = &RoutezOptions{} @@ -826,7 +848,7 @@ func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) { rs.Import = perms.Import rs.Export = perms.Export } - rs.Name = s.getOpts().ServerName + rs.Name = s.info.Name addRoute := func(r *client) { r.mu.Lock() @@ -1003,7 +1025,15 @@ func (s *Server) Subsz(opts *SubszOptions) (*Subsz, error) { slStats := &SublistStats{} // FIXME(dlc) - Make account aware. - sz := &Subsz{s.info.ID, time.Now().UTC(), slStats, 0, offset, limit, nil} + sz := &Subsz{ + ID: s.info.ID, + Now: time.Now().UTC(), + SublistStats: slStats, + Total: 0, + Offset: offset, + Limit: limit, + Subs: nil, + } if subdetail { var raw [4096]*subscription @@ -1100,12 +1130,7 @@ func (s *Server) HandleSubsz(w http.ResponseWriter, r *http.Request) { } var b []byte - - if len(st.Subs) == 0 { - b, err = json.MarshalIndent(st.SublistStats, "", " ") - } else { - b, err = json.MarshalIndent(st, "", " ") - } + b, err = json.MarshalIndent(st, "", " ") if err != nil { s.Errorf("Error marshaling response to /subscriptionsz request: %v", err) } @@ -1237,6 +1262,8 @@ type Varz struct { InBytes int64 `json:"in_bytes"` // InBytes is the number of bytes this server received OutBytes int64 `json:"out_bytes"` // OutMsgs is the number of bytes this server sent SlowConsumers int64 `json:"slow_consumers"` // SlowConsumers is the total count of clients that were disconnected since start due to being slow consumers + StaleConnections int64 `json:"stale_connections"` // StaleConnections is the total count of stale connections that were detected + StalledClients int64 `json:"stalled_clients"` // StalledClients is the total number of times that clients have been stalled. Subscriptions uint32 `json:"subscriptions"` // Subscriptions is the count of active subscriptions HTTPReqStats map[string]uint64 `json:"http_req_stats"` // HTTPReqStats is the number of requests each HTTP endpoint received ConfigLoadTime time.Time `json:"config_load_time"` // ConfigLoadTime is the time the configuration was loaded or reloaded @@ -1247,8 +1274,10 @@ type Varz struct { TrustedOperatorsClaim []*jwt.OperatorClaims `json:"trusted_operators_claim,omitempty"` // TrustedOperatorsClaim is the decoded claims for each trusted operator SystemAccount string `json:"system_account,omitempty"` // SystemAccount is the name of the System account PinnedAccountFail uint64 `json:"pinned_account_fails,omitempty"` // PinnedAccountFail is how often user logon fails due to the issuer account not being pinned. - OCSPResponseCache *OCSPResponseCacheVarz `json:"ocsp_peer_cache,omitempty"` // OCSPResponseCache is the state of the OCSP cache // OCSPResponseCache holds information about - SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats"` // SlowConsumersStats is statistics about all detected Slow Consumer + 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 + Proxies *ProxiesOptsVarz `json:"proxies,omitempty"` // Proxies hold information about network proxy devices } // JetStreamVarz contains basic runtime information about jetstream @@ -1365,6 +1394,16 @@ type OCSPResponseCacheVarz struct { Unknowns int64 `json:"cached_unknown_responses,omitempty"` // Unknowns is how many of the stored cache entries are unknown responses } +// ProxiesOptsVarz contains proxies information +type ProxiesOptsVarz struct { + Trusted []*ProxyOptsVarz `json:"trusted,omitempty"` // Trusted holds a list of trusted proxies +} + +// ProxyOptsVarz contains proxy information +type ProxyOptsVarz struct { + Key string `json:"key"` // Key is the public key of the proxy +} + // VarzOptions are the options passed to Varz(). // Currently, there are no options defined. type VarzOptions struct{} @@ -1377,6 +1416,14 @@ type SlowConsumersStats struct { Leafs uint64 `json:"leafs"` // Leafs is how many Leafnodes were slow consumers } +// StaleConnectionStats contains information about the stale connections from different type of connections. +type StaleConnectionStats struct { + Clients uint64 `json:"clients"` // Clients is how many Client connections became stale connections + Routes uint64 `json:"routes"` // Routes is how many Route connections became stale connections + Gateways uint64 `json:"gateways"` // Gateways is how many Gateway connections became stale connections + Leafs uint64 `json:"leafs"` // Leafs is how many Leafnode connections became stale connections +} + func myUptime(d time.Duration) string { // Just use total seconds for uptime, and display days / years tsecs := d / time.Second @@ -1615,7 +1662,6 @@ func (s *Server) createVarz(pcpu float64, rss int64) *Varz { MaxSubs: opts.MaxSubs, Cores: runtime.NumCPU(), MaxProcs: runtime.GOMAXPROCS(0), - Tags: opts.Tags, TrustedOperatorsJwt: opts.operatorJWT, TrustedOperatorsClaim: opts.TrustedOperators, } @@ -1702,6 +1748,8 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) { v.WriteDeadline = opts.WriteDeadline v.ConfigLoadTime = s.configTime.UTC() v.ConfigDigest = opts.configDigest + v.Tags = opts.Tags + v.Metadata = opts.Metadata // Update route URLs if applicable if s.varzUpdateRouteURLs { v.Cluster.URLs = urlsToStrings(opts.Routes) @@ -1714,6 +1762,19 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) { v.Websocket.TLSPinnedCerts = getPinnedCertsAsSlice(opts.Websocket.TLSPinnedCerts) v.TLSOCSPPeerVerify = s.ocspPeerVerify && v.TLSRequired && s.opts.tlsConfigOpts != nil && s.opts.tlsConfigOpts.OCSPPeerConfig != nil && s.opts.tlsConfigOpts.OCSPPeerConfig.Verify + + if opts.Proxies != nil { + if v.Proxies == nil { + v.Proxies = &ProxiesOptsVarz{} + } + trusted := make([]*ProxyOptsVarz, 0, len(opts.Proxies.Trusted)) + for _, t := range opts.Proxies.Trusted { + trusted = append(trusted, &ProxyOptsVarz{Key: t.Key}) + } + v.Proxies.Trusted = trusted + } else { + v.Proxies = nil + } } func getPinnedCertsAsSlice(certs PinnedCertSet) []string { @@ -1752,12 +1813,20 @@ func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64 v.OutMsgs = atomic.LoadInt64(&s.outMsgs) v.OutBytes = atomic.LoadInt64(&s.outBytes) v.SlowConsumers = atomic.LoadInt64(&s.slowConsumers) + v.StalledClients = atomic.LoadInt64(&s.stalls) v.SlowConsumersStats = &SlowConsumersStats{ Clients: s.NumSlowConsumersClients(), Routes: s.NumSlowConsumersRoutes(), Gateways: s.NumSlowConsumersGateways(), Leafs: s.NumSlowConsumersLeafs(), } + v.StaleConnections = atomic.LoadInt64(&s.staleConnections) + v.StaleConnectionStats = &StaleConnectionStats{ + Clients: s.NumStaleConnectionsClients(), + Routes: s.NumStaleConnectionsRoutes(), + Gateways: s.NumStaleConnectionsGateways(), + Leafs: s.NumStaleConnectionsLeafs(), + } v.PinnedAccountFail = atomic.LoadUint64(&s.pinnedAccFail) // Make sure to reset in case we are re-using. @@ -2248,20 +2317,22 @@ type LeafzOptions struct { // LeafInfo has detailed information on each remote leafnode connection. type LeafInfo struct { - ID uint64 `json:"id"` - Name string `json:"name"` - IsSpoke bool `json:"is_spoke"` - Account string `json:"account"` - IP string `json:"ip"` - Port int `json:"port"` - RTT string `json:"rtt,omitempty"` - InMsgs int64 `json:"in_msgs"` - OutMsgs int64 `json:"out_msgs"` - InBytes int64 `json:"in_bytes"` - OutBytes int64 `json:"out_bytes"` - NumSubs uint32 `json:"subscriptions"` - Subs []string `json:"subscriptions_list,omitempty"` - Compression string `json:"compression,omitempty"` + ID uint64 `json:"id"` + Name string `json:"name"` + IsSpoke bool `json:"is_spoke"` + IsIsolated bool `json:"is_isolated,omitempty"` + Account string `json:"account"` + IP string `json:"ip"` + Port int `json:"port"` + RTT string `json:"rtt,omitempty"` + InMsgs int64 `json:"in_msgs"` + OutMsgs int64 `json:"out_msgs"` + InBytes int64 `json:"in_bytes"` + OutBytes int64 `json:"out_bytes"` + NumSubs uint32 `json:"subscriptions"` + Subs []string `json:"subscriptions_list,omitempty"` + Compression string `json:"compression,omitempty"` + Proxy *ProxyInfo `json:"proxy,omitempty"` } // Leafz returns a Leafz structure containing information about leafnodes. @@ -2294,6 +2365,7 @@ func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) { ID: ln.cid, Name: ln.leaf.remoteServer, IsSpoke: ln.isSpokeLeafNode(), + IsIsolated: ln.leaf.isolated, Account: ln.acc.Name, IP: ln.host, Port: int(ln.port), @@ -2304,6 +2376,7 @@ func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) { OutBytes: ln.outBytes, NumSubs: uint32(len(ln.subs)), Compression: ln.leaf.compression, + Proxy: createProxyInfo(ln), } if opts != nil && opts.Subscriptions { lni.Subs = make([]string, 0, len(ln.subs)) @@ -2373,7 +2446,7 @@ func (s *Server) AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error) s.accounts.Range(func(key, a any) bool { acc := a.(*Account) acc.mu.RLock() - if opts.IncludeUnused || acc.numLocalConnections() != 0 { + if (opts != nil && opts.IncludeUnused) || acc.numLocalConnections() != 0 { stz.Accounts = append(stz.Accounts, acc.statz()) } acc.mu.RUnlock() @@ -2516,6 +2589,10 @@ func (reason ClosedState) String() string { return "Cluster Names Identical" case Kicked: return "Kicked" + case ProxyNotTrusted: + return "Proxy Not Trusted" + case ProxyRequired: + return "Proxy Required" } return "Unknown State" 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 abf08dc5e..f3646ed9c 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 @@ -514,7 +514,7 @@ func (s *Server) startMQTT() { hp := net.JoinHostPort(o.Host, strconv.Itoa(port)) s.mu.Lock() s.mqtt.sessmgr.sessions = make(map[string]*mqttAccountSessionManager) - hl, err = net.Listen("tcp", hp) + hl, err = natsListen("tcp", hp) s.mqtt.listenerErr = err if err != nil { s.mu.Unlock() @@ -647,7 +647,7 @@ func (s *Server) createMQTTClient(conn net.Conn, ws *websocket) *client { if tlsRequired { c.Debugf("TLS handshake complete") cs := c.nc.(*tls.Conn).ConnectionState() - c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite)) + c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite)) } c.mu.Unlock() diff --git a/vendor/github.com/nats-io/nats-server/v2/server/nkey.go b/vendor/github.com/nats-io/nats-server/v2/server/nkey.go index f0579177c..97195b0ec 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/nkey.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/nkey.go @@ -34,7 +34,7 @@ func (s *Server) NonceRequired() bool { // nonceRequired tells us if we should send a nonce. // Lock should be held on entry. func (s *Server) nonceRequired() bool { - return s.getOpts().AlwaysEnableNonce || len(s.nkeys) > 0 || s.trustedKeys != nil + return s.getOpts().AlwaysEnableNonce || len(s.nkeys) > 0 || s.trustedKeys != nil || len(s.proxiesKeyPairs) > 0 } // Generate a nonce for INFO challenge. 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 07b207c0b..e041d4322 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 @@ -77,6 +77,7 @@ type ClusterOpts struct { Advertise string `json:"-"` NoAdvertise bool `json:"-"` ConnectRetries int `json:"-"` + ConnectBackoff bool `json:"-"` PoolSize int `json:"-"` PinnedAccounts []string `json:"-"` Compression CompressionOpts `json:"-"` @@ -121,6 +122,7 @@ type GatewayOpts struct { TLSPinnedCerts PinnedCertSet `json:"-"` Advertise string `json:"advertise,omitempty"` ConnectRetries int `json:"connect_retries,omitempty"` + ConnectBackoff bool `json:"connect_backoff,omitempty"` Gateways []*RemoteGatewayOpts `json:"gateways,omitempty"` RejectUnknown bool `json:"reject_unknown,omitempty"` // config got renamed to reject_unknown_cluster @@ -149,6 +151,7 @@ type LeafNodeOpts struct { Port int `json:"port,omitempty"` Username string `json:"-"` Password string `json:"-"` + ProxyRequired bool `json:"-"` Nkey string `json:"-"` Account string `json:"-"` Users []*User `json:"-"` @@ -186,6 +189,10 @@ type LeafNodeOpts struct { // least" test). MinVersion string + // Isolate subject interest from other leafnode connections, preventing + // east-west propagation. + IsolateLeafnodeInterest bool `json:"-"` + // Not exported, for tests. resolver netResolver dialTimeout time.Duration @@ -245,14 +252,30 @@ type RemoteLeafOpts struct { // If JetStreamClusterMigrate is set to true, this is the time after which the leader // will be migrated away from this server if still disconnected. JetStreamClusterMigrateDelay time.Duration `json:"jetstream_cluster_migrate_delay,omitempty"` + + // LocalIsolation isolates this remote from east-west subject interest originating locally. + LocalIsolation bool `json:"local_isolation,omitempty"` + + // RequestIsolation asks the remote side to isolate us from their east-west subject interest. + RequestIsolation bool `json:"request_isolation,omitempty"` + + // If this is set to true, the connection to this remote will not be solicited. + // During a configuration reload, if this is changed from `false` to `true`, the + // existing connection will be closed and not solicited again (until it is changed + // to `false` again. + Disabled bool `json:"-"` } // JSLimitOpts are active limits for the meta cluster type JSLimitOpts struct { - MaxRequestBatch int `json:"max_request_batch,omitempty"` // MaxRequestBatch is the maximum amount of updates that can be sent in a batch - MaxAckPending int `json:"max_ack_pending,omitempty"` // MaxAckPending is the server limit for maximum amount of outstanding Acks - MaxHAAssets int `json:"max_ha_assets,omitempty"` // MaxHAAssets is the maximum of Streams and Consumers that may have more than 1 replica - Duplicates time.Duration `json:"max_duplicate_window,omitempty"` // Duplicates is the maximum value for duplicate tracking on Streams + MaxRequestBatch int `json:"max_request_batch,omitempty"` // MaxRequestBatch is the maximum amount of updates that can be sent in a batch + MaxAckPending int `json:"max_ack_pending,omitempty"` // MaxAckPending is the server limit for maximum amount of outstanding Acks + MaxHAAssets int `json:"max_ha_assets,omitempty"` // MaxHAAssets is the maximum of Streams and Consumers that may have more than 1 replica + Duplicates time.Duration `json:"max_duplicate_window,omitempty"` // Duplicates is the maximum value for duplicate tracking on Streams + MaxBatchInflightPerStream int `json:"max_batch_inflight_per_stream,omitempty"` // MaxBatchInflightPerStream is the maximum amount of open batches per stream + MaxBatchInflightTotal int `json:"max_batch_inflight_total,omitempty"` // MaxBatchInflightTotal is the maximum amount of total open batches per server + MaxBatchSize int `json:"max_batch_size,omitempty"` // MaxBatchSize is the maximum amount of messages allowed in a batch publish to a Stream + MaxBatchTimeout time.Duration `json:"max_batch_timeout,omitempty"` // MaxBatchTimeout is the maximum time to receive the commit message after receiving the first message of a batch } type JSTpmOpts struct { @@ -291,7 +314,8 @@ type Options struct { Trace bool `json:"-"` Debug bool `json:"-"` TraceVerbose bool `json:"-"` - // TraceHeaders if true will only trace message headers, not the payload + + // TraceHeaders if true will only trace message headers, not the payload. TraceHeaders bool `json:"-"` NoLog bool `json:"-"` NoSigs bool `json:"-"` @@ -312,6 +336,7 @@ type Options struct { NoSystemAccount bool `json:"-"` Username string `json:"-"` Password string `json:"-"` + ProxyRequired bool `json:"-"` Authorization string `json:"-"` AuthCallout *AuthCallout `json:"-"` PingInterval time.Duration `json:"ping_interval"` @@ -329,7 +354,7 @@ type Options struct { Gateway GatewayOpts `json:"gateway,omitempty"` LeafNode LeafNodeOpts `json:"leaf,omitempty"` JetStream bool `json:"jetstream"` - JetStreamStrict bool `json:"-"` + NoJetStreamStrict bool `json:"-"` // Strict by default. JetStreamMaxMemory int64 `json:"-"` JetStreamMaxStore int64 `json:"-"` JetStreamDomain string `json:"-"` @@ -426,10 +451,16 @@ type Options struct { // and used as a filter criteria for some system requests. Tags jwt.TagList `json:"-"` + // Metadata describing the server. They will be included in 'Z' responses. + Metadata map[string]string `json:"-"` + // OCSPConfig enables OCSP Stapling in the server. OCSPConfig *OCSPConfig tlsConfigOpts *TLSConfigOpts + // Proxies configuration. + Proxies *ProxiesConfig + // private fields, used to know if bool options are explicitly // defined in config and/or command line params. inConfig map[string]bool @@ -714,6 +745,8 @@ type authorization struct { token string nkey string acc string + // If connection must come through proxy + proxyRequired bool // Multiple Nkeys/Users nkeys []*NkeyUser users []*User @@ -737,6 +770,7 @@ type TLSConfigOpts struct { FallbackDelay time.Duration // Where supported, indicates how long to wait for the handshake before falling back to sending the INFO protocol first. Timeout float64 RateLimit int64 + AllowInsecureCiphers bool Ciphers []uint16 CurvePreferences []tls.CurveID PinnedCerts PinnedCertSet @@ -765,6 +799,17 @@ type OCSPConfig struct { OverrideURLs []string } +// ProxiesConfig represents the options of Proxies. +type ProxiesConfig struct { + Trusted []*ProxyConfig +} + +// ProxyConfig represents the options of Proxy. +type ProxyConfig struct { + // Public key. + Key string +} + var tlsUsage = ` TLS configuration is specified in the tls section of a configuration file: @@ -1047,6 +1092,7 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin o.authBlockDefined = true o.Username = auth.user o.Password = auth.pass + o.ProxyRequired = auth.proxyRequired o.Authorization = auth.token o.AuthTimeout = auth.timeout o.AuthCallout = auth.callout @@ -1649,6 +1695,24 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin *errors = append(*errors, err) return } + case "server_metadata": + var err error + switch v := v.(type) { + case map[string]any: + for mk, mv := range v { + tk, mv = unwrapValue(mv, <) + if o.Metadata == nil { + o.Metadata = make(map[string]string) + } + o.Metadata[mk] = mv.(string) + } + default: + err = &configErr{tk, fmt.Sprintf("error parsing metadata: unsupported type %T", v)} + } + if err != nil { + *errors = append(*errors, err) + return + } case "default_js_domain": vv, ok := v.(map[string]any) if !ok { @@ -1693,6 +1757,13 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin o.NoFastProducerStall = v.(bool) case "max_closed_clients": o.MaxClosedClients = int(v.(int64)) + case "proxies": + proxies, err := parseProxies(tk, errors) + if err != nil { + *errors = append(*errors, err) + return + } + o.Proxies = proxies default: if au := atomic.LoadInt32(&allowUnknownTopLevelField); au == 0 && !tk.IsUsedVariable() { err := &unknownConfigFieldErr{ @@ -1883,6 +1954,8 @@ func parseCluster(v any, opts *Options, errors *[]error, warnings *[]error) erro trackExplicitVal(&opts.inConfig, "Cluster.NoAdvertise", opts.Cluster.NoAdvertise) case "connect_retries": opts.Cluster.ConnectRetries = int(mv.(int64)) + case "connect_backoff": + opts.Cluster.ConnectBackoff = mv.(bool) case "permissions": perms, err := parseUserPermissions(mv, errors) if err != nil { @@ -2091,6 +2164,8 @@ func parseGateway(v any, o *Options, errors *[]error, warnings *[]error) error { o.Gateway.Advertise = mv.(string) case "connect_retries": o.Gateway.ConnectRetries = int(mv.(int64)) + case "connect_backoff": + o.Gateway.ConnectBackoff = mv.(bool) case "gateways": gateways, err := parseGateways(mv, errors, warnings) if err != nil { @@ -2265,7 +2340,7 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error { var lt token tk, v := unwrapValue(v, <) - lim := JSLimitOpts{} + opts.JetStreamLimits = JSLimitOpts{} vv, ok := v.(map[string]any) if !ok { @@ -2275,14 +2350,57 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error { tk, mv = unwrapValue(mv, <) switch strings.ToLower(mk) { case "max_ack_pending": - lim.MaxAckPending = int(mv.(int64)) + opts.JetStreamLimits.MaxAckPending = int(mv.(int64)) case "max_ha_assets": - lim.MaxHAAssets = int(mv.(int64)) + opts.JetStreamLimits.MaxHAAssets = int(mv.(int64)) case "max_request_batch": - lim.MaxRequestBatch = int(mv.(int64)) + opts.JetStreamLimits.MaxRequestBatch = int(mv.(int64)) case "duplicate_window": var err error - lim.Duplicates, err = time.ParseDuration(mv.(string)) + opts.JetStreamLimits.Duplicates, err = time.ParseDuration(mv.(string)) + if err != nil { + *errors = append(*errors, err) + } + case "batch": + if err := parseJetStreamLimitsBatch(tk, opts, errors); err != nil { + return err + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + return nil +} + +func parseJetStreamLimitsBatch(v any, opts *Options, errors *[]error) error { + var lt token + tk, v := unwrapValue(v, <) + + vv, ok := v.(map[string]any) + if !ok { + return &configErr{tk, fmt.Sprintf("Expected a map to define batch limits, got %T", v)} + } + for mk, mv := range vv { + tk, mv = unwrapValue(mv, <) + switch strings.ToLower(mk) { + case "max_inflight_per_stream": + opts.JetStreamLimits.MaxBatchInflightPerStream = int(mv.(int64)) + case "max_inflight_total": + opts.JetStreamLimits.MaxBatchInflightTotal = int(mv.(int64)) + case "max_msgs": + opts.JetStreamLimits.MaxBatchSize = int(mv.(int64)) + case "timeout": + var err error + opts.JetStreamLimits.MaxBatchTimeout, err = time.ParseDuration(mv.(string)) if err != nil { *errors = append(*errors, err) } @@ -2299,7 +2417,6 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error { } } } - opts.JetStreamLimits = lim return nil } @@ -2308,7 +2425,7 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error { var lt token tk, v := unwrapValue(v, <) - tpm := JSTpmOpts{} + opts.JetStreamTpm = JSTpmOpts{} vv, ok := v.(map[string]interface{}) if !ok { @@ -2318,13 +2435,13 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error { tk, mv = unwrapValue(mv, <) switch strings.ToLower(mk) { case "keys_file": - tpm.KeysFile = mv.(string) + opts.JetStreamTpm.KeysFile = mv.(string) case "encryption_password": - tpm.KeyPassword = mv.(string) + opts.JetStreamTpm.KeyPassword = mv.(string) case "srk_password": - tpm.SrkPassword = mv.(string) + opts.JetStreamTpm.SrkPassword = mv.(string) case "pcr": - tpm.Pcr = int(mv.(int64)) + opts.JetStreamTpm.Pcr = int(mv.(int64)) case "cipher": if err := setJetStreamEkCipher(opts, mv, tk); err != nil { return err @@ -2342,7 +2459,6 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error { } } } - opts.JetStreamTpm = tpm return nil } @@ -2384,7 +2500,7 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er switch strings.ToLower(mk) { case "strict": if v, ok := mv.(bool); ok { - opts.JetStreamStrict = v + opts.NoJetStreamStrict = !v } else { return &configErr{tk, fmt.Sprintf("Expected 'true' or 'false' for bool value, got '%s'", mv)} } @@ -2521,6 +2637,7 @@ func parseLeafNodes(v any, opts *Options, errors *[]error, warnings *[]error) er } opts.LeafNode.Username = auth.user opts.LeafNode.Password = auth.pass + opts.LeafNode.ProxyRequired = auth.proxyRequired opts.LeafNode.AuthTimeout = auth.timeout opts.LeafNode.Account = auth.acc opts.LeafNode.Users = auth.users @@ -2575,6 +2692,8 @@ func parseLeafNodes(v any, opts *Options, errors *[]error, warnings *[]error) er *errors = append(*errors, err) continue } + case "isolate_leafnode_interest", "isolate": + opts.LeafNode.IsolateLeafnodeInterest = mv.(bool) default: if !tk.IsUsedVariable() { err := &unknownConfigFieldErr{ @@ -2647,6 +2766,8 @@ func parseLeafAuthorization(v any, errors, warnings *[]error) (*authorization, e auth.users = users case "account": auth.acc = mv.(string) + case "proxy_required": + auth.proxyRequired = mv.(bool) default: if !tk.IsUsedVariable() { err := &unknownConfigFieldErr{ @@ -2705,6 +2826,8 @@ func parseLeafUsers(mv any, errors *[]error) ([]*User, error) { // we need to create internal objects to store u/p and account // name and have a server structure to hold that. user.Account = NewAccount(v.(string)) + case "proxy_required": + user.ProxyRequired = v.(bool) default: if !tk.IsUsedVariable() { err := &unknownConfigFieldErr{ @@ -2854,6 +2977,10 @@ func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteL default: *errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected boolean or map for jetstream_cluster_migrate, got %T", v)}) } + case "isolate_leafnode_interest", "isolate": + remote.LocalIsolation = v.(bool) + case "request_isolation": + remote.RequestIsolation = v.(bool) case "compression": if err := parseCompression(&remote.Compression, CompressionS2Auto, tk, k, v); err != nil { *errors = append(*errors, err) @@ -2861,6 +2988,8 @@ func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteL } case "first_info_timeout": remote.FirstInfoTimeout = parseDuration(k, tk, v, errors, warnings) + case "disabled": + remote.Disabled = v.(bool) default: if !tk.IsUsedVariable() { err := &unknownConfigFieldErr{ @@ -4192,6 +4321,8 @@ func parseAuthorization(v any, errors, warnings *[]error) (*authorization, error continue } auth.callout = ac + case "proxy_required": + auth.proxyRequired = mv.(bool) default: if !tk.IsUsedVariable() { err := &unknownConfigFieldErr{ @@ -4264,6 +4395,9 @@ func parseUsers(mv any, errors *[]error) ([]*NkeyUser, []*User, error) { cts := parseAllowedConnectionTypes(tk, <, v, errors) nkey.AllowedConnectionTypes = cts user.AllowedConnectionTypes = cts + case "proxy_required": + nkey.ProxyRequired = v.(bool) + user.ProxyRequired = v.(bool) default: if !tk.IsUsedVariable() { err := &unknownConfigFieldErr{ @@ -4639,12 +4773,11 @@ func PrintTLSHelpAndDie() { os.Exit(0) } -func parseCipher(cipherName string) (uint16, error) { +func parseCipher(cipherName string) (*tls.CipherSuite, error) { cipher, exists := cipherMap[cipherName] if !exists { - return 0, fmt.Errorf("unrecognized cipher %s", cipherName) + return nil, fmt.Errorf("unrecognized cipher %s", cipherName) } - return cipher, nil } @@ -4680,6 +4813,7 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) { tlsm map[string]any tc = TLSConfigOpts{} lt token + ics []*tls.CipherSuite // Insecure ciphers found ) defer convertPanicToError(<, &retErr) @@ -4739,6 +4873,12 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) { tc.Verify = verify } tc.TLSCheckKnownURLs = verify + case "allow_insecure_cipher_suites": + allow, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'allow_insecure_cipher_suites' to be a boolean"} + } + tc.AllowInsecureCiphers = allow case "cipher_suites": ra := mv.([]any) if len(ra) == 0 { @@ -4751,7 +4891,10 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) { if err != nil { return nil, &configErr{tk, err.Error()} } - tc.Ciphers = append(tc.Ciphers, cipher) + tc.Ciphers = append(tc.Ciphers, cipher.ID) + if cipher.Insecure { + ics = append(ics, cipher) + } } case "curve_preferences": ra := mv.([]any) @@ -4969,6 +5112,16 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) { tc.CurvePreferences = defaultCurvePreferences() } + // If we don't allow insecure ciphers, and yet some were configured, then we + // should error. + if !tc.AllowInsecureCiphers && len(ics) > 0 { + names := make([]string, 0, len(ics)) + for _, ic := range ics { + names = append(names, ic.Name) + } + return nil, &configErr{tk, fmt.Sprintf("insecure cipher suites configured without 'allow_insecure_cipher_suites' option set: %s", strings.Join(names, ", "))} + } + return &tc, nil } @@ -5258,6 +5411,95 @@ func parseMQTT(v any, o *Options, errors *[]error, warnings *[]error) error { return nil } +func parseProxies(mv any, errors *[]error) (*ProxiesConfig, error) { + var ( + tk token + lt token + proxies = &ProxiesConfig{} + ) + defer convertPanicToErrorList(<, errors) + + tk, mv = unwrapValue(mv, <) + pm, ok := mv.(map[string]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("expected proxies to be a map/struct, got %T", mv)} + } + for mk, mv := range pm { + tk, _ = unwrapValue(mv, <) + + switch strings.ToLower(mk) { + case "trusted": + trusted, err := parseProxiesTrusted(tk, errors) + if err != nil { + *errors = append(*errors, err) + continue + } + proxies.Trusted = trusted + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + } + } + return proxies, nil +} + +func parseProxiesTrusted(mv any, errors *[]error) ([]*ProxyConfig, error) { + var ( + tk token + lt token + trusted []*ProxyConfig + ) + defer convertPanicToErrorList(<, errors) + + tk, mv = unwrapValue(mv, <) + ta, ok := mv.([]any) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("expected proxies' trusted field to be an array, got %T", mv)} + } + for _, t := range ta { + tk, t = unwrapValue(t, <) + // Check its a map/struct + tm, ok := t.(map[string]any) + if !ok { + err := &configErr{tk, fmt.Sprintf("expected proxies' trusted entry to be a map/struct, got %T", t)} + *errors = append(*errors, err) + continue + } + proxy := &ProxyConfig{} + for k, v := range tm { + tk, v = unwrapValue(v, <) + switch strings.ToLower(k) { + case "key", "public_key": + proxy.Key = v.(string) + if !nkeys.IsValidPublicKey(proxy.Key) { + *errors = append(*errors, &configErr{tk, fmt.Sprintf("invalid proxy key %q", proxy.Key)}) + continue + } + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + trusted = append(trusted, proxy) + } + return trusted, nil +} + // GenTLSConfig loads TLS related configuration parameters. func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) { // Create the tls.Config from our options before including the certs. 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 902f12078..69f09f9c3 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 @@ -45,6 +45,7 @@ type RaftNode interface { SendSnapshot(snap []byte) error NeedSnapshot() bool Applied(index uint64) (entries uint64, bytes uint64) + Processed(index uint64, applied uint64) (entries uint64, bytes uint64) State() RaftState Size() (entries, bytes uint64) Progress() (index, commit, applied uint64) @@ -98,7 +99,7 @@ type WAL interface { State() StreamState FastState(*StreamState) Stop() error - Delete() error + Delete(inline bool) error } type Peer struct { @@ -168,12 +169,13 @@ type raft struct { llqrt time.Time // Last quorum lost time lsut time.Time // Last scale-up time - term uint64 // The current vote term - pterm uint64 // Previous term from the last snapshot - pindex uint64 // Previous index from the last snapshot - commit uint64 // Index of the most recent commit - applied uint64 // Index of the most recently applied commit - papplied uint64 // First sequence of our log, matches when we last installed a snapshot. + term uint64 // The current vote term + pterm uint64 // Previous term from the last snapshot + pindex uint64 // Previous index from the last snapshot + commit uint64 // Index of the most recent commit + processed uint64 // Index of the most recently processed commit + applied uint64 // Index of the most recently applied commit + papplied uint64 // First sequence of our log, matches when we last installed a snapshot. aflr uint64 // Index when to signal initial messages have been applied after becoming leader. 0 means signaling is disabled. @@ -220,12 +222,13 @@ type raft struct { leadc chan bool // Leader changes quit chan struct{} // Raft group shutdown - lxfer bool // Are we doing a leadership transfer? - hcbehind bool // Were we falling behind at the last health check? (see: isCurrent) - maybeLeader bool // The group had a preferred leader. And is maybe already acting as leader prior to scale up. - paused bool // Whether or not applies are paused - observer bool // The node is observing, i.e. not able to become leader - pobserver bool // Were we previously an observer? + lxfer bool // Are we doing a leadership transfer? + hcbehind bool // Were we falling behind at the last health check? (see: isCurrent) + maybeLeader bool // The group had a preferred leader. And is maybe already acting as leader prior to scale up. + paused bool // Whether or not applies are paused + observer bool // The node is observing, i.e. not able to become leader + initializing bool // The node is new, and "empty log" checks can be temporarily relaxed. + scaleUp bool // The node is part of a scale up, puts us in observer mode until the log contains data. } type proposedEntry struct { @@ -281,6 +284,16 @@ type RaftConfig struct { Log WAL Track bool Observer bool + + // Recovering must be set for a Raft group that's recovering after a restart, or if it's + // first seen after a catchup from another server. If a server recovers with an empty log, + // we know to protect against data loss. + Recovering bool + + // ScaleUp identifies the Raft peer set is being scaled up. + // We need to protect against losing state due to the new peers starting with an empty log. + // Therefore, these empty servers can't try to become leader until they at least have _some_ state. + ScaleUp bool } var ( @@ -539,6 +552,17 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel n.Lock() n.resetElectionTimeout() n.llqrt = time.Now() + + // If our log is empty, and we're initializing, relax the "empty log" checks temporarily. + if !cfg.Recovering && n.pindex == 0 { + n.initializing = true + // If we're scaling up and our log is empty, must put ourselves into observer + // and wait for data from the leader. + if !cfg.Observer && cfg.ScaleUp { + n.scaleUp = true + n.setObserverLocked(true, extUndetermined) + } + } n.Unlock() // Register the Raft group. @@ -842,12 +866,13 @@ 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 { + n.Lock() + defer n.Unlock() + // 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) return errNotLeader } - n.Lock() - defer n.Unlock() // Error if we had a previous write error. if werr := n.werr; werr != nil { @@ -857,15 +882,16 @@ func (n *raft) Propose(data []byte) error { return nil } -// ProposeDirect will propose multiple entries at once. +// ProposeMulti will propose multiple entries at once. // This should only be called on the leader. func (n *raft) ProposeMulti(entries []*Entry) error { - if state := n.State(); state != Leader { - n.debug("Direct proposal ignored, not leader (state: %v)", state) - return errNotLeader - } 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) + return errNotLeader + } // Error if we had a previous write error. if werr := n.werr; werr != nil { @@ -893,10 +919,12 @@ func (n *raft) ForwardProposal(entry []byte) error { // ProposeAddPeer is called to add a peer to the group. func (n *raft) ProposeAddPeer(peer string) error { + n.RLock() + // Check state under lock, we might not be leader anymore. if n.State() != Leader { + n.RUnlock() return errNotLeader } - n.RLock() // Error if we had a previous write error. if werr := n.werr; werr != nil { n.RUnlock() @@ -984,10 +1012,13 @@ func (n *raft) AdjustBootClusterSize(csz int) error { // AdjustClusterSize will change the cluster set size. // Must be the leader. func (n *raft) AdjustClusterSize(csz int) error { + n.Lock() + defer n.Unlock() + + // Check state under lock, we might not be leader anymore. if n.State() != Leader { return errNotLeader } - n.Lock() // Same floor as bootstrap. if csz < 2 { csz = 2 @@ -997,7 +1028,6 @@ func (n *raft) AdjustClusterSize(csz int) error { // a quorum. n.csz = csz n.qn = n.csz/2 + 1 - n.Unlock() n.sendPeerState() return nil @@ -1023,7 +1053,6 @@ func (n *raft) PauseApply() error { n.paused = true n.hcommit = n.commit // Also prevent us from trying to become a leader while paused and catching up. - n.pobserver, n.observer = n.observer, true n.resetElect(observerModeInterval) return nil @@ -1066,8 +1095,7 @@ func (n *raft) ResumeApply() { } } - // Clear our observer and paused state after we apply. - n.observer, n.pobserver = n.pobserver, false + // Clear our paused state after we apply. n.paused = false n.hcommit = 0 @@ -1084,6 +1112,16 @@ func (n *raft) ResumeApply() { // apply queue. It will return the number of entries and an estimation of the // byte size that could be removed with a snapshot/compact. func (n *raft) Applied(index uint64) (entries uint64, bytes uint64) { + return n.Processed(index, index) +} + +// Processed is a callback that must be called by the upper layer when it +// has processed the committed entries that it received from the apply queue, +// but it (maybe) hasn't applied all the processed entries yet. +// Used to indicate a commit was processed, even if it wasn't applied yet and +// can't be compacted away by a snapshot just yet. Which allows us to try to +// become leader if we've processed all commits, even if they're not all applied. +func (n *raft) Processed(index uint64, applied uint64) (entries uint64, bytes uint64) { n.Lock() defer n.Unlock() @@ -1092,13 +1130,22 @@ func (n *raft) Applied(index uint64) (entries uint64, bytes uint64) { return 0, 0 } - // Ignore if already applied. - if index > n.applied { - n.applied = index + // Ignore if already processed. + if index > n.processed { + n.processed = index } - // If it was set, and we reached the minimum applied index, reset and send signal to upper layer. - if n.aflr > 0 && index >= n.aflr { + // Ignore if already applied. + if applied > index { + applied = index + } + if applied > n.applied { + n.applied = applied + } + + // If it was set, and we reached the minimum processed index, reset and send signal to upper layer. + // We're not waiting for processed AND applied, because applying could take longer. + if n.aflr > 0 && n.processed >= n.aflr { n.aflr = 0 // Quick sanity-check to confirm we're still leader. // In which case we must signal, since switchToLeader would not have done so already. @@ -1165,7 +1212,10 @@ func (n *raft) encodeSnapshot(snap *snapshot) []byte { // Should only be used when the upper layers know this is most recent. // Used when restoring streams, moving a stream from R1 to R>1, etc. func (n *raft) SendSnapshot(data []byte) error { - n.sendAppendEntry([]*Entry{{EntrySnapshot, data}}) + n.Lock() + defer n.Unlock() + // Don't check if we're leader before sending and storing, this is used on scaleup. + n.sendAppendEntryLocked([]*Entry{{EntrySnapshot, data}}, false) return nil } @@ -1592,11 +1642,12 @@ func (n *raft) selectNextLeader() string { // StepDown will have a leader stepdown and optionally do a leader transfer. func (n *raft) StepDown(preferred ...string) error { + n.Lock() + // Check state under lock, we might not be leader anymore. if n.State() != Leader { + n.Unlock() return errNotLeader } - - n.Lock() if len(preferred) > 1 { n.Unlock() return errTooManyPrefs @@ -1686,6 +1737,7 @@ func (n *raft) CampaignImmediately() error { n.Lock() defer n.Unlock() n.maybeLeader = true + n.resetInitializing() return n.campaign(minCampaignTimeout / 2) } @@ -1775,21 +1827,27 @@ func (n *raft) Peers() []*Peer { // Update and propose our known set of peers. func (n *raft) ProposeKnownPeers(knownPeers []string) { + n.Lock() + defer n.Unlock() // If we are the leader update and send this update out. if n.State() != Leader { return } - n.UpdateKnownPeers(knownPeers) + n.updateKnownPeersLocked(knownPeers) n.sendPeerState() } // Update our known set of peers. func (n *raft) UpdateKnownPeers(knownPeers []string) { n.Lock() + n.updateKnownPeersLocked(knownPeers) + n.Unlock() +} + +func (n *raft) updateKnownPeersLocked(knownPeers []string) { // Process like peer state update. ps := &peerState{knownPeers, len(knownPeers), n.extSt} n.processPeerState(ps) - n.Unlock() } // ApplyQ returns the apply queue that new commits will be sent to for the @@ -1826,7 +1884,7 @@ func (n *raft) Delete() { defer n.Unlock() if wal := n.wal; wal != nil { - wal.Delete() + wal.Delete(false) } os.RemoveAll(n.sd) n.debug("Deleted") @@ -2082,15 +2140,10 @@ func (n *raft) SetObserver(isObserver bool) { func (n *raft) setObserver(isObserver bool, extSt extensionState) { n.Lock() defer n.Unlock() + n.setObserverLocked(isObserver, extSt) +} - if n.paused { - // Applies are paused so we're already in observer state. - // Resuming the applies will set the state back to whatever - // is in "pobserver", so update that instead. - n.pobserver = isObserver - return - } - +func (n *raft) setObserverLocked(isObserver bool, extSt extensionState) { wasObserver := n.observer n.observer = isObserver n.extSt = extSt @@ -2387,7 +2440,7 @@ func (ae *appendEntry) encode(b []byte) ([]byte, error) { } // This can not be used post the wire level callback since we do not copy. -func (n *raft) decodeAppendEntry(msg []byte, sub *subscription, reply string) (*appendEntry, error) { +func decodeAppendEntry(msg []byte, sub *subscription, reply string) (*appendEntry, error) { if len(msg) < appendEntryBaseLen { return nil, errBadAppendEntry } @@ -2472,7 +2525,7 @@ func (ar *appendEntryResponse) encode(b []byte) []byte { // Track all peers we may have ever seen to use an string interns for appendEntryResponse decoding. var peers sync.Map -func (n *raft) decodeAppendEntryResponse(msg []byte) *appendEntryResponse { +func decodeAppendEntryResponse(msg []byte) *appendEntryResponse { if len(msg) != appendEntryResponseLen { return nil } @@ -2496,16 +2549,18 @@ func (n *raft) decodeAppendEntryResponse(msg []byte) *appendEntryResponse { func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { n.debug("Received forwarded remove peer proposal: %q", msg) - if n.State() != Leader { - n.debug("Ignoring forwarded peer removal proposal, not leader") - return - } if len(msg) != idLen { n.warn("Received invalid peer name for remove proposal: %q", msg) return } n.RLock() + // Check state under lock, we might not be leader anymore. + if n.State() != Leader { + n.debug("Ignoring forwarded peer removal proposal, not leader") + n.RUnlock() + return + } prop, werr := n.prop, n.werr n.RUnlock() @@ -2521,14 +2576,16 @@ func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _ // Called when a peer has forwarded a proposal. func (n *raft) handleForwardedProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { - if n.State() != Leader { - n.debug("Ignoring forwarded proposal, not leader") - return - } // Need to copy since this is underlying client/route buffer. msg = copyBytes(msg) n.RLock() + // Check state under lock, we might not be leader anymore. + if n.State() != Leader { + n.debug("Ignoring forwarded proposal, not leader") + n.RUnlock() + return + } prop, werr := n.prop, n.werr n.RUnlock() @@ -2564,7 +2621,6 @@ func (n *raft) runAsLeader() { n.Unlock() return } - n.Unlock() // Cleanup our subscription when we leave. defer func() { @@ -2576,6 +2632,7 @@ func (n *raft) runAsLeader() { // To send out our initial peer state. n.sendPeerState() + n.Unlock() hb := time.NewTicker(hbInterval) defer hb.Stop() @@ -2728,6 +2785,7 @@ func (n *raft) runCatchup(ar *appendEntryResponse, indexUpdatesQ *ipQueue[uint64 n.RLock() s, reply := n.s, n.areply peer, subj, term, pterm, last := ar.peer, ar.reply, n.term, n.pterm, n.pindex + leader := n.State() == Leader // Grab while holding lock, to not race. n.RUnlock() defer s.grWG.Done() @@ -2749,6 +2807,10 @@ func (n *raft) runCatchup(ar *appendEntryResponse, indexUpdatesQ *ipQueue[uint64 indexUpdatesQ.unregister() }() + if !leader { + n.debug("Canceling catchup for %q, not leader anymore", peer) + return + } n.debug("Running catchup for %q [%d:%d] to [%d:%d]", peer, ar.term, ar.index, pterm, last) const maxOutstanding = 2 * 1024 * 1024 // 2MB for now. @@ -2935,7 +2997,7 @@ func (n *raft) loadEntry(index uint64) (*appendEntry, error) { if err != nil { return nil, err } - return n.decodeAppendEntry(sm.msg, nil, _EMPTY_) + return decodeAppendEntry(sm.msg, nil, _EMPTY_) } // applyCommit will update our commit index and apply the entry to the apply queue. @@ -3189,6 +3251,7 @@ func (n *raft) runAsCandidate() { votes := map[string]struct{}{ n.ID(): {}, } + emptyVotes := map[string]struct{}{} for n.State() == Candidate { elect := n.electTimer() @@ -3216,16 +3279,28 @@ func (n *raft) runAsCandidate() { } n.RLock() nterm := n.term + csz := n.csz n.RUnlock() if vresp.granted && nterm == vresp.term { // only track peers that would be our followers n.trackPeer(vresp.peer) - votes[vresp.peer] = struct{}{} + 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. n.switchToLeader() return + } else if len(votes)+len(emptyVotes) == csz { + // Become LEADER if we've got voted in by ALL servers. + // We couldn't get quorum based on just our normal votes. + // But, we have heard from the full cluster, and some servers came up empty. + // We know for sure we have the most up-to-date log. + n.switchToLeader() + return } } else if vresp.term > nterm { // if we observe a bigger term, we should start over again or risk forming a quorum fully knowing @@ -3252,7 +3327,7 @@ func (n *raft) runAsCandidate() { // is an internal callback from the "asubj" append entry subscription. func (n *raft) handleAppendEntry(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { msg = copyBytes(msg) - if ae, err := n.decodeAppendEntry(msg, sub, reply); err == nil { + if ae, err := decodeAppendEntry(msg, sub, reply); err == nil { // Push to the new entry channel. From here one of the worker // goroutines (runAsLeader, runAsFollower, runAsCandidate) will // pick it up. @@ -3327,10 +3402,13 @@ func (n *raft) truncateWAL(term, index uint64) { } if index < n.commit { assert.Unreachable("WAL truncate lost commits", map[string]any{ - "term": term, - "index": index, - "commit": n.commit, - "applied": n.applied, + "n.accName": n.accName, + "n.group": n.group, + "n.id": n.id, + "term": term, + "index": index, + "commit": n.commit, + "applied": n.applied, }) } @@ -3345,8 +3423,11 @@ func (n *raft) truncateWAL(term, index uint64) { if n.commit > n.pindex { n.commit = n.pindex } - if n.applied > n.commit { - n.applied = n.commit + if n.processed > n.commit { + n.processed = n.commit + } + if n.applied > n.processed { + n.applied = n.processed } if n.papplied > n.applied { n.papplied = n.applied @@ -3432,11 +3513,13 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) { assert.Unreachable( "Two leaders using the same term", map[string]any{ - "Node id": n.id, - "Node term": n.term, - "AppendEntry id": ae.leader, - "AppendEntry term": ae.term, - "AppendEntry lterm": ae.lterm, + "n.accName": n.accName, + "n.group": n.group, + "n.id": n.id, + "n.term": n.term, + "ae.leader": ae.leader, + "ae.term": ae.term, + "ae.lterm": ae.lterm, }) } n.debug("Received append entry from another leader, stepping down to %q", ae.leader) @@ -3660,6 +3743,7 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) { n.Unlock() return } + n.resetInitializing() // Now send snapshot to upper levels. Only send the snapshot, not the peerstate entry. n.apply.push(newCommittedEntry(n.commit, ae.entries[:1])) @@ -3690,6 +3774,7 @@ CONTINUE: return } n.cachePendingEntry(ae) + n.resetInitializing() } else { // This is a replay on startup so just take the appendEntry version. n.pterm = ae.term @@ -3710,7 +3795,7 @@ CONTINUE: if !n.observer && !n.paused { n.lxfer = true n.xferCampaign() - } else if n.paused && !n.pobserver { + } else if n.paused { // Here we can become a leader but need to wait for resume of the apply queue. n.lxfer = true } @@ -3768,6 +3853,17 @@ CONTINUE: } } +// resetInitializing resets the notion of initializing. +// If we were scaling up, also leaves observer mode. +// Lock should be held. +func (n *raft) resetInitializing() { + n.initializing = false + if n.scaleUp { + n.scaleUp = false + n.setObserverLocked(false, extUndetermined) + } +} + // processPeerState is called when a peer state entry is received // over the wire or when we're updating known peers. // Lock should be held. @@ -3828,7 +3924,7 @@ func (n *raft) processAppendEntryResponse(ar *appendEntryResponse) { // handleAppendEntryResponse processes responses to append entries. func (n *raft) handleAppendEntryResponse(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { - ar := n.decodeAppendEntryResponse(msg) + ar := decodeAppendEntryResponse(msg) ar.reply = reply n.resp.push(ar) } @@ -3892,6 +3988,15 @@ const ( func (n *raft) sendAppendEntry(entries []*Entry) { n.Lock() defer n.Unlock() + n.sendAppendEntryLocked(entries, true) +} +func (n *raft) sendAppendEntryLocked(entries []*Entry, checkLeader bool) { + // Safeguard against sending an append entry right after a stepdown from a different goroutine. + // Specifically done while holding the lock to not race. + if checkLeader && n.State() != Leader { + n.debug("Not sending append entry, not leader") + return + } ae := n.buildAppendEntry(entries) var err error @@ -3998,14 +4103,19 @@ func (n *raft) peerNames() []string { func (n *raft) currentPeerState() *peerState { n.RLock() - ps := &peerState{n.peerNames(), n.csz, n.extSt} + ps := n.currentPeerStateLocked() n.RUnlock() return ps } +func (n *raft) currentPeerStateLocked() *peerState { + return &peerState{n.peerNames(), n.csz, n.extSt} +} + // sendPeerState will send our current peer state to the cluster. +// Lock should be held. func (n *raft) sendPeerState() { - n.sendAppendEntry([]*Entry{{EntryPeerState, encodePeerState(n.currentPeerState())}}) + n.sendAppendEntryLocked([]*Entry{{EntryPeerState, encodePeerState(n.currentPeerStateLocked())}}, true) } // Send a heartbeat. @@ -4198,6 +4308,7 @@ type voteResponse struct { term uint64 peer string granted bool + empty bool // "Empty vote", whether this peer's log is empty. } const voteResponseLen = 8 + 8 + 1 @@ -4208,9 +4319,10 @@ func (vr *voteResponse) encode() []byte { le.PutUint64(buf[0:], vr.term) copy(buf[8:], vr.peer) if vr.granted { - buf[16] = 1 - } else { - buf[16] = 0 + buf[16] |= 1 + } + if vr.empty { + buf[16] |= 2 } return buf[:voteResponseLen] } @@ -4221,7 +4333,8 @@ func decodeVoteResponse(msg []byte) *voteResponse { } var le = binary.LittleEndian vr := &voteResponse{term: le.Uint64(msg[0:]), peer: string(msg[8:16])} - vr.granted = msg[16] == 1 + vr.granted = msg[16]&1 != 0 + vr.empty = msg[16]&2 != 0 return vr } @@ -4255,7 +4368,7 @@ func (n *raft) processVoteRequest(vr *voteRequest) error { n.Lock() - vresp := &voteResponse{n.term, n.id, false} + vresp := &voteResponse{n.term, n.id, false, n.pindex == 0} defer n.debug("Sending a voteResponse %+v -> %q", vresp, vr.reply) // Ignore if we are newer. This is important so that we don't accidentally process @@ -4281,6 +4394,15 @@ func (n *raft) processVoteRequest(vr *voteRequest) error { // Only way we get to yes is through here. voteOk := n.vote == noVote || n.vote == vr.candidate + + // If we have an empty log, but are initializing. + if voteOk && vresp.empty && n.initializing { + // Reset notion of having an empty log if we're voting during initialization/scale up. + // Ensures they only need quorum, and not need to hear from all servers. + vresp.empty = false + } + + // Other server's log needs to be equal or more up-to-date than ours. if voteOk && (vr.lastTerm > n.pterm || vr.lastTerm == n.pterm && vr.lastIndex >= n.pindex) { vresp.granted = true n.term = vr.term @@ -4450,7 +4572,7 @@ func (n *raft) switchToCandidate() { // If we are catching up or are in observer mode we can not switch. // Avoid petitioning to become leader if we're behind on applies. - if n.observer || n.paused || n.applied < n.commit { + if n.observer || n.paused || n.processed < n.commit { n.resetElect(minElectionTimeout / 4) return } 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 784b90577..4a49026a6 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 @@ -369,6 +369,19 @@ func (u *tagsOption) IsStatszChange() bool { return true } +// metadataOption implements the option interface for the `metadata` setting. +type metadataOption struct { + noopOption // Not authOption because this is a no-op; will be reloaded with options. +} + +func (u *metadataOption) Apply(server *Server) { + server.Noticef("Reloaded: metadata") +} + +func (u *metadataOption) IsStatszChange() bool { + return true +} + // usersOption implements the option interface for the authorization `users` // setting. type usersOption struct { @@ -862,6 +875,7 @@ type leafNodeOption struct { noopOption tlsFirstChanged bool compressionChanged bool + disabledChanged bool } func (l *leafNodeOption) Apply(s *Server) { @@ -873,8 +887,9 @@ func (l *leafNodeOption) Apply(s *Server) { s.Noticef("Reloaded: LeafNode Remote to %v TLS HandshakeFirst value is: %v", r.URLs, r.TLSHandshakeFirst) } } - if l.compressionChanged { + if l.compressionChanged || l.disabledChanged { var leafs []*client + var solicit []*leafNodeCfg acceptSideCompOpts := &opts.LeafNode.Compression s.mu.RLock() @@ -887,10 +902,15 @@ func (l *leafNodeOption) Apply(s *Server) { if l := len(s.leafRemoteCfgs); l < max { max = l } - for i := 0; i < max; i++ { + for i := range max { lr := s.leafRemoteCfgs[i] + or := opts.LeafNode.Remotes[i] lr.Lock() - lr.Compression = opts.LeafNode.Remotes[i].Compression + lr.Compression = or.Compression + if lr.Disabled && !or.Disabled { + solicit = append(solicit, lr) + } + lr.Disabled = or.Disabled lr.Unlock() } @@ -899,6 +919,13 @@ func (l *leafNodeOption) Apply(s *Server) { l.mu.Lock() if r := l.leaf.remote; r != nil { + // If newly marked as disabled, collect and ignore the rest. + if r.Disabled { + l.flags.set(noReconnect) + leafs = append(leafs, l) + l.mu.Unlock() + continue + } co = &r.Compression } else { co = acceptSideCompOpts @@ -929,11 +956,25 @@ func (l *leafNodeOption) Apply(s *Server) { l.mu.Unlock() } s.mu.RUnlock() - // Close the connections for which negotiation is required. + // Close the connections for which negotiation is required, or that + // have been disabled. for _, l := range leafs { l.closeConnection(ClientClosed) } - s.Noticef("Reloaded: LeafNode compression settings") + if l.compressionChanged { + s.Noticef("Reloaded: LeafNode compression settings") + } + if l.disabledChanged { + if len(leafs) > 0 { + s.Noticef("Reloaded: LeafNode(s) disabled") + } + if len(solicit) > 0 { + for _, remote := range solicit { + s.startGoRoutine(func() { s.connectToRemoteLeafNode(remote, true) }) + } + s.Noticef("Reloaded: LeafNode(s) enabled") + } + } } } @@ -1011,6 +1052,38 @@ func (s *Server) recheckPinnedCerts(curOpts *Options, newOpts *Options) { } } +type proxiesReload struct { + noopOption + add []string + del []string +} + +func (p *proxiesReload) Apply(s *Server) { + var clients []*client + s.mu.Lock() + for _, k := range p.del { + cc := s.proxiedConns[k] + delete(s.proxiedConns, k) + if len(cc) > 0 { + for _, c := range cc { + clients = append(clients, c) + } + } + } + s.processProxiesTrustedKeys() + s.mu.Unlock() + if len(p.del) > 0 { + for _, c := range clients { + c.setAuthError(ErrAuthProxyNotTrusted) + c.authViolation() + } + s.Noticef("Reloaded: proxies trusted keys %q were removed", p.add) + } + if len(p.add) > 0 { + s.Noticef("Reloaded: proxies trusted keys %q were added", p.add) + } +} + // Reload reads the current configuration file and calls out to ReloadOptions // to apply the changes. This returns an error if the server was not started // with a config file or an option which doesn't support hot-swapping was changed. @@ -1186,7 +1259,7 @@ func imposeOrder(value any) error { slices.Sort(value.AllowedOrigins) case string, bool, uint8, uint16, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, PinnedCertSet, *URLAccResolver, *MemAccResolver, *DirAccResolver, *CacheDirAccResolver, Authentication, MQTTOpts, jwt.TagList, - *OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig: + *OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig, *ProxiesConfig: // explicitly skipped types case *AuthCallout: case JSTpmOpts: @@ -1283,6 +1356,8 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { diffOpts = append(diffOpts, &passwordOption{}) case "tags": diffOpts = append(diffOpts, &tagsOption{}) + case "metadata": + diffOpts = append(diffOpts, &metadataOption{}) case "authorization": diffOpts = append(diffOpts, &authorizationOption{}) case "authtimeout": @@ -1300,7 +1375,7 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { co := &clusterOption{ newValue: newClusterOpts, permsChanged: !reflect.DeepEqual(newClusterOpts.Permissions, oldClusterOpts.Permissions), - compressChanged: !reflect.DeepEqual(oldClusterOpts.Compression, newClusterOpts.Compression), + compressChanged: !compressOptsEqual(&oldClusterOpts.Compression, &newClusterOpts.Compression), } co.diffPoolAndAccounts(&oldClusterOpts) // If there are added accounts, first make sure that we can look them up. @@ -1413,18 +1488,26 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { } // We also support config reload for compression. Check if it changed before // blanking them out for the deep-equal check at the end. - compressionChanged := !reflect.DeepEqual(tmpOld.Compression, tmpNew.Compression) + compressionChanged := !compressOptsEqual(&tmpOld.Compression, &tmpNew.Compression) if compressionChanged { tmpOld.Compression, tmpNew.Compression = CompressionOpts{}, CompressionOpts{} } else if len(tmpOld.Remotes) == len(tmpNew.Remotes) { // Same that for tls first check, do the remotes now. - for i := 0; i < len(tmpOld.Remotes); i++ { - if !reflect.DeepEqual(tmpOld.Remotes[i].Compression, tmpNew.Remotes[i].Compression) { + for i := range len(tmpOld.Remotes) { + if !compressOptsEqual(&tmpOld.Remotes[i].Compression, &tmpNew.Remotes[i].Compression) { compressionChanged = true break } } } + // Check if the "disabled" option of each remote has changed. + var disabledChanged bool + for i := range len(tmpOld.Remotes) { + if tmpOld.Remotes[i].Disabled != tmpNew.Remotes[i].Disabled { + disabledChanged = true + break + } + } // Need to do the same for remote leafnodes' TLS configs. // But we can't just set remotes' TLSConfig to nil otherwise this @@ -1517,6 +1600,7 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { diffOpts = append(diffOpts, &leafNodeOption{ tlsFirstChanged: handshakeFirstChanged, compressionChanged: compressionChanged, + disabledChanged: disabledChanged, }) case "jetstream": new := newValue.(bool) @@ -1670,6 +1754,12 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { continue case "nofastproducerstall": diffOpts = append(diffOpts, &noFastProdStallReload{noStall: newValue.(bool)}) + case "proxies": + new := newValue.(*ProxiesConfig) + old := oldValue.(*ProxiesConfig) + if add, del := diffProxiesTrustedKeys(old.Trusted, new.Trusted); len(add) > 0 || len(del) > 0 { + diffOpts = append(diffOpts, &proxiesReload{add: add, del: del}) + } default: // TODO(ik): Implement String() on those options to have a nice print. // %v is difficult to figure what's what, %+v print private fields and @@ -1729,6 +1819,8 @@ func copyRemoteLNConfigForReloadCompare(current []*RemoteLeafOpts) []*RemoteLeaf cp.DenyImports, cp.DenyExports = nil, nil // Remove compression mode cp.Compression = CompressionOpts{} + // Reset disabled status + cp.Disabled = false rlns = append(rlns, &cp) } return rlns @@ -2535,3 +2627,24 @@ addLoop: return add, remove } + +func diffProxiesTrustedKeys(old, new []*ProxyConfig) ([]string, []string) { + var add []string + var del []string + // Both "old" and "new" lists should be small... + for _, op := range old { + if !slices.ContainsFunc(new, func(pc *ProxyConfig) bool { + return pc.Key == op.Key + }) { + del = append(del, op.Key) + } + } + for _, np := range new { + if !slices.ContainsFunc(old, func(pc *ProxyConfig) bool { + return pc.Key == np.Key + }) { + add = append(add, np.Key) + } + } + return add, del +} diff --git a/vendor/github.com/nats-io/nats-server/v2/server/route.go b/vendor/github.com/nats-io/nats-server/v2/server/route.go index ac4d9c66b..008e6ede5 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/route.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/route.go @@ -143,6 +143,7 @@ const ( // Can be changed for tests var ( routeConnectDelay = DEFAULT_ROUTE_CONNECT + routeConnectMaxDelay = DEFAULT_ROUTE_CONNECT_MAX routeMaxPingInterval = defaultRouteMaxPingInterval ) @@ -2054,7 +2055,7 @@ func (s *Server) createRoute(conn net.Conn, rURL *url.URL, rtype RouteType, goss if tlsRequired { c.Debugf("TLS handshake complete") cs := c.nc.(*tls.Conn).ConnectionState() - c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite)) + c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite)) } // Queue Connect proto if we solicited the connection. @@ -2893,6 +2894,7 @@ func (s *Server) connectToRoute(rURL *url.URL, rtype RouteType, firstConnect boo excludedAddresses := s.routesToSelf s.mu.RUnlock() + attemptDelay := routeConnectDelay for attempts := 0; s.isRunning(); { if tryForEver { if !s.routeStillValid(rURL) { @@ -2935,7 +2937,14 @@ func (s *Server) connectToRoute(rURL *url.URL, rtype RouteType, firstConnect boo select { case <-s.quitCh: return - case <-time.After(routeConnectDelay): + case <-time.After(attemptDelay): + if opts.Cluster.ConnectBackoff { + // Use exponential backoff for connection attempts. + attemptDelay *= 2 + if attemptDelay > routeConnectMaxDelay { + attemptDelay = routeConnectMaxDelay + } + } continue } } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/scheduler.go b/vendor/github.com/nats-io/nats-server/v2/server/scheduler.go new file mode 100644 index 000000000..fc626471b --- /dev/null +++ b/vendor/github.com/nats-io/nats-server/v2/server/scheduler.go @@ -0,0 +1,239 @@ +// Copyright 2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "encoding/binary" + "errors" + "io" + "math" + "slices" + "time" + + "github.com/nats-io/nats-server/v2/server/thw" +) + +// Error for when we try to decode a binary-encoded message schedule with an unknown version number. +var ErrMsgScheduleInvalidVersion = errors.New("msg scheduling: encoded version not known") + +const ( + headerLen = 17 // 1 byte magic + 2x uint64s +) + +type MsgScheduling struct { + run func() + ttls *thw.HashWheel + timer *time.Timer + schedules map[string]*MsgSchedule + seqToSubj map[uint64]string + inflight map[string]struct{} +} + +type MsgSchedule struct { + seq uint64 + ts int64 +} + +func newMsgScheduling(run func()) *MsgScheduling { + return &MsgScheduling{ + run: run, + ttls: thw.NewHashWheel(), + schedules: make(map[string]*MsgSchedule), + seqToSubj: make(map[uint64]string), + inflight: make(map[string]struct{}), + } +} + +func (ms *MsgScheduling) add(seq uint64, subj string, ts int64) { + ms.init(seq, subj, ts) + ms.resetTimer() +} + +func (ms *MsgScheduling) init(seq uint64, subj string, ts int64) { + if sched, ok := ms.schedules[subj]; ok { + delete(ms.seqToSubj, sched.seq) + // Remove and add separately, since they'll have different sequences. + ms.ttls.Remove(sched.seq, sched.ts) + ms.ttls.Add(seq, ts) + sched.ts, sched.seq = ts, seq + } else { + ms.ttls.Add(seq, ts) + ms.schedules[subj] = &MsgSchedule{seq: seq, ts: ts} + } + ms.seqToSubj[seq] = subj + delete(ms.inflight, subj) +} + +func (ms *MsgScheduling) markInflight(subj string) { + if _, ok := ms.schedules[subj]; ok { + ms.inflight[subj] = struct{}{} + } +} + +func (ms *MsgScheduling) isInflight(subj string) bool { + _, ok := ms.inflight[subj] + return ok +} + +func (ms *MsgScheduling) remove(seq uint64) { + if subj, ok := ms.seqToSubj[seq]; ok { + delete(ms.seqToSubj, seq) + delete(ms.schedules, subj) + } +} + +func (ms *MsgScheduling) clearInflight() { + ms.inflight = make(map[string]struct{}) +} + +func (ms *MsgScheduling) resetTimer() { + next := ms.ttls.GetNextExpiration(math.MaxInt64) + if next == math.MaxInt64 { + clearTimer(&ms.timer) + return + } + fireIn := time.Until(time.Unix(0, next)) + + // Make sure we aren't firing too often either way, otherwise we can + // negatively impact stream ingest performance. + if fireIn < 250*time.Millisecond { + fireIn = 250 * time.Millisecond + } + + if ms.timer != nil { + ms.timer.Reset(fireIn) + } else { + ms.timer = time.AfterFunc(fireIn, ms.run) + } +} + +func (ms *MsgScheduling) getScheduledMessages(loadMsg func(seq uint64, smv *StoreMsg) *StoreMsg) []*inMsg { + var ( + smv StoreMsg + sm *StoreMsg + msgs []*inMsg + ) + ms.ttls.ExpireTasks(func(seq uint64, ts int64) bool { + // Need to grab the message for the specified sequence, and check + // if it hasn't been removed in the meantime. + sm = loadMsg(seq, &smv) + if sm != nil { + // If already inflight, don't duplicate a scheduled message. The stream could + // be replicated and the scheduled message could take some time to propagate. + if ms.isInflight(sm.subj) { + return false + } + // Validate the contents are correct if not, we just remove it from THW. + ttl, ok := getMessageScheduleTTL(sm.hdr) + if !ok { + ms.remove(seq) + return true + } + target := getMessageScheduleTarget(sm.hdr) + if target == _EMPTY_ { + ms.remove(seq) + return true + } + + // Copy, as this is retrieved directly from storage, and we'll need to keep hold of this for some time. + // And in the case of headers, we'll copy all of them, but make changes. + hdr, msg := copyBytes(sm.hdr), copyBytes(sm.msg) + + // Strip headers specific to the schedule. + hdr = removeHeaderIfPresent(hdr, JSSchedulePattern) + hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Schedule-") + hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Expected-") + hdr = removeHeaderIfPresent(hdr, JSMsgId) + hdr = removeHeaderIfPresent(hdr, JSMessageTTL) + hdr = removeHeaderIfPresent(hdr, JSMsgRollup) + + // Add headers for the scheduled message. + hdr = genHeader(hdr, JSScheduler, sm.subj) + hdr = genHeader(hdr, JSScheduleNext, JSScheduleNextPurge) // Purge the schedule message itself. + if ttl != _EMPTY_ { + hdr = genHeader(hdr, JSMessageTTL, ttl) + } + msgs = append(msgs, &inMsg{seq: seq, subj: target, hdr: hdr, msg: msg}) + ms.markInflight(sm.subj) + return false + } + ms.remove(seq) + return true + }) + // THW is unordered, so must sort by sequence. + slices.SortFunc(msgs, func(a, b *inMsg) int { + if a.seq == b.seq { + return 0 + } else if a.seq < b.seq { + return -1 + } else { + return 1 + } + }) + return msgs +} + +// encode writes out the contents of the schedule into a binary snapshot +// and returns it. The high seq number is included in the snapshot and will +// be returned on decode. +func (ms *MsgScheduling) encode(highSeq uint64) []byte { + count := uint64(len(ms.schedules)) + b := make([]byte, 0, headerLen+(count*(2*binary.MaxVarintLen64))) + b = append(b, 1) // Magic version + b = binary.LittleEndian.AppendUint64(b, count) // Entry count + b = binary.LittleEndian.AppendUint64(b, highSeq) // Stamp + for subj, sched := range ms.schedules { + slen := min(uint64(len(subj)), math.MaxUint16) + b = binary.LittleEndian.AppendUint16(b, uint16(slen)) + b = append(b, subj[:slen]...) + b = binary.AppendVarint(b, sched.ts) + b = binary.AppendUvarint(b, sched.seq) + } + return b +} + +// decode snapshots a binary-encoded schedule and replaces the contents of this +// schedule with them. Returns the high seq number from the snapshot. +func (ms *MsgScheduling) decode(b []byte) (uint64, error) { + if len(b) < headerLen { + return 0, io.ErrShortBuffer + } + if b[0] != 1 { + return 0, ErrMsgScheduleInvalidVersion + } + + count := binary.LittleEndian.Uint64(b[1:]) + stamp := binary.LittleEndian.Uint64(b[9:]) + b = b[headerLen:] + for i := uint64(0); i < count; i++ { + sl := int(binary.LittleEndian.Uint16(b)) + b = b[2:] + if len(b) < sl { + return 0, io.ErrUnexpectedEOF + } + subj := string(b[:sl]) + b = b[sl:] + ts, tn := binary.Varint(b) + if tn < 0 { + return 0, io.ErrUnexpectedEOF + } + seq, vn := binary.Uvarint(b[tn:]) + if vn < 0 { + return 0, io.ErrUnexpectedEOF + } + ms.init(seq, subj, ts) + b = b[tn+vn:] + } + return stamp, nil +} 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 cf539630f..2971fbe8e 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 @@ -30,6 +30,7 @@ import ( "os" "path" "path/filepath" + "reflect" "regexp" "runtime" "runtime/pprof" @@ -129,6 +130,10 @@ type Info struct { WSConnectURLs []string `json:"ws_connect_urls,omitempty"` // Contains URLs a ws client can connect to. LameDuckMode bool `json:"ldm,omitempty"` Compression string `json:"compression,omitempty"` + ConnectInfo bool `json:"connect_info,omitempty"` // When true this is the server INFO response to CONNECT + RemoteAccount string `json:"remote_account,omitempty"` // Lets the client or leafnode side know the remote account that they bind to. + IsSystemAccount bool `json:"acc_is_sys,omitempty"` // Indicates if the account is a system account. + JSApiLevel int `json:"api_lvl,omitempty"` // Route Specific Import *SubjectPermission `json:"import,omitempty"` @@ -136,7 +141,6 @@ type Info struct { LNOC bool `json:"lnoc,omitempty"` LNOCU bool `json:"lnocu,omitempty"` InfoOnConnect bool `json:"info_on_connect,omitempty"` // When true the server will respond to CONNECT with an INFO - ConnectInfo bool `json:"connect_info,omitempty"` // When true this is the server INFO response to CONNECT RoutePoolSize int `json:"route_pool_size,omitempty"` RoutePoolIdx int `json:"route_pool_idx,omitempty"` RouteAccount string `json:"route_account,omitempty"` @@ -153,8 +157,7 @@ type Info struct { GatewayIOM bool `json:"gateway_iom,omitempty"` // Indicate that all accounts will be switched to InterestOnly mode "right away" // LeafNode Specific - LeafNodeURLs []string `json:"leafnode_urls,omitempty"` // LeafNode URLs that the server can reconnect to. - RemoteAccount string `json:"remote_account,omitempty"` // Lets the other side know the remote account that they bind to. + LeafNodeURLs []string `json:"leafnode_urls,omitempty"` // LeafNode URLs that the server can reconnect to. XKey string `json:"xkey,omitempty"` // Public server's x25519 key. } @@ -167,6 +170,7 @@ type Server struct { pinnedAccFail uint64 stats scStats + staleStats mu sync.RWMutex reloadMu sync.RWMutex // Write-locked when a config reload is taking place ONLY kp nkeys.KeyPair @@ -259,8 +263,6 @@ type Server struct { // Used internally for quick look-ups. clientConnectURLsMap refCountedUrlSet - lastCURLsUpdate int64 - // For Gateways gatewayListener net.Listener // Accept listener gatewayListenerErr error @@ -371,6 +373,11 @@ type Server struct { // Controls whether or not the account NRG capability is set in statsz. // Currently used by unit tests to simulate nodes not supporting account NRG. accountNRGAllowed atomic.Bool + + // List of proxies trusted keys in `KeyPair` form so we can do signature + // verification when processing incoming proxy connections. + proxiesKeyPairs []nkeys.KeyPair + proxiedConns map[string]map[uint64]*client } // For tracking JS nodes. @@ -390,11 +397,13 @@ type nodeInfo struct { } type stats struct { - inMsgs int64 - outMsgs int64 - inBytes int64 - outBytes int64 - slowConsumers int64 + inMsgs int64 + outMsgs int64 + inBytes int64 + outBytes int64 + slowConsumers int64 + staleConnections int64 + stalls int64 } // scStats includes the total and per connection counters of Slow Consumers. @@ -405,6 +414,14 @@ type scStats struct { gateways atomic.Uint64 } +// staleStats includes the total and per connection counters of Stale Connections. +type staleStats struct { + clients atomic.Uint64 + routes atomic.Uint64 + leafs atomic.Uint64 + gateways atomic.Uint64 +} + // This is used by tests so we can run all server tests with a default route // or leafnode compression mode. For instance: // go test -race -v ./server -cluster_compression=fast @@ -625,6 +642,32 @@ func selectS2AutoModeBasedOnRTT(rtt time.Duration, rttThresholds []time.Duration return CompressionS2Best } +func compressOptsEqual(c1, c2 *CompressionOpts) bool { + if c1 == c2 { + return true + } + if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) { + return false + } + if c1.Mode != c2.Mode { + return false + } + // For s2_auto, if one has an empty RTTThresholds, it is equivalent + // to the defaultCompressionS2AutoRTTThresholds array, so compare with that. + if c1.Mode == CompressionS2Auto { + if len(c1.RTTThresholds) == 0 && !reflect.DeepEqual(c2.RTTThresholds, defaultCompressionS2AutoRTTThresholds) { + return false + } + if len(c2.RTTThresholds) == 0 && !reflect.DeepEqual(c1.RTTThresholds, defaultCompressionS2AutoRTTThresholds) { + return false + } + if !reflect.DeepEqual(c1.RTTThresholds, c2.RTTThresholds) { + return false + } + } + return true +} + // Returns an array of s2 WriterOption based on the route compression mode. // So far we return a single option, but this way we can call s2.NewWriter() // with a nil []s2.WriterOption, but not with a nil s2.WriterOption, so @@ -706,6 +749,7 @@ func NewServer(opts *Options) (*Server, error) { Headers: !opts.NoHeaderSupport, Cluster: opts.Cluster.Name, Domain: opts.JetStreamDomain, + JSApiLevel: JSApiLevel, } if tlsReq && !info.TLSRequired { @@ -774,6 +818,11 @@ func NewServer(opts *Options) (*Server, error) { s.mu.Lock() defer s.mu.Unlock() + // If there are proxies trusted public keys in the configuration + // this will fill create the corresponding list of nkeys.KeyPair + // that we can use for signature verification. + s.processProxiesTrustedKeys() + // Place ourselves in the JetStream nodeInfo if needed. if opts.JetStream { ourNode := getHash(serverName) @@ -1099,6 +1148,10 @@ func validateOptions(o *Options) error { if err := validateAuth(o); err != nil { return err } + // Check that proxies is properly configured. + if err := validateProxies(o); err != nil { + return err + } // Check that gateway is properly configured. Returns no error // if there is no gateway defined. if err := validateGatewayOptions(o); err != nil { @@ -2352,7 +2405,7 @@ func (s *Server) Start() { StoreDir: opts.StoreDir, SyncInterval: opts.SyncInterval, SyncAlways: opts.SyncAlways, - Strict: opts.JetStreamStrict, + Strict: !opts.NoJetStreamStrict, MaxMemory: opts.JetStreamMaxMemory, MaxStore: opts.JetStreamMaxStore, Domain: opts.JetStreamDomain, @@ -3396,7 +3449,7 @@ func (s *Server) createClientEx(conn net.Conn, inProcess bool) *client { if tlsRequired { c.Debugf("TLS handshake complete") cs := c.nc.(*tls.Conn).ConnectionState() - c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite)) + c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite)) } c.mu.Unlock() @@ -3501,8 +3554,6 @@ func (s *Server) updateServerINFOAndSendINFOToClients(curls, wsurls []string, ad updateInfo(&s.info.WSConnectURLs, s.websocket.connectURLs, s.websocket.connectURLsMap) } if cliUpdated || wsUpdated { - // Update the time of this update - s.lastCURLsUpdate = time.Now().UnixNano() // Send to all registered clients that support async INFO protocols. s.sendAsyncInfoToClients(cliUpdated, wsUpdated) } @@ -3554,15 +3605,6 @@ func tlsVersionFromString(ver string) (uint16, error) { return 0, fmt.Errorf("unknown version: %v", ver) } -// We use hex here so we don't need multiple versions -func tlsCipher(cs uint16) string { - name, present := cipherMapByID[cs] - if present { - return name - } - return fmt.Sprintf("Unknown [0x%x]", cs) -} - // Remove a client or route from our internal accounting. func (s *Server) removeClient(c *client) { // kind is immutable, so can check without lock @@ -3574,6 +3616,7 @@ func (s *Server) removeClient(c *client) { if c.kind == CLIENT && c.opts.Protocol >= ClientProtoInfo { updateProtoInfoCount = true } + proxyKey := c.proxyKey c.mu.Unlock() s.mu.Lock() @@ -3581,6 +3624,9 @@ func (s *Server) removeClient(c *client) { if updateProtoInfoCount { s.cproto-- } + if proxyKey != _EMPTY_ { + s.removeProxiedConn(proxyKey, cid) + } s.mu.Unlock() case ROUTER: s.removeRoute(c) @@ -3591,6 +3637,18 @@ func (s *Server) removeClient(c *client) { } } +// Remove the connection with id `cid` from the map of connections +// under the public key `key` of the trusted proxies. +// +// Server lock must be held on entry. +func (s *Server) removeProxiedConn(key string, cid uint64) { + conns := s.proxiedConns[key] + delete(conns, cid) + if len(conns) == 0 { + delete(s.proxiedConns, key) + } +} + func (s *Server) removeFromTempClients(cid uint64) { s.grMu.Lock() delete(s.grTmpClients, cid) @@ -3699,6 +3757,11 @@ func (s *Server) NumSlowConsumers() int64 { return atomic.LoadInt64(&s.slowConsumers) } +// NumStalledClients will report the total number of times clients have been stalled. +func (s *Server) NumStalledClients() int64 { + return atomic.LoadInt64(&s.stalls) +} + // NumSlowConsumersClients will report the number of slow consumers clients. func (s *Server) NumSlowConsumersClients() uint64 { return s.scStats.clients.Load() @@ -3719,6 +3782,31 @@ func (s *Server) NumSlowConsumersLeafs() uint64 { return s.scStats.leafs.Load() } +// NumStaleConnections will report the number of stale connections. +func (s *Server) NumStaleConnections() int64 { + return atomic.LoadInt64(&s.staleConnections) +} + +// NumStaleConnectionsClients will report the number of stale client connections. +func (s *Server) NumStaleConnectionsClients() uint64 { + return s.staleStats.clients.Load() +} + +// NumStaleConnectionsRoutes will report the number of stale route connections. +func (s *Server) NumStaleConnectionsRoutes() uint64 { + return s.staleStats.routes.Load() +} + +// NumStaleConnectionsGateways will report the number of stale gateway connections. +func (s *Server) NumStaleConnectionsGateways() uint64 { + return s.staleStats.gateways.Load() +} + +// NumStaleConnectionsLeafs will report the number of stale leaf connections. +func (s *Server) NumStaleConnectionsLeafs() uint64 { + return s.staleStats.leafs.Load() +} + // ConfigTime will report the last time the server configuration was loaded. func (s *Server) ConfigTime() time.Time { s.mu.RLock() 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 271c02cbf..358d7ec29 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 @@ -35,8 +35,6 @@ const ( FileStorage = StorageType(22) // MemoryStorage specifies in memory only. MemoryStorage = StorageType(33) - // Any is for internals. - AnyStorage = StorageType(44) ) var ( @@ -86,14 +84,16 @@ type StorageUpdateHandler func(msgs, bytes int64, seq uint64, subj string) // Used to call back into the upper layers to remove a message. type StorageRemoveMsgHandler func(seq uint64) -// Used to call back into the upper layers to report on newly created subject delete markers. -type SubjectDeleteMarkerUpdateHandler func(*inMsg) +// Used to call back into the upper layers to process a JetStream message. +// Will propose the message if the stream is replicated. +type ProcessJetStreamMsgHandler func(*inMsg) type StreamStore interface { StoreMsg(subject string, hdr, msg []byte, ttl int64) (uint64, int64, error) StoreRawMsg(subject string, hdr, msg []byte, seq uint64, ts int64, ttl int64) error SkipMsg() uint64 SkipMsgs(seq uint64, num uint64) error + FlushAllPending() LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error) LoadNextMsg(filter string, wc bool, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) @@ -120,16 +120,17 @@ type StreamStore interface { SyncDeleted(dbs DeleteBlocks) Type() StorageType RegisterStorageUpdates(StorageUpdateHandler) - RegisterStorageRemoveMsg(handler StorageRemoveMsgHandler) - RegisterSubjectDeleteMarkerUpdates(SubjectDeleteMarkerUpdateHandler) + RegisterStorageRemoveMsg(StorageRemoveMsgHandler) + RegisterProcessJetStreamMsg(ProcessJetStreamMsgHandler) UpdateConfig(cfg *StreamConfig) error - Delete() error + Delete(inline bool) error Stop() error ConsumerStore(name string, cfg *ConsumerConfig) (ConsumerStore, error) AddConsumer(o ConsumerStore) error RemoveConsumer(o ConsumerStore) error Snapshot(deadline time.Duration, includeConsumers, checkMsgs bool) (*SnapshotResult, error) Utilization() (total, reported uint64, err error) + ResetState() } // RetentionPolicy determines how messages in a set are retained. @@ -461,6 +462,7 @@ type Pending struct { } // TemplateStore stores templates. +// Deprecated: stream templates are deprecated and will be removed in a future version. type TemplateStore interface { Store(*streamTemplate) error Delete(*streamTemplate) error @@ -555,13 +557,11 @@ func (dp *DiscardPolicy) UnmarshalJSON(data []byte) error { const ( memoryStorageJSONString = `"memory"` fileStorageJSONString = `"file"` - anyStorageJSONString = `"any"` ) var ( memoryStorageJSONBytes = []byte(memoryStorageJSONString) fileStorageJSONBytes = []byte(fileStorageJSONString) - anyStorageJSONBytes = []byte(anyStorageJSONString) ) func (st StorageType) String() string { @@ -570,8 +570,6 @@ func (st StorageType) String() string { return "Memory" case FileStorage: return "File" - case AnyStorage: - return "Any" default: return "Unknown Storage Type" } @@ -583,8 +581,6 @@ func (st StorageType) MarshalJSON() ([]byte, error) { return memoryStorageJSONBytes, nil case FileStorage: return fileStorageJSONBytes, nil - case AnyStorage: - return anyStorageJSONBytes, nil default: return nil, fmt.Errorf("can not marshal %v", st) } @@ -596,8 +592,6 @@ func (st *StorageType) UnmarshalJSON(data []byte) error { *st = MemoryStorage case fileStorageJSONString: *st = FileStorage - case anyStorageJSONString: - *st = AnyStorage default: return fmt.Errorf("can not unmarshal %q", data) } 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 03330205a..7dbc7d2b4 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 @@ -21,6 +21,7 @@ import ( "fmt" "io" "math" + "math/big" "math/rand" "os" "path/filepath" @@ -62,7 +63,7 @@ type StreamConfig struct { Storage StorageType `json:"storage"` Replicas int `json:"num_replicas"` NoAck bool `json:"no_ack,omitempty"` - Template string `json:"template_owner,omitempty"` + Template string `json:"template_owner,omitempty"` // Deprecated: stream templates are deprecated and will be removed in a future version. Duplicates time.Duration `json:"duplicate_window,omitempty"` Placement *Placement `json:"placement,omitempty"` Mirror *StreamSource `json:"mirror,omitempty"` @@ -109,6 +110,18 @@ type StreamConfig struct { // subject delete markers. SubjectDeleteMarkerTTL time.Duration `json:"subject_delete_marker_ttl,omitempty"` + // AllowMsgCounter allows a stream to use (only) counter CRDTs. + AllowMsgCounter bool `json:"allow_msg_counter,omitempty"` + + // AllowAtomicPublish allows atomic batch publishing into the stream. + AllowAtomicPublish bool `json:"allow_atomic,omitempty"` + + // AllowMsgSchedules allows the scheduling of messages. + AllowMsgSchedules bool `json:"allow_msg_schedules,omitempty"` + + // PersistMode allows to opt-in to different persistence mode settings. + PersistMode PersistModeType `json:"persist_mode,omitempty"` + // Metadata is additional metadata for the Stream. Metadata map[string]string `json:"metadata,omitempty"` } @@ -167,6 +180,63 @@ type RePublish struct { HeadersOnly bool `json:"headers_only,omitempty"` } +// PersistModeType determines what persistence mode the stream uses. +type PersistModeType int + +const ( + // DefaultPersistMode specifies the default persist mode. Writes to the stream will immediately be flushed. + // The publish acknowledgement will be sent after the persisting completes. + DefaultPersistMode = PersistModeType(iota) + // AsyncPersistMode specifies writes to the stream will be flushed asynchronously. + // The publish acknowledgement may be sent before the persisting completes. + // This means writes could be lost if they weren't flushed prior to a hard kill of the server. + AsyncPersistMode +) + +const ( + defaultPersistModeJSONString = `"default"` + asyncPersistModeJSONString = `"async"` +) + +var ( + defaultPersistModeJSONBytes = []byte(defaultPersistModeJSONString) + asyncPersistModeJSONBytes = []byte(asyncPersistModeJSONString) +) + +func (wc PersistModeType) String() string { + switch wc { + case DefaultPersistMode: + return "Default" + case AsyncPersistMode: + return "Async" + default: + return "Unknown Persist Mode Type" + } +} + +func (wc PersistModeType) MarshalJSON() ([]byte, error) { + switch wc { + case DefaultPersistMode: + return defaultPersistModeJSONBytes, nil + case AsyncPersistMode: + return asyncPersistModeJSONBytes, nil + default: + return nil, fmt.Errorf("can not marshal %v", wc) + } +} + +func (wc *PersistModeType) UnmarshalJSON(data []byte) error { + switch string(data) { + case defaultPersistModeJSONString, `""`: + *wc = DefaultPersistMode + case asyncPersistModeJSONString: + *wc = AsyncPersistMode + default: + return fmt.Errorf("can not unmarshal %q", data) + } + return nil +} + // JSPubAckResponse is a formal response to a publish operation. type JSPubAckResponse struct { Error *ApiError `json:"error,omitempty"` @@ -189,8 +259,21 @@ type PubAck struct { Sequence uint64 `json:"seq"` Domain string `json:"domain,omitempty"` Duplicate bool `json:"duplicate,omitempty"` + Value string `json:"val,omitempty"` + BatchId string `json:"batch,omitempty"` + BatchSize int `json:"count,omitempty"` } +// CounterValue is the body of a message when used as a counter. +// e.g. {"val":"123"} +type CounterValue struct { + Value string `json:"val"` +} + +// CounterSources is the body of the Nats-Counter-Sources header. +// e.g. {"stream":{"subject":"123"}} +type CounterSources map[string]map[string]string + // StreamInfo shows config and current state for this stream. type StreamInfo struct { Config StreamConfig `json:"config"` @@ -283,8 +366,23 @@ func (ext *ExternalStream) Domain() string { // For managing stream ingest. const ( - streamDefaultMaxQueueMsgs = 10_000 - streamDefaultMaxQueueBytes = 1024 * 1024 * 128 + streamDefaultMaxQueueMsgs = 100_000 + streamDefaultMaxQueueBytes = 128 * 1024 * 1024 +) + +// For managing stream batches. +const ( + streamDefaultMaxBatchInflightPerStream = 50 + streamDefaultMaxBatchInflightTotal = 1000 + streamDefaultMaxBatchSize = 1000 + streamDefaultMaxBatchTimeout = 10 * time.Second +) + +var ( + streamMaxBatchInflightPerStream = streamDefaultMaxBatchInflightPerStream + streamMaxBatchInflightTotal = streamDefaultMaxBatchInflightTotal + streamMaxBatchSize = streamDefaultMaxBatchSize + streamMaxBatchTimeout = streamDefaultMaxBatchTimeout ) // Stream is a jetstream stream of messages. When we receive a message internally destined @@ -317,6 +415,7 @@ type stream struct { created time.Time // Time the stream was created. stype StorageType // The storage type. tier string // The tier is the number of replicas for the stream (e.g. "R1" or "R3"). + ddMu sync.Mutex // Lock for dedupe state. ddmap map[string]*ddentry // The dedupe map. ddarr []*ddentry // The dedupe array. ddindex int // The dedupe index. @@ -325,8 +424,7 @@ type stream struct { mqch chan struct{} // The monitor's quit channel. active bool // Indicates that there are active internal subscriptions (for the subject filters) // and/or mirror/sources consumers are scheduled to be established or already started. - ddloaded bool // set to true when the deduplication structures are been built. - closed atomic.Bool // Set to true when stop() is called on the stream. + closed atomic.Bool // Set to true when stop() is called on the stream. // Mirror mirror *sourceInfo @@ -372,23 +470,44 @@ type stream struct { clMu sync.Mutex // The mutex for clseq and clfs. clseq uint64 // The current last seq being proposed to the NRG layer. clfs uint64 // The count (offset) of the number of failed NRG sequences used to compute clseq. - inflight map[uint64]uint64 // Inflight message sizes per clseq. lqsent time.Time // The time at which the last lost quorum advisory was sent. Used to rate limit. uch chan struct{} // The channel to signal updates to the monitor routine. inMonitor bool // True if the monitor routine has been started. - expectedPerSubjectSequence map[uint64]string // Inflight 'expected per subject' subjects per clseq. - expectedPerSubjectInProcess map[string]struct{} // Current 'expected per subject' subjects in process. + inflight map[string]*inflightSubjectRunningTotal // Inflight message sizes per subject. + clusteredCounterTotal map[string]*msgCounterRunningTotal // Inflight counter totals. + expectedPerSubjectSequence map[uint64]string // Inflight 'expected per subject' subjects per clseq. + expectedPerSubjectInProcess map[string]struct{} // Current 'expected per subject' subjects in process. // Direct get subscription. - directSub *subscription - lastBySub *subscription + directLeaderSub *subscription + directSub *subscription + lastBySub *subscription + mirrorDirectSub *subscription // Mirrors only. + mirrorLastBySub *subscription // Mirrors only. monitorWg sync.WaitGroup // Wait group for the monitor routine. // If standalone/single-server, the offline reason needs to be stored directly in the stream. // Otherwise, if clustered it will be part of the stream assignment. offlineReason string + + batches *batching // Inflight batches prior to committing them. + batchApply *batchApply // State to check for batch completeness before applying it. +} + +// inflightSubjectRunningTotal stores a running total of inflight messages for a specific subject. +type inflightSubjectRunningTotal struct { + bytes uint64 // Running total of inflight bytes for inflight messages. + ops uint64 // Inflight operations, i.e. inflight messages for this subject. If this reaches zero, we can remove the running total. +} + +// msgCounterRunningTotal stores a running total and a number of inflight +// but not yet applied clustered proposals/operations for this counter. +type msgCounterRunningTotal struct { + total *big.Int // Running total. + sources CounterSources // Last seen counter sources. + ops uint64 // Inflight operations. If this reaches zero, we can remove the running total. } type sourceInfo struct { @@ -397,14 +516,6 @@ type sourceInfo struct { cname string // The name of the current consumer for this source. sub *subscription // The subscription to the consumer. - // (mirrors only) The subscription to the direct get request subject for - // the source stream's name on the `_sys_` queue group. - dsub *subscription - - // (mirrors only) The subscription to the direct get last per subject request subject for - // the source stream's name on the `_sys_` queue group. - lbsub *subscription - msgs *ipQueue[*inMsg] // Intra-process queue for incoming messages. sseq uint64 // Last stream message sequence number seen from the source. dseq uint64 // Last delivery (i.e. consumer's) sequence number. @@ -444,6 +555,14 @@ const ( JSResponseType = "Nats-Response-Type" JSMessageTTL = "Nats-TTL" JSMarkerReason = "Nats-Marker-Reason" + JSMessageIncr = "Nats-Incr" + JSMessageCounterSources = "Nats-Counter-Sources" + JSBatchId = "Nats-Batch-Id" + JSBatchSeq = "Nats-Batch-Sequence" + JSBatchCommit = "Nats-Batch-Commit" + JSSchedulePattern = "Nats-Schedule" + JSScheduleTTL = "Nats-Schedule-TTL" + JSScheduleTarget = "Nats-Schedule-Target" ) // Headers for published KV messages. @@ -452,14 +571,21 @@ var ( KVOperationValuePurge = []byte("PURGE") ) -// Headers for republished messages and direct gets. +// Headers for scheduled messages. +const ( + JSScheduler = "Nats-Scheduler" + JSScheduleNext = "Nats-Schedule-Next" + JSScheduleNextPurge = "purge" // If it's a non-repeating/delayed message, the schedule is purged. +) + +// Headers for republished messages and direct get responses. const ( JSStream = "Nats-Stream" JSSequence = "Nats-Sequence" JSTimeStamp = "Nats-Time-Stamp" JSSubject = "Nats-Subject" - JSLastSequence = "Nats-Last-Sequence" - JSNumPending = "Nats-Num-Pending" + JSLastSequence = "Nats-Last-Sequence" // The sequence number of the precedent message either republished or in a batch of responses. 0 in the first message. + JSNumPending = "Nats-Num-Pending" // Number of messages pending in the multi/batched get response JSUpToSequence = "Nats-UpTo-Sequence" ) @@ -756,11 +882,19 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt } } fsCfg.StoreDir = storeDir - fsCfg.AsyncFlush = false // Grab configured sync interval. fsCfg.SyncInterval = s.getOpts().SyncInterval fsCfg.SyncAlways = s.getOpts().SyncAlways fsCfg.Compression = config.Compression + // Async flushing is only allowed if the stream has a sync log backing it. + fsCfg.AsyncFlush = !fsCfg.SyncAlways && config.Replicas > 1 + + // Async persist mode opts in to async flushing, + // sync always would also be disabled if it was configured. + if config.PersistMode == AsyncPersistMode { + fsCfg.SyncAlways = false + fsCfg.AsyncFlush = true + } if err := mset.setupStore(fsCfg); err != nil { mset.stop(true, false) @@ -769,9 +903,9 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt // Create our pubAck template here. Better than json marshal each time on success. if domain := s.getOpts().JetStreamDomain; domain != _EMPTY_ { - mset.pubAck = []byte(fmt.Sprintf("{%q:%q, %q:%q, %q:", "stream", cfg.Name, "domain", domain, "seq")) + mset.pubAck = fmt.Appendf(nil, "{%q:%q,%q:%q,%q:", "stream", cfg.Name, "domain", domain, "seq") } else { - mset.pubAck = []byte(fmt.Sprintf("{%q:%q, %q:", "stream", cfg.Name, "seq")) + mset.pubAck = fmt.Appendf(nil, "{%q:%q,%q:", "stream", cfg.Name, "seq") } end := len(mset.pubAck) mset.pubAck = mset.pubAck[:end:end] @@ -783,12 +917,12 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt // Possible race with consumer.setLeader during recovery. mset.mu.Lock() mset.lseq = state.LastSeq - mset.mu.Unlock() - // If no msgs (new stream), set dedupe state loaded to true. - if state.Msgs == 0 { - mset.ddloaded = true - } + // Ensure dedupe state is loaded. + mset.ddMu.Lock() + mset.rebuildDedupe() + mset.ddMu.Unlock() + mset.mu.Unlock() // Set our stream assignment if in clustered mode. reserveResources := true @@ -1031,10 +1165,11 @@ func (mset *stream) setLeader(isLeader bool) error { // Stop responding to sync requests. mset.stopClusterSubs() // Unsubscribe from direct stream. - mset.unsubscribeToStream(false) + 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. @@ -1061,9 +1196,17 @@ func (mset *stream) stopClusterSubs() { // account gets the account for this stream. func (mset *stream) account() *Account { - mset.mu.RLock() + return mset.accountLocked(true) +} + +func (mset *stream) accountLocked(needLock bool) *Account { + if needLock { + mset.mu.RLock() + } jsa := mset.jsa - mset.mu.RUnlock() + if needLock { + mset.mu.RUnlock() + } if jsa == nil { return nil } @@ -1143,14 +1286,8 @@ func (mset *stream) autoTuneFileStorageBlockSize(fsCfg *FileStoreConfig) { // Will be called lazily to avoid penalizing startup times. // TODO(dlc) - Might be good to know if this should be checked at all for streams with no // headers and msgId in them. Would need signaling from the storage layer. -// Lock should be held. +// mset.mu and mset.ddMu locks should be held. func (mset *stream) rebuildDedupe() { - if mset.ddloaded { - return - } - - mset.ddloaded = true - // We have some messages. Lookup starting sequence by duplicate time window. sseq := mset.store.GetSeqFromTime(time.Now().Add(-mset.cfg.Duplicates)) if sseq == 0 { @@ -1178,8 +1315,9 @@ func (mset *stream) rebuildDedupe() { } } +// Lock should be held. func (mset *stream) lastSeqAndCLFS() (uint64, uint64) { - return mset.lastSeq(), mset.getCLFS() + return mset.lseq, mset.getCLFS() } func (mset *stream) getCLFS() uint64 { @@ -1290,6 +1428,34 @@ func (mset *stream) sendUpdateAdvisoryLocked() { } } +func (mset *stream) sendStreamBatchAbandonedAdvisory(batchId string, reason BatchAbandonReason) { + if mset == nil { + return + } + s := mset.srv + stream, acc := mset.name(), mset.account() + subj := JSAdvisoryStreamBatchAbandonedPre + "." + stream + adv := &JSStreamBatchAbandonedAdvisory{ + TypedEvent: TypedEvent{ + Type: JSStreamBatchAbandonedAdvisoryType, + ID: nuid.Next(), + Time: time.Now().UTC(), + }, + Stream: stream, + Domain: s.getOpts().JetStreamDomain, + BatchId: batchId, + Reason: reason, + } + + // Send to the user's account if not the system account. + if acc != s.SystemAccount() { + s.publishAdvisory(acc, subj, adv) + } + // Now do system level one. Place account info in adv, and nil account means system. + adv.Account = acc.GetName() + s.publishAdvisory(nil, subj, adv) +} + // Created returns created time. func (mset *stream) createdTime() time.Time { mset.mu.RLock() @@ -1353,10 +1519,24 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo cfg := *config + if _, err := cfg.Retention.MarshalJSON(); err != nil { + return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("invalid retention")) + } + if _, err := cfg.Discard.MarshalJSON(); err != nil { + return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("invalid discard policy")) + } + if _, err := cfg.Compression.MarshalJSON(); err != nil { + return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("invalid compression")) + } + // Make file the default. if cfg.Storage == 0 { cfg.Storage = FileStorage } + if _, err := cfg.Storage.MarshalJSON(); err != nil { + return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("invalid storage type")) + } + if cfg.Replicas == 0 { cfg.Replicas = 1 } @@ -1366,41 +1546,59 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo if cfg.Replicas < 0 { return cfg, NewJSReplicasCountCannotBeNegativeError() } - if cfg.MaxMsgs == 0 { + if cfg.MaxMsgs == 0 || cfg.MaxMsgs < -1 { + if pedantic && cfg.MaxMsgs < -1 { + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_msgs must be set to -1")) + } cfg.MaxMsgs = -1 } - if cfg.MaxMsgsPer == 0 { + if cfg.MaxMsgsPer == 0 || cfg.MaxMsgsPer < -1 { + if pedantic && cfg.MaxMsgsPer < -1 { + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_msgs_per_subject must be set to -1")) + } cfg.MaxMsgsPer = -1 } - if cfg.MaxBytes == 0 { + if cfg.MaxBytes == 0 || cfg.MaxBytes < -1 { + if pedantic && cfg.MaxBytes < -1 { + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_bytes must be set to -1")) + } cfg.MaxBytes = -1 } - if cfg.MaxMsgSize == 0 { + if cfg.MaxMsgSize == 0 || cfg.MaxMsgSize < -1 { + if pedantic && cfg.MaxMsgSize < -1 { + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_msg_size must be set to -1")) + } cfg.MaxMsgSize = -1 } - if cfg.MaxConsumers == 0 { + if cfg.MaxConsumers == 0 || cfg.MaxConsumers < -1 { + if pedantic && cfg.MaxConsumers < -1 { + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_consumers must be set to -1")) + } cfg.MaxConsumers = -1 } + if cfg.MaxAge < 0 { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("max age can not be negative")) + } + if cfg.MaxAge != 0 && cfg.MaxAge < 100*time.Millisecond { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("max age needs to be >= 100ms")) + } if cfg.Duplicates == 0 && cfg.Mirror == nil { maxWindow := StreamDefaultDuplicatesWindow if lim.Duplicates > 0 && maxWindow > lim.Duplicates { if pedantic { - return StreamConfig{}, NewJSPedanticError(fmt.Errorf("pedantic mode: duplicate window limits are higher than current limits")) + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("duplicate window limits are higher than current limits")) } maxWindow = lim.Duplicates } if cfg.MaxAge != 0 && cfg.MaxAge < maxWindow { if pedantic { - return StreamConfig{}, NewJSPedanticError(fmt.Errorf("pedantic mode: duplicate window cannot be bigger than max age")) + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("duplicate window cannot be bigger than max age")) } cfg.Duplicates = cfg.MaxAge } else { cfg.Duplicates = maxWindow } } - if cfg.MaxAge > 0 && cfg.MaxAge < 100*time.Millisecond { - return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("max age needs to be >= 100ms")) - } if cfg.Duplicates < 0 { return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window can not be negative")) } @@ -1420,6 +1618,19 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("roll-ups require the purge permission")) } + // Counter is not compatible with some settings. + if cfg.AllowMsgCounter { + if cfg.Discard == DiscardNew { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use discard new")) + } + if cfg.AllowMsgTTL { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message TTLs")) + } + if cfg.Retention != LimitsPolicy { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream can only use limits retention")) + } + } + // Check for new discard new per subject, we require the discard policy to also be new. if cfg.DiscardNewPer { if cfg.Discard != DiscardNew { @@ -1450,6 +1661,27 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subject delete marker TTL must not be negative")) } + if cfg.AllowMsgSchedules { + if !cfg.AllowRollup { + if pedantic { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("message scheduling cannot be set if roll-ups are disabled")) + } + cfg.AllowRollup, cfg.DenyPurge = true, false + } + } + + if cfg.PersistMode == AsyncPersistMode { + if cfg.Storage != FileStorage { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("async persist mode is only supported on file storage")) + } + if cfg.Replicas > 1 { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("async persist mode is not supported on replicated streams")) + } + if cfg.AllowAtomicPublish { + return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("async persist mode is not supported with atomic batch publish")) + } + } + getStream := func(streamName string) (bool, StreamConfig) { var exists bool var cfg StreamConfig @@ -1489,6 +1721,15 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo if len(cfg.Sources) > 0 { return StreamConfig{}, NewJSMirrorWithSourcesError() } + if cfg.AllowMsgCounter { + return StreamConfig{}, NewJSMirrorWithCountersError() + } + if cfg.AllowAtomicPublish { + return StreamConfig{}, NewJSMirrorWithAtomicPublishError() + } + if cfg.AllowMsgSchedules { + return StreamConfig{}, NewJSMirrorWithMsgSchedulesError() + } if cfg.Mirror.FilterSubject != _EMPTY_ && len(cfg.Mirror.SubjectTransforms) != 0 { return StreamConfig{}, NewJSMirrorMultipleFiltersNotAllowedError() } @@ -1564,6 +1805,12 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo } } + if len(cfg.Sources) > 0 { + if cfg.AllowMsgSchedules { + return StreamConfig{}, NewJSSourceWithMsgSchedulesError() + } + } + // check for duplicates var iNames = make(map[string]struct{}) for _, src := range cfg.Sources { @@ -1772,7 +2019,7 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo // Empty same as all. if cfg.RePublish.Source == _EMPTY_ { if pedantic { - return StreamConfig{}, NewJSPedanticError(fmt.Errorf("pedantic mode: republish source can not be empty")) + return StreamConfig{}, NewJSPedanticError(fmt.Errorf("republish source can not be empty")) } cfg.RePublish.Source = fwcs } @@ -1877,7 +2124,8 @@ func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig, s *Server, pedan return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not cancel deny purge")) } // Check for mirror changes which are not allowed. - if !reflect.DeepEqual(cfg.Mirror, old.Mirror) { + // We will allow removing the mirror config to "promote" the mirror to a normal stream. + if cfg.Mirror != nil && !reflect.DeepEqual(cfg.Mirror, old.Mirror) { return nil, NewJSStreamMirrorNotUpdatableError() } @@ -1896,6 +2144,20 @@ func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig, s *Server, pedan return nil, NewJSStreamInvalidConfigError(fmt.Errorf("message TTL status can not be disabled")) } + // Can't change counter setting. + if cfg.AllowMsgCounter != old.AllowMsgCounter { + return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not change message counter setting")) + } + + // Can't disable message schedules setting. + if old.AllowMsgSchedules && !cfg.AllowMsgSchedules { + return nil, NewJSStreamInvalidConfigError(fmt.Errorf("message schedules can not be disabled")) + } + + if old.PersistMode != cfg.PersistMode { + return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not change persist mode")) + } + // Do some adjustments for being sealed. // Pedantic mode will allow those changes to be made, as they are deterministic and important to get a sealed stream. if cfg.Sealed { @@ -2028,6 +2290,12 @@ func (mset *stream) updateWithAdvisory(config *StreamConfig, sendAdvisory bool, mset.mu.Lock() if mset.isLeader() { + // Check for mirror promotion. + if ocfg.Mirror != nil && cfg.Mirror == nil { + mset.cancelMirrorConsumer() + mset.mirror = nil + } + // Now check for subject interest differences. current := make(map[string]struct{}, len(ocfg.Subjects)) for _, s := range ocfg.Subjects { @@ -2055,10 +2323,12 @@ func (mset *stream) updateWithAdvisory(config *StreamConfig, sendAdvisory bool, } // Check for the Duplicates + mset.ddMu.Lock() if cfg.Duplicates != ocfg.Duplicates && mset.ddtmr != nil { // Let it fire right away, it will adjust properly on purge. mset.ddtmr.Reset(time.Microsecond) } + mset.ddMu.Unlock() // Check for Sources. if len(cfg.Sources) > 0 || len(ocfg.Sources) > 0 { @@ -2120,7 +2390,7 @@ func (mset *stream) updateWithAdvisory(config *StreamConfig, sendAdvisory bool, } } - // Check for a change in allow direct status. + // Check for a change in allow direct/mirror status. // These will run on all members, so just update as appropriate here. // We do make sure we are caught up under monitorStream() during initial startup. if cfg.AllowDirect != ocfg.AllowDirect { @@ -2130,6 +2400,13 @@ func (mset *stream) updateWithAdvisory(config *StreamConfig, sendAdvisory bool, mset.unsubscribeToDirect() } } + if cfg.MirrorDirect != ocfg.MirrorDirect { + if cfg.MirrorDirect { + mset.subscribeToMirrorDirect() + } else { + mset.unsubscribeToMirrorDirect() + } + } // Check for changes to RePublish. if cfg.RePublish != nil { @@ -2200,6 +2477,12 @@ func (mset *stream) updateWithAdvisory(config *StreamConfig, sendAdvisory bool, } } + // If atomic publish is disabled, delete any in-progress batches. + if !cfg.AllowAtomicPublish { + mset.deleteInflightBatches(false) + mset.deleteBatchApplyState() + } + // Now update config and store's version of our config. // Although we are under the stream write lock, we will also assign the new // configuration under mset.cfgMu lock. This is so that in places where @@ -2272,17 +2555,21 @@ func (mset *stream) getCfgName() string { // Purge will remove all messages from the stream and underlying store based on the request. func (mset *stream) purge(preq *JSApiStreamPurgeRequest) (purged uint64, err error) { - mset.mu.RLock() + return mset.purgeLocked(preq, true) +} + +func (mset *stream) purgeLocked(preq *JSApiStreamPurgeRequest, needLock bool) (purged uint64, err error) { + if needLock { + mset.mu.Lock() + defer mset.mu.Unlock() + } if mset.closed.Load() { - mset.mu.RUnlock() return 0, errStreamClosed } if mset.cfg.Sealed { - mset.mu.RUnlock() return 0, errors.New("sealed stream") } store, mlseq := mset.store, mset.lseq - mset.mu.RUnlock() if preq != nil { purged, err = mset.store.PurgeEx(preq.Subject, preq.Sequence, preq.Keep) @@ -2298,7 +2585,6 @@ func (mset *stream) purge(preq *JSApiStreamPurgeRequest) (purged uint64, err err store.FastState(&state) fseq, lseq := state.FirstSeq, state.LastSeq - mset.mu.Lock() // Check if our last has moved past what our original last sequence was, if so reset. if lseq > mlseq { mset.setLastSeq(lseq) @@ -2306,7 +2592,6 @@ func (mset *stream) purge(preq *JSApiStreamPurgeRequest) (purged uint64, err err // Clear any pending acks below first seq. mset.clearAllPreAcksBelowFloor(fseq) - mset.mu.Unlock() // Purge consumers. // Check for filtered purge. @@ -2648,7 +2933,7 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool { // Check if we allow mirror direct here. If so check they we have mostly caught up. // The reason we do not require 0 is if the source is active we may always be slightly behind. - if mset.cfg.MirrorDirect && mset.mirror.dsub == nil && pending < dgetCaughtUpThresh { + if mset.cfg.MirrorDirect && mset.mirrorDirectSub == nil && pending < dgetCaughtUpThresh { if err := mset.subscribeToMirrorDirect(); err != nil { // Disable since we had problems above. mset.cfg.MirrorDirect = false @@ -2677,13 +2962,13 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool { var err error if node != nil { if js.limitsExceeded(stype) { - s.resourcesExceededError() + s.resourcesExceededError(stype) err = ApiErrors[JSInsufficientResourcesErr] } else { err = node.Propose(encodeStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts, true)) } } else { - err = mset.processJetStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts, nil, true) + err = mset.processJetStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts, nil, true, true) } if err != nil { if strings.Contains(err.Error(), "no space left") { @@ -3047,6 +3332,8 @@ func (mset *stream) setupMirrorConsumer() error { if len(hdr) > 0 { // Remove any Nats-Expected- headers as we don't want to validate them. hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Expected-") + // Remove any Nats-Batch- headers, batching is not supported when mirroring. + hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Batch-") } mset.queueInbound(msgs, subject, reply, hdr, msg, nil, nil) mirror.last.Store(time.Now().UnixNano()) @@ -3146,11 +3433,6 @@ func (mset *stream) cancelSourceInfo(si *sourceInfo) { mset.unsubscribe(si.sub) si.sub = nil } - // In case we had a mirror direct subscription. - if si.dsub != nil { - mset.unsubscribe(si.dsub) - si.dsub = nil - } mset.removeInternalConsumer(si) if si.qch != nil { close(si.qch) @@ -3536,12 +3818,11 @@ func (m *inMsg) isControlMsg() bool { } // Sends a reply to a flow control request. +// Lock should be held. func (mset *stream) sendFlowControlReply(reply string) { - mset.mu.RLock() if mset.isLeader() && mset.outq != nil { mset.outq.sendMsg(reply, nil) } - mset.mu.RUnlock() } // handleFlowControl will properly handle flow control messages for both R==1 and R>1. @@ -3633,9 +3914,11 @@ func (mset *stream) processInboundSourceMsg(si *sourceInfo, m *inMsg) bool { hdr = removeHeaderIfPresent(hdr, JSStreamSource) // Remove any Nats-Expected- headers as we don't want to validate them. hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Expected-") + // Remove any Nats-Batch- headers, batching is not supported when sourcing. + hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Batch-") } // Hold onto the origin reply which has all the metadata. - hdr = genHeader(hdr, JSStreamSource, si.genSourceHeader(m.rply)) + hdr = genHeader(hdr, JSStreamSource, si.genSourceHeader(m.subj, m.rply)) // Do the subject transform for the source if there's one if len(si.trs) > 0 { @@ -3657,7 +3940,7 @@ func (mset *stream) processInboundSourceMsg(si *sourceInfo, m *inMsg) bool { if node != nil { err = mset.processClusteredInboundMsg(m.subj, _EMPTY_, hdr, msg, nil, true) } else { - err = mset.processJetStreamMsg(m.subj, _EMPTY_, hdr, msg, 0, 0, nil, true) + err = mset.processJetStreamMsg(m.subj, _EMPTY_, hdr, msg, 0, 0, nil, true, true) } if err != nil { @@ -3704,7 +3987,7 @@ func (mset *stream) processInboundSourceMsg(si *sourceInfo, m *inMsg) bool { } // Generate a new (2.10) style source header (stream name, sequence number, source filter, source destination transform). -func (si *sourceInfo) genSourceHeader(reply string) string { +func (si *sourceInfo) genSourceHeader(orig, reply string) string { var b strings.Builder iNameParts := strings.Split(si.iname, " ") @@ -3729,6 +4012,8 @@ func (si *sourceInfo) genSourceHeader(reply string) string { b.WriteString(iNameParts[1]) b.WriteByte(' ') b.WriteString(iNameParts[2]) + b.WriteByte(' ') + b.WriteString(orig) return b.String() } @@ -4067,26 +4352,27 @@ func (mset *stream) unsubscribeToDirect() { // Lock should be held. func (mset *stream) subscribeToMirrorDirect() error { - if mset.mirror == nil { + if mset.cfg.Mirror == nil { return nil } + mirrorName := mset.cfg.Mirror.Name // We will make this listen on a queue group by default, which can allow mirrors to participate on opt-in basis. - if mset.mirror.dsub == nil { - dsubj := fmt.Sprintf(JSDirectMsgGetT, mset.mirror.name) + if mset.mirrorDirectSub == nil { + dsubj := fmt.Sprintf(JSDirectMsgGetT, mirrorName) // We will make this listen on a queue group by default, which can allow mirrors to participate on opt-in basis. if sub, err := mset.queueSubscribeInternal(dsubj, dgetGroup, mset.processDirectGetRequest); err == nil { - mset.mirror.dsub = sub + mset.mirrorDirectSub = sub } else { return err } } // Now the one that will have subject appended past stream name. - if mset.mirror.lbsub == nil { - dsubj := fmt.Sprintf(JSDirectGetLastBySubjectT, mset.mirror.name, fwcs) + if mset.mirrorLastBySub == nil { + dsubj := fmt.Sprintf(JSDirectGetLastBySubjectT, mirrorName, fwcs) // We will make this listen on a queue group by default, which can allow mirrors to participate on opt-in basis. if sub, err := mset.queueSubscribeInternal(dsubj, dgetGroup, mset.processDirectGetLastBySubjectRequest); err == nil { - mset.mirror.lbsub = sub + mset.mirrorLastBySub = sub } else { return err } @@ -4095,6 +4381,18 @@ func (mset *stream) subscribeToMirrorDirect() error { return nil } +// Lock should be held. +func (mset *stream) unsubscribeToMirrorDirect() { + if mset.mirrorDirectSub != nil { + mset.unsubscribe(mset.mirrorDirectSub) + mset.mirrorDirectSub = nil + } + if mset.mirrorLastBySub != nil { + mset.unsubscribe(mset.mirrorLastBySub) + mset.mirrorLastBySub = nil + } +} + // Stop our source consumers. // Lock should be held. func (mset *stream) stopSourceConsumers() { @@ -4113,7 +4411,7 @@ func (mset *stream) removeInternalConsumer(si *sourceInfo) { // Will unsubscribe from the stream. // Lock should be held. -func (mset *stream) unsubscribeToStream(stopping bool) error { +func (mset *stream) unsubscribeToStream(stopping, shuttingDown bool) error { for _, subject := range mset.cfg.Subjects { mset.unsubscribeInternal(subject) } @@ -4125,16 +4423,53 @@ func (mset *stream) unsubscribeToStream(stopping bool) error { if len(mset.sources) > 0 { mset.stopSourceConsumers() } + // Clear batching state. + mset.deleteInflightBatches(shuttingDown) - // In case we had a direct get subscriptions. if stopping { + // In case we had a direct get subscriptions. mset.unsubscribeToDirect() + mset.unsubscribeToMirrorDirect() + } + + if mset.directLeaderSub == nil { + // Always unsubscribe the leader sub. + mset.unsubscribe(mset.directLeaderSub) + mset.directLeaderSub = nil } mset.active = false return nil } +// Lock should be held. +func (mset *stream) deleteInflightBatches(shuttingDown bool) { + if mset.batches != nil { + mset.batches.mu.Lock() + for batchId, b := range mset.batches.group { + // If shutting down, do fixup during startup. In-memory batches don't require manual cleanup. + if shuttingDown { + b.stopLocked() + } else { + b.cleanupLocked(batchId, mset.batches) + } + } + mset.batches.mu.Unlock() + mset.batches = nil + } +} + +// Lock should be held. +func (mset *stream) deleteBatchApplyState() { + if batch := mset.batchApply; batch != nil { + // Need to return entries (if any) to the pool. + for _, bce := range batch.entries { + bce.ReturnToPool() + } + mset.batchApply = nil + } +} + // Lock does NOT need to be held, we set the client on setup and never change it at this point. func (mset *stream) subscribeInternal(subject string, cb msgHandler) (*subscription, error) { if mset.closed.Load() { @@ -4239,13 +4574,13 @@ func (mset *stream) setupStore(fsCfg *FileStoreConfig) error { mset.removeMsg(seq) } }) - mset.store.RegisterSubjectDeleteMarkerUpdates(func(im *inMsg) { + mset.store.RegisterProcessJetStreamMsg(func(im *inMsg) { if mset.IsClustered() { if mset.IsLeader() { mset.processClusteredInboundMsg(im.subj, im.rply, im.hdr, im.msg, im.mt, false) } } else { - mset.processJetStreamMsg(im.subj, im.rply, im.hdr, im.msg, 0, 0, im.mt, false) + mset.processJetStreamMsg(im.subj, im.rply, im.hdr, im.msg, 0, 0, im.mt, false, true) } }) mset.mu.Unlock() @@ -4289,20 +4624,14 @@ func (mset *stream) storeUpdates(md, bd int64, seq uint64, subj string) { // NumMsgIds returns the number of message ids being tracked for duplicate suppression. func (mset *stream) numMsgIds() int { - mset.mu.Lock() - defer mset.mu.Unlock() - if !mset.ddloaded { - mset.rebuildDedupe() - } + mset.ddMu.Lock() + defer mset.ddMu.Unlock() return len(mset.ddmap) } // checkMsgId will process and check for duplicates. -// Lock should be held. +// mset.ddMu lock should be held. func (mset *stream) checkMsgId(id string) *ddentry { - if !mset.ddloaded { - mset.rebuildDedupe() - } if id == _EMPTY_ || len(mset.ddmap) == 0 { return nil } @@ -4312,13 +4641,15 @@ func (mset *stream) checkMsgId(id string) *ddentry { // Will purge the entries that are past the window. // Should be called from a timer. func (mset *stream) purgeMsgIds() { - mset.mu.Lock() - defer mset.mu.Unlock() - now := time.Now().UnixNano() + mset.cfgMu.RLock() tmrNext := mset.cfg.Duplicates + mset.cfgMu.RUnlock() window := int64(tmrNext) + mset.ddMu.Lock() + defer mset.ddMu.Unlock() + for i, dde := range mset.ddarr[mset.ddindex:] { if now-dde.ts >= window { delete(mset.ddmap, dde.id) @@ -4355,8 +4686,15 @@ func (mset *stream) purgeMsgIds() { } } +// storeMsgId will store the message id for duplicate detection. +func (mset *stream) storeMsgId(dde *ddentry) { + mset.ddMu.Lock() + defer mset.ddMu.Unlock() + mset.storeMsgIdLocked(dde) +} + // storeMsgIdLocked will store the message id for duplicate detection. -// Lock should be held. +// mset.ddMu lock should be held. func (mset *stream) storeMsgIdLocked(dde *ddentry) { if mset.ddmap == nil { mset.ddmap = make(map[string]*ddentry) @@ -4383,9 +4721,9 @@ func getExpectedStream(hdr []byte) string { return string(getHeader(JSExpectedStream, hdr)) } -// Fast lookup of expected stream. +// Fast lookup of expected last sequence. func getExpectedLastSeq(hdr []byte) (uint64, bool) { - bseq := getHeader(JSExpectedLastSeq, hdr) + bseq := sliceHeader(JSExpectedLastSeq, hdr) if len(bseq) == 0 { return 0, false } @@ -4403,7 +4741,7 @@ func getRollup(hdr []byte) string { // Fast lookup of expected stream sequence per subject. func getExpectedLastSeqPerSubject(hdr []byte) (uint64, bool) { - bseq := getHeader(JSExpectedLastSubjSeq, hdr) + bseq := sliceHeader(JSExpectedLastSubjSeq, hdr) if len(bseq) == 0 { return 0, false } @@ -4412,7 +4750,7 @@ func getExpectedLastSeqPerSubject(hdr []byte) (uint64, bool) { // Fast lookup of expected subject for the expected stream sequence per subject. func getExpectedLastSeqPerSubjectForSubject(hdr []byte) string { - return string(getHeader(JSExpectedLastSubjSeqSubj, hdr)) + return bytesToString(sliceHeader(JSExpectedLastSubjSeqSubj, hdr)) } // Fast lookup of the message TTL from headers: @@ -4452,6 +4790,79 @@ func parseMessageTTL(ttl string) (int64, error) { return t, nil } +// Fast lookup of the message Incr from headers. +// Return includes the value or nil, and success. +func getMessageIncr(hdr []byte) (*big.Int, bool) { + incr := sliceHeader(JSMessageIncr, hdr) + if len(incr) == 0 { + return nil, true + } + var v big.Int + return v.SetString(bytesToString(incr), 10) +} + +// Fast lookup of message schedule. +func getMessageSchedule(hdr []byte) (time.Time, bool) { + if len(hdr) == 0 { + return time.Time{}, true + } + val := bytesToString(sliceHeader(JSSchedulePattern, hdr)) + if val == _EMPTY_ { + return time.Time{}, true + } + if !strings.HasPrefix(val, "@at ") { + return time.Time{}, false + } + t, err := time.Parse(time.RFC3339, val[4:]) + return t, err == nil +} + +// Fast lookup of the message schedule TTL from headers. +// The TTL is confirmed to be valid, but the raw TTL string is returned. +func getMessageScheduleTTL(hdr []byte) (string, bool) { + ttl := getHeader(JSScheduleTTL, hdr) + if len(ttl) == 0 { + return _EMPTY_, true + } + if _, err := parseMessageTTL(bytesToString(ttl)); err != nil { + return _EMPTY_, false + } + return string(ttl), true +} + +// Fast lookup of message schedule target. +func getMessageScheduleTarget(hdr []byte) string { + if len(hdr) == 0 { + return _EMPTY_ + } + return string(getHeader(JSScheduleTarget, hdr)) +} + +// Fast lookup of message scheduler. +func getMessageScheduler(hdr []byte) string { + if len(hdr) == 0 { + return _EMPTY_ + } + return string(getHeader(JSScheduler, hdr)) +} + +// Fast lookup of batch ID. +func getBatchId(hdr []byte) string { + if len(hdr) == 0 { + return _EMPTY_ + } + return string(getHeader(JSBatchId, hdr)) +} + +// Fast lookup of batch sequence. +func getBatchSequence(hdr []byte) (uint64, bool) { + bseq := sliceHeader(JSBatchSeq, hdr) + if len(bseq) == 0 { + return 0, false + } + return uint64(parseInt64(bseq)), true +} + // Signal if we are clustered. Will acquire rlock. func (mset *stream) IsClustered() bool { mset.mu.RLock() @@ -4472,6 +4883,7 @@ type inMsg struct { msg []byte si *sourceInfo mt *msgTrace + seq uint64 // seq that can be optionally used for sorting out-of-band. } var inMsgPool = sync.Pool{ @@ -4518,7 +4930,12 @@ func (mset *stream) processDirectGetRequest(_ *subscription, c *client, _ *Accou if len(reply) == 0 { return } - _, msg := c.msgParts(rmsg) + hdr, msg := c.msgParts(rmsg) + if errorOnRequiredApiLevel(hdr) { + hdr := []byte("NATS/1.0 412 Required Api Level\r\n\r\n") + mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + return + } if len(msg) == 0 { hdr := []byte("NATS/1.0 408 Empty Request\r\n\r\n") mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) @@ -4567,13 +4984,23 @@ func (mset *stream) processDirectGetLastBySubjectRequest(_ *subscription, c *cli if len(reply) == 0 { return } - _, msg := c.msgParts(rmsg) - // This version expects no payload. - if len(msg) != 0 { - hdr := []byte("NATS/1.0 408 Bad Request\r\n\r\n") + hdr, msg := c.msgParts(rmsg) + if errorOnRequiredApiLevel(hdr) { + hdr := []byte("NATS/1.0 412 Required Api Level\r\n\r\n") mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) return } + + var req JSApiMsgGetRequest + if len(msg) > 0 { + err := json.Unmarshal(msg, &req) + if err != nil { + hdr := []byte("NATS/1.0 408 Malformed Request\r\n\r\n") + mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + return + } + } + // Extract the key. var key string for i, n := 0, 0; i < len(subject); i++ { @@ -4593,7 +5020,17 @@ func (mset *stream) processDirectGetLastBySubjectRequest(_ *subscription, c *cli return } - req := JSApiMsgGetRequest{LastFor: key} + // Error if request fields are provided that don't make sense for this endpoint. + switch { + case req.LastFor != _EMPTY_, req.NextFor != _EMPTY_, + len(req.MultiLastFor) > 0, req.Seq != 0, req.Batch != 0, + req.StartTime != nil, req.UpToSeq != 0, req.UpToTime != nil: + hdr := []byte("NATS/1.0 408 Bad Request\r\n\r\n") + mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0)) + return + default: + req.LastFor = key + } inlineOk := c.kind != ROUTER && c.kind != GATEWAY && c.kind != LEAF if !inlineOk { @@ -4618,9 +5055,10 @@ func (mset *stream) getDirectMulti(req *JSApiMsgGetRequest, reply string) { // TODO(dlc) - Make configurable? const maxAllowedResponses = 1024 - // We hold the lock here to try to avoid changes out from underneath of us. + // Ensure this read request is isolated and doesn't interleave with writes. mset.mu.RLock() defer mset.mu.RUnlock() + // Grab store and name. store, name, s := mset.store, mset.cfg.Name, mset.srv @@ -4670,7 +5108,7 @@ func (mset *stream) getDirectMulti(req *JSApiMsgGetRequest, reply string) { return } - np, lseq, sentBytes, sent := uint64(len(seqs)-1), uint64(0), 0, 0 + np, lseq, sentBytes, sent := uint64(len(seqs)), uint64(0), 0, 0 for _, seq := range seqs { if seq < req.Seq { if np > 0 { @@ -4689,6 +5127,10 @@ func (mset *stream) getDirectMulti(req *JSApiMsgGetRequest, reply string) { hdr := sm.hdr ts := time.Unix(0, sm.ts).UTC() + // Decrement num pending. This is an optimization, and we do not continue to look it up for these operations. + if np > 0 { + np-- + } if len(hdr) == 0 { hdr = fmt.Appendf(nil, dgb, name, sm.subj, sm.seq, ts.Format(time.RFC3339Nano), np, lseq) } else { @@ -4700,10 +5142,6 @@ func (mset *stream) getDirectMulti(req *JSApiMsgGetRequest, reply string) { hdr = genHeader(hdr, JSNumPending, strconv.FormatUint(np, 10)) hdr = genHeader(hdr, JSLastSequence, strconv.FormatUint(lseq, 10)) } - // Decrement num pending. This is optimization and we do not continue to look it up for these operations. - if np > 0 { - np-- - } // Track our lseq lseq = sm.seq // Send out our message. @@ -4733,9 +5171,11 @@ func (mset *stream) getDirectRequest(req *JSApiMsgGetRequest, reply string) { return } + // Ensure this read request is isolated and doesn't interleave with writes. mset.mu.RLock() + defer mset.mu.RUnlock() + store, name, s := mset.store, mset.cfg.Name, mset.srv - mset.mu.RUnlock() var seq uint64 // Lookup start seq if AsOfTime is set. @@ -4775,8 +5215,8 @@ func (mset *stream) getDirectRequest(req *JSApiMsgGetRequest, reply string) { err error ) if seq > 0 && req.NextFor == _EMPTY_ { - // Only do direct lookup for first in a batch. - if i == 0 { + // Only do direct lookup for a non batch. + if i == 0 && !isBatchRequest { sm, err = store.LoadMsg(seq, &svp) } else { // We want to use load next with fwcs to step over deleted msgs. @@ -4801,32 +5241,36 @@ func (mset *stream) getDirectRequest(req *JSApiMsgGetRequest, reply string) { return } - hdr := sm.hdr ts := time.Unix(0, sm.ts).UTC() - - if isBatchRequest { - if len(hdr) == 0 { - hdr = fmt.Appendf(nil, dgb, name, sm.subj, sm.seq, ts.Format(time.RFC3339Nano), np, lseq) + var hdr []byte + if !req.NoHeaders { + hdr = sm.hdr + if isBatchRequest { + // Decrement num pending. This is an optimization, and we do not continue to look it up for these operations. + if np > 0 { + np-- + } + if len(hdr) == 0 { + hdr = fmt.Appendf(nil, dgb, name, sm.subj, sm.seq, ts.Format(time.RFC3339Nano), np, lseq) + } else { + hdr = copyBytes(hdr) + hdr = genHeader(hdr, JSStream, name) + hdr = genHeader(hdr, JSSubject, sm.subj) + hdr = genHeader(hdr, JSSequence, strconv.FormatUint(sm.seq, 10)) + hdr = genHeader(hdr, JSTimeStamp, ts.Format(time.RFC3339Nano)) + hdr = genHeader(hdr, JSNumPending, strconv.FormatUint(np, 10)) + hdr = genHeader(hdr, JSLastSequence, strconv.FormatUint(lseq, 10)) + } } else { - hdr = copyBytes(hdr) - hdr = genHeader(hdr, JSStream, name) - hdr = genHeader(hdr, JSSubject, sm.subj) - hdr = genHeader(hdr, JSSequence, strconv.FormatUint(sm.seq, 10)) - hdr = genHeader(hdr, JSTimeStamp, ts.Format(time.RFC3339Nano)) - hdr = genHeader(hdr, JSNumPending, strconv.FormatUint(np, 10)) - hdr = genHeader(hdr, JSLastSequence, strconv.FormatUint(lseq, 10)) - } - // Decrement num pending. This is optimization and we do not continue to look it up for these operations. - np-- - } else { - if len(hdr) == 0 { - hdr = fmt.Appendf(nil, dg, name, sm.subj, sm.seq, ts.Format(time.RFC3339Nano)) - } else { - hdr = copyBytes(hdr) - hdr = genHeader(hdr, JSStream, name) - hdr = genHeader(hdr, JSSubject, sm.subj) - hdr = genHeader(hdr, JSSequence, strconv.FormatUint(sm.seq, 10)) - hdr = genHeader(hdr, JSTimeStamp, ts.Format(time.RFC3339Nano)) + if len(hdr) == 0 { + hdr = fmt.Appendf(nil, dg, name, sm.subj, sm.seq, ts.Format(time.RFC3339Nano)) + } else { + hdr = copyBytes(hdr) + hdr = genHeader(hdr, JSStream, name) + hdr = genHeader(hdr, JSSubject, sm.subj) + hdr = genHeader(hdr, JSSequence, strconv.FormatUint(sm.seq, 10)) + hdr = genHeader(hdr, JSTimeStamp, ts.Format(time.RFC3339Nano)) + } } } // Track our lseq @@ -4880,7 +5324,7 @@ var ( ) // processJetStreamMsg is where we try to actually process the stream msg. -func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, mt *msgTrace, sourced bool) (retErr error) { +func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, mt *msgTrace, sourced bool, needLock bool) (retErr error) { if mt != nil { // Only the leader/standalone will have mt!=nil. On exit, send the // message trace event. @@ -4889,11 +5333,17 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, }() } - if mset.closed.Load() { + if mset == nil || mset.closed.Load() { return errStreamClosed } - mset.mu.Lock() + // Hold lock while storing the message, and potentially purging as part of a rollup. + // If we're writing an atomic batch of multiple messages, the lock is already held. + if needLock { + mset.mu.Lock() + defer mset.mu.Unlock() + } + s, store := mset.srv, mset.store traceOnly := mt.traceOnly() @@ -4926,17 +5376,30 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, maxMsgSize := int(mset.cfg.MaxMsgSize) numConsumers := len(mset.consumers) interestRetention := mset.cfg.Retention == InterestPolicy + allowMsgCounter, allowMsgSchedules := mset.cfg.AllowMsgCounter, mset.cfg.AllowMsgSchedules // Snapshot if we are the leader and if we can respond. isLeader, isSealed := mset.isLeaderNodeState(), mset.cfg.Sealed + isClustered := mset.isClustered() + canConsistencyCheck := !isClustered || traceOnly canRespond := doAck && len(reply) > 0 && isLeader + outq := mset.outq var resp = &JSPubAckResponse{} + var batchId string + var batchSeq uint64 + if len(hdr) > 0 { + // Populate batch details. + if batchId = getBatchId(hdr); batchId != _EMPTY_ { + batchSeq, _ = getBatchSequence(hdr) + // Disable consistency checking if this was already done + // earlier as part of the batch consistency check. + canConsistencyCheck = traceOnly + } + } + // Bail here if sealed. - if isSealed { - outq := mset.outq - mset.mu.Unlock() - bumpCLFS() + if canConsistencyCheck && isSealed { if canRespond && outq != nil { resp.PubAck = &PubAck{Stream: name} resp.Error = ApiErrors[JSStreamSealedErr] @@ -4951,7 +5414,6 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, // If this is a non-clustered msg and we are not considered active, meaning no active subscription, do not process. if lseq == 0 && ts == 0 && !mset.active { - mset.mu.Unlock() return nil } @@ -4974,8 +5436,6 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, } // Really is a mismatch. if isMisMatch { - outq := mset.outq - mset.mu.Unlock() if canRespond && outq != nil { resp.PubAck = &PubAck{Stream: name} resp.Error = ApiErrors[JSStreamSequenceNotMatchErr] @@ -4995,20 +5455,75 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, // Process additional msg headers if still present. var msgId string + var incr *big.Int var rollupSub, rollupAll bool - isClustered := mset.isClustered() if len(hdr) > 0 { - outq := mset.outq - // Certain checks have already been performed if in clustered mode, so only check if not. // Note, for cluster mode but with message tracing (without message delivery), we need // to do this check here since it was not done in processClusteredInboundMsg(). - if !isClustered || traceOnly { + if canConsistencyCheck { + // Counter increments. + // Only supported on counter streams, and payload must be empty (if not coming from a source). + var ok bool + if incr, ok = getMessageIncr(hdr); !ok { + apiErr := NewJSMessageIncrInvalidError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } else if incr != nil && !sourced { + // Only do checks if the message isn't sourced. Otherwise, we need to store verbatim. + if !allowMsgCounter { + apiErr := NewJSMessageIncrDisabledError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } else if len(msg) > 0 { + apiErr := NewJSMessageIncrPayloadError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } else { + // Check for incompatible headers. + var doErr bool + if getRollup(hdr) != _EMPTY_ || + getExpectedStream(hdr) != _EMPTY_ || + getExpectedLastMsgId(hdr) != _EMPTY_ || + getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ { + doErr = true + } else if _, ok := getExpectedLastSeq(hdr); ok { + doErr = true + } else if _, ok := getExpectedLastSeqPerSubject(hdr); ok { + doErr = true + } + + if doErr { + apiErr := NewJSMessageIncrInvalidError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } + } + } + // Expected stream. if sname := getExpectedStream(hdr); sname != _EMPTY_ && sname != name { - mset.mu.Unlock() - bumpCLFS() if canRespond { resp.PubAck = &PubAck{Stream: name} resp.Error = NewJSStreamNotMatchError() @@ -5017,31 +5532,166 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, } return errStreamMismatch } - } - // TTL'd messages are rejected entirely if TTLs are not enabled on the stream. - // Shouldn't happen in clustered mode since we should have already caught this - // in processClusteredInboundMsg, but needed here for non-clustered etc. - if ttl, _ := getMessageTTL(hdr); !sourced && ttl != 0 && !mset.cfg.AllowMsgTTL { - mset.mu.Unlock() - bumpCLFS() - if canRespond { - resp.PubAck = &PubAck{Stream: name} - resp.Error = NewJSMessageTTLDisabledError() - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) + // TTL'd messages are rejected entirely if TTLs are not enabled on the stream. + // Shouldn't happen in clustered mode since we should have already caught this + // in processClusteredInboundMsg, but needed here for non-clustered etc. + if ttl, _ := getMessageTTL(hdr); !sourced && ttl != 0 && !mset.cfg.AllowMsgTTL { + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSMessageTTLDisabledError() + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return errMsgTTLDisabled + } + + // Expected last sequence per subject. + if seq, exists := getExpectedLastSeqPerSubject(hdr); exists { + // Allow override of the subject used for the check. + seqSubj := subject + if optSubj := getExpectedLastSeqPerSubjectForSubject(hdr); optSubj != _EMPTY_ { + seqSubj = optSubj + } + + // TODO(dlc) - We could make a new store func that does this all in one. + var smv StoreMsg + var fseq uint64 + sm, err := store.LoadLastMsg(seqSubj, &smv) + if sm != nil { + fseq = sm.seq + } + if err == ErrStoreMsgNotFound && seq == 0 { + fseq, err = 0, nil + } + if err != nil || fseq != seq { + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSStreamWrongLastSequenceError(fseq) + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, fseq) + } + } else if getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ { + apiErr := NewJSStreamExpectedLastSeqPerSubjectInvalidError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } + + // Expected last sequence. + if seq, exists := getExpectedLastSeq(hdr); exists && seq != mset.lseq { + mlseq := mset.lseq + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSStreamWrongLastSequenceError(mlseq) + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return fmt.Errorf("last sequence mismatch: %d vs %d", seq, mlseq) + } + + // Message scheduling. + if schedule, ok := getMessageSchedule(hdr); !ok { + apiErr := NewJSMessageSchedulesPatternInvalidError() + if !allowMsgSchedules { + apiErr = NewJSMessageSchedulesDisabledError() + } + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } else if !schedule.IsZero() { + if !allowMsgSchedules { + apiErr := NewJSMessageSchedulesDisabledError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } else if scheduleTtl, ok := getMessageScheduleTTL(hdr); !ok { + apiErr := NewJSMessageSchedulesTTLInvalidError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } else if scheduleTtl != _EMPTY_ && !mset.cfg.AllowMsgTTL { + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSMessageTTLDisabledError() + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return errMsgTTLDisabled + } else if scheduleTarget := getMessageScheduleTarget(hdr); scheduleTarget == _EMPTY_ || + !IsValidPublishSubject(scheduleTarget) || SubjectsCollide(scheduleTarget, subject) { + apiErr := NewJSMessageSchedulesTargetInvalidError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } else { + match := slices.ContainsFunc(mset.cfg.Subjects, func(subj string) bool { + return SubjectsCollide(subj, scheduleTarget) + }) + if !match { + apiErr := NewJSMessageSchedulesTargetInvalidError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } + + // Add a rollup sub header if it doesn't already exist. + // Otherwise, it must exist already as a rollup on the subject. + if rollup := getRollup(hdr); rollup == _EMPTY_ { + hdr = genHeader(hdr, JSMsgRollup, JSMsgRollupSubject) + } else if rollup != JSMsgRollupSubject { + apiErr := NewJSMessageSchedulesRollupInvalidError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } + } } - return errMsgTTLDisabled } - // Dedupe detection. This is done at the cluster level for dedupe detectiom above the + // Dedupe detection. This is done at the cluster level for dedupe detection above the // lower layers. But we still need to pull out the msgId. if msgId = getMsgId(hdr); msgId != _EMPTY_ { // Do real check only if not clustered or traceOnly flag is set. - if !isClustered || traceOnly { - if dde := mset.checkMsgId(msgId); dde != nil { - mset.mu.Unlock() - bumpCLFS() + if canConsistencyCheck { + var seq uint64 + mset.ddMu.Lock() + dde := mset.checkMsgId(msgId) + if dde != nil { + seq = dde.seq + } + mset.ddMu.Unlock() + if seq > 0 { if canRespond { response := append(pubAck, strconv.FormatUint(dde.seq, 10)...) response = append(response, ",\"duplicate\": true}"...) @@ -5052,67 +5702,10 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, } } - // Expected last sequence per subject. - if seq, exists := getExpectedLastSeqPerSubject(hdr); exists { - // Allow override of the subject used for the check. - seqSubj := subject - if optSubj := getExpectedLastSeqPerSubjectForSubject(hdr); optSubj != _EMPTY_ { - seqSubj = optSubj - } - - // TODO(dlc) - We could make a new store func that does this all in one. - var smv StoreMsg - var fseq uint64 - sm, err := store.LoadLastMsg(seqSubj, &smv) - if sm != nil { - fseq = sm.seq - } - if err == ErrStoreMsgNotFound { - if seq == 0 { - fseq, err = 0, nil - } else if mset.isClustered() { - // Do not bump clfs in case message was not found and could have been deleted. - var ss StreamState - store.FastState(&ss) - if seq <= ss.LastSeq { - fseq, err = seq, nil - } - } - } - if err != nil || fseq != seq { - mset.mu.Unlock() - bumpCLFS() - if canRespond { - resp.PubAck = &PubAck{Stream: name} - resp.Error = NewJSStreamWrongLastSequenceError(fseq) - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - return fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, fseq) - } - } - - // Expected last sequence. - if seq, exists := getExpectedLastSeq(hdr); exists && seq != mset.lseq { - mlseq := mset.lseq - mset.mu.Unlock() - bumpCLFS() - if canRespond { - resp.PubAck = &PubAck{Stream: name} - resp.Error = NewJSStreamWrongLastSequenceError(mlseq) - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - return fmt.Errorf("last sequence mismatch: %d vs %d", seq, mlseq) - } // Expected last msgId. if lmsgId := getExpectedLastMsgId(hdr); lmsgId != _EMPTY_ { - if mset.lmsgId == _EMPTY_ && !mset.ddloaded { - mset.rebuildDedupe() - } if lmsgId != mset.lmsgId { last := mset.lmsgId - mset.mu.Unlock() bumpCLFS() if canRespond { resp.PubAck = &PubAck{Stream: name} @@ -5125,26 +5718,8 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, } // Check for any rollups. if rollup := getRollup(hdr); rollup != _EMPTY_ { - if !mset.cfg.AllowRollup || mset.cfg.DenyPurge { - mset.mu.Unlock() - bumpCLFS() - if canRespond { - resp.PubAck = &PubAck{Stream: name} - resp.Error = NewJSStreamRollupFailedError(errors.New("rollup not permitted")) - b, _ := json.Marshal(resp) - outq.sendMsg(reply, b) - } - return errors.New("rollup not permitted") - } - switch rollup { - case JSMsgRollupSubject: - rollupSub = true - case JSMsgRollupAll: - rollupAll = true - default: - mset.mu.Unlock() - bumpCLFS() - err := fmt.Errorf("rollup value invalid: %q", rollup) + if canConsistencyCheck && (!mset.cfg.AllowRollup || mset.cfg.DenyPurge) { + err := errors.New("rollup not permitted") if canRespond { resp.PubAck = &PubAck{Stream: name} resp.Error = NewJSStreamRollupFailedError(err) @@ -5153,9 +5728,37 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, } return err } + switch rollup { + case JSMsgRollupSubject: + rollupSub = true + case JSMsgRollupAll: + rollupAll = true + default: + if canConsistencyCheck { + err := fmt.Errorf("rollup value invalid: %q", rollup) + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSStreamRollupFailedError(err) + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return err + } + } } } + if canConsistencyCheck && incr == nil && allowMsgCounter { + apiErr := NewJSMessageIncrMissingError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } + // Response Ack. var ( response []byte @@ -5163,44 +5766,148 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, err error ) + // Apply increment for counter. + // But only if it's allowed for this stream. This can happen when we store verbatim for a sourced stream. + if canConsistencyCheck && incr != nil && allowMsgCounter { + var initial big.Int + var sources CounterSources + var smv StoreMsg + sm, err := store.LoadLastMsg(subject, &smv) + if err == nil && sm != nil { + var val CounterValue + // Return an error if the counter is broken somehow. + if json.Unmarshal(sm.msg, &val) != nil { + apiErr := NewJSMessageCounterBrokenError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } + if ncs := sliceHeader(JSMessageCounterSources, sm.hdr); len(ncs) > 0 { + if err := json.Unmarshal(ncs, &sources); err != nil { + apiErr := NewJSMessageCounterBrokenError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } + } + initial.SetString(val.Value, 10) + } + srchdr := sliceHeader(JSStreamSource, hdr) + if len(srchdr) > 0 { + // This is a sourced message, so we can't apply Nats-Incr but + // instead should just update the source count header. + fields := strings.Split(string(srchdr), " ") + origStream := fields[0] + origSubj := subject + if len(fields) >= 5 { + origSubj = fields[4] + } + var val CounterValue + if json.Unmarshal(msg, &val) != nil { + apiErr := NewJSMessageCounterBrokenError() + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = apiErr + b, _ := json.Marshal(resp) + outq.sendMsg(reply, b) + } + return apiErr + } + var sourced big.Int + sourced.SetString(val.Value, 10) + if sources == nil { + sources = map[string]map[string]string{} + } + if _, ok := sources[origStream]; !ok { + sources[origStream] = map[string]string{} + } + prevVal := sources[origStream][origSubj] + sources[origStream][origSubj] = sourced.String() + // We will also replace the Nats-Incr header with the diff + // between our last value from this source and this one, so + // that the arithmetic is always correct. + var previous big.Int + previous.SetString(prevVal, 10) + incr.Sub(&sourced, &previous) + hdr = setHeader(JSMessageIncr, incr.String(), hdr) + } + // Now make the change. + initial.Add(&initial, incr) + // Generate the new payload. + var _msg [128]byte + msg = fmt.Appendf(_msg[:0], "{%q:%q}", "val", initial.String()) + // Write the updated source count headers. + if len(sources) > 0 { + nhdr, err := json.Marshal(sources) + if err != nil { + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSMessageCounterBrokenError() + response, _ = json.Marshal(resp) + outq.sendMsg(reply, response) + } + return err + } + hdr = setHeader(JSMessageCounterSources, string(nhdr), hdr) + } + + // Check to see if we are over the max msg size. + // Subtract to prevent against overflows. + maxPayload := int64(mset.srv.getOpts().MaxPayload) + hdrLen, msgLen := int64(len(hdr)), int64(len(msg)) + if hdrLen > maxPayload || msgLen > maxPayload-hdrLen { + if canRespond { + resp.PubAck = &PubAck{Stream: name} + resp.Error = NewJSStreamMessageExceedsMaximumError() + response, _ = json.Marshal(resp) + outq.sendMsg(reply, response) + } + return ErrMaxPayload + } + } + // Check to see if we are over the max msg size. - if maxMsgSize >= 0 && (len(hdr)+len(msg)) > maxMsgSize { - mset.mu.Unlock() - bumpCLFS() + // Subtract to prevent against overflows. + if canConsistencyCheck && maxMsgSize >= 0 && (len(hdr) > maxMsgSize || len(msg) > maxMsgSize-len(hdr)) { if canRespond { resp.PubAck = &PubAck{Stream: name} resp.Error = NewJSStreamMessageExceedsMaximumError() response, _ = json.Marshal(resp) - mset.outq.sendMsg(reply, response) + outq.sendMsg(reply, response) } return ErrMaxPayload } - if len(hdr) > math.MaxUint16 { - mset.mu.Unlock() - bumpCLFS() + if canConsistencyCheck && len(hdr) > math.MaxUint16 { if canRespond { resp.PubAck = &PubAck{Stream: name} resp.Error = NewJSStreamHeaderExceedsMaximumError() response, _ = json.Marshal(resp) - mset.outq.sendMsg(reply, response) + outq.sendMsg(reply, response) } return ErrMaxPayload } // Check to see if we have exceeded our limits. - if js.limitsExceeded(stype) { - s.resourcesExceededError() - mset.mu.Unlock() - bumpCLFS() + // Don't error and log/stepdown if we're tracing when clustered. + if !isClustered && js.limitsExceeded(stype) { + s.resourcesExceededError(stype) if canRespond { resp.PubAck = &PubAck{Stream: name} resp.Error = NewJSInsufficientResourcesError() response, _ = json.Marshal(resp) - mset.outq.sendMsg(reply, response) + outq.sendMsg(reply, response) } // Stepdown regardless. - if node := mset.raftNode(); node != nil { + if node := mset.node; node != nil { node.StepDown() } return NewJSInsufficientResourcesError() @@ -5222,7 +5929,6 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mt.updateJetStreamEvent(subject, noInterest) if traceOnly { - mset.mu.Unlock() return nil } @@ -5232,14 +5938,17 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mset.lmsgId = msgId // If we have a msgId make sure to save. if msgId != _EMPTY_ { - mset.storeMsgIdLocked(&ddentry{msgId, mset.lseq, ts}) + mset.storeMsgId(&ddentry{msgId, mset.lseq, ts}) } if canRespond { response = append(pubAck, strconv.FormatUint(mset.lseq, 10)...) - response = append(response, '}') - mset.outq.sendMsg(reply, response) + if batchId != _EMPTY_ { + response = append(response, fmt.Sprintf(",\"batch\":%q,\"count\":%d}", batchId, batchSeq)...) + } else { + response = append(response, '}') + } + outq.sendMsg(reply, response) } - mset.mu.Unlock() return nil } @@ -5271,19 +5980,19 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, } // If clustered this was already checked and we do not want to check here and possibly introduce skew. + // Don't error and log if we're tracing when clustered. if !isClustered { if exceeded, err := jsa.wouldExceedLimits(stype, tierName, mset.cfg.Replicas, subject, hdr, msg); exceeded { if err == nil { err = NewJSAccountResourcesExceededError() } - s.RateLimitWarnf("JetStream resource limits exceeded for account: %q", accName) + s.RateLimitWarnf("JetStream %s resource limits exceeded for account: %q", strings.ToLower(stype.String()), accName) if canRespond { resp.PubAck = &PubAck{Stream: name} resp.Error = err response, _ = json.Marshal(resp) - mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) } - mset.mu.Unlock() return err } } @@ -5295,9 +6004,8 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, resp.PubAck = &PubAck{Stream: name} resp.Error = NewJSMessageTTLInvalidError() response, _ = json.Marshal(resp) - mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) } - mset.mu.Unlock() return err } @@ -5327,10 +6035,9 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, if err != nil { if isPermissionError(err) { - mset.mu.Unlock() // messages in block cache could be lost in the worst case. // In the clustered mode it is very highly unlikely as a result of replication. - mset.srv.DisableJetStream() + go mset.srv.DisableJetStream() mset.srv.Warnf("Filesystem permission denied while writing msg, disabling JetStream: %v", err) return err } @@ -5339,7 +6046,6 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, mset.store.FastState(&state) mset.lseq = state.LastSeq mset.lmsgId = olmsgId - mset.mu.Unlock() bumpCLFS() switch err { @@ -5354,14 +6060,17 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, resp.PubAck = &PubAck{Stream: name} resp.Error = NewJSStreamStoreFailedError(err, Unless(err)) response, _ = json.Marshal(resp) - mset.outq.sendMsg(reply, response) + outq.sendMsg(reply, response) } return err } + // If here we succeeded in storing the message. + // If we have a msgId make sure to save. // This will replace our estimate from the cluster layer if we are clustered. if msgId != _EMPTY_ { + mset.ddMu.Lock() if isClustered && isLeader && mset.ddmap != nil { if dde := mset.ddmap[msgId]; dde != nil { dde.seq, dde.ts = seq, ts @@ -5372,16 +6081,20 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, // R1 or not leader.. mset.storeMsgIdLocked(&ddentry{msgId, seq, ts}) } + mset.ddMu.Unlock() } - // If here we succeeded in storing the message. - mset.mu.Unlock() - // No errors, this is the normal path. if rollupSub { - mset.purge(&JSApiStreamPurgeRequest{Subject: subject, Keep: 1}) + mset.purgeLocked(&JSApiStreamPurgeRequest{Subject: subject, Keep: 1}, false) } else if rollupAll { - mset.purge(&JSApiStreamPurgeRequest{Keep: 1}) + mset.purgeLocked(&JSApiStreamPurgeRequest{Keep: 1}, false) + } else if scheduleNext := sliceHeader(JSScheduleNext, hdr); len(scheduleNext) > 0 && bytesToString(scheduleNext) == JSScheduleNextPurge { + // Purge the message schedule. + scheduler := getMessageScheduler(hdr) + if scheduler != _EMPTY_ { + mset.purgeLocked(&JSApiStreamPurgeRequest{Subject: scheduler}, false) + } } // Check for republish. @@ -5410,14 +6123,22 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, hdr = fmt.Appendf(hdr[:end:end], htho[hoff:], name, subject, seq, tsStr, tlseq, len(msg)) } } - mset.outq.send(newJSPubMsg(tsubj, _EMPTY_, _EMPTY_, hdr, rpMsg, nil, seq)) + outq.send(newJSPubMsg(tsubj, _EMPTY_, _EMPTY_, hdr, rpMsg, nil, seq)) } // Send response here. if canRespond { response = append(pubAck, strconv.FormatUint(seq, 10)...) - response = append(response, '}') - mset.outq.sendMsg(reply, response) + if batchId != _EMPTY_ { + response = append(response, fmt.Sprintf(",\"batch\":%q,\"count\":%d}", batchId, batchSeq)...) + } else if allowMsgCounter { + var counter CounterValue + json.Unmarshal(msg, &counter) + response = append(response, fmt.Sprintf(",\"val\":%q}", counter.Value)...) + } else { + response = append(response, '}') + } + outq.sendMsg(reply, response) } // Signal consumers for new messages. @@ -5432,6 +6153,410 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, return nil } +// processJetStreamBatchMsg processes a JetStream message that's part of an atomic batch publish. +// Handles constraints around the batch, storing messages, doing consistency checks, and performing the commit. +func (mset *stream) processJetStreamBatchMsg(batchId, subject, reply string, hdr, msg []byte, mt *msgTrace) (retErr error) { + // For possible error response. + var response []byte + + mset.mu.RLock() + canRespond := !mset.cfg.NoAck && len(reply) > 0 + name, stype := mset.cfg.Name, mset.cfg.Storage + discard, discardNewPer, maxMsgs, maxMsgsPer, maxBytes := mset.cfg.Discard, mset.cfg.DiscardNewPer, mset.cfg.MaxMsgs, mset.cfg.MaxMsgsPer, mset.cfg.MaxBytes + s, js, jsa, st, r, tierName, outq, node := mset.srv, mset.js, mset.jsa, mset.cfg.Storage, mset.cfg.Replicas, mset.tier, mset.outq, mset.node + maxMsgSize, lseq := int(mset.cfg.MaxMsgSize), mset.lseq + isLeader, isClustered, isSealed, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules, allowAtomicPublish := mset.isLeader(), mset.isClustered(), mset.cfg.Sealed, mset.cfg.AllowRollup, mset.cfg.DenyPurge, mset.cfg.AllowMsgTTL, mset.cfg.AllowMsgCounter, mset.cfg.AllowMsgSchedules, mset.cfg.AllowAtomicPublish + mset.mu.RUnlock() + + // If message tracing (with message delivery), we will need to send the + // event on exit in case there was an error (if message was not proposed). + // Otherwise, the event will be sent from processJetStreamMsg when + // invoked by the leader (from applyStreamEntries). + if mt != nil { + defer func() { + if retErr != nil { + mt.sendEventFromJetStream(retErr) + } + }() + } + + // Check that we are the leader. This can be false if we have scaled up from an R1 that had inbound queued messages. + if !isLeader { + return NewJSClusterNotLeaderError() + } + + // Bail here if sealed. + if isSealed { + var resp = JSPubAckResponse{PubAck: &PubAck{Stream: mset.name()}, Error: NewJSStreamSealedError()} + b, _ := json.Marshal(resp) + mset.outq.sendMsg(reply, b) + return NewJSStreamSealedError() + } + + // Check here pre-emptively if we have exceeded this server limits. + if js.limitsExceeded(stype) { + s.resourcesExceededError(stype) + if canRespond { + b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: NewJSInsufficientResourcesError()}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, b, nil, 0)) + } + // Stepdown regardless. + if node := mset.raftNode(); node != nil { + node.StepDown() + } + return NewJSInsufficientResourcesError() + } + + // Check here pre-emptively if we have exceeded our account limits. + if exceeded, err := jsa.wouldExceedLimits(st, tierName, r, subject, hdr, msg); exceeded { + if err == nil { + err = NewJSAccountResourcesExceededError() + } + s.RateLimitWarnf("JetStream account limits exceeded for '%s': %s", jsa.acc().GetName(), err.Error()) + if canRespond { + var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} + resp.Error = err + response, _ = json.Marshal(resp) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) + } + return err + } + + // Check msgSize if we have a limit set there. Again this works if it goes through but better to be pre-emptive. + // Subtract to prevent against overflows. + if maxMsgSize >= 0 && (len(hdr) > maxMsgSize || len(msg) > maxMsgSize-len(hdr)) { + err := fmt.Errorf("JetStream message size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name) + s.RateLimitWarnf("%s", err.Error()) + if canRespond { + var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}} + resp.Error = NewJSStreamMessageExceedsMaximumError() + response, _ = json.Marshal(resp) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0)) + } + return err + } + + if !allowAtomicPublish { + err := NewJSAtomicPublishDisabledError() + if canRespond { + b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: err}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, b, nil, 0)) + } + return err + } + + // Batch ID is too long. + if len(batchId) > 64 { + err := NewJSAtomicPublishInvalidBatchIDError() + if canRespond { + b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: err}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, b, nil, 0)) + } + return err + } + + batchSeq, exists := getBatchSequence(hdr) + if !exists { + err := NewJSAtomicPublishMissingSeqError() + if canRespond { + b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: err}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, b, nil, 0)) + } + return err + } + commit := len(sliceHeader(JSBatchCommit, hdr)) != 0 + + mset.mu.Lock() + if mset.batches == nil { + mset.batches = &batching{ + group: make(map[string]*batchGroup, 1), + } + } + batches := mset.batches + mset.mu.Unlock() + + respondIncompleteBatch := func() error { + err := NewJSAtomicPublishIncompleteBatchError() + if canRespond { + buf, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: err}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, buf, nil, 0)) + } + return err + } + + // Get batch. + batches.mu.Lock() + b, ok := batches.group[batchId] + if !ok { + if batchSeq != 1 { + batches.mu.Unlock() + maxBatchSize := streamMaxBatchSize + opts := s.getOpts() + if opts.JetStreamLimits.MaxBatchSize > 0 { + maxBatchSize = opts.JetStreamLimits.MaxBatchSize + } + if batchSeq > uint64(maxBatchSize) { + err := NewJSAtomicPublishTooLargeBatchError(maxBatchSize) + if canRespond { + buf, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: err}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, buf, nil, 0)) + } + return err + } + return respondIncompleteBatch() + } + + // Limits. + maxInflightPerStream := streamMaxBatchInflightPerStream + maxInflightTotal := streamMaxBatchInflightTotal + opts := s.getOpts() + if opts.JetStreamLimits.MaxBatchInflightPerStream > 0 { + maxInflightPerStream = opts.JetStreamLimits.MaxBatchInflightPerStream + } + if opts.JetStreamLimits.MaxBatchInflightTotal > 0 { + maxInflightTotal = opts.JetStreamLimits.MaxBatchInflightTotal + } + + // Confirm we can facilitate an additional batch. + if len(batches.group)+1 > maxInflightPerStream { + batches.mu.Unlock() + return respondIncompleteBatch() + } + + // Confirm we'll not exceed the server limit. + if globalInflightBatches.Add(1) > int32(maxInflightTotal) { + globalInflightBatches.Add(-1) + batches.mu.Unlock() + return respondIncompleteBatch() + } + + var err error + if b, err = batches.newBatchGroup(mset, batchId); err != nil { + globalInflightBatches.Add(-1) + batches.mu.Unlock() + return respondIncompleteBatch() + } + batches.group[batchId] = b + } + + // Detect gaps. + b.lseq++ + if b.lseq != batchSeq { + b.cleanupLocked(batchId, batches) + batches.mu.Unlock() + mset.sendStreamBatchAbandonedAdvisory(batchId, BatchIncomplete) + return respondIncompleteBatch() + } + + // Confirm the batch doesn't exceed the allowed size. + maxSize := streamMaxBatchSize + if maxBatchSize := s.getOpts().JetStreamLimits.MaxBatchSize; maxBatchSize > 0 { + maxSize = maxBatchSize + } + if batchSeq > uint64(maxSize) { + b.cleanupLocked(batchId, batches) + batches.mu.Unlock() + mset.sendStreamBatchAbandonedAdvisory(batchId, BatchLarge) + err := NewJSAtomicPublishTooLargeBatchError(maxSize) + if canRespond { + buf, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: err}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, buf, nil, 0)) + } + return err + } + + // Persist, but optimize if we're committing because we already know last. + // If the underlying store is file-based we need to persist everything to survive hard kills, because we're R1. + if !commit || b.store.Type() == FileStorage { + seq, _, err := b.store.StoreMsg(subject, hdr, msg, 0) + if err != nil || seq != batchSeq { + b.cleanupLocked(batchId, batches) + batches.mu.Unlock() + mset.sendStreamBatchAbandonedAdvisory(batchId, BatchIncomplete) + return respondIncompleteBatch() + } + } + if !commit { + batches.mu.Unlock() + // Send empty ack to let them know we've persisted the data prior to commit. + if canRespond { + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, nil, nil, 0)) + } + return nil + } + + // Ensure the batch is prepared for the commit and will not be cleaned up while committing. + if !b.readyForCommit() { + // Don't do cleanup, this is already done. + batches.mu.Unlock() + mset.sendStreamBatchAbandonedAdvisory(batchId, BatchTimeout) + return respondIncompleteBatch() + } + + // Proceed with proposing the batch. + ts := time.Now().UnixNano() + + // Need to hold this lock even if we're not clustered, because we'll + // be accessing state that requires this lock (even if it's empty). + mset.clMu.Lock() + + // If not clustered, set up the last sequence. + if !isClustered { + mset.clseq, mset.clfs = mset.lseq, 0 + } + + // We only use mset.clseq for clustering and in case we run ahead of actual commits. + // Check if we need to set initial value here + if isClustered && (mset.clseq == 0 || mset.clseq < lseq+mset.clfs) { + // Need to unlock and re-acquire the locks in the proper order. + mset.clMu.Unlock() + // Locking order is stream -> batchMu -> clMu + mset.mu.RLock() + batch := mset.batchApply + var batchCount uint64 + if batch != nil { + batch.mu.Lock() + batchCount = batch.count + } + mset.clMu.Lock() + // Re-capture + lseq = mset.lseq + mset.clseq = lseq + mset.clfs + batchCount + // Keep hold of the mset.clMu, but unlock the others. + if batch != nil { + batch.mu.Unlock() + } + mset.mu.RUnlock() + } + + rollback := func(seq uint64) { + if isClustered { + // TODO(mvv): reset in-memory expected header maps + mset.clseq -= seq - 1 + } + mset.clMu.Unlock() + } + + errorOnUnsupported := func(seq uint64, header string) *ApiError { + apiErr := NewJSAtomicPublishUnsupportedHeaderBatchError(header) + rollback(seq) + b.cleanupLocked(batchId, batches) + batches.mu.Unlock() + if canRespond { + buf, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: apiErr}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, buf, nil, 0)) + } + return apiErr + } + + var ( + entries []*Entry + bsubj string + bhdr []byte + bmsg []byte + apiErr *ApiError + err error + smv StoreMsg + sm *StoreMsg + sz int + ) + + diff := &batchStagedDiff{} + for seq := uint64(1); seq <= batchSeq; seq++ { + if seq == batchSeq && b.store.Type() != FileStorage { + bsubj, bhdr, bmsg = subject, hdr, msg + } else if sm, err = b.store.LoadMsg(seq, &smv); sm != nil && err == nil { + bsubj, bhdr, bmsg = sm.subj, sm.hdr, sm.msg + } else { + rollback(seq) + b.cleanupLocked(batchId, batches) + batches.mu.Unlock() + return respondIncompleteBatch() + } + + // Reject unsupported headers. + if msgId := getMsgId(bhdr); msgId != _EMPTY_ { + return errorOnUnsupported(seq, JSMsgId) + } + if getExpectedLastMsgId(hdr) != _EMPTY_ { + return errorOnUnsupported(seq, JSExpectedLastMsgId) + } + + if bhdr, bmsg, _, apiErr, err = checkMsgHeadersPreClusteredProposal(diff, mset, subject, bhdr, bmsg, false, name, jsa, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules, discard, discardNewPer, maxMsgSize, maxMsgs, maxMsgsPer, maxBytes); err != nil { + rollback(seq) + b.cleanupLocked(batchId, batches) + batches.mu.Unlock() + if canRespond { + buf, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: apiErr}) + outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, buf, nil, 0)) + } + return err + } + + if isClustered { + var _reply string + isCommit := seq == batchSeq + if isCommit { + _reply = reply + } + esm := encodeStreamMsgAllowCompressAndBatch(bsubj, _reply, bhdr, bmsg, mset.clseq, ts, false, batchId, seq, isCommit) + entries = append(entries, newEntry(EntryNormal, esm)) + sz += len(esm) + } + mset.clseq++ + } + + // Commit batch. + if !isClustered { + // Reset, we only used this to do the batching checks. + mset.clseq, mset.clfs = 0, 0 + mset.clMu.Unlock() + + // Ensure the whole batch is fully isolated, and reads + // can only happen after the full batch is committed. + mset.mu.Lock() + for seq := uint64(1); seq <= batchSeq; seq++ { + if seq == batchSeq && b.store.Type() != FileStorage { + bsubj, bhdr, bmsg = subject, hdr, msg + } else if sm, err = b.store.LoadMsg(seq, &smv); sm != nil && err == nil { + bsubj, bhdr, bmsg = sm.subj, sm.hdr, sm.msg + } else { + // Should not happen, we've already checked this message existed while doing consistency checks. + // We'll just exit here, the batch is already inconsistent without the message at this sequence. + // No use in trying to still store the rest. + break + } + var _reply string + if seq == batchSeq { + _reply = reply + } + mset.processJetStreamMsg(bsubj, _reply, bhdr, bmsg, 0, 0, mt, false, false) + } + mset.mu.Unlock() + } else { + // 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. + diff.commit(mset) + if err := node.ProposeMulti(entries); err == nil { + mset.trackReplicationTraffic(node, sz, r) + } else { + // TODO(mvv): reset in-memory expected header maps + mset.clseq -= batchSeq + } + + // Check to see if we are being overrun. + // TODO(dlc) - Make this a limit where we drop messages to protect ourselves, but allow to be configured. + if mset.clseq-(lseq+mset.clfs) > streamLagWarnThreshold { + lerr := fmt.Errorf("JetStream stream '%s > %s' has high message lag", jsa.acc().Name, name) + s.RateLimitWarnf("%s", lerr.Error()) + } + mset.clMu.Unlock() + } + b.cleanupLocked(batchId, batches) + batches.mu.Unlock() + return nil +} + // Used to signal inbound message to registered consumers. type cMsg struct { seq uint64 @@ -5623,11 +6748,17 @@ func (mset *stream) accName() string { // Name returns the stream name. func (mset *stream) name() string { + return mset.nameLocked(true) +} + +func (mset *stream) nameLocked(needLock bool) string { if mset == nil { return _EMPTY_ } - mset.mu.RLock() - defer mset.mu.RUnlock() + if needLock { + mset.mu.RLock() + defer mset.mu.RUnlock() + } return mset.cfg.Name } @@ -5729,10 +6860,12 @@ func (mset *stream) internalLoop() { ims := msgs.pop() for _, im := range ims { // If we are clustered we need to propose this message to the underlying raft group. - if isClustered { + if batchId := getBatchId(im.hdr); batchId != _EMPTY_ { + mset.processJetStreamBatchMsg(batchId, im.subj, im.rply, im.hdr, im.msg, im.mt) + } else if isClustered { mset.processClusteredInboundMsg(im.subj, im.rply, im.hdr, im.msg, im.mt, false) } else { - mset.processJetStreamMsg(im.subj, im.rply, im.hdr, im.msg, 0, 0, im.mt, false) + mset.processJetStreamMsg(im.subj, im.rply, im.hdr, im.msg, 0, 0, im.mt, false, true) } im.returnToPool() } @@ -5798,6 +6931,8 @@ func (mset *stream) stop(deleteFlag, advisory bool) error { return NewJSNotEnabledForAccountError() } + isShuttingDown := js.isShuttingDown() + // Remove from our account map first. jsa.mu.Lock() // Preserve in the account if it's marked offline, to have it remain queryable. @@ -5823,7 +6958,7 @@ func (mset *stream) stop(deleteFlag, advisory bool) error { // Stop responding to sync requests. mset.stopClusterSubs() // Unsubscribe from direct stream. - mset.unsubscribeToStream(true) + mset.unsubscribeToStream(true, isShuttingDown) // Our info sub if we spun it up. if mset.infoSub != nil { @@ -5857,7 +6992,6 @@ func (mset *stream) stop(deleteFlag, advisory bool) error { } mset.mu.Unlock() - isShuttingDown := js.isShuttingDown() for _, o := range obs { if !o.isClosed() { // Third flag says do not broadcast a signal. @@ -5896,6 +7030,7 @@ func (mset *stream) stop(deleteFlag, advisory bool) error { } // Cleanup duplicate timer if running. + mset.ddMu.Lock() if mset.ddtmr != nil { mset.ddtmr.Stop() mset.ddtmr = nil @@ -5903,6 +7038,7 @@ func (mset *stream) stop(deleteFlag, advisory bool) error { mset.ddarr = nil mset.ddindex = 0 } + mset.ddMu.Unlock() sysc := mset.sysc mset.sysc = nil @@ -5947,7 +7083,7 @@ func (mset *stream) stop(deleteFlag, advisory bool) error { accDir := filepath.Join(js.config.StoreDir, accName) if store != nil { // Ignore errors. - store.Delete() + store.Delete(false) } else { streamDir := filepath.Join(accDir, streamsDir) os.RemoveAll(filepath.Join(streamDir, name)) @@ -6729,6 +7865,9 @@ func (mset *stream) clearMonitorRunning() { mset.mu.Lock() defer mset.mu.Unlock() mset.inMonitor = false + + // We've stopped running the monitor goroutine, now also do additional cleanup. + mset.deleteBatchApplyState() } // Check if our monitor is running. @@ -6737,3 +7876,20 @@ func (mset *stream) isMonitorRunning() bool { defer mset.mu.RUnlock() return mset.inMonitor } + +// Adjust accounting for sent messages as part of replication. +func (mset *stream) trackReplicationTraffic(node RaftNode, sz int, r int) { + // If we are using the system account for NRG, add in the extra sent msgs and bytes to our account + // so that the end user / account owner has visibility. + if node.IsSystemAccount() && mset.acc != nil && r > 1 { + outMsgs := int64(r - 1) + outBytes := int64(sz * (r - 1)) + + mset.acc.stats.Lock() + mset.acc.stats.outMsgs += outMsgs + mset.acc.stats.outBytes += outBytes + mset.acc.stats.rt.outMsgs += outMsgs + mset.acc.stats.rt.outBytes += outBytes + mset.acc.stats.Unlock() + } +} diff --git a/vendor/github.com/nats-io/nats-server/v2/server/subject_transform.go b/vendor/github.com/nats-io/nats-server/v2/server/subject_transform.go index 80f08ea4f..e131b176e 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/subject_transform.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/subject_transform.go @@ -16,6 +16,8 @@ package server import ( "fmt" "hash/fnv" + "math" + "math/rand" "regexp" "strconv" "strings" @@ -33,6 +35,7 @@ var ( splitMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit\s*\((.*)\)\s*}}`) leftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[lL]eft\s*\((.*)\)\s*}}`) rightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[rR]ight\s*\((.*)\)\s*}}`) + randomMappingFunctionRegEx = regexp.MustCompile(`{{\s*[rR]andom\s*\((.*)\)\s*}}`) ) // Enum for the subject mapping subjectTransform function types @@ -48,6 +51,7 @@ const ( Split Left Right + Random ) // Transforms for arbitrarily mapping subjects from one to another for maps, tees and filters. @@ -123,17 +127,16 @@ func NewSubjectTransformWithStrict(src, dest string, strict bool) (*subjectTrans } } - if npwcs == 0 { - if tranformType != NoTransform { - return nil, &mappingDestinationErr{token, ErrMappingDestinationIndexOutOfRange} - } - } - if tranformType == NoTransform { dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform) dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1}) dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1) dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_) + } else if tranformType == Random { + dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, Random) + dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{}) + dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, transfomArgInt) + dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_) } else { nphs += len(transformArgWildcardIndexes) // Now build up our runtime mapping from dest to source tokens. @@ -158,12 +161,22 @@ func NewSubjectTransformWithStrict(src, dest string, strict bool) (*subjectTrans } else { // no wildcards used in the source: check that no transform functions are used in the destination for _, token := range dtokens { - tranformType, _, _, _, err := indexPlaceHolders(token) + tranformType, _, transfomArgInt, _, err := indexPlaceHolders(token) if err != nil { return nil, err } - if tranformType != NoTransform { + if tranformType == NoTransform { + dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform) + dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1}) + dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1) + dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_) + } else if tranformType == Random || tranformType == Partition { + dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, tranformType) + dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{}) + dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, transfomArgInt) + dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_) + } else { return nil, &mappingDestinationErr{token, ErrMappingDestinationIndexOutOfRange} } } @@ -255,12 +268,19 @@ func indexPlaceHolders(token string) (int16, []int, int32, string, error) { // partition(number of partitions, token1, token2, ...) args = getMappingFunctionArgs(partitionMappingFunctionRegEx, token) if args != nil { - if len(args) < 2 { + if len(args) < 1 { return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs} } + if len(args) == 1 { + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil || mappingFunctionIntArg > math.MaxInt32 { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg} + } + return Partition, []int{}, int32(mappingFunctionIntArg), _EMPTY_, nil + } if len(args) >= 2 { mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " ")) - if err != nil { + if err != nil || mappingFunctionIntArg > math.MaxInt32 { return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg} } var numPositions = len(args[1:]) @@ -333,6 +353,19 @@ func indexPlaceHolders(token string) (int16, []int, int32, string, error) { return Split, []int{i}, -1, args[1], nil } + // Random(max) + args = getMappingFunctionArgs(randomMappingFunctionRegEx, token) + if args != nil { + if len(args) != 1 { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs} + } + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil || mappingFunctionIntArg > math.MaxInt32 { + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg} + } + return Random, []int{}, int32(mappingFunctionIntArg), _EMPTY_, nil + } + return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction} } } @@ -423,7 +456,21 @@ func (tr *subjectTransform) TransformSubject(subject string) string { return tr.TransformTokenizedSubject(tokenizeSubject(subject)) } +func (tr *subjectTransform) getRandomPartition(ceiling int) string { + // Avoid an integer divide by zero panic below. + if ceiling == 0 { + return "0" + } + + return strconv.Itoa(int(rand.Int31()) % ceiling) +} + func (tr *subjectTransform) getHashPartition(key []byte, numBuckets int) string { + // Avoid an integer divide by zero panic below. + if numBuckets == 0 { + return "0" + } + h := fnv.New32a() _, _ = h.Write(key) @@ -454,8 +501,14 @@ func (tr *subjectTransform) TransformTokenizedSubject(tokens []string) string { _buffer [64]byte keyForHashing = _buffer[:0] ) - for _, sourceToken := range tr.dtokmftokindexesargs[i] { - keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...) + if len(tr.dtokmftokindexesargs[i]) > 0 { + // When token positions are specified. + for _, sourceToken := range tr.dtokmftokindexesargs[i] { + keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...) + } + } else { + // When using the shorthand partition(n). + keyForHashing = append(keyForHashing, strings.Join(tokens, ".")...) } b.WriteString(tr.getHashPartition(keyForHashing, int(tr.dtokmfintargs[i]))) case Wildcard: // simple substitution @@ -558,6 +611,8 @@ func (tr *subjectTransform) TransformTokenizedSubject(tokens []string) string { } else { // too small to slice at the requested size: don't slice b.WriteString(sourceToken) } + case Random: + b.WriteString(tr.getRandomPartition(int(tr.dtokmfintargs[i]))) } } diff --git a/vendor/github.com/nats-io/nats-server/v2/server/sublist.go b/vendor/github.com/nats-io/nats-server/v2/server/sublist.go index 078445c98..2bed802c1 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/sublist.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/sublist.go @@ -1292,7 +1292,8 @@ func ValidateMapping(src string, dest string) error { !splitFromRightMappingFunctionRegEx.MatchString(t) && !sliceFromLeftMappingFunctionRegEx.MatchString(t) && !sliceFromRightMappingFunctionRegEx.MatchString(t) && - !splitMappingFunctionRegEx.MatchString(t) { + !splitMappingFunctionRegEx.MatchString(t) && + !randomMappingFunctionRegEx.MatchString(t) { return &mappingDestinationErr{t, ErrUnknownMappingDestinationFunction} } else { continue diff --git a/vendor/github.com/nats-io/nats-server/v2/server/thw/thw.go b/vendor/github.com/nats-io/nats-server/v2/server/thw/thw.go index a74265ef0..712783312 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/thw/thw.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/thw/thw.go @@ -211,7 +211,7 @@ func (hw *HashWheel) Count() uint64 { return hw.count } -// AppendEncode writes out the contents of the THW into a binary snapshot +// Encode writes out the contents of the THW into a binary snapshot // and returns it. The high seq number is included in the snapshot and will // be returned on decode. func (hw *HashWheel) Encode(highSeq uint64) []byte { diff --git a/vendor/github.com/nats-io/nats-server/v2/server/util.go b/vendor/github.com/nats-io/nats-server/v2/server/util.go index f9fd695c3..dcfa1000d 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/util.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/util.go @@ -312,7 +312,7 @@ func getURLsAsString(urls []*url.URL) []string { return a } -// copyBytes make a new slice of the same size than `src` and copy its content. +// copyBytes make a new slice of the same size as `src` and copy its content. // If `src` is nil or its length is 0, then this returns `nil` func copyBytes(src []byte) []byte { if len(src) == 0 { diff --git a/vendor/github.com/nats-io/nats-server/v2/server/websocket.go b/vendor/github.com/nats-io/nats-server/v2/server/websocket.go index 67239c375..cc83b465b 100644 --- a/vendor/github.com/nats-io/nats-server/v2/server/websocket.go +++ b/vendor/github.com/nats-io/nats-server/v2/server/websocket.go @@ -1147,21 +1147,24 @@ func (s *Server) startWebsocketServer() { // regardless of NoTLS. If we don't have a TLS config, it means that the // user has configured NoTLS because otherwise the server would have failed // to start due to options validation. + var config *tls.Config if o.TLSConfig != nil { proto = wsSchemePrefixTLS - config := o.TLSConfig.Clone() + config = o.TLSConfig.Clone() config.GetConfigForClient = s.wsGetTLSConfig - hl, err = tls.Listen("tcp", hp, config) } else { proto = wsSchemePrefix - hl, err = net.Listen("tcp", hp) } + hl, err = natsListen("tcp", hp) s.websocket.listenerErr = err if err != nil { s.mu.Unlock() s.Fatalf("Unable to listen for websocket connections: %v", err) return } + if config != nil { + hl = tls.NewListener(hl, config) + } if port == 0 { o.Port = hl.Addr().(*net.TCPAddr).Port } diff --git a/vendor/modules.txt b/vendor/modules.txt index c2a58bdd6..ac8f9e51f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1105,10 +1105,10 @@ github.com/mschoch/smat # github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 ## explicit github.com/munnerz/goautoneg -# github.com/nats-io/jwt/v2 v2.7.4 +# github.com/nats-io/jwt/v2 v2.8.0 ## explicit; go 1.23.0 github.com/nats-io/jwt/v2 -# github.com/nats-io/nats-server/v2 v2.11.9 +# github.com/nats-io/nats-server/v2 v2.12.0 ## explicit; go 1.24.0 github.com/nats-io/nats-server/v2/conf github.com/nats-io/nats-server/v2/internal/fastrand @@ -1119,6 +1119,7 @@ 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/certidp github.com/nats-io/nats-server/v2/server/certstore +github.com/nats-io/nats-server/v2/server/elastic github.com/nats-io/nats-server/v2/server/gsl github.com/nats-io/nats-server/v2/server/pse github.com/nats-io/nats-server/v2/server/stree