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] <support@github.com>
This commit is contained in:
dependabot[bot]
2025-09-29 11:04:16 +02:00
committed by Ralf Haferkamp
parent 4a0cc1004f
commit 703b8dd084
41 changed files with 6938 additions and 1700 deletions
+2 -2
View File
@@ -56,7 +56,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/mna/pigeon v1.3.0 github.com/mna/pigeon v1.3.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 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/nats-io/nats.go v1.46.0
github.com/oklog/run v1.2.0 github.com/oklog/run v1.2.0
github.com/olekukonko/tablewriter v1.0.9 github.com/olekukonko/tablewriter v1.0.9
@@ -296,7 +296,7 @@ require (
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect github.com/nxadm/tail v1.4.8 // indirect
+4 -4
View File
@@ -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-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/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/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.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.11.9 h1:k7nzHZjUf51W1b08xiQih63Rdxh0yr5O4K892Mx5gQA= github.com/nats-io/nats-server/v2 v2.12.0 h1:OIwe8jZUqJFrh+hhiyKu8snNib66qsx806OslqJuo74=
github.com/nats-io/nats-server/v2 v2.11.9/go.mod h1:1MQgsAQX1tVjpf3Yzrk3x2pzdsZiNL/TVP3Amhp3CR8= 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 h1:iUcX+MLT0HHXskGkz+Sg20sXrPtJLsOojMDTDzOHSb8=
github.com/nats-io/nats.go v1.46.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.46.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
+1
View File
@@ -36,6 +36,7 @@ type UserPermissionLimits struct {
Permissions Permissions
Limits Limits
BearerToken bool `json:"bearer_token,omitempty"` BearerToken bool `json:"bearer_token,omitempty"`
ProxyRequired bool `json:"proxy_required,omitempty"`
AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"` AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"`
} }
+171 -13
View File
@@ -25,6 +25,7 @@ import (
"net" "net"
"net/url" "net/url"
"regexp" "regexp"
"slices"
"strings" "strings"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -65,6 +66,7 @@ type NkeyUser struct {
Account *Account `json:"account,omitempty"` Account *Account `json:"account,omitempty"`
SigningKey string `json:"signing_key,omitempty"` SigningKey string `json:"signing_key,omitempty"`
AllowedConnectionTypes map[string]struct{} `json:"connection_types,omitempty"` AllowedConnectionTypes map[string]struct{} `json:"connection_types,omitempty"`
ProxyRequired bool `json:"proxy_required,omitempty"`
} }
// User is for multiple accounts/users. // User is for multiple accounts/users.
@@ -75,6 +77,7 @@ type User struct {
Account *Account `json:"account,omitempty"` Account *Account `json:"account,omitempty"`
ConnectionDeadline time.Time `json:"connection_deadline,omitempty"` ConnectionDeadline time.Time `json:"connection_deadline,omitempty"`
AllowedConnectionTypes map[string]struct{} `json:"connection_types,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 // 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 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. // Check if we have auth callouts enabled at the server level or in the bound account.
defer func() { defer func() {
// Default reason authErr := c.getAuthError()
reason := AuthenticationViolation.String() if authErr == nil {
authErr = ErrAuthentication
}
reason := getAuthErrClosedState(authErr).String()
// No-op // No-op
if juc == nil && opts.AuthCallout == nil { if juc == nil && opts.AuthCallout == nil {
if !authorized { 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 // 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. // so we will callout to our auth backend for processing.
if !skip { 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. // 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 { if authorized {
@@ -697,6 +720,11 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
if !authRequired { if !authRequired {
// TODO(dlc) - If they send us credentials should we fail? // TODO(dlc) - If they send us credentials should we fail?
s.mu.Unlock() 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 return true
} }
var ( var (
@@ -769,6 +797,10 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
c.Debugf("User JWT not valid: %v", err) c.Debugf("User JWT not valid: %v", err)
return false return false
} }
if proxyRequired = juc.ProxyRequired; proxyRequired && !trustedProxy {
s.mu.Unlock()
return setProxyAuthError(ErrAuthProxyRequired)
}
vr := jwt.CreateValidationResults() vr := jwt.CreateValidationResults()
juc.Validate(vr) juc.Validate(vr)
if vr.IsBlocking(true) { if vr.IsBlocking(true) {
@@ -1045,6 +1077,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
} }
if nkey != nil { if nkey != nil {
if proxyRequired = nkey.ProxyRequired; proxyRequired && !trustedProxy {
return setProxyAuthError(ErrAuthProxyRequired)
}
// If we did not match noAuthUser check signature which is required. // If we did not match noAuthUser check signature which is required.
if nkey.Nkey != noAuthUser { if nkey.Nkey != noAuthUser {
if c.opts.Sig == _EMPTY_ { if c.opts.Sig == _EMPTY_ {
@@ -1076,6 +1111,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
return true return true
} }
if user != nil { if user != nil {
if proxyRequired = user.ProxyRequired; proxyRequired && !trustedProxy {
return setProxyAuthError(ErrAuthProxyRequired)
}
ok = comparePasswords(user.Password, c.opts.Password) ok = comparePasswords(user.Password, c.opts.Password)
// If we are authorized, register the user which will properly setup any permissions // If we are authorized, register the user which will properly setup any permissions
// for pub/sub authorizations. // for pub/sub authorizations.
@@ -1086,6 +1124,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
} }
if c.kind == CLIENT { if c.kind == CLIENT {
if proxyRequired = opts.ProxyRequired; proxyRequired && !trustedProxy {
return setProxyAuthError(ErrAuthProxyRequired)
}
if token != _EMPTY_ { if token != _EMPTY_ {
return comparePasswords(token, c.opts.Token) return comparePasswords(token, c.opts.Token)
} else if username != _EMPTY_ { } else if username != _EMPTY_ {
@@ -1094,16 +1135,65 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
} }
return comparePasswords(password, c.opts.Password) 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 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 { func getTLSAuthDCs(rdns *pkix.RDNSequence) string {
dcOID := asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25} dcOID := asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}
dcs := []string{} dcs := []string{}
@@ -1352,7 +1442,25 @@ func (s *Server) registerLeafWithAccount(c *client, account string) bool {
func (s *Server) isLeafNodeAuthorized(c *client) bool { func (s *Server) isLeafNodeAuthorized(c *client) bool {
opts := s.getOpts() 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 { if username != c.opts.Username {
return false 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 // The user in CONNECT must match. We will bind to the account associated
// with that user (from the leafnode's authorization{} config). // with that user (from the leafnode's authorization{} config).
if opts.LeafNode.Username != _EMPTY_ { 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_ { } 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 { if c.opts.Nkey != opts.LeafNode.Nkey {
return false return false
} }
@@ -1421,7 +1537,7 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool {
} }
// This will authorize since are using an existing user, // This will authorize since are using an existing user,
// but it will also register with proper account. // 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. // This is expected to be a very small array.
@@ -1431,7 +1547,7 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool {
if u.Account != nil { if u.Account != nil {
accName = u.Account.Name accName = u.Account.Name
} }
return isAuthorized(u.Username, u.Password, accName) return isAuthorized(u.Username, u.Password, accName, u.ProxyRequired)
} }
} }
return false 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`, `no_auth_user: "%s" not present as user or nkey in authorization block or account configuration`,
noAuthUser) 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
}
}
+12 -2
View File
@@ -33,7 +33,7 @@ const (
) )
// Process a callout on this client's behalf. // 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 isOperatorMode := len(opts.TrustedKeys) > 0
// this is the account the user connected in, or the one running the callout // 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()) respCh <- titleCase(err.Error())
return 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() vr := jwt.CreateValidationResults()
arc.Validate(vr) arc.Validate(vr)
if len(vr.Issues) > 0 { if len(vr.Issues) > 0 {
@@ -369,7 +379,7 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
conn := c.nc.(*tls.Conn) conn := c.nc.(*tls.Conn)
cs := conn.ConnectionState() cs := conn.ConnectionState()
ct.Version = tlsVersion(cs.Version) ct.Version = tlsVersion(cs.Version)
ct.Cipher = tlsCipher(cs.CipherSuite) ct.Cipher = tls.CipherSuiteName(cs.CipherSuite)
// Check verified chains. // Check verified chains.
for _, vs := range cs.VerifiedChains { for _, vs := range cs.VerifiedChains {
var certs []string var certs []string
+23 -64
View File
@@ -17,85 +17,44 @@ import (
"crypto/tls" "crypto/tls"
) )
// Where we maintain all of the available ciphers func init() {
var cipherMap = map[string]uint16{ for _, cs := range tls.CipherSuites() {
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, cipherMap[cs.Name] = cs
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, cipherMapByID[cs.ID] = cs
"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, for _, cs := range tls.InsecureCipherSuites() {
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, cipherMap[cs.Name] = cs
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, cipherMapByID[cs.ID] = cs
"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,
} }
var cipherMapByID = map[uint16]string{ var cipherMap = map[string]*tls.CipherSuite{}
tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA", var cipherMapByID = map[uint16]*tls.CipherSuite{}
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",
}
func defaultCipherSuites() []uint16 { func defaultCipherSuites() []uint16 {
return []uint16{ ciphers := tls.CipherSuites()
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, defaults := make([]uint16, 0, len(ciphers))
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, for _, cs := range ciphers {
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, defaults = append(defaults, cs.ID)
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,
} }
return defaults
} }
// Where we maintain available curve preferences // Where we maintain available curve preferences
var curvePreferenceMap = map[string]tls.CurveID{ var curvePreferenceMap = map[string]tls.CurveID{
"X25519": tls.X25519, "X25519MLKEM768": tls.X25519MLKEM768,
"CurveP256": tls.CurveP256, "X25519": tls.X25519,
"CurveP384": tls.CurveP384, "CurveP256": tls.CurveP256,
"CurveP521": tls.CurveP521, "CurveP384": tls.CurveP384,
"CurveP521": tls.CurveP521,
} }
// reorder to default to the highest level of security. See: // reorder to default to the highest level of security. See:
// https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go // https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
func defaultCurvePreferences() []tls.CurveID { func defaultCurvePreferences() []tls.CurveID {
return []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.CurveP256,
tls.CurveP384, tls.CurveP384,
tls.CurveP521, tls.CurveP521,
+322 -130
View File
@@ -35,6 +35,8 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"slices"
"github.com/klauspost/compress/s2" "github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2" "github.com/nats-io/jwt/v2"
"github.com/nats-io/nats-server/v2/internal/fastrand" "github.com/nats-io/nats-server/v2/internal/fastrand"
@@ -222,6 +224,8 @@ const (
MinimumVersionRequired MinimumVersionRequired
ClusterNamesIdentical ClusterNamesIdentical
Kicked Kicked
ProxyNotTrusted
ProxyRequired
) )
// Some flags passed to processMsgResults // Some flags passed to processMsgResults
@@ -255,6 +259,8 @@ type client struct {
pubKey string pubKey string
nc net.Conn nc net.Conn
ncs atomic.Value ncs atomic.Value
ncsAcc atomic.Value
ncsUser atomic.Value
out outbound out outbound
user *NkeyUser user *NkeyUser
host string host string
@@ -270,6 +276,7 @@ type client struct {
msgb [msgScratchSize]byte msgb [msgScratchSize]byte
last time.Time last time.Time
lastIn time.Time lastIn time.Time
proxyKey string
repliesSincePrune uint16 repliesSincePrune uint16
lastReplyPrune time.Time lastReplyPrune time.Time
@@ -298,6 +305,13 @@ type client struct {
nameTag string nameTag string
tlsTo *time.Timer 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 { type rrTracking struct {
@@ -494,15 +508,13 @@ func (rcf readCacheFlag) isSet(c readCacheFlag) bool {
} }
const ( const (
defaultMaxPerAccountCacheSize = 8192 defaultMaxPerAccountCacheSize = 8192
defaultPrunePerAccountCacheSize = 1024 defaultClosedSubsCheckInterval = 5 * time.Minute
defaultClosedSubsCheckInterval = 5 * time.Minute
) )
var ( var (
maxPerAccountCacheSize = defaultMaxPerAccountCacheSize maxPerAccountCacheSize = defaultMaxPerAccountCacheSize
prunePerAccountCacheSize = defaultPrunePerAccountCacheSize closedSubsCheckInterval = defaultClosedSubsCheckInterval
closedSubsCheckInterval = defaultClosedSubsCheckInterval
) )
// perAccountCache is for L1 semantics for inbound messages from a route or gateway to mimic the performance of clients. // 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 // Leafnodes
RemoteAccount string `json:"remote_account,omitempty"` 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} 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: case ReadError, WriteError, SlowConsumerPendingBytes, SlowConsumerWriteDeadline, TLSHandshakeError:
c.flags.set(skipFlushOnClose) c.flags.set(skipFlushOnClose)
skipFlush = true 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) { if c.flags.isSet(connMarkedClosed) {
return return
@@ -1867,14 +1895,25 @@ func (c *client) markConnAsClosed(reason ClosedState) {
// Be consistent with the creation: for routes, gateways and leaf, // Be consistent with the creation: for routes, gateways and leaf,
// we use Noticef on create, so use that too for delete. // we use Noticef on create, so use that too for delete.
if c.srv != nil { if c.srv != nil {
if c.kind == LEAF { if c.kind == LEAF || c.kind == ROUTER || c.kind == GATEWAY {
if c.acc != nil { var tags []string
c.Noticef("%s connection closed: %s - Account: %s", c.kindString(), reason, c.acc.traceLabel()) 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 { } else {
c.Noticef("%s connection closed: %s", c.kindString(), reason) 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. } else { // Client, System, Jetstream, and Account connections.
c.Debugf("%s connection closed: %s", c.kindString(), reason) c.Debugf("%s connection closed: %s", c.kindString(), reason)
} }
@@ -1917,7 +1956,7 @@ func (c *client) flushSignal() {
// Traces a message. // Traces a message.
// Will NOT check if tracing is enabled, does NOT need the client lock. // 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() opts := c.srv.getOpts()
maxTrace := opts.MaxTracedMsgLen maxTrace := opts.MaxTracedMsgLen
headersOnly := opts.TraceHeaders 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 TraceHeaders is enabled, extract only the header portion of the msg.
// If a header is present, it ends with an additional trailing CRLF. // If a header is present, it ends with an additional trailing CRLF.
if headersOnly { if headersOnly {
msg, _ = c.msgParts(msg) if hdrSize > 0 && len(msg) >= hdrSize {
suffix += LEN_CR_LF 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. // Do not emit a log line for zero-length payloads.
@@ -1936,14 +1980,32 @@ func (c *client) traceMsg(msg []byte) {
return return
} }
const (
traceInPrefix string = "<<-"
traceOutPrefix string = "->>"
)
var prefix string
if delivered {
prefix = traceOutPrefix
} else {
prefix = traceInPrefix
}
if maxTrace > 0 && l > maxTrace { if maxTrace > 0 && l > maxTrace {
tm := fmt.Sprintf("%q", msg[: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 { } 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. // Traces an incoming operation.
// Will NOT check if tracing is enabled, does NOT need the client lock. // Will NOT check if tracing is enabled, does NOT need the client lock.
func (c *client) traceInOp(op string, arg []byte) { 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 // Indicate that the CONNECT protocol has been received, and that the
// server now knows which protocol this client supports. // server now knows which protocol this client supports.
firstConnect := !c.flags.isSet(connectReceived)
c.flags.set(connectReceived) c.flags.set(connectReceived)
// Capture these under lock // Capture these under lock
c.echo = c.opts.Echo c.echo = c.opts.Echo
@@ -2103,30 +2166,6 @@ func (c *client) processConnect(arg []byte) error {
account := c.opts.Account account := c.opts.Account
accountNew := c.opts.AccountNew 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 websocket client, maybe some options through cookies
if ws := c.ws; ws != nil { if ws := c.ws; ws != nil {
// if JWT not in the CONNECT, use the cookie JWT (possibly empty). // 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. // By default register with the global account.
c.registerWithAccount(srv.globalAccount()) 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 { switch kind {
@@ -2238,12 +2326,12 @@ func (c *client) processConnect(arg []byte) error {
func (c *client) sendErrAndErr(err string) { func (c *client) sendErrAndErr(err string) {
c.sendErr(err) c.sendErr(err)
c.Errorf(err) c.RateLimitErrorf(err)
} }
func (c *client) sendErrAndDebug(err string) { func (c *client) sendErrAndDebug(err string) {
c.sendErr(err) c.sendErr(err)
c.Debugf(err) c.RateLimitDebugf(err)
} }
func (c *client) authTimeout() { func (c *client) authTimeout() {
@@ -2262,38 +2350,28 @@ func (c *client) accountAuthExpired() {
} }
func (c *client) authViolation() { func (c *client) authViolation() {
var s *Server authErr := c.getAuthError()
var hasTrustedNkeys, hasNkeys, hasUsers bool if authErr == nil {
if s = c.srv; s != nil { authErr = ErrAuthentication
s.mu.RLock()
hasTrustedNkeys = s.trustedKeys != nil
hasNkeys = s.nkeys != nil
hasUsers = s.users != nil
s.mu.RUnlock()
defer s.sendAuthErrorEvent(c)
} }
reason := getAuthErrClosedState(authErr)
if hasTrustedNkeys { var s *Server
c.Errorf("%v", ErrAuthentication) if s = c.srv; s != nil {
} else if hasNkeys { defer s.sendAuthErrorEvent(c, reason.String())
c.Errorf("%s - Nkey %q", if c.getRawAuthUser() != _EMPTY_ {
ErrAuthentication.Error(), c.Errorf("%v - %s", ErrAuthentication, c.getAuthUser())
c.opts.Nkey) } else {
} else if hasUsers {
c.Errorf("%s - User %q",
ErrAuthentication.Error(),
c.opts.Username)
} else {
if c.srv != nil {
c.Errorf(ErrAuthentication.Error()) c.Errorf(ErrAuthentication.Error())
} }
} }
if c.isMqtt() { if c.isMqtt() {
c.mqttEnqueueConnAck(mqttConnAckRCNotAuthorized, false) c.mqttEnqueueConnAck(mqttConnAckRCNotAuthorized, false)
} else { } else {
// Send this to client, regardless of the authErr override.
c.sendErr("Authorization Violation") c.sendErr("Authorization Violation")
} }
c.closeConnection(AuthenticationViolation) c.closeConnection(reason)
} }
func (c *client) maxAccountConnExceeded() { 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 we are here, the CONNECT has been received so we know
// if this client supports async INFO or not. // if this client supports async INFO or not.
var ( var (
checkInfoChange bool sendConnectInfo bool
srv = c.srv srv = c.srv
) )
// For older clients, just flip the firstPongSent flag if not already // For the first PING (so firstPongSet is false) and for clients
// set and we are done. // that support async INFO protocols, we will send one with ConnectInfo=true,
if c.opts.Protocol < ClientProtoInfo || srv == nil { // the name of the account the client is bound to, and if the
c.flags.setIfNotSet(firstPongSent) // account is the system account.
} else { if !c.flags.isSet(firstPongSent) {
// This is a client that supports async INFO protocols. // Flip the flag.
// If this is the first PING (so firstPongSent is not set yet), c.flags.set(firstPongSent)
// we will need to check if there was a change in cluster topology // Evaluate if we should send the INFO protocol.
// or we have a different max payload. We will send this first before sendConnectInfo = srv != nil && c.opts.Protocol >= ClientProtoInfo
// pong since most clients do flush after connect call.
checkInfoChange = !c.flags.isSet(firstPongSent)
} }
c.mu.Unlock() c.mu.Unlock()
if checkInfoChange { if sendConnectInfo {
opts := srv.getOpts()
srv.mu.Lock() srv.mu.Lock()
info := srv.copyInfo()
c.mu.Lock() c.mu.Lock()
// Now that we are under both locks, we can flip the flag. info.RemoteAccount = c.acc.Name
// This prevents sendAsyncInfoToClients() and code here to info.IsSystemAccount = c.acc == srv.SystemAccount()
// send a double INFO protocol. info.ConnectInfo = true
c.flags.set(firstPongSent) c.enqueueProto(c.generateClientInfoJSON(info))
// 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()))
}
c.mu.Unlock() c.mu.Unlock()
srv.mu.Unlock() srv.mu.Unlock()
} }
@@ -3469,6 +3540,12 @@ func (c *client) stalledWait(producer *client) {
c.mu.Unlock() c.mu.Unlock()
defer c.mu.Lock() 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. // Now check if we are close to total allowed.
if producer.in.tst+ttl > stallTotalAllowed { if producer.in.tst+ttl > stallTotalAllowed {
ttl = stallTotalAllowed - producer.in.tst 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. // support we need to strip the headers from the payload.
// The actual header would have been processed correctly for us, so just // The actual header would have been processed correctly for us, so just
// need to update payload. // need to update payload.
hdrSize := c.pa.hdr
if c.pa.hdr > 0 && !sub.client.headers { if c.pa.hdr > 0 && !sub.client.headers {
msg = msg[c.pa.hdr:] msg = msg[c.pa.hdr:]
} }
@@ -3762,6 +3840,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su
if client.trace { if client.trace {
client.traceOutOp(bytesToString(mh[:len(mh)-LEN_CR_LF]), nil) client.traceOutOp(bytesToString(mh[:len(mh)-LEN_CR_LF]), nil)
client.traceMsgDelivery(msg, hdrSize)
} }
client.mu.Unlock() client.mu.Unlock()
@@ -4197,7 +4276,8 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
c.mu.Lock() c.mu.Lock()
if c.opts.NoResponders { if c.opts.NoResponders {
if sub := c.subForReply(c.pa.reply); sub != nil { 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.queueOutbound([]byte(proto))
c.addToPCD(c) c.addToPCD(c)
} }
@@ -4257,7 +4337,7 @@ func (c *client) setupResponseServiceImport(acc *Account, si *serviceImport, tra
// Will remove a header if present. // Will remove a header if present.
func removeHeaderIfPresent(hdr []byte, key string) []byte { 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' // key can't be first and we want to check that it is preceded by a '\n'
if start < 1 || hdr[start-1] != '\n' { if start < 1 || hdr[start-1] != '\n' {
return hdr return hdr
@@ -4406,6 +4486,27 @@ func sliceHeader(key string, hdr []byte) []byte {
return hdr[start:index:index] 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. // For bytes.HasPrefix below.
var ( var (
jsRequestNextPreB = []byte(jsRequestNextPre) jsRequestNextPreB = []byte(jsRequestNextPre)
@@ -5272,18 +5373,16 @@ func (c *client) pubPermissionViolation(subject []byte) {
mt.setIngressError(errTxt) mt.setIngressError(errTxt)
} }
c.sendErr(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) { func (c *client) subPermissionViolation(sub *subscription) {
errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q", sub.subject) errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q", sub.subject)
logTxt := fmt.Sprintf("Subscription Violation - %s, Subject %q, SID %s", logTxt := fmt.Sprintf("Subscription Violation - Subject %q, SID %s", sub.subject, sub.sid)
c.getAuthUser(), sub.subject, sub.sid)
if sub.queue != nil { if sub.queue != nil {
errTxt = fmt.Sprintf("Permissions Violation for Subscription to %q using queue %q", sub.subject, sub.queue) 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", logTxt = fmt.Sprintf("Subscription Violation - Subject %q, Queue: %q, SID %s", sub.subject, sub.queue, sub.sid)
c.getAuthUser(), sub.subject, sub.queue, sub.sid)
} }
c.sendErr(errTxt) c.sendErr(errTxt)
@@ -5296,13 +5395,12 @@ func (c *client) replySubjectViolation(reply []byte) {
mt.setIngressError(errTxt) mt.setIngressError(errTxt)
} }
c.sendErr(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) { func (c *client) maxTokensViolation(sub *subscription) {
errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q, too many tokens", sub.subject) 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", logTxt := fmt.Sprintf("Subscription Violation Too Many Tokens - Subject %q, SID %s", sub.subject, sub.sid)
c.getAuthUser(), sub.subject, sub.sid)
c.sendErr(errTxt) c.sendErr(errTxt)
c.Errorf(logTxt) 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. // Unsubscribe all that need to be removed and report back to client and logs.
for _, sub := range removed { for _, sub := range removed {
c.unsubscribe(acc, sub, true, true) c.unsubscribe(acc, sub, true, true)
c.sendErr(fmt.Sprintf("Permissions Violation for Subscription to %q (sid %q)", c.sendErr(fmt.Sprintf("Permissions Violation for Subscription to %q (sid %q)", sub.subject, sub.sid))
sub.subject, sub.sid)) srv.Noticef("Removed sub %q (sid %q) for %s - not authorized", sub.subject, sub.sid, c.getAuthUser())
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 { if genid := atomic.LoadUint64(&sl.genid); genid != pac.genid {
ok = false ok = false
c.in.pacache = make(map[string]*perAccountCache) clear(c.in.pacache)
} else { } else {
acc = pac.acc acc = pac.acc
r = pac.results r = pac.results
@@ -5937,13 +6033,33 @@ func (c *client) getAccAndResultFromCache() (*Account, *SublistResult) {
// Match against the account sublist. // Match against the account sublist.
r = sl.MatchBytes(c.pa.subject) 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 { 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. // 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 return acc, r
} }
@@ -5959,17 +6075,6 @@ func (c *client) Account() *Account {
return acc 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 // pruneClosedSubFromPerAccountCache remove entries that contain subscriptions
// that have been closed. // that have been closed.
func (c *client) pruneClosedSubFromPerAccountCache() { 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 // 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. // 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 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. // Logging functionality scoped to a client or route.
func (c *client) Error(err error) { 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) { func (c *client) Errorf(format string, v ...any) {
format = fmt.Sprintf("%s - %s", c, format) c.srv.Errorf(c.format(format), v...)
c.srv.Errorf(format, v...)
} }
func (c *client) Debugf(format string, v ...any) { func (c *client) Debugf(format string, v ...any) {
format = fmt.Sprintf("%s - %s", c, format) c.srv.Debugf(c.format(format), v...)
c.srv.Debugf(format, v...)
} }
func (c *client) Noticef(format string, v ...any) { func (c *client) Noticef(format string, v ...any) {
format = fmt.Sprintf("%s - %s", c, format) c.srv.Noticef(c.format(format), v...)
c.srv.Noticef(format, v...)
} }
func (c *client) Tracef(format string, v ...any) { func (c *client) Tracef(format string, v ...any) {
format = fmt.Sprintf("%s - %s", c, format) c.srv.Tracef(c.format(format), v...)
c.srv.Tracef(format, v...)
} }
func (c *client) Warnf(format string, v ...any) { func (c *client) Warnf(format string, v ...any) {
format = fmt.Sprintf("%s - %s", c, format) c.srv.Warnf(c.format(format), v...)
c.srv.Warnf(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) { 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 { if _, loaded := c.srv.rateLimitLogging.LoadOrStore(format, time.Now()); loaded {
return return
} }
statement := fmt.Sprintf(format, v...) 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) { func (c *client) RateLimitWarnf(format string, v ...any) {
// Do the check before adding the client info to the format... // 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 { if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
return 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. // 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) 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
}
+5 -2
View File
@@ -66,7 +66,7 @@ func init() {
const ( const (
// VERSION is the current version for the server. // VERSION is the current version for the server.
VERSION = "2.11.9" VERSION = "2.12.0"
// PROTO is the currently supported protocol. // PROTO is the currently supported protocol.
// 0 was the original // 0 was the original
@@ -146,7 +146,10 @@ const (
// DEFAULT_ROUTE_CONNECT Route solicitation intervals. // DEFAULT_ROUTE_CONNECT Route solicitation intervals.
DEFAULT_ROUTE_CONNECT = 1 * time.Second 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_RECONNECT = 1 * time.Second
// DEFAULT_ROUTE_DIAL Route dial timeout. // DEFAULT_ROUTE_DIAL Route dial timeout.
+249 -37
View File
@@ -19,6 +19,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
@@ -227,18 +228,22 @@ const (
PriorityOverflow PriorityOverflow
// Single client takes over handling of the messages, while others are on standby. // Single client takes over handling of the messages, while others are on standby.
PriorityPinnedClient PriorityPinnedClient
// Clients with lowest priority will be selected first.
PriorityPrioritized
) )
const ( const (
PriorityNoneJSONString = `"none"` PriorityNoneJSONString = `"none"`
PriorityOverflowJSONString = `"overflow"` PriorityOverflowJSONString = `"overflow"`
PriorityPinnedClientJSONString = `"pinned_client"` PriorityPinnedClientJSONString = `"pinned_client"`
PriorityPrioritizedJSONString = `"prioritized"`
) )
var ( var (
PriorityNoneJSONBytes = []byte(PriorityNoneJSONString) PriorityNoneJSONBytes = []byte(PriorityNoneJSONString)
PriorityOverflowJSONBytes = []byte(PriorityOverflowJSONString) PriorityOverflowJSONBytes = []byte(PriorityOverflowJSONString)
PriorityPinnedClientJSONBytes = []byte(PriorityPinnedClientJSONString) PriorityPinnedClientJSONBytes = []byte(PriorityPinnedClientJSONString)
PriorityPrioritizedJSONBytes = []byte(PriorityPrioritizedJSONString)
) )
func (pp PriorityPolicy) String() string { func (pp PriorityPolicy) String() string {
@@ -247,6 +252,8 @@ func (pp PriorityPolicy) String() string {
return PriorityOverflowJSONString return PriorityOverflowJSONString
case PriorityPinnedClient: case PriorityPinnedClient:
return PriorityPinnedClientJSONString return PriorityPinnedClientJSONString
case PriorityPrioritized:
return PriorityPrioritizedJSONString
default: default:
return PriorityNoneJSONString return PriorityNoneJSONString
} }
@@ -258,6 +265,8 @@ func (pp PriorityPolicy) MarshalJSON() ([]byte, error) {
return PriorityOverflowJSONBytes, nil return PriorityOverflowJSONBytes, nil
case PriorityPinnedClient: case PriorityPinnedClient:
return PriorityPinnedClientJSONBytes, nil return PriorityPinnedClientJSONBytes, nil
case PriorityPrioritized:
return PriorityPrioritizedJSONBytes, nil
case PriorityNone: case PriorityNone:
return PriorityNoneJSONBytes, nil return PriorityNoneJSONBytes, nil
default: default:
@@ -271,6 +280,8 @@ func (pp *PriorityPolicy) UnmarshalJSON(data []byte) error {
*pp = PriorityOverflow *pp = PriorityOverflow
case PriorityPinnedClientJSONString: case PriorityPinnedClientJSONString:
*pp = PriorityPinnedClient *pp = PriorityPinnedClient
case PriorityPrioritizedJSONString:
*pp = PriorityPrioritized
case PriorityNoneJSONString: case PriorityNoneJSONString:
*pp = PriorityNone *pp = PriorityNone
default: default:
@@ -554,6 +565,63 @@ const (
// Helper function to set consumer config defaults from above. // Helper function to set consumer config defaults from above.
func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig, lim *JSLimitOpts, accLim *JetStreamAccountLimits, pedantic bool) *ApiError { 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. // Set to default if not specified.
if config.DeliverSubject == _EMPTY_ && config.MaxWaiting == 0 { if config.DeliverSubject == _EMPTY_ && config.MaxWaiting == 0 {
config.MaxWaiting = JSWaitQueueDefaultMax config.MaxWaiting = JSWaitQueueDefaultMax
@@ -562,10 +630,6 @@ func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig,
if config.AckWait == 0 && (config.AckPolicy == AckExplicit || config.AckPolicy == AckAll) { if config.AckWait == 0 && (config.AckPolicy == AckExplicit || config.AckPolicy == AckAll) {
config.AckWait = JsAckWaitDefault 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 BackOff was specified that will override the AckWait and the MaxDeliver.
if len(config.BackOff) > 0 { if len(config.BackOff) > 0 {
if pedantic && config.AckWait != 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. // 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 { if (config.AckPolicy == AckExplicit || config.AckPolicy == AckAll) && config.MaxAckPending == 0 {
accPending := JsDefaultMaxAckPending ackPending := JsDefaultMaxAckPending
if lim.MaxAckPending > 0 && lim.MaxAckPending < accPending { if lim.MaxAckPending > 0 && lim.MaxAckPending < ackPending {
accPending = lim.MaxAckPending ackPending = lim.MaxAckPending
} }
if accLim.MaxAckPending > 0 && accLim.MaxAckPending < accPending { if accLim.MaxAckPending > 0 && accLim.MaxAckPending < ackPending {
accPending = accLim.MaxAckPending ackPending = accLim.MaxAckPending
} }
config.MaxAckPending = accPending config.MaxAckPending = ackPending
} }
// if applicable set max request batch size // if applicable set max request batch size
if config.DeliverSubject == _EMPTY_ && config.MaxRequestBatch == 0 && lim.MaxRequestBatch > 0 { 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. // 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 { if lbo := len(config.BackOff); lbo > 0 && config.MaxDeliver != -1 && lbo > config.MaxDeliver {
return NewJSConsumerMaxDeliverBackoffError() return NewJSConsumerMaxDeliverBackoffError()
@@ -690,7 +771,7 @@ func checkConsumerCfg(
return NewJSConsumerMaxRequestBatchNegativeError() return NewJSConsumerMaxRequestBatchNegativeError()
} }
if config.MaxRequestExpires != 0 && config.MaxRequestExpires < time.Millisecond { if config.MaxRequestExpires != 0 && config.MaxRequestExpires < time.Millisecond {
return NewJSConsumerMaxRequestExpiresToSmallError() return NewJSConsumerMaxRequestExpiresTooSmallError()
} }
if srvLim.MaxRequestBatch > 0 && config.MaxRequestBatch > srvLim.MaxRequestBatch { if srvLim.MaxRequestBatch > 0 && config.MaxRequestBatch > srvLim.MaxRequestBatch {
return NewJSConsumerMaxRequestBatchExceededError(srvLim.MaxRequestBatch) return NewJSConsumerMaxRequestBatchExceededError(srvLim.MaxRequestBatch)
@@ -849,6 +930,14 @@ func checkConsumerCfg(
return NewJSConsumerInvalidGroupNameError() 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. // For now don't allow preferred server in placement.
@@ -3445,6 +3534,7 @@ type PriorityGroup struct {
MinPending int64 `json:"min_pending,omitempty"` MinPending int64 `json:"min_pending,omitempty"`
MinAckPending int64 `json:"min_ack_pending,omitempty"` MinAckPending int64 `json:"min_ack_pending,omitempty"`
Id string `json:"id,omitempty"` Id string `json:"id,omitempty"`
Priority int `json:"priority,omitempty"`
} }
// Used in nextReqFromMsg, since the json.Unmarshal causes the request // Used in nextReqFromMsg, since the json.Unmarshal causes the request
@@ -3574,6 +3664,33 @@ var (
errWaitQueueNil = errors.New("wait queue is nil") 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. // Adds in a new request.
func (wq *waitQueue) add(wr *waitingRequest) error { func (wq *waitQueue) add(wr *waitingRequest) error {
if wq == nil { 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. // 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. // This will now place a request that still has pending items at the ends of the list.
func (wq *waitQueue) pop() *waitingRequest { func (wq *waitQueue) pop() *waitingRequest {
@@ -3656,6 +3784,74 @@ func (wq *waitQueue) pop() *waitingRequest {
return wr 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. // Removes the current read pointer (head FIFO) entry.
func (wq *waitQueue) removeCurrent() { func (wq *waitQueue) removeCurrent() {
wq.remove(nil, wq.head) wq.remove(nil, wq.head)
@@ -3817,17 +4013,17 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
if needNewPin { if needNewPin {
o.sendPinnedAdvisoryLocked(priorityGroup) 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) { } else if time.Since(wr.received) < defaultGatewayRecentSubExpiration && (o.srv.leafNodeEnabled || o.srv.gateway.enabled) {
if needNewPin { if needNewPin {
o.sendPinnedAdvisoryLocked(priorityGroup) 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) { } else if o.srv.gateway.enabled && o.srv.hasGatewayInterest(wr.acc.Name, wr.interest) {
if needNewPin { if needNewPin {
o.sendPinnedAdvisoryLocked(priorityGroup) o.sendPinnedAdvisoryLocked(priorityGroup)
} }
return o.waiting.pop() return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy)
} }
} else { } else {
// We do check for expiration in `processWaiting`, but it is possible to hit the expiry here, and not there. // 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 // 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 // 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. // 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_ { if reply == _EMPTY_ {
return return
} }
@@ -3914,7 +4110,12 @@ func (o *consumer) processNextMsgReq(_ *subscription, c *client, _ *Account, _,
return 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))) 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 { if priorityGroup.Id != _EMPTY_ && o.cfg.PriorityPolicy != PriorityPinnedClient {
sendErr(400, "Bad Request - Not a Pinned Client Priority consumer") 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 { if priorityGroup != nil && o.cfg.PriorityPolicy != PriorityNone {
@@ -4038,15 +4243,25 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) {
wr.b = maxBytes wr.b = maxBytes
wr.received = time.Now() wr.received = time.Now()
if err := o.waiting.add(wr); err != nil { if o.cfg.PriorityPolicy == PriorityPrioritized {
// If the client has a heartbeat interval set, don't bother responding with a 409, if err := o.waiting.addPrioritized(wr); err != nil {
// otherwise we can end up in a hot loop with the client re-requesting instead of if hb == 0 {
// waiting for the missing heartbeats instead and retrying. sendErr(409, "Exceeded MaxWaiting")
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() o.signalNewMessages()
// If we are clustered update our followers about this request. // 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 return pmsg, 1, err
} }
// Hold onto this since we release the lock.
store := o.mset.store
var sseq uint64 var sseq uint64
var err error var err error
var sm *StoreMsg var sm *StoreMsg
@@ -4255,13 +4467,13 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
filters, subjf, fseq := o.filters, o.subjf, o.sseq filters, subjf, fseq := o.filters, o.subjf, o.sseq
// Check if we are multi-filtered or not. // Check if we are multi-filtered or not.
if filters != nil { 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. } else if len(subjf) > 0 { // Means single filtered subject since o.filters means > 1.
filter, wc := subjf[0].subject, subjf[0].hasWildcard 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 { } else {
// No filter here. // 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 { if sm == nil {
pmsg.returnToPool() pmsg.returnToPool()
@@ -4670,14 +4882,14 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
} }
if err == ErrStoreMsgNotFound || err == errDeletedMsg || err == ErrStoreEOF || err == errMaxAckPending { if err == ErrStoreMsgNotFound || err == errDeletedMsg || err == ErrStoreEOF || err == errMaxAckPending {
goto waitForMsgs 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 { } else {
s.Errorf("Received an error looking up message for consumer '%s > %s > %s': %v", if pmsg != nil {
o.mset.acc, stream, o.cfg.Name, err) 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 goto waitForMsgs
} }
} }
+60
View File
@@ -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()
}
+8
View File
@@ -31,6 +31,14 @@ var (
// ErrAuthExpired represents an expired authorization due to timeout. // ErrAuthExpired represents an expired authorization due to timeout.
ErrAuthExpired = errors.New("authentication expired") 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 represents an error condition when the payload is too big.
ErrMaxPayload = errors.New("maximum payload exceeded") ErrMaxPayload = errors.New("maximum payload exceeded")
+291 -1
View File
@@ -1140,7 +1140,7 @@
"deprecates": "" "deprecates": ""
}, },
{ {
"constant": "JSConsumerMaxRequestExpiresToSmall", "constant": "JSConsumerMaxRequestExpiresTooSmall",
"code": 400, "code": 400,
"error_code": 10115, "error_code": 10115,
"description": "consumer max request expires needs to be \u003e= 1ms", "description": "consumer max request expires needs to be \u003e= 1ms",
@@ -1659,6 +1659,106 @@
"url": "", "url": "",
"deprecates": "" "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", "constant": "JSConsumerPushWithPriorityGroupErr",
"code": 400, "code": 400,
@@ -1669,6 +1769,156 @@
"url": "", "url": "",
"deprecates": "" "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", "constant": "JSStreamOfflineReasonErrF",
"code": 500, "code": 500,
@@ -1688,5 +1938,45 @@
"help": "", "help": "",
"url": "", "url": "",
"deprecates": "" "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": ""
} }
] ]
+87 -71
View File
@@ -247,13 +247,14 @@ type ServerCapability uint64
// ServerInfo identifies remote servers. // ServerInfo identifies remote servers.
type ServerInfo struct { type ServerInfo struct {
Name string `json:"name"` Name string `json:"name"`
Host string `json:"host"` Host string `json:"host"`
ID string `json:"id"` ID string `json:"id"`
Cluster string `json:"cluster,omitempty"` Cluster string `json:"cluster,omitempty"`
Domain string `json:"domain,omitempty"` Domain string `json:"domain,omitempty"`
Version string `json:"ver"` Version string `json:"ver"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
// Whether JetStream is enabled (deprecated in favor of the `ServerCapability`). // Whether JetStream is enabled (deprecated in favor of the `ServerCapability`).
JetStream bool `json:"jetstream"` JetStream bool `json:"jetstream"`
// Generic capability flags // Generic capability flags
@@ -362,24 +363,27 @@ func (ci *ClientInfo) forAdvisory() *ClientInfo {
// ServerStats hold various statistics that we will periodically send out. // ServerStats hold various statistics that we will periodically send out.
type ServerStats struct { type ServerStats struct {
Start time.Time `json:"start"` Start time.Time `json:"start"`
Mem int64 `json:"mem"` Mem int64 `json:"mem"`
Cores int `json:"cores"` Cores int `json:"cores"`
CPU float64 `json:"cpu"` CPU float64 `json:"cpu"`
Connections int `json:"connections"` Connections int `json:"connections"`
TotalConnections uint64 `json:"total_connections"` TotalConnections uint64 `json:"total_connections"`
ActiveAccounts int `json:"active_accounts"` ActiveAccounts int `json:"active_accounts"`
NumSubs uint32 `json:"subscriptions"` NumSubs uint32 `json:"subscriptions"`
Sent DataStats `json:"sent"` Sent DataStats `json:"sent"`
Received DataStats `json:"received"` Received DataStats `json:"received"`
SlowConsumers int64 `json:"slow_consumers"` SlowConsumers int64 `json:"slow_consumers"`
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"` SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"`
Routes []*RouteStat `json:"routes,omitempty"` StaleConnections int64 `json:"stale_connections,omitempty"`
Gateways []*GatewayStat `json:"gateways,omitempty"` StaleConnectionStats *StaleConnectionStats `json:"stale_connection_stats,omitempty"`
ActiveServers int `json:"active_servers,omitempty"` StalledClients int64 `json:"stalled_clients,omitempty"`
JetStream *JetStreamVarz `json:"jetstream,omitempty"` Routes []*RouteStat `json:"routes,omitempty"`
MemLimit int64 `json:"gomemlimit,omitempty"` Gateways []*GatewayStat `json:"gateways,omitempty"`
MaxProcs int `json:"gomaxprocs,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. // RouteStat holds route statistics.
@@ -400,17 +404,17 @@ type GatewayStat struct {
NumInbound int `json:"inbound_connections"` NumInbound int `json:"inbound_connections"`
} }
type dataStats struct { type MsgBytes struct {
Msgs int64 `json:"msgs"` Msgs int64 `json:"msgs"`
Bytes int64 `json:"bytes"` Bytes int64 `json:"bytes"`
} }
// DataStats reports how may msg and bytes. Applicable for both sent and received. // DataStats reports how may msg and bytes. Applicable for both sent and received.
type DataStats struct { type DataStats struct {
dataStats MsgBytes
Gateways dataStats `json:"gateways,omitempty"` Gateways *MsgBytes `json:"gateways,omitempty"`
Routes dataStats `json:"routes,omitempty"` Routes *MsgBytes `json:"routes,omitempty"`
Leafs dataStats `json:"leafs,omitempty"` Leafs *MsgBytes `json:"leafs,omitempty"`
} }
// Used for internally queueing up messages that the server wants to send. // Used for internally queueing up messages that the server wants to send.
@@ -512,8 +516,9 @@ RESET:
} }
s.mu.RUnlock() s.mu.RUnlock()
// Grab tags. // Grab tags and metadata.
tags := s.getOpts().Tags opts := s.getOpts()
tags, metadata := opts.Tags, opts.Metadata
for s.eventsRunning() { for s.eventsRunning() {
select { select {
@@ -530,6 +535,7 @@ RESET:
si.Version = VERSION si.Version = VERSION
si.Time = time.Now().UTC() si.Time = time.Now().UTC()
si.Tags = tags si.Tags = tags
si.Metadata = metadata
si.Flags = 0 si.Flags = 0
if js { if js {
// New capability based flags. // New capability based flags.
@@ -860,13 +866,13 @@ func routeStat(r *client) *RouteStat {
rs := &RouteStat{ rs := &RouteStat{
ID: r.cid, ID: r.cid,
Sent: DataStats{ Sent: DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: r.outMsgs, Msgs: r.outMsgs,
Bytes: r.outBytes, Bytes: r.outBytes,
}, },
}, },
Received: DataStats{ Received: DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: atomic.LoadInt64(&r.inMsgs), Msgs: atomic.LoadInt64(&r.inMsgs),
Bytes: atomic.LoadInt64(&r.inBytes), 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 { if scs.Clients != 0 || scs.Routes != 0 || scs.Gateways != 0 || scs.Leafs != 0 {
m.Stats.SlowConsumersStats = scs 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() m.Stats.NumSubs = s.numSubscriptions()
// Routes // Routes
s.forEachRoute(func(r *client) { 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, // Note that *client.out[Msgs|Bytes] are not set using atomic,
// unlike the in[Msgs|bytes]. // unlike the in[Msgs|bytes].
gs.Sent = DataStats{ gs.Sent = DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: c.outMsgs, Msgs: c.outMsgs,
Bytes: c.outBytes, 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 // Common filter options for system requests STATSZ VARZ SUBSZ CONNZ ROUTEZ GATEWAYZ LEAFZ
type EventFilterOptions struct { type EventFilterOptions struct {
Name string `json:"server_name,omitempty"` // filter by server name Name string `json:"server_name,omitempty"` // filter by server name
Cluster string `json:"cluster,omitempty"` // filter by cluster name Cluster string `json:"cluster,omitempty"` // filter by cluster name
Host string `json:"host,omitempty"` // filter by host name Host string `json:"host,omitempty"` // filter by host name
Tags []string `json:"tags,omitempty"` // filter by tags (must match all tags) ExactMatch bool `json:"exact_match,omitempty"` // if the above filters should use exact matching or only "contains"
Domain string `json:"domain,omitempty"` // filter by JS domain 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 // 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. // 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 { func (s *Server) filterRequest(fOpts *EventFilterOptions) bool {
if fOpts.Name != _EMPTY_ && !strings.Contains(s.info.Name, fOpts.Name) { if fOpts == nil {
return true return false
} }
if fOpts.Host != _EMPTY_ && !strings.Contains(s.info.Host, fOpts.Host) { if fOpts.ExactMatch {
return true if (fOpts.Name != _EMPTY_ && fOpts.Name != s.info.Name) ||
} (fOpts.Host != _EMPTY_ && fOpts.Host != s.info.Host) ||
if fOpts.Cluster != _EMPTY_ { (fOpts.Cluster != _EMPTY_ && fOpts.Cluster != s.ClusterName()) {
if !strings.Contains(s.ClusterName(), fOpts.Cluster) {
return true 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 { if len(fOpts.Tags) > 0 {
opts := s.getOpts() opts := s.getOpts()
@@ -2428,37 +2449,37 @@ func (a *Account) statz() *AccountStat {
a.stats.Lock() a.stats.Lock()
received := DataStats{ received := DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: a.stats.inMsgs, Msgs: a.stats.inMsgs,
Bytes: a.stats.inBytes, Bytes: a.stats.inBytes,
}, },
Gateways: dataStats{ Gateways: &MsgBytes{
Msgs: a.stats.gw.inMsgs, Msgs: a.stats.gw.inMsgs,
Bytes: a.stats.gw.inBytes, Bytes: a.stats.gw.inBytes,
}, },
Routes: dataStats{ Routes: &MsgBytes{
Msgs: a.stats.rt.inMsgs, Msgs: a.stats.rt.inMsgs,
Bytes: a.stats.rt.inBytes, Bytes: a.stats.rt.inBytes,
}, },
Leafs: dataStats{ Leafs: &MsgBytes{
Msgs: a.stats.ln.inMsgs, Msgs: a.stats.ln.inMsgs,
Bytes: a.stats.ln.inBytes, Bytes: a.stats.ln.inBytes,
}, },
} }
sent := DataStats{ sent := DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: a.stats.outMsgs, Msgs: a.stats.outMsgs,
Bytes: a.stats.outBytes, Bytes: a.stats.outBytes,
}, },
Gateways: dataStats{ Gateways: &MsgBytes{
Msgs: a.stats.gw.outMsgs, Msgs: a.stats.gw.outMsgs,
Bytes: a.stats.gw.outBytes, Bytes: a.stats.gw.outBytes,
}, },
Routes: dataStats{ Routes: &MsgBytes{
Msgs: a.stats.rt.outMsgs, Msgs: a.stats.rt.outMsgs,
Bytes: a.stats.rt.outBytes, Bytes: a.stats.rt.outBytes,
}, },
Leafs: dataStats{ Leafs: &MsgBytes{
Msgs: a.stats.ln.outMsgs, Msgs: a.stats.ln.outMsgs,
Bytes: a.stats.ln.outBytes, Bytes: a.stats.ln.outBytes,
}, },
@@ -2504,13 +2525,11 @@ func (s *Server) accountConnectEvent(c *client) {
s.mu.Unlock() s.mu.Unlock()
return return
} }
gacc := s.gacc
eid := s.nextEventID() eid := s.nextEventID()
s.mu.Unlock() s.mu.Unlock()
c.mu.Lock() c.mu.Lock()
// Ignore global account activity if c.acc == nil {
if c.acc == nil || c.acc == gacc {
c.mu.Unlock() c.mu.Unlock()
return return
} }
@@ -2553,18 +2572,15 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string)
s.mu.Unlock() s.mu.Unlock()
return return
} }
gacc := s.gacc
eid := s.nextEventID() eid := s.nextEventID()
s.mu.Unlock() s.mu.Unlock()
c.mu.Lock() c.mu.Lock()
// Ignore global account activity if c.acc == nil {
if c.acc == nil || c.acc == gacc {
c.mu.Unlock() c.mu.Unlock()
return return
} }
m := DisconnectEventMsg{ m := DisconnectEventMsg{
TypedEvent: TypedEvent{ TypedEvent: TypedEvent{
Type: DisconnectEventMsgType, Type: DisconnectEventMsgType,
@@ -2591,13 +2607,13 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string)
MQTTClient: c.getMQTTClientID(), MQTTClient: c.getMQTTClientID(),
}, },
Sent: DataStats{ Sent: DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: atomic.LoadInt64(&c.inMsgs), Msgs: atomic.LoadInt64(&c.inMsgs),
Bytes: atomic.LoadInt64(&c.inBytes), Bytes: atomic.LoadInt64(&c.inBytes),
}, },
}, },
Received: DataStats{ Received: DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: c.outMsgs, Msgs: c.outMsgs,
Bytes: c.outBytes, 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. // 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() s.mu.Lock()
if !s.eventsEnabled() { if !s.eventsEnabled() {
s.mu.Unlock() s.mu.Unlock()
@@ -2649,18 +2665,18 @@ func (s *Server) sendAuthErrorEvent(c *client) {
MQTTClient: c.getMQTTClientID(), MQTTClient: c.getMQTTClientID(),
}, },
Sent: DataStats{ Sent: DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: c.inMsgs, Msgs: c.inMsgs,
Bytes: c.inBytes, Bytes: c.inBytes,
}, },
}, },
Received: DataStats{ Received: DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: c.outMsgs, Msgs: c.outMsgs,
Bytes: c.outBytes, Bytes: c.outBytes,
}, },
}, },
Reason: AuthenticationViolation.String(), Reason: reason,
} }
c.mu.Unlock() c.mu.Unlock()
@@ -2711,13 +2727,13 @@ func (s *Server) sendAccountAuthErrorEvent(c *client, acc *Account, reason strin
MQTTClient: c.getMQTTClientID(), MQTTClient: c.getMQTTClientID(),
}, },
Sent: DataStats{ Sent: DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: c.inMsgs, Msgs: c.inMsgs,
Bytes: c.inBytes, Bytes: c.inBytes,
}, },
}, },
Received: DataStats{ Received: DataStats{
dataStats: dataStats{ MsgBytes: MsgBytes{
Msgs: c.outMsgs, Msgs: c.outMsgs,
Bytes: c.outBytes, Bytes: c.outBytes,
}, },
File diff suppressed because it is too large Load Diff
+16 -6
View File
@@ -35,6 +35,7 @@ import (
const ( const (
defaultSolicitGatewaysDelay = time.Second defaultSolicitGatewaysDelay = time.Second
defaultGatewayConnectDelay = time.Second defaultGatewayConnectDelay = time.Second
defaultGatewayConnectMaxDelay = 30 * time.Second
defaultGatewayReconnectDelay = time.Second defaultGatewayReconnectDelay = time.Second
defaultGatewayRecentSubExpiration = 2 * time.Second defaultGatewayRecentSubExpiration = 2 * time.Second
defaultGatewayMaxRUnsubBeforeSwitch = 1000 defaultGatewayMaxRUnsubBeforeSwitch = 1000
@@ -59,6 +60,7 @@ const (
var ( var (
gatewayConnectDelay = defaultGatewayConnectDelay gatewayConnectDelay = defaultGatewayConnectDelay
gatewayConnectMaxDelay = defaultGatewayConnectMaxDelay
gatewayReconnectDelay = defaultGatewayReconnectDelay gatewayReconnectDelay = defaultGatewayReconnectDelay
gatewayMaxRUnsubBeforeSwitch = defaultGatewayMaxRUnsubBeforeSwitch gatewayMaxRUnsubBeforeSwitch = defaultGatewayMaxRUnsubBeforeSwitch
gatewaySolicitDelay = int64(defaultSolicitGatewaysDelay) 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. // to the given Gateway. It will return once a connection has been created.
func (s *Server) solicitGateway(cfg *gatewayCfg, firstConnect bool) { func (s *Server) solicitGateway(cfg *gatewayCfg, firstConnect bool) {
var ( var (
opts = s.getOpts() opts = s.getOpts()
isImplicit = cfg.isImplicit() isImplicit = cfg.isImplicit()
attempts int attemptDelay = gatewayConnectDelay
typeStr string attempts int
typeStr string
) )
if isImplicit { if isImplicit {
typeStr = "implicit" typeStr = "implicit"
@@ -769,7 +772,14 @@ func (s *Server) solicitGateway(cfg *gatewayCfg, firstConnect bool) {
select { select {
case <-s.quitCh: case <-s.quitCh:
return 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 continue
} }
} }
@@ -923,7 +933,7 @@ func (s *Server) createGateway(cfg *gatewayCfg, url *url.URL, conn net.Conn) {
if tlsRequired { if tlsRequired {
c.Debugf("TLS handshake complete") c.Debugf("TLS handshake complete")
cs := c.nc.(*tls.Conn).ConnectionState() 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 // For outbound, we can't set the normal ping timer yet since the other
+75 -6
View File
@@ -156,8 +156,8 @@ type jsAccount struct {
storeDir string storeDir string
inflight sync.Map inflight sync.Map
streams map[string]*stream streams map[string]*stream
templates map[string]*streamTemplate templates map[string]*streamTemplate // Deprecated: stream templates are deprecated and will be removed in a future version.
store TemplateStore store TemplateStore // Deprecated: stream templates are deprecated and will be removed in a future version.
// From server // From server
sendq *ipQueue[*pubMsg] sendq *ipQueue[*pubMsg]
@@ -560,7 +560,7 @@ func (s *Server) restartJetStream() error {
MaxMemory: opts.JetStreamMaxMemory, MaxMemory: opts.JetStreamMaxMemory,
MaxStore: opts.JetStreamMaxStore, MaxStore: opts.JetStreamMaxStore,
Domain: opts.JetStreamDomain, Domain: opts.JetStreamDomain,
Strict: opts.JetStreamStrict, Strict: !opts.NoJetStreamStrict,
} }
s.Noticef("Restarting JetStream") s.Noticef("Restarting JetStream")
err := s.EnableJetStream(&cfg) err := s.EnableJetStream(&cfg)
@@ -1451,6 +1451,61 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
mset.setCreatedTime(cfg.Created) 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() state := mset.state()
s.Noticef(" Restored %s messages for stream '%s > %s' in %v", 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)) 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() opts := s.getOpts()
// Strict mode. // Strict mode.
jsc.Strict = opts.JetStreamStrict jsc.Strict = !opts.NoJetStreamStrict
// Sync options. // Sync options.
jsc.SyncInterval = opts.SyncInterval 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 // 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. // 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 { type StreamTemplateConfig struct {
Name string `json:"name"` Name string `json:"name"`
Config *StreamConfig `json:"config"` Config *StreamConfig `json:"config"`
@@ -2694,12 +2750,14 @@ type StreamTemplateConfig struct {
} }
// StreamTemplateInfo // StreamTemplateInfo
// Deprecated: stream templates are deprecated and will be removed in a future version.
type StreamTemplateInfo struct { type StreamTemplateInfo struct {
Config *StreamTemplateConfig `json:"config"` Config *StreamTemplateConfig `json:"config"`
Streams []string `json:"streams"` Streams []string `json:"streams"`
} }
// streamTemplate // streamTemplate
// Deprecated: stream templates are deprecated and will be removed in a future version.
type streamTemplate struct { type streamTemplate struct {
mu sync.Mutex mu sync.Mutex
tc *client tc *client
@@ -2708,6 +2766,7 @@ type streamTemplate struct {
streams []string streams []string
} }
// Deprecated: stream templates are deprecated and will be removed in a future version.
func (t *StreamTemplateConfig) deepCopy() *StreamTemplateConfig { func (t *StreamTemplateConfig) deepCopy() *StreamTemplateConfig {
copy := *t copy := *t
cfg := *t.Config 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. // 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) { func (a *Account) addStreamTemplate(tc *StreamTemplateConfig) (*streamTemplate, error) {
s, jsa, err := a.checkForJetStream() s, jsa, err := a.checkForJetStream()
if err != nil { if err != nil {
@@ -2772,6 +2832,7 @@ func (a *Account) addStreamTemplate(tc *StreamTemplateConfig) (*streamTemplate,
return t, nil return t, nil
} }
// Deprecated: stream templates are deprecated and will be removed in a future version.
func (t *streamTemplate) createTemplateSubscriptions() error { func (t *streamTemplate) createTemplateSubscriptions() error {
if t == nil { if t == nil {
return fmt.Errorf("no template") return fmt.Errorf("no template")
@@ -2795,6 +2856,7 @@ func (t *streamTemplate) createTemplateSubscriptions() error {
return nil 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) { func (t *streamTemplate) processInboundTemplateMsg(_ *subscription, pc *client, acc *Account, subject, reply string, msg []byte) {
if t == nil || t.jsa == nil { if t == nil || t.jsa == nil {
return return
@@ -2842,6 +2904,7 @@ func (t *streamTemplate) processInboundTemplateMsg(_ *subscription, pc *client,
} }
// lookupStreamTemplate looks up the names stream template. // 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) { func (a *Account) lookupStreamTemplate(name string) (*streamTemplate, error) {
_, jsa, err := a.checkForJetStream() _, jsa, err := a.checkForJetStream()
if err != nil { 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. // 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) { func (a *Account) validateStreams(t *streamTemplate) {
t.mu.Lock() t.mu.Lock()
var vstreams []string var vstreams []string
@@ -2872,6 +2936,7 @@ func (a *Account) validateStreams(t *streamTemplate) {
t.mu.Unlock() t.mu.Unlock()
} }
// Deprecated: stream templates are deprecated and will be removed in a future version.
func (t *streamTemplate) delete() error { func (t *streamTemplate) delete() error {
if t == nil { if t == nil {
return fmt.Errorf("nil stream template") return fmt.Errorf("nil stream template")
@@ -2930,6 +2995,7 @@ func (t *streamTemplate) delete() error {
return lastErr return lastErr
} }
// Deprecated: stream templates are deprecated and will be removed in a future version.
func (a *Account) deleteStreamTemplate(name string) error { func (a *Account) deleteStreamTemplate(name string) error {
t, err := a.lookupStreamTemplate(name) t, err := a.lookupStreamTemplate(name)
if err != nil { if err != nil {
@@ -2938,6 +3004,7 @@ func (a *Account) deleteStreamTemplate(name string) error {
return t.delete() return t.delete()
} }
// Deprecated: stream templates are deprecated and will be removed in a future version.
func (a *Account) templates() []*streamTemplate { func (a *Account) templates() []*streamTemplate {
var ts []*streamTemplate var ts []*streamTemplate
_, jsa, err := a.checkForJetStream() _, jsa, err := a.checkForJetStream()
@@ -2956,6 +3023,7 @@ func (a *Account) templates() []*streamTemplate {
} }
// Will add a stream to a template, this is for recovery. // 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 { func (jsa *jsAccount) addStreamNameToTemplate(tname, mname string) error {
if jsa.templates == nil { if jsa.templates == nil {
return fmt.Errorf("template not found") 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. // This will check if a template owns this stream.
// jsAccount lock should be held // 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 { func (jsa *jsAccount) checkTemplateOwnership(tname, sname string) bool {
if jsa.templates == nil { if jsa.templates == nil {
return false return false
@@ -3022,12 +3091,12 @@ func canonicalName(name string) string {
} }
// To throttle the out of resources errors. // To throttle the out of resources errors.
func (s *Server) resourcesExceededError() { func (s *Server) resourcesExceededError(storeType StorageType) {
var didAlert bool var didAlert bool
s.rerrMu.Lock() s.rerrMu.Lock()
if now := time.Now(); now.Sub(s.rerrLast) > 10*time.Second { 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 s.rerrLast = now
didAlert = true didAlert = true
} }
+231 -56
View File
@@ -49,20 +49,24 @@ const (
// JSApiTemplateCreate is the endpoint to create new stream templates. // JSApiTemplateCreate is the endpoint to create new stream templates.
// Will return JSON response. // Will return JSON response.
// Deprecated: stream templates are deprecated and will be removed in a future version.
JSApiTemplateCreate = "$JS.API.STREAM.TEMPLATE.CREATE.*" JSApiTemplateCreate = "$JS.API.STREAM.TEMPLATE.CREATE.*"
JSApiTemplateCreateT = "$JS.API.STREAM.TEMPLATE.CREATE.%s" JSApiTemplateCreateT = "$JS.API.STREAM.TEMPLATE.CREATE.%s"
// JSApiTemplates is the endpoint to list all stream template names for this account. // JSApiTemplates is the endpoint to list all stream template names for this account.
// Will return JSON response. // Will return JSON response.
// Deprecated: stream templates are deprecated and will be removed in a future version.
JSApiTemplates = "$JS.API.STREAM.TEMPLATE.NAMES" JSApiTemplates = "$JS.API.STREAM.TEMPLATE.NAMES"
// JSApiTemplateInfo is for obtaining general information about a named stream template. // JSApiTemplateInfo is for obtaining general information about a named stream template.
// Will return JSON response. // Will return JSON response.
// Deprecated: stream templates are deprecated and will be removed in a future version.
JSApiTemplateInfo = "$JS.API.STREAM.TEMPLATE.INFO.*" JSApiTemplateInfo = "$JS.API.STREAM.TEMPLATE.INFO.*"
JSApiTemplateInfoT = "$JS.API.STREAM.TEMPLATE.INFO.%s" JSApiTemplateInfoT = "$JS.API.STREAM.TEMPLATE.INFO.%s"
// JSApiTemplateDelete is the endpoint to delete stream templates. // JSApiTemplateDelete is the endpoint to delete stream templates.
// Will return JSON response. // Will return JSON response.
// Deprecated: stream templates are deprecated and will be removed in a future version.
JSApiTemplateDelete = "$JS.API.STREAM.TEMPLATE.DELETE.*" JSApiTemplateDelete = "$JS.API.STREAM.TEMPLATE.DELETE.*"
JSApiTemplateDeleteT = "$JS.API.STREAM.TEMPLATE.DELETE.%s" JSApiTemplateDeleteT = "$JS.API.STREAM.TEMPLATE.DELETE.%s"
@@ -305,6 +309,9 @@ const (
// JSAdvisoryStreamQuorumLostPre notification that a stream and its consumers are stalled. // JSAdvisoryStreamQuorumLostPre notification that a stream and its consumers are stalled.
JSAdvisoryStreamQuorumLostPre = "$JS.EVENT.ADVISORY.STREAM.QUORUM_LOST" 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 notification that a replicated consumer has elected a leader.
JSAdvisoryConsumerLeaderElectedPre = "$JS.EVENT.ADVISORY.CONSUMER.LEADER_ELECTED" JSAdvisoryConsumerLeaderElectedPre = "$JS.EVENT.ADVISORY.CONSUMER.LEADER_ELECTED"
@@ -325,6 +332,12 @@ const (
JSAuditAdvisory = "$JS.EVENT.ADVISORY.API" 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 denyAllClientJs = []string{jsAllAPI, "$KV.>", "$OBJ.>"}
var denyAllJs = []string{jscAllSubj, raftAllSubj, 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"` LastFor string `json:"last_by_subj,omitempty"`
NextFor string `json:"next_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. // Can be used with simple starting seq, but also NextFor with wildcards.
Batch int `json:"batch,omitempty"` Batch int `json:"batch,omitempty"`
// This will make sure we limit how much data we blast out. If not set we will // 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"` UpToSeq uint64 `json:"up_to_seq,omitempty"`
// Only return messages up to this time. // Only return messages up to this time.
UpToTime *time.Time `json:"up_to_time,omitempty"` 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 { type JSApiMsgGetResponse struct {
@@ -766,44 +781,52 @@ type JSApiConsumerGetNextRequest struct {
} }
// JSApiStreamTemplateCreateResponse for creating templates. // JSApiStreamTemplateCreateResponse for creating templates.
// Deprecated: stream templates are deprecated and will be removed in a future version.
type JSApiStreamTemplateCreateResponse struct { type JSApiStreamTemplateCreateResponse struct {
ApiResponse ApiResponse
*StreamTemplateInfo *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" 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 { type JSApiStreamTemplateDeleteResponse struct {
ApiResponse ApiResponse
Success bool `json:"success,omitempty"` 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" const JSApiStreamTemplateDeleteResponseType = "io.nats.jetstream.api.v1.stream_template_delete_response"
// JSApiStreamTemplateInfoResponse for information about stream templates. // JSApiStreamTemplateInfoResponse for information about stream templates.
// Deprecated: stream templates are deprecated and will be removed in a future version.
type JSApiStreamTemplateInfoResponse struct { type JSApiStreamTemplateInfoResponse struct {
ApiResponse ApiResponse
*StreamTemplateInfo *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" 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 { type JSApiStreamTemplatesRequest struct {
ApiPagedRequest ApiPagedRequest
} }
// JSApiStreamTemplateNamesResponse list of templates // JSApiStreamTemplateNamesResponse list of templates
// Deprecated: stream templates are deprecated and will be removed in a future version.
type JSApiStreamTemplateNamesResponse struct { type JSApiStreamTemplateNamesResponse struct {
ApiResponse ApiResponse
ApiPaged ApiPaged
Templates []string `json:"streams"` 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" const JSApiStreamTemplateNamesResponseType = "io.nats.jetstream.api.v1.stream_template_names_response"
// Structure that holds state for a JetStream API request that is processed // 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 // in a separate long-lived go routine. This is to avoid blocking connections.
// ROUTE and GATEWAY connections.
type jsAPIRoutedReq struct { type jsAPIRoutedReq struct {
jsub *subscription jsub *subscription
sub *subscription sub *subscription
@@ -872,17 +895,6 @@ func (js *jetStream) apiDispatch(sub *subscription, c *client, acc *Account, sub
} }
jsub := rr.psubs[0] 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 // We need to make sure not to block. We will send the request to a long-lived
// pool of go routines. // pool of go routines.
@@ -1277,13 +1289,18 @@ func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, _ *Account,
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiAccountInfoResponse{ApiResponse: ApiResponse{Type: JSApiAccountInfoResponseType}} 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -1333,17 +1350,23 @@ func consumerNameFromSubject(subject string) string {
} }
// Request to create a new template. // 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) { func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
if c == nil { if c == nil {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamTemplateCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateCreateResponseType}} 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() { if !acc.JetStreamEnabled() {
resp.Error = NewJSNotEnabledForAccountError() resp.Error = NewJSNotEnabledForAccountError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) 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. // 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) { func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
if c == nil { if c == nil {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamTemplateNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateNamesResponseType}} 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() { if !acc.JetStreamEnabled() {
resp.Error = NewJSNotEnabledForAccountError() resp.Error = NewJSNotEnabledForAccountError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) 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. // 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) { func (s *Server) jsTemplateInfoRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
if c == nil { if c == nil {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamTemplateInfoResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateInfoResponseType}} 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() { if !acc.JetStreamEnabled() {
resp.Error = NewJSNotEnabledForAccountError() resp.Error = NewJSNotEnabledForAccountError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) 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. // 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) { func (s *Server) jsTemplateDeleteRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
if c == nil { if c == nil {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamTemplateDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateDeleteResponseType}} 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() { if !acc.JetStreamEnabled() {
resp.Error = NewJSNotEnabledForAccountError() resp.Error = NewJSNotEnabledForAccountError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) 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() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}} 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -1676,13 +1722,18 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account,
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -1731,8 +1782,7 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account,
// Handle clustered version here. // Handle clustered version here.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
// Always do in separate Go routine. s.jsClusteredStreamUpdateRequest(ci, acc, subject, reply, copyBytes(rmsg), &cfg, nil, ncfg.Pedantic)
go s.jsClusteredStreamUpdateRequest(ci, acc, subject, reply, copyBytes(rmsg), &cfg, nil, ncfg.Pedantic)
return return
} }
@@ -1775,13 +1825,18 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, _ *Account,
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamNamesResponseType}} 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -1902,7 +1957,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -1912,6 +1967,11 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s
ApiResponse: ApiResponse{Type: JSApiStreamListResponseType}, ApiResponse: ApiResponse{Type: JSApiStreamListResponseType},
Streams: []*StreamInfo{}, 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { 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 { if rt := getHeader(JSResponseType, hdr); len(rt) > 0 && string(rt) == jsCreateResponse {
resp.ApiResponse.Type = JSApiStreamCreateResponseType 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 var clusterWideConsCount int
@@ -2234,7 +2299,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, _ *
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -2244,6 +2309,11 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, _ *
name := tokenAt(subject, 6) name := tokenAt(subject, 6)
var resp = JSApiStreamLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiStreamLeaderStepDownResponseType}} 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 we are not in clustered mode this is a failed request.
if !s.JetStreamIsClustered() { if !s.JetStreamIsClustered() {
@@ -2344,13 +2414,18 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, _
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiConsumerLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiConsumerLeaderStepDownResponseType}} 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 we are not in clustered mode this is a failed request.
if !s.JetStreamIsClustered() { if !s.JetStreamIsClustered() {
@@ -2462,7 +2537,7 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, _ *Acco
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -2472,6 +2547,11 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, _ *Acco
name := tokenAt(subject, 6) name := tokenAt(subject, 6)
var resp = JSApiStreamRemovePeerResponse{ApiResponse: ApiResponse{Type: JSApiStreamRemovePeerResponseType}} 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 we are not in clustered mode this is a failed request.
if !s.JetStreamIsClustered() { if !s.JetStreamIsClustered() {
@@ -2565,7 +2645,7 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, _ *Ac
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -2589,6 +2669,11 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, _ *Ac
} }
var resp = JSApiMetaServerRemoveResponse{ApiResponse: ApiResponse{Type: JSApiMetaServerRemoveResponseType}} 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) { if isEmptyRequest(msg) {
resp.Error = NewJSBadRequestError() resp.Error = NewJSBadRequestError()
@@ -2674,7 +2759,7 @@ func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -2702,6 +2787,11 @@ func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _
} }
var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} 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 var req JSApiMetaServerStreamMoveRequest
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil {
@@ -2833,7 +2923,7 @@ func (s *Server) jsLeaderServerStreamCancelMoveRequest(sub *subscription, c *cli
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -2854,6 +2944,11 @@ func (s *Server) jsLeaderServerStreamCancelMoveRequest(sub *subscription, c *cli
} }
var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} 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) accName := tokenAt(subject, 6)
streamName := tokenAt(subject, 7) streamName := tokenAt(subject, 7)
@@ -2943,7 +3038,7 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -2960,6 +3055,11 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac
accName := tokenAt(subject, 5) accName := tokenAt(subject, 5)
var resp = JSApiAccountPurgeResponse{ApiResponse: ApiResponse{Type: JSApiAccountPurgeResponseType}} 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() { if !s.JetStreamIsClustered() {
var streams []*stream var streams []*stream
@@ -3030,7 +3130,7 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Accoun
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -3058,6 +3158,11 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Accoun
var preferredLeader string var preferredLeader string
var resp = JSApiLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiLeaderStepDownResponseType}} 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) { if isJSONObjectOrArray(msg) {
var req JSApiLeaderStepdownRequest var req JSApiLeaderStepdownRequest
@@ -3203,13 +3308,18 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, _ *Account,
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamDeleteResponseType}} 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -3271,7 +3381,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -3280,6 +3390,11 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su
stream := tokenAt(subject, 6) stream := tokenAt(subject, 6)
var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}} 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 we are in clustered mode we need to be the stream leader to proceed.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -3390,7 +3505,7 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -3399,6 +3514,11 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje
stream := tokenAt(subject, 6) stream := tokenAt(subject, 6)
var resp = JSApiMsgGetResponse{ApiResponse: ApiResponse{Type: JSApiMsgGetResponseType}} 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 we are in clustered mode we need to be the stream leader to proceed.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -3499,6 +3619,10 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje
var svp StoreMsg var svp StoreMsg
var sm *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. // If AsOfTime is set, perform this first to get the sequence.
var seq uint64 var seq uint64
if req.StartTime != nil { if req.StartTime != nil {
@@ -3522,10 +3646,12 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje
resp.Message = &StoredMsg{ resp.Message = &StoredMsg{
Subject: sm.subj, Subject: sm.subj,
Sequence: sm.seq, Sequence: sm.seq,
Header: sm.hdr,
Data: sm.msg, Data: sm.msg,
Time: time.Unix(0, sm.ts).UTC(), 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. // Don't send response through API layer for this call.
s.sendInternalAccountMsg(nil, reply, s.jsonResponse(resp)) s.sendInternalAccountMsg(nil, reply, s.jsonResponse(resp))
@@ -3536,7 +3662,7 @@ func (s *Server) jsConsumerUnpinRequest(sub *subscription, c *client, _ *Account
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -3547,6 +3673,11 @@ func (s *Server) jsConsumerUnpinRequest(sub *subscription, c *client, _ *Account
var req JSApiConsumerUnpinRequest var req JSApiConsumerUnpinRequest
var resp = JSApiConsumerUnpinResponse{ApiResponse: ApiResponse{Type: JSApiConsumerUnpinResponseType}} 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 { if err := json.Unmarshal(msg, &req); err != nil {
resp.Error = NewJSInvalidJSONError(err) resp.Error = NewJSInvalidJSONError(err)
@@ -3670,7 +3801,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account,
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -3679,6 +3810,11 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account,
stream := streamNameFromSubject(subject) stream := streamNameFromSubject(subject)
var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}} 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 we are in clustered mode we need to be the stream leader to proceed.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -3812,13 +3948,18 @@ func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, _ *Account
if c == nil || !s.JetStreamIsLeader() { if c == nil || !s.JetStreamIsLeader() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}} 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() { if !acc.JetStreamEnabled() {
resp.Error = NewJSNotEnabledForAccountError() resp.Error = NewJSNotEnabledForAccountError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) 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. // TODO(dlc) - We could check apriori and cancel initial request if we know it won't fit.
total += len(msg) total += len(msg)
if js.wouldExceedLimits(FileStorage, total) { if js.wouldExceedLimits(FileStorage, total) {
s.resourcesExceededError() s.resourcesExceededError(FileStorage)
resultCh <- result{NewJSInsufficientResourcesError(), reply} resultCh <- result{NewJSInsufficientResourcesError(), reply}
return return
} }
@@ -4092,7 +4233,7 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -4107,6 +4248,11 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun
} }
var resp = JSApiStreamSnapshotResponse{ApiResponse: ApiResponse{Type: JSApiStreamSnapshotResponseType}} 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() { if !acc.JetStreamEnabled() {
resp.Error = NewJSNotEnabledForAccountError() resp.Error = NewJSNotEnabledForAccountError()
s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) 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 return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} 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 var req CreateConsumerRequest
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { 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 isClustered && !req.Config.Direct {
// If we are inline with client, we still may need to do a callout for consumer info s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action, req.Pedantic)
// 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)
}
return return
} }
@@ -4552,7 +4696,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, _ *Account
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -4562,6 +4706,11 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, _ *Account
ApiResponse: ApiResponse{Type: JSApiConsumerNamesResponseType}, ApiResponse: ApiResponse{Type: JSApiConsumerNamesResponseType},
Consumers: []string{}, 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -4674,7 +4823,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account,
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -4684,6 +4833,11 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account,
ApiResponse: ApiResponse{Type: JSApiConsumerListResponseType}, ApiResponse: ApiResponse{Type: JSApiConsumerListResponseType},
Consumers: []*ConsumerInfo{}, 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { 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 hasJS, doErr := acc.checkJetStream(); !hasJS {
if doErr { if doErr {
resp.Error = NewJSNotEnabledForAccountError() resp.Error = NewJSNotEnabledForAccountError()
@@ -4777,7 +4937,7 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, _ *Account,
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -4787,6 +4947,11 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, _ *Account,
consumerName := consumerNameFromSubject(subject) consumerName := consumerNameFromSubject(subject)
var resp = JSApiConsumerInfoResponse{ApiResponse: ApiResponse{Type: JSApiConsumerInfoResponseType}} 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) { if !isEmptyRequest(msg) {
resp.Error = NewJSNotEmptyRequestError() resp.Error = NewJSNotEmptyRequestError()
@@ -4973,13 +5138,18 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, _ *Accoun
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
} }
var resp = JSApiConsumerDeleteResponse{ApiResponse: ApiResponse{Type: JSApiConsumerDeleteResponseType}} 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. // Determine if we should proceed here when we are in clustered mode.
if s.JetStreamIsClustered() { if s.JetStreamIsClustered() {
@@ -5045,7 +5215,7 @@ func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account
if c == nil || !s.JetStreamEnabled() { if c == nil || !s.JetStreamEnabled() {
return return
} }
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg) ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
if err != nil { if err != nil {
s.Warnf(badAPIRequestT, msg) s.Warnf(badAPIRequestT, msg)
return return
@@ -5053,6 +5223,11 @@ func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account
var req JSApiConsumerPauseRequest var req JSApiConsumerPauseRequest
var resp = JSApiConsumerPauseResponse{ApiResponse: ApiResponse{Type: JSApiConsumerPauseResponseType}} 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 isJSONObjectOrArray(msg) {
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil {
+653
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
+592 -174
View File
@@ -8,6 +8,24 @@ const (
// JSAccountResourcesExceededErr resource limits exceeded for account // JSAccountResourcesExceededErr resource limits exceeded for account
JSAccountResourcesExceededErr ErrorIdentifier = 10002 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 bad request
JSBadRequestErr ErrorIdentifier = 10003 JSBadRequestErr ErrorIdentifier = 10003
@@ -44,9 +62,18 @@ const (
// JSClusterUnSupportFeatureErr not currently supported in clustered mode // JSClusterUnSupportFeatureErr not currently supported in clustered mode
JSClusterUnSupportFeatureErr ErrorIdentifier = 10036 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 action CREATE is used for a existing consumer with a different config (consumer already exists)
JSConsumerAlreadyExists ErrorIdentifier = 10148 JSConsumerAlreadyExists ErrorIdentifier = 10148
// JSConsumerBackOffNegativeErr consumer backoff needs to be positive
JSConsumerBackOffNegativeErr ErrorIdentifier = 10184
// JSConsumerBadDurableNameErr durable name can not contain '.', '*', '>' // JSConsumerBadDurableNameErr durable name can not contain '.', '*', '>'
JSConsumerBadDurableNameErr ErrorIdentifier = 10103 JSConsumerBadDurableNameErr ErrorIdentifier = 10103
@@ -149,8 +176,8 @@ const (
// JSConsumerMaxRequestBatchNegativeErr consumer max request batch needs to be > 0 // JSConsumerMaxRequestBatchNegativeErr consumer max request batch needs to be > 0
JSConsumerMaxRequestBatchNegativeErr ErrorIdentifier = 10114 JSConsumerMaxRequestBatchNegativeErr ErrorIdentifier = 10114
// JSConsumerMaxRequestExpiresToSmall consumer max request expires needs to be >= 1ms // JSConsumerMaxRequestExpiresTooSmall consumer max request expires needs to be >= 1ms
JSConsumerMaxRequestExpiresToSmall ErrorIdentifier = 10115 JSConsumerMaxRequestExpiresTooSmall ErrorIdentifier = 10115
// JSConsumerMaxWaitingNegativeErr consumer max waiting needs to be positive // JSConsumerMaxWaitingNegativeErr consumer max waiting needs to be positive
JSConsumerMaxWaitingNegativeErr ErrorIdentifier = 10087 JSConsumerMaxWaitingNegativeErr ErrorIdentifier = 10087
@@ -185,6 +212,12 @@ const (
// JSConsumerOverlappingSubjectFilters consumer subject filters cannot overlap // JSConsumerOverlappingSubjectFilters consumer subject filters cannot overlap
JSConsumerOverlappingSubjectFilters ErrorIdentifier = 10138 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 Setting PriorityPolicy requires at least one PriorityGroup to be set
JSConsumerPriorityPolicyWithoutGroup ErrorIdentifier = 10159 JSConsumerPriorityPolicyWithoutGroup ErrorIdentifier = 10159
@@ -206,6 +239,9 @@ const (
// JSConsumerReplacementWithDifferentNameErr consumer replacement durable config not the same // JSConsumerReplacementWithDifferentNameErr consumer replacement durable config not the same
JSConsumerReplacementWithDifferentNameErr ErrorIdentifier = 10106 JSConsumerReplacementWithDifferentNameErr ErrorIdentifier = 10106
// JSConsumerReplayPolicyInvalidErr consumer replay policy invalid
JSConsumerReplayPolicyInvalidErr ErrorIdentifier = 10182
// JSConsumerReplicasExceedsStream consumer config replica count exceeds parent stream // JSConsumerReplicasExceedsStream consumer config replica count exceeds parent stream
JSConsumerReplicasExceedsStream ErrorIdentifier = 10126 JSConsumerReplicasExceedsStream ErrorIdentifier = 10126
@@ -248,6 +284,36 @@ const (
// JSMemoryResourcesExceededErr insufficient memory resources available // JSMemoryResourcesExceededErr insufficient memory resources available
JSMemoryResourcesExceededErr ErrorIdentifier = 10028 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 per-message TTL is disabled
JSMessageTTLDisabledErr ErrorIdentifier = 10166 JSMessageTTLDisabledErr ErrorIdentifier = 10166
@@ -275,9 +341,18 @@ const (
// JSMirrorOverlappingSubjectFilters mirror subject filters can not overlap // JSMirrorOverlappingSubjectFilters mirror subject filters can not overlap
JSMirrorOverlappingSubjectFilters ErrorIdentifier = 10152 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 stream mirrors can not have first sequence configured
JSMirrorWithFirstSeqErr ErrorIdentifier = 10143 JSMirrorWithFirstSeqErr ErrorIdentifier = 10143
// JSMirrorWithMsgSchedulesErr stream mirrors can not also schedule messages
JSMirrorWithMsgSchedulesErr ErrorIdentifier = 10186
// JSMirrorWithSourcesErr stream mirrors can not also contain other sources // JSMirrorWithSourcesErr stream mirrors can not also contain other sources
JSMirrorWithSourcesErr ErrorIdentifier = 10031 JSMirrorWithSourcesErr ErrorIdentifier = 10031
@@ -320,6 +395,9 @@ const (
// JSReplicasCountCannotBeNegative replicas count cannot be negative // JSReplicasCountCannotBeNegative replicas count cannot be negative
JSReplicasCountCannotBeNegative ErrorIdentifier = 10133 JSReplicasCountCannotBeNegative ErrorIdentifier = 10133
// JSRequiredApiLevelErr JetStream minimum api level required
JSRequiredApiLevelErr ErrorIdentifier = 10185
// JSRestoreSubscribeFailedErrF JetStream unable to subscribe to restore snapshot {subject}: {err} // JSRestoreSubscribeFailedErrF JetStream unable to subscribe to restore snapshot {subject}: {err}
JSRestoreSubscribeFailedErrF ErrorIdentifier = 10042 JSRestoreSubscribeFailedErrF ErrorIdentifier = 10042
@@ -353,6 +431,9 @@ const (
// JSSourceOverlappingSubjectFilters source filters can not overlap // JSSourceOverlappingSubjectFilters source filters can not overlap
JSSourceOverlappingSubjectFilters ErrorIdentifier = 10147 JSSourceOverlappingSubjectFilters ErrorIdentifier = 10147
// JSSourceWithMsgSchedulesErr stream source can not also schedule messages
JSSourceWithMsgSchedulesErr ErrorIdentifier = 10187
// JSStorageResourcesExceededErr insufficient storage resources available // JSStorageResourcesExceededErr insufficient storage resources available
JSStorageResourcesExceededErr ErrorIdentifier = 10047 JSStorageResourcesExceededErr ErrorIdentifier = 10047
@@ -368,6 +449,9 @@ const (
// JSStreamDuplicateMessageConflict duplicate message id is in process // JSStreamDuplicateMessageConflict duplicate message id is in process
JSStreamDuplicateMessageConflict ErrorIdentifier = 10158 JSStreamDuplicateMessageConflict ErrorIdentifier = 10158
// JSStreamExpectedLastSeqPerSubjectInvalid missing sequence for expected last sequence per subject
JSStreamExpectedLastSeqPerSubjectInvalid ErrorIdentifier = 10193
// JSStreamExpectedLastSeqPerSubjectNotReady expected last sequence per subject temporarily unavailable // JSStreamExpectedLastSeqPerSubjectNotReady expected last sequence per subject temporarily unavailable
JSStreamExpectedLastSeqPerSubjectNotReady ErrorIdentifier = 10163 JSStreamExpectedLastSeqPerSubjectNotReady ErrorIdentifier = 10163
@@ -407,6 +491,9 @@ const (
// JSStreamMessageExceedsMaximumErr message size exceeds maximum allowed // JSStreamMessageExceedsMaximumErr message size exceeds maximum allowed
JSStreamMessageExceedsMaximumErr ErrorIdentifier = 10054 JSStreamMessageExceedsMaximumErr ErrorIdentifier = 10054
// JSStreamMinLastSeqErr min last sequence
JSStreamMinLastSeqErr ErrorIdentifier = 10180
// JSStreamMirrorNotUpdatableErr stream mirror configuration can not be updated // JSStreamMirrorNotUpdatableErr stream mirror configuration can not be updated
JSStreamMirrorNotUpdatableErr ErrorIdentifier = 10055 JSStreamMirrorNotUpdatableErr ErrorIdentifier = 10055
@@ -515,175 +602,204 @@ const (
var ( var (
ApiErrors = map[ErrorIdentifier]*ApiError{ ApiErrors = map[ErrorIdentifier]*ApiError{
JSAccountResourcesExceededErr: {Code: 400, ErrCode: 10002, Description: "resource limits exceeded for account"}, JSAccountResourcesExceededErr: {Code: 400, ErrCode: 10002, Description: "resource limits exceeded for account"},
JSBadRequestErr: {Code: 400, ErrCode: 10003, Description: "bad request"}, JSAtomicPublishDisabledErr: {Code: 400, ErrCode: 10174, Description: "atomic publish is disabled"},
JSClusterIncompleteErr: {Code: 503, ErrCode: 10004, Description: "incomplete results"}, JSAtomicPublishIncompleteBatchErr: {Code: 400, ErrCode: 10176, Description: "atomic publish batch is incomplete"},
JSClusterNoPeersErrF: {Code: 400, ErrCode: 10005, Description: "{err}"}, JSAtomicPublishInvalidBatchIDErr: {Code: 400, ErrCode: 10179, Description: "atomic publish batch ID is invalid"},
JSClusterNotActiveErr: {Code: 500, ErrCode: 10006, Description: "JetStream not in clustered mode"}, JSAtomicPublishMissingSeqErr: {Code: 400, ErrCode: 10175, Description: "atomic publish sequence is missing"},
JSClusterNotAssignedErr: {Code: 500, ErrCode: 10007, Description: "JetStream cluster not assigned to this server"}, JSAtomicPublishTooLargeBatchErrF: {Code: 400, ErrCode: 10199, Description: "atomic publish batch is too large: {size}"},
JSClusterNotAvailErr: {Code: 503, ErrCode: 10008, Description: "JetStream system temporarily unavailable"}, JSAtomicPublishUnsupportedHeaderBatchErr: {Code: 400, ErrCode: 10177, Description: "atomic publish unsupported header used: {header}"},
JSClusterNotLeaderErr: {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"}, JSBadRequestErr: {Code: 400, ErrCode: 10003, Description: "bad request"},
JSClusterPeerNotMemberErr: {Code: 400, ErrCode: 10040, Description: "peer not a member"}, JSClusterIncompleteErr: {Code: 503, ErrCode: 10004, Description: "incomplete results"},
JSClusterRequiredErr: {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"}, JSClusterNoPeersErrF: {Code: 400, ErrCode: 10005, Description: "{err}"},
JSClusterServerNotMemberErr: {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"}, JSClusterNotActiveErr: {Code: 500, ErrCode: 10006, Description: "JetStream not in clustered mode"},
JSClusterTagsErr: {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"}, JSClusterNotAssignedErr: {Code: 500, ErrCode: 10007, Description: "JetStream cluster not assigned to this server"},
JSClusterUnSupportFeatureErr: {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"}, JSClusterNotAvailErr: {Code: 503, ErrCode: 10008, Description: "JetStream system temporarily unavailable"},
JSConsumerAlreadyExists: {Code: 400, ErrCode: 10148, Description: "consumer already exists"}, JSClusterNotLeaderErr: {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"},
JSConsumerBadDurableNameErr: {Code: 400, ErrCode: 10103, Description: "durable name can not contain '.', '*', '>'"}, JSClusterPeerNotMemberErr: {Code: 400, ErrCode: 10040, Description: "peer not a member"},
JSConsumerConfigRequiredErr: {Code: 400, ErrCode: 10078, Description: "consumer config required"}, JSClusterRequiredErr: {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"},
JSConsumerCreateDurableAndNameMismatch: {Code: 400, ErrCode: 10132, Description: "Consumer Durable and Name have to be equal if both are provided"}, JSClusterServerNotMemberErr: {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"},
JSConsumerCreateErrF: {Code: 500, ErrCode: 10012, Description: "{err}"}, JSClusterTagsErr: {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"},
JSConsumerCreateFilterSubjectMismatchErr: {Code: 400, ErrCode: 10131, Description: "Consumer create request did not match filtered subject from create subject"}, JSClusterUnSupportFeatureErr: {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"},
JSConsumerDeliverCycleErr: {Code: 400, ErrCode: 10081, Description: "consumer deliver subject forms a cycle"}, JSConsumerAckPolicyInvalidErr: {Code: 400, ErrCode: 10181, Description: "consumer ack policy invalid"},
JSConsumerDeliverToWildcardsErr: {Code: 400, ErrCode: 10079, Description: "consumer deliver subject has wildcards"}, JSConsumerAckWaitNegativeErr: {Code: 400, ErrCode: 10183, Description: "consumer ack wait needs to be positive"},
JSConsumerDescriptionTooLongErrF: {Code: 400, ErrCode: 10107, Description: "consumer description is too long, maximum allowed is {max}"}, JSConsumerAlreadyExists: {Code: 400, ErrCode: 10148, Description: "consumer already exists"},
JSConsumerDirectRequiresEphemeralErr: {Code: 400, ErrCode: 10091, Description: "consumer direct requires an ephemeral consumer"}, JSConsumerBackOffNegativeErr: {Code: 400, ErrCode: 10184, Description: "consumer backoff needs to be positive"},
JSConsumerDirectRequiresPushErr: {Code: 400, ErrCode: 10090, Description: "consumer direct requires a push based consumer"}, JSConsumerBadDurableNameErr: {Code: 400, ErrCode: 10103, Description: "durable name can not contain '.', '*', '>'"},
JSConsumerDoesNotExist: {Code: 400, ErrCode: 10149, Description: "consumer does not exist"}, JSConsumerConfigRequiredErr: {Code: 400, ErrCode: 10078, Description: "consumer config required"},
JSConsumerDuplicateFilterSubjects: {Code: 400, ErrCode: 10136, Description: "consumer cannot have both FilterSubject and FilterSubjects specified"}, JSConsumerCreateDurableAndNameMismatch: {Code: 400, ErrCode: 10132, Description: "Consumer Durable and Name have to be equal if both are provided"},
JSConsumerDurableNameNotInSubjectErr: {Code: 400, ErrCode: 10016, Description: "consumer expected to be durable but no durable name set in subject"}, JSConsumerCreateErrF: {Code: 500, ErrCode: 10012, Description: "{err}"},
JSConsumerDurableNameNotMatchSubjectErr: {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"}, JSConsumerCreateFilterSubjectMismatchErr: {Code: 400, ErrCode: 10131, Description: "Consumer create request did not match filtered subject from create subject"},
JSConsumerDurableNameNotSetErr: {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"}, JSConsumerDeliverCycleErr: {Code: 400, ErrCode: 10081, Description: "consumer deliver subject forms a cycle"},
JSConsumerEmptyFilter: {Code: 400, ErrCode: 10139, Description: "consumer filter in FilterSubjects cannot be empty"}, JSConsumerDeliverToWildcardsErr: {Code: 400, ErrCode: 10079, Description: "consumer deliver subject has wildcards"},
JSConsumerEmptyGroupName: {Code: 400, ErrCode: 10161, Description: "Group name cannot be an empty string"}, JSConsumerDescriptionTooLongErrF: {Code: 400, ErrCode: 10107, Description: "consumer description is too long, maximum allowed is {max}"},
JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"}, JSConsumerDirectRequiresEphemeralErr: {Code: 400, ErrCode: 10091, Description: "consumer direct requires an ephemeral consumer"},
JSConsumerEphemeralWithDurableNameErr: {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"}, JSConsumerDirectRequiresPushErr: {Code: 400, ErrCode: 10090, Description: "consumer direct requires a push based consumer"},
JSConsumerExistingActiveErr: {Code: 400, ErrCode: 10105, Description: "consumer already exists and is still active"}, JSConsumerDoesNotExist: {Code: 400, ErrCode: 10149, Description: "consumer does not exist"},
JSConsumerFCRequiresPushErr: {Code: 400, ErrCode: 10089, Description: "consumer flow control requires a push based consumer"}, JSConsumerDuplicateFilterSubjects: {Code: 400, ErrCode: 10136, Description: "consumer cannot have both FilterSubject and FilterSubjects specified"},
JSConsumerFilterNotSubsetErr: {Code: 400, ErrCode: 10093, Description: "consumer filter subject is not a valid subset of the interest subjects"}, JSConsumerDurableNameNotInSubjectErr: {Code: 400, ErrCode: 10016, Description: "consumer expected to be durable but no durable name set in subject"},
JSConsumerHBRequiresPushErr: {Code: 400, ErrCode: 10088, Description: "consumer idle heartbeat requires a push based consumer"}, JSConsumerDurableNameNotMatchSubjectErr: {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"},
JSConsumerInactiveThresholdExcess: {Code: 400, ErrCode: 10153, Description: "consumer inactive threshold exceeds system limit of {limit}"}, JSConsumerDurableNameNotSetErr: {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"},
JSConsumerInvalidDeliverSubject: {Code: 400, ErrCode: 10112, Description: "invalid push consumer deliver subject"}, JSConsumerEmptyFilter: {Code: 400, ErrCode: 10139, Description: "consumer filter in FilterSubjects cannot be empty"},
JSConsumerInvalidGroupNameErr: {Code: 400, ErrCode: 10162, Description: "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters"}, JSConsumerEmptyGroupName: {Code: 400, ErrCode: 10161, Description: "Group name cannot be an empty string"},
JSConsumerInvalidPolicyErrF: {Code: 400, ErrCode: 10094, Description: "{err}"}, JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"},
JSConsumerInvalidPriorityGroupErr: {Code: 400, ErrCode: 10160, Description: "Provided priority group does not exist for this consumer"}, JSConsumerEphemeralWithDurableNameErr: {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"},
JSConsumerInvalidSamplingErrF: {Code: 400, ErrCode: 10095, Description: "failed to parse consumer sampling configuration: {err}"}, JSConsumerExistingActiveErr: {Code: 400, ErrCode: 10105, Description: "consumer already exists and is still active"},
JSConsumerMaxDeliverBackoffErr: {Code: 400, ErrCode: 10116, Description: "max deliver is required to be > length of backoff values"}, JSConsumerFCRequiresPushErr: {Code: 400, ErrCode: 10089, Description: "consumer flow control requires a push based consumer"},
JSConsumerMaxPendingAckExcessErrF: {Code: 400, ErrCode: 10121, Description: "consumer max ack pending exceeds system limit of {limit}"}, JSConsumerFilterNotSubsetErr: {Code: 400, ErrCode: 10093, Description: "consumer filter subject is not a valid subset of the interest subjects"},
JSConsumerMaxPendingAckPolicyRequiredErr: {Code: 400, ErrCode: 10082, Description: "consumer requires ack policy for max ack pending"}, JSConsumerHBRequiresPushErr: {Code: 400, ErrCode: 10088, Description: "consumer idle heartbeat requires a push based consumer"},
JSConsumerMaxRequestBatchExceededF: {Code: 400, ErrCode: 10125, Description: "consumer max request batch exceeds server limit of {limit}"}, JSConsumerInactiveThresholdExcess: {Code: 400, ErrCode: 10153, Description: "consumer inactive threshold exceeds system limit of {limit}"},
JSConsumerMaxRequestBatchNegativeErr: {Code: 400, ErrCode: 10114, Description: "consumer max request batch needs to be > 0"}, JSConsumerInvalidDeliverSubject: {Code: 400, ErrCode: 10112, Description: "invalid push consumer deliver subject"},
JSConsumerMaxRequestExpiresToSmall: {Code: 400, ErrCode: 10115, Description: "consumer max request expires needs to be >= 1ms"}, JSConsumerInvalidGroupNameErr: {Code: 400, ErrCode: 10162, Description: "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters"},
JSConsumerMaxWaitingNegativeErr: {Code: 400, ErrCode: 10087, Description: "consumer max waiting needs to be positive"}, JSConsumerInvalidPolicyErrF: {Code: 400, ErrCode: 10094, Description: "{err}"},
JSConsumerMetadataLengthErrF: {Code: 400, ErrCode: 10135, Description: "consumer metadata exceeds maximum size of {limit}"}, JSConsumerInvalidPriorityGroupErr: {Code: 400, ErrCode: 10160, Description: "Provided priority group does not exist for this consumer"},
JSConsumerMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10137, Description: "consumer with multiple subject filters cannot use subject based API"}, JSConsumerInvalidSamplingErrF: {Code: 400, ErrCode: 10095, Description: "failed to parse consumer sampling configuration: {err}"},
JSConsumerNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10127, Description: "Consumer name can not contain path separators"}, JSConsumerMaxDeliverBackoffErr: {Code: 400, ErrCode: 10116, Description: "max deliver is required to be > length of backoff values"},
JSConsumerNameExistErr: {Code: 400, ErrCode: 10013, Description: "consumer name already in use"}, JSConsumerMaxPendingAckExcessErrF: {Code: 400, ErrCode: 10121, Description: "consumer max ack pending exceeds system limit of {limit}"},
JSConsumerNameTooLongErrF: {Code: 400, ErrCode: 10102, Description: "consumer name is too long, maximum allowed is {max}"}, JSConsumerMaxPendingAckPolicyRequiredErr: {Code: 400, ErrCode: 10082, Description: "consumer requires ack policy for max ack pending"},
JSConsumerNotFoundErr: {Code: 404, ErrCode: 10014, Description: "consumer not found"}, JSConsumerMaxRequestBatchExceededF: {Code: 400, ErrCode: 10125, Description: "consumer max request batch exceeds server limit of {limit}"},
JSConsumerOfflineErr: {Code: 500, ErrCode: 10119, Description: "consumer is offline"}, JSConsumerMaxRequestBatchNegativeErr: {Code: 400, ErrCode: 10114, Description: "consumer max request batch needs to be > 0"},
JSConsumerOfflineReasonErrF: {Code: 500, ErrCode: 10195, Description: "consumer is offline: {err}"}, JSConsumerMaxRequestExpiresTooSmall: {Code: 400, ErrCode: 10115, Description: "consumer max request expires needs to be >= 1ms"},
JSConsumerOnMappedErr: {Code: 400, ErrCode: 10092, Description: "consumer direct on a mapped consumer"}, JSConsumerMaxWaitingNegativeErr: {Code: 400, ErrCode: 10087, Description: "consumer max waiting needs to be positive"},
JSConsumerOverlappingSubjectFilters: {Code: 400, ErrCode: 10138, Description: "consumer subject filters cannot overlap"}, JSConsumerMetadataLengthErrF: {Code: 400, ErrCode: 10135, Description: "consumer metadata exceeds maximum size of {limit}"},
JSConsumerPriorityPolicyWithoutGroup: {Code: 400, ErrCode: 10159, Description: "Setting PriorityPolicy requires at least one PriorityGroup to be set"}, JSConsumerMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10137, Description: "consumer with multiple subject filters cannot use subject based API"},
JSConsumerPullNotDurableErr: {Code: 400, ErrCode: 10085, Description: "consumer in pull mode requires a durable name"}, JSConsumerNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10127, Description: "Consumer name can not contain path separators"},
JSConsumerPullRequiresAckErr: {Code: 400, ErrCode: 10084, Description: "consumer in pull mode requires explicit ack policy on workqueue stream"}, JSConsumerNameExistErr: {Code: 400, ErrCode: 10013, Description: "consumer name already in use"},
JSConsumerPullWithRateLimitErr: {Code: 400, ErrCode: 10086, Description: "consumer in pull mode can not have rate limit set"}, JSConsumerNameTooLongErrF: {Code: 400, ErrCode: 10102, Description: "consumer name is too long, maximum allowed is {max}"},
JSConsumerPushMaxWaitingErr: {Code: 400, ErrCode: 10080, Description: "consumer in push mode can not set max waiting"}, JSConsumerNotFoundErr: {Code: 404, ErrCode: 10014, Description: "consumer not found"},
JSConsumerPushWithPriorityGroupErr: {Code: 400, ErrCode: 10178, Description: "priority groups can not be used with push consumers"}, JSConsumerOfflineErr: {Code: 500, ErrCode: 10119, Description: "consumer is offline"},
JSConsumerReplacementWithDifferentNameErr: {Code: 400, ErrCode: 10106, Description: "consumer replacement durable config not the same"}, JSConsumerOfflineReasonErrF: {Code: 500, ErrCode: 10195, Description: "consumer is offline: {err}"},
JSConsumerReplicasExceedsStream: {Code: 400, ErrCode: 10126, Description: "consumer config replica count exceeds parent stream"}, JSConsumerOnMappedErr: {Code: 400, ErrCode: 10092, Description: "consumer direct on a mapped consumer"},
JSConsumerReplicasShouldMatchStream: {Code: 400, ErrCode: 10134, Description: "consumer config replicas must match interest retention stream's replicas"}, JSConsumerOverlappingSubjectFilters: {Code: 400, ErrCode: 10138, Description: "consumer subject filters cannot overlap"},
JSConsumerSmallHeartbeatErr: {Code: 400, ErrCode: 10083, Description: "consumer idle heartbeat needs to be >= 100ms"}, JSConsumerPinnedTTLWithoutPriorityPolicyNone: {Code: 400, ErrCode: 10197, Description: "PinnedTTL cannot be set when PriorityPolicy is none"},
JSConsumerStoreFailedErrF: {Code: 500, ErrCode: 10104, Description: "error creating store for consumer: {err}"}, JSConsumerPriorityGroupWithPolicyNone: {Code: 400, ErrCode: 10196, Description: "consumer can not have priority groups when policy is none"},
JSConsumerWQConsumerNotDeliverAllErr: {Code: 400, ErrCode: 10101, Description: "consumer must be deliver all on workqueue stream"}, JSConsumerPriorityPolicyWithoutGroup: {Code: 400, ErrCode: 10159, Description: "Setting PriorityPolicy requires at least one PriorityGroup to be set"},
JSConsumerWQConsumerNotUniqueErr: {Code: 400, ErrCode: 10100, Description: "filtered consumer not unique on workqueue stream"}, JSConsumerPullNotDurableErr: {Code: 400, ErrCode: 10085, Description: "consumer in pull mode requires a durable name"},
JSConsumerWQMultipleUnfilteredErr: {Code: 400, ErrCode: 10099, Description: "multiple non-filtered consumers not allowed on workqueue stream"}, JSConsumerPullRequiresAckErr: {Code: 400, ErrCode: 10084, Description: "consumer in pull mode requires explicit ack policy on workqueue stream"},
JSConsumerWQRequiresExplicitAckErr: {Code: 400, ErrCode: 10098, Description: "workqueue stream requires explicit ack"}, JSConsumerPullWithRateLimitErr: {Code: 400, ErrCode: 10086, Description: "consumer in pull mode can not have rate limit set"},
JSConsumerWithFlowControlNeedsHeartbeats: {Code: 400, ErrCode: 10108, Description: "consumer with flow control also needs heartbeats"}, JSConsumerPushMaxWaitingErr: {Code: 400, ErrCode: 10080, Description: "consumer in push mode can not set max waiting"},
JSInsufficientResourcesErr: {Code: 503, ErrCode: 10023, Description: "insufficient resources"}, JSConsumerPushWithPriorityGroupErr: {Code: 400, ErrCode: 10178, Description: "priority groups can not be used with push consumers"},
JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON: {err}"}, JSConsumerReplacementWithDifferentNameErr: {Code: 400, ErrCode: 10106, Description: "consumer replacement durable config not the same"},
JSMaximumConsumersLimitErr: {Code: 400, ErrCode: 10026, Description: "maximum consumers limit reached"}, JSConsumerReplayPolicyInvalidErr: {Code: 400, ErrCode: 10182, Description: "consumer replay policy invalid"},
JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"}, JSConsumerReplicasExceedsStream: {Code: 400, ErrCode: 10126, Description: "consumer config replica count exceeds parent stream"},
JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"}, JSConsumerReplicasShouldMatchStream: {Code: 400, ErrCode: 10134, Description: "consumer config replicas must match interest retention stream's replicas"},
JSMessageTTLDisabledErr: {Code: 400, ErrCode: 10166, Description: "per-message TTL is disabled"}, JSConsumerSmallHeartbeatErr: {Code: 400, ErrCode: 10083, Description: "consumer idle heartbeat needs to be >= 100ms"},
JSMessageTTLInvalidErr: {Code: 400, ErrCode: 10165, Description: "invalid per-message TTL"}, JSConsumerStoreFailedErrF: {Code: 500, ErrCode: 10104, Description: "error creating store for consumer: {err}"},
JSMirrorConsumerSetupFailedErrF: {Code: 500, ErrCode: 10029, Description: "{err}"}, JSConsumerWQConsumerNotDeliverAllErr: {Code: 400, ErrCode: 10101, Description: "consumer must be deliver all on workqueue stream"},
JSMirrorInvalidStreamName: {Code: 400, ErrCode: 10142, Description: "mirrored stream name is invalid"}, JSConsumerWQConsumerNotUniqueErr: {Code: 400, ErrCode: 10100, Description: "filtered consumer not unique on workqueue stream"},
JSMirrorInvalidSubjectFilter: {Code: 400, ErrCode: 10151, Description: "mirror transform source: {err}"}, JSConsumerWQMultipleUnfilteredErr: {Code: 400, ErrCode: 10099, Description: "multiple non-filtered consumers not allowed on workqueue stream"},
JSMirrorInvalidTransformDestination: {Code: 400, ErrCode: 10154, Description: "mirror transform: {err}"}, JSConsumerWQRequiresExplicitAckErr: {Code: 400, ErrCode: 10098, Description: "workqueue stream requires explicit ack"},
JSMirrorMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"}, JSConsumerWithFlowControlNeedsHeartbeats: {Code: 400, ErrCode: 10108, Description: "consumer with flow control also needs heartbeats"},
JSMirrorMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10150, Description: "mirror with multiple subject transforms cannot also have a single subject filter"}, JSInsufficientResourcesErr: {Code: 503, ErrCode: 10023, Description: "insufficient resources"},
JSMirrorOverlappingSubjectFilters: {Code: 400, ErrCode: 10152, Description: "mirror subject filters can not overlap"}, JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON: {err}"},
JSMirrorWithFirstSeqErr: {Code: 400, ErrCode: 10143, Description: "stream mirrors can not have first sequence configured"}, JSMaximumConsumersLimitErr: {Code: 400, ErrCode: 10026, Description: "maximum consumers limit reached"},
JSMirrorWithSourcesErr: {Code: 400, ErrCode: 10031, Description: "stream mirrors can not also contain other sources"}, JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"},
JSMirrorWithStartSeqAndTimeErr: {Code: 400, ErrCode: 10032, Description: "stream mirrors can not have both start seq and start time configured"}, JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"},
JSMirrorWithSubjectFiltersErr: {Code: 400, ErrCode: 10033, Description: "stream mirrors can not contain filtered subjects"}, JSMessageCounterBrokenErr: {Code: 400, ErrCode: 10172, Description: "message counter is broken"},
JSMirrorWithSubjectsErr: {Code: 400, ErrCode: 10034, Description: "stream mirrors can not contain subjects"}, JSMessageIncrDisabledErr: {Code: 400, ErrCode: 10168, Description: "message counters is disabled"},
JSNoAccountErr: {Code: 503, ErrCode: 10035, Description: "account not found"}, JSMessageIncrInvalidErr: {Code: 400, ErrCode: 10171, Description: "message counter increment is invalid"},
JSNoLimitsErr: {Code: 400, ErrCode: 10120, Description: "no JetStream default or applicable tiered limit present"}, JSMessageIncrMissingErr: {Code: 400, ErrCode: 10169, Description: "message counter increment is missing"},
JSNoMessageFoundErr: {Code: 404, ErrCode: 10037, Description: "no message found"}, JSMessageIncrPayloadErr: {Code: 400, ErrCode: 10170, Description: "message counter has payload"},
JSNotEmptyRequestErr: {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"}, JSMessageSchedulesDisabledErr: {Code: 400, ErrCode: 10188, Description: "message schedules is disabled"},
JSNotEnabledErr: {Code: 503, ErrCode: 10076, Description: "JetStream not enabled"}, JSMessageSchedulesPatternInvalidErr: {Code: 400, ErrCode: 10189, Description: "message schedules pattern is invalid"},
JSNotEnabledForAccountErr: {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account"}, JSMessageSchedulesRollupInvalidErr: {Code: 400, ErrCode: 10192, Description: "message schedules invalid rollup"},
JSPedanticErrF: {Code: 400, ErrCode: 10157, Description: "pedantic mode: {err}"}, JSMessageSchedulesTTLInvalidErr: {Code: 400, ErrCode: 10191, Description: "message schedules invalid per-message TTL"},
JSPeerRemapErr: {Code: 503, ErrCode: 10075, Description: "peer remap failed"}, JSMessageSchedulesTargetInvalidErr: {Code: 400, ErrCode: 10190, Description: "message schedules target is invalid"},
JSRaftGeneralErrF: {Code: 500, ErrCode: 10041, Description: "{err}"}, JSMessageTTLDisabledErr: {Code: 400, ErrCode: 10166, Description: "per-message TTL is disabled"},
JSReplicasCountCannotBeNegative: {Code: 400, ErrCode: 10133, Description: "replicas count cannot be negative"}, JSMessageTTLInvalidErr: {Code: 400, ErrCode: 10165, Description: "invalid per-message TTL"},
JSRestoreSubscribeFailedErrF: {Code: 500, ErrCode: 10042, Description: "JetStream unable to subscribe to restore snapshot {subject}: {err}"}, JSMirrorConsumerSetupFailedErrF: {Code: 500, ErrCode: 10029, Description: "{err}"},
JSSequenceNotFoundErrF: {Code: 400, ErrCode: 10043, Description: "sequence {seq} not found"}, JSMirrorInvalidStreamName: {Code: 400, ErrCode: 10142, Description: "mirrored stream name is invalid"},
JSSnapshotDeliverSubjectInvalidErr: {Code: 400, ErrCode: 10015, Description: "deliver subject not valid"}, JSMirrorInvalidSubjectFilter: {Code: 400, ErrCode: 10151, Description: "mirror transform source: {err}"},
JSSourceConsumerSetupFailedErrF: {Code: 500, ErrCode: 10045, Description: "{err}"}, JSMirrorInvalidTransformDestination: {Code: 400, ErrCode: 10154, Description: "mirror transform: {err}"},
JSSourceDuplicateDetected: {Code: 400, ErrCode: 10140, Description: "duplicate source configuration detected"}, JSMirrorMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"},
JSSourceInvalidStreamName: {Code: 400, ErrCode: 10141, Description: "sourced stream name is invalid"}, JSMirrorMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10150, Description: "mirror with multiple subject transforms cannot also have a single subject filter"},
JSSourceInvalidSubjectFilter: {Code: 400, ErrCode: 10145, Description: "source transform source: {err}"}, JSMirrorOverlappingSubjectFilters: {Code: 400, ErrCode: 10152, Description: "mirror subject filters can not overlap"},
JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform: {err}"}, JSMirrorWithAtomicPublishErr: {Code: 400, ErrCode: 10198, Description: "stream mirrors can not also use atomic publishing"},
JSSourceMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"}, JSMirrorWithCountersErr: {Code: 400, ErrCode: 10173, Description: "stream mirrors can not also calculate counters"},
JSSourceMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10144, Description: "source with multiple subject transforms cannot also have a single subject filter"}, JSMirrorWithFirstSeqErr: {Code: 400, ErrCode: 10143, Description: "stream mirrors can not have first sequence configured"},
JSSourceOverlappingSubjectFilters: {Code: 400, ErrCode: 10147, Description: "source filters can not overlap"}, JSMirrorWithMsgSchedulesErr: {Code: 400, ErrCode: 10186, Description: "stream mirrors can not also schedule messages"},
JSStorageResourcesExceededErr: {Code: 500, ErrCode: 10047, Description: "insufficient storage resources available"}, JSMirrorWithSourcesErr: {Code: 400, ErrCode: 10031, Description: "stream mirrors can not also contain other sources"},
JSStreamAssignmentErrF: {Code: 500, ErrCode: 10048, Description: "{err}"}, JSMirrorWithStartSeqAndTimeErr: {Code: 400, ErrCode: 10032, Description: "stream mirrors can not have both start seq and start time configured"},
JSStreamCreateErrF: {Code: 500, ErrCode: 10049, Description: "{err}"}, JSMirrorWithSubjectFiltersErr: {Code: 400, ErrCode: 10033, Description: "stream mirrors can not contain filtered subjects"},
JSStreamDeleteErrF: {Code: 500, ErrCode: 10050, Description: "{err}"}, JSMirrorWithSubjectsErr: {Code: 400, ErrCode: 10034, Description: "stream mirrors can not contain subjects"},
JSStreamDuplicateMessageConflict: {Code: 409, ErrCode: 10158, Description: "duplicate message id is in process"}, JSNoAccountErr: {Code: 503, ErrCode: 10035, Description: "account not found"},
JSStreamExpectedLastSeqPerSubjectNotReady: {Code: 503, ErrCode: 10163, Description: "expected last sequence per subject temporarily unavailable"}, JSNoLimitsErr: {Code: 400, ErrCode: 10120, Description: "no JetStream default or applicable tiered limit present"},
JSStreamExternalApiOverlapErrF: {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"}, JSNoMessageFoundErr: {Code: 404, ErrCode: 10037, Description: "no message found"},
JSStreamExternalDelPrefixOverlapsErrF: {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"}, JSNotEmptyRequestErr: {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"},
JSStreamGeneralErrorF: {Code: 500, ErrCode: 10051, Description: "{err}"}, JSNotEnabledErr: {Code: 503, ErrCode: 10076, Description: "JetStream not enabled"},
JSStreamHeaderExceedsMaximumErr: {Code: 400, ErrCode: 10097, Description: "header size exceeds maximum allowed of 64k"}, JSNotEnabledForAccountErr: {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account"},
JSStreamInfoMaxSubjectsErr: {Code: 500, ErrCode: 10117, Description: "subject details would exceed maximum allowed"}, JSPedanticErrF: {Code: 400, ErrCode: 10157, Description: "pedantic mode: {err}"},
JSStreamInvalidConfigF: {Code: 500, ErrCode: 10052, Description: "{err}"}, JSPeerRemapErr: {Code: 503, ErrCode: 10075, Description: "peer remap failed"},
JSStreamInvalidErr: {Code: 500, ErrCode: 10096, Description: "stream not valid"}, JSRaftGeneralErrF: {Code: 500, ErrCode: 10041, Description: "{err}"},
JSStreamInvalidExternalDeliverySubjErrF: {Code: 400, ErrCode: 10024, Description: "stream external delivery prefix {prefix} must not contain wildcards"}, JSReplicasCountCannotBeNegative: {Code: 400, ErrCode: 10133, Description: "replicas count cannot be negative"},
JSStreamLimitsErrF: {Code: 500, ErrCode: 10053, Description: "{err}"}, JSRequiredApiLevelErr: {Code: 412, ErrCode: 10185, Description: "JetStream minimum api level required"},
JSStreamMaxBytesRequired: {Code: 400, ErrCode: 10113, Description: "account requires a stream config to have max bytes set"}, JSRestoreSubscribeFailedErrF: {Code: 500, ErrCode: 10042, Description: "JetStream unable to subscribe to restore snapshot {subject}: {err}"},
JSStreamMaxStreamBytesExceeded: {Code: 400, ErrCode: 10122, Description: "stream max bytes exceeds account limit max stream bytes"}, JSSequenceNotFoundErrF: {Code: 400, ErrCode: 10043, Description: "sequence {seq} not found"},
JSStreamMessageExceedsMaximumErr: {Code: 400, ErrCode: 10054, Description: "message size exceeds maximum allowed"}, JSSnapshotDeliverSubjectInvalidErr: {Code: 400, ErrCode: 10015, Description: "deliver subject not valid"},
JSStreamMirrorNotUpdatableErr: {Code: 400, ErrCode: 10055, Description: "stream mirror configuration can not be updated"}, JSSourceConsumerSetupFailedErrF: {Code: 500, ErrCode: 10045, Description: "{err}"},
JSStreamMismatchErr: {Code: 400, ErrCode: 10056, Description: "stream name in subject does not match request"}, JSSourceDuplicateDetected: {Code: 400, ErrCode: 10140, Description: "duplicate source configuration detected"},
JSStreamMoveAndScaleErr: {Code: 400, ErrCode: 10123, Description: "can not move and scale a stream in a single update"}, JSSourceInvalidStreamName: {Code: 400, ErrCode: 10141, Description: "sourced stream name is invalid"},
JSStreamMoveInProgressF: {Code: 400, ErrCode: 10124, Description: "stream move already in progress: {msg}"}, JSSourceInvalidSubjectFilter: {Code: 400, ErrCode: 10145, Description: "source transform source: {err}"},
JSStreamMoveNotInProgress: {Code: 400, ErrCode: 10129, Description: "stream move not in progress"}, JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform: {err}"},
JSStreamMsgDeleteFailedF: {Code: 500, ErrCode: 10057, Description: "{err}"}, JSSourceMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"},
JSStreamNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10128, Description: "Stream name can not contain path separators"}, JSSourceMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10144, Description: "source with multiple subject transforms cannot also have a single subject filter"},
JSStreamNameExistErr: {Code: 400, ErrCode: 10058, Description: "stream name already in use with a different configuration"}, JSSourceOverlappingSubjectFilters: {Code: 400, ErrCode: 10147, Description: "source filters can not overlap"},
JSStreamNameExistRestoreFailedErr: {Code: 400, ErrCode: 10130, Description: "stream name already in use, cannot restore"}, JSSourceWithMsgSchedulesErr: {Code: 400, ErrCode: 10187, Description: "stream source can not also schedule messages"},
JSStreamNotFoundErr: {Code: 404, ErrCode: 10059, Description: "stream not found"}, JSStorageResourcesExceededErr: {Code: 500, ErrCode: 10047, Description: "insufficient storage resources available"},
JSStreamNotMatchErr: {Code: 400, ErrCode: 10060, Description: "expected stream does not match"}, JSStreamAssignmentErrF: {Code: 500, ErrCode: 10048, Description: "{err}"},
JSStreamOfflineErr: {Code: 500, ErrCode: 10118, Description: "stream is offline"}, JSStreamCreateErrF: {Code: 500, ErrCode: 10049, Description: "{err}"},
JSStreamOfflineReasonErrF: {Code: 500, ErrCode: 10194, Description: "stream is offline: {err}"}, JSStreamDeleteErrF: {Code: 500, ErrCode: 10050, Description: "{err}"},
JSStreamPurgeFailedF: {Code: 500, ErrCode: 10110, Description: "{err}"}, JSStreamDuplicateMessageConflict: {Code: 409, ErrCode: 10158, Description: "duplicate message id is in process"},
JSStreamReplicasNotSupportedErr: {Code: 500, ErrCode: 10074, Description: "replicas > 1 not supported in non-clustered mode"}, JSStreamExpectedLastSeqPerSubjectInvalid: {Code: 400, ErrCode: 10193, Description: "missing sequence for expected last sequence per subject"},
JSStreamReplicasNotUpdatableErr: {Code: 400, ErrCode: 10061, Description: "Replicas configuration can not be updated"}, JSStreamExpectedLastSeqPerSubjectNotReady: {Code: 503, ErrCode: 10163, Description: "expected last sequence per subject temporarily unavailable"},
JSStreamRestoreErrF: {Code: 500, ErrCode: 10062, Description: "restore failed: {err}"}, JSStreamExternalApiOverlapErrF: {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"},
JSStreamRollupFailedF: {Code: 500, ErrCode: 10111, Description: "{err}"}, JSStreamExternalDelPrefixOverlapsErrF: {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"},
JSStreamSealedErr: {Code: 400, ErrCode: 10109, Description: "invalid operation on sealed stream"}, JSStreamGeneralErrorF: {Code: 500, ErrCode: 10051, Description: "{err}"},
JSStreamSequenceNotMatchErr: {Code: 503, ErrCode: 10063, Description: "expected stream sequence does not match"}, JSStreamHeaderExceedsMaximumErr: {Code: 400, ErrCode: 10097, Description: "header size exceeds maximum allowed of 64k"},
JSStreamSnapshotErrF: {Code: 500, ErrCode: 10064, Description: "snapshot failed: {err}"}, JSStreamInfoMaxSubjectsErr: {Code: 500, ErrCode: 10117, Description: "subject details would exceed maximum allowed"},
JSStreamStoreFailedF: {Code: 503, ErrCode: 10077, Description: "{err}"}, JSStreamInvalidConfigF: {Code: 500, ErrCode: 10052, Description: "{err}"},
JSStreamSubjectOverlapErr: {Code: 400, ErrCode: 10065, Description: "subjects overlap with an existing stream"}, JSStreamInvalidErr: {Code: 500, ErrCode: 10096, Description: "stream not valid"},
JSStreamTemplateCreateErrF: {Code: 500, ErrCode: 10066, Description: "{err}"}, JSStreamInvalidExternalDeliverySubjErrF: {Code: 400, ErrCode: 10024, Description: "stream external delivery prefix {prefix} must not contain wildcards"},
JSStreamTemplateDeleteErrF: {Code: 500, ErrCode: 10067, Description: "{err}"}, JSStreamLimitsErrF: {Code: 500, ErrCode: 10053, Description: "{err}"},
JSStreamTemplateNotFoundErr: {Code: 404, ErrCode: 10068, Description: "template not found"}, JSStreamMaxBytesRequired: {Code: 400, ErrCode: 10113, Description: "account requires a stream config to have max bytes set"},
JSStreamTooManyRequests: {Code: 429, ErrCode: 10167, Description: "too many requests"}, JSStreamMaxStreamBytesExceeded: {Code: 400, ErrCode: 10122, Description: "stream max bytes exceeds account limit max stream bytes"},
JSStreamTransformInvalidDestination: {Code: 400, ErrCode: 10156, Description: "stream transform: {err}"}, JSStreamMessageExceedsMaximumErr: {Code: 400, ErrCode: 10054, Description: "message size exceeds maximum allowed"},
JSStreamTransformInvalidSource: {Code: 400, ErrCode: 10155, Description: "stream transform source: {err}"}, JSStreamMinLastSeqErr: {Code: 412, ErrCode: 10180, Description: "min last sequence"},
JSStreamUpdateErrF: {Code: 500, ErrCode: 10069, Description: "{err}"}, JSStreamMirrorNotUpdatableErr: {Code: 400, ErrCode: 10055, Description: "stream mirror configuration can not be updated"},
JSStreamWrongLastMsgIDErrF: {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"}, JSStreamMismatchErr: {Code: 400, ErrCode: 10056, Description: "stream name in subject does not match request"},
JSStreamWrongLastSequenceConstantErr: {Code: 400, ErrCode: 10164, Description: "wrong last sequence"}, JSStreamMoveAndScaleErr: {Code: 400, ErrCode: 10123, Description: "can not move and scale a stream in a single update"},
JSStreamWrongLastSequenceErrF: {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"}, JSStreamMoveInProgressF: {Code: 400, ErrCode: 10124, Description: "stream move already in progress: {msg}"},
JSTempStorageFailedErr: {Code: 500, ErrCode: 10072, Description: "JetStream unable to open temp storage for restore"}, JSStreamMoveNotInProgress: {Code: 400, ErrCode: 10129, Description: "stream move not in progress"},
JSTemplateNameNotMatchSubjectErr: {Code: 400, ErrCode: 10073, Description: "template name in subject does not match request"}, 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 Deprecated by JSClusterNotActiveErr ApiError, use IsNatsError() for comparisons
ErrJetStreamNotClustered = ApiErrors[JSClusterNotActiveErr] ErrJetStreamNotClustered = ApiErrors[JSClusterNotActiveErr]
@@ -719,6 +835,78 @@ func NewJSAccountResourcesExceededError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSAccountResourcesExceededErr] 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" // NewJSBadRequestError creates a new JSBadRequestErr error: "bad request"
func NewJSBadRequestError(opts ...ErrorOption) *ApiError { func NewJSBadRequestError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -845,6 +1033,26 @@ func NewJSClusterUnSupportFeatureError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSClusterUnSupportFeatureErr] 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" // NewJSConsumerAlreadyExistsError creates a new JSConsumerAlreadyExists error: "consumer already exists"
func NewJSConsumerAlreadyExistsError(opts ...ErrorOption) *ApiError { func NewJSConsumerAlreadyExistsError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -855,6 +1063,16 @@ func NewJSConsumerAlreadyExistsError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerAlreadyExists] 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 '.', '*', '>'" // NewJSConsumerBadDurableNameError creates a new JSConsumerBadDurableNameErr error: "durable name can not contain '.', '*', '>'"
func NewJSConsumerBadDurableNameError(opts ...ErrorOption) *ApiError { func NewJSConsumerBadDurableNameError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -1237,14 +1455,14 @@ func NewJSConsumerMaxRequestBatchNegativeError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerMaxRequestBatchNegativeErr] return ApiErrors[JSConsumerMaxRequestBatchNegativeErr]
} }
// NewJSConsumerMaxRequestExpiresToSmallError creates a new JSConsumerMaxRequestExpiresToSmall error: "consumer max request expires needs to be >= 1ms" // NewJSConsumerMaxRequestExpiresTooSmallError creates a new JSConsumerMaxRequestExpiresTooSmall error: "consumer max request expires needs to be >= 1ms"
func NewJSConsumerMaxRequestExpiresToSmallError(opts ...ErrorOption) *ApiError { func NewJSConsumerMaxRequestExpiresTooSmallError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok { if ae, ok := eopts.err.(*ApiError); ok {
return ae return ae
} }
return ApiErrors[JSConsumerMaxRequestExpiresToSmall] return ApiErrors[JSConsumerMaxRequestExpiresTooSmall]
} }
// NewJSConsumerMaxWaitingNegativeError creates a new JSConsumerMaxWaitingNegativeErr error: "consumer max waiting needs to be positive" // 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] 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" // NewJSConsumerPriorityPolicyWithoutGroupError creates a new JSConsumerPriorityPolicyWithoutGroup error: "Setting PriorityPolicy requires at least one PriorityGroup to be set"
func NewJSConsumerPriorityPolicyWithoutGroupError(opts ...ErrorOption) *ApiError { func NewJSConsumerPriorityPolicyWithoutGroupError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -1445,6 +1683,16 @@ func NewJSConsumerReplacementWithDifferentNameError(opts ...ErrorOption) *ApiErr
return ApiErrors[JSConsumerReplacementWithDifferentNameErr] 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" // NewJSConsumerReplicasExceedsStreamError creates a new JSConsumerReplicasExceedsStream error: "consumer config replica count exceeds parent stream"
func NewJSConsumerReplicasExceedsStreamError(opts ...ErrorOption) *ApiError { func NewJSConsumerReplicasExceedsStreamError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -1597,6 +1845,106 @@ func NewJSMemoryResourcesExceededError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSMemoryResourcesExceededErr] 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" // NewJSMessageTTLDisabledError creates a new JSMessageTTLDisabledErr error: "per-message TTL is disabled"
func NewJSMessageTTLDisabledError(opts ...ErrorOption) *ApiError { func NewJSMessageTTLDisabledError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -1705,6 +2053,26 @@ func NewJSMirrorOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSMirrorOverlappingSubjectFilters] 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" // NewJSMirrorWithFirstSeqError creates a new JSMirrorWithFirstSeqErr error: "stream mirrors can not have first sequence configured"
func NewJSMirrorWithFirstSeqError(opts ...ErrorOption) *ApiError { func NewJSMirrorWithFirstSeqError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -1715,6 +2083,16 @@ func NewJSMirrorWithFirstSeqError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSMirrorWithFirstSeqErr] 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" // NewJSMirrorWithSourcesError creates a new JSMirrorWithSourcesErr error: "stream mirrors can not also contain other sources"
func NewJSMirrorWithSourcesError(opts ...ErrorOption) *ApiError { func NewJSMirrorWithSourcesError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -1867,6 +2245,16 @@ func NewJSReplicasCountCannotBeNegativeError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSReplicasCountCannotBeNegative] 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}" // NewJSRestoreSubscribeFailedError creates a new JSRestoreSubscribeFailedErrF error: "JetStream unable to subscribe to restore snapshot {subject}: {err}"
func NewJSRestoreSubscribeFailedError(err error, subject interface{}, opts ...ErrorOption) *ApiError { func NewJSRestoreSubscribeFailedError(err error, subject interface{}, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -2007,6 +2395,16 @@ func NewJSSourceOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSSourceOverlappingSubjectFilters] 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" // NewJSStorageResourcesExceededError creates a new JSStorageResourcesExceededErr error: "insufficient storage resources available"
func NewJSStorageResourcesExceededError(opts ...ErrorOption) *ApiError { func NewJSStorageResourcesExceededError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -2075,6 +2473,16 @@ func NewJSStreamDuplicateMessageConflictError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSStreamDuplicateMessageConflict] 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" // NewJSStreamExpectedLastSeqPerSubjectNotReadyError creates a new JSStreamExpectedLastSeqPerSubjectNotReady error: "expected last sequence per subject temporarily unavailable"
func NewJSStreamExpectedLastSeqPerSubjectNotReadyError(opts ...ErrorOption) *ApiError { func NewJSStreamExpectedLastSeqPerSubjectNotReadyError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
@@ -2241,6 +2649,16 @@ func NewJSStreamMessageExceedsMaximumError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSStreamMessageExceedsMaximumErr] 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" // NewJSStreamMirrorNotUpdatableError creates a new JSStreamMirrorNotUpdatableErr error: "stream mirror configuration can not be updated"
func NewJSStreamMirrorNotUpdatableError(opts ...ErrorOption) *ApiError { func NewJSStreamMirrorNotUpdatableError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts) eopts := parseOpts(opts)
+22 -1
View File
@@ -73,7 +73,7 @@ type JSStreamActionAdvisory struct {
TypedEvent TypedEvent
Stream string `json:"stream"` Stream string `json:"stream"`
Action ActionAdvisoryType `json:"action"` 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"` Domain string `json:"domain,omitempty"`
} }
@@ -253,6 +253,27 @@ type JSStreamQuorumLostAdvisory struct {
Domain string `json:"domain,omitempty"` 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. // JSConsumerLeaderElectedAdvisoryType is sent when the system elects a leader for a consumer.
const JSConsumerLeaderElectedAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_leader_elected" const JSConsumerLeaderElectedAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_leader_elected"
+31 -1
View File
@@ -17,7 +17,7 @@ import "strconv"
const ( const (
// JSApiLevel is the maximum supported JetStream API level for this server. // JSApiLevel is the maximum supported JetStream API level for this server.
JSApiLevel int = 1 JSApiLevel int = 2
JSRequiredLevelMetadataKey = "_nats.req.level" JSRequiredLevelMetadataKey = "_nats.req.level"
JSServerVersionMetadataKey = "_nats.ver" JSServerVersionMetadataKey = "_nats.ver"
@@ -62,6 +62,26 @@ func setStaticStreamMetadata(cfg *StreamConfig) {
requires(1) 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) cfg.Metadata[JSRequiredLevelMetadataKey] = strconv.Itoa(requiredApiLevel)
} }
@@ -204,3 +224,13 @@ func deleteDynamicMetadata(metadata map[string]string) {
delete(metadata, JSServerVersionMetadataKey) delete(metadata, JSServerVersionMetadataKey)
delete(metadata, JSServerLevelMetadataKey) 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
}
+50 -1
View File
@@ -82,6 +82,8 @@ type leaf struct {
remoteDomain string remoteDomain string
// account name of remote server // account name of remote server
remoteAccName string 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 // 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 // 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. // 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 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. // This will spin up go routines to solicit the remote leaf node connections.
func (s *Server) solicitLeafNodeRemotes(remotes []*RemoteLeafOpts) { func (s *Server) solicitLeafNodeRemotes(remotes []*RemoteLeafOpts) {
sysAccName := _EMPTY_ sysAccName := _EMPTY_
@@ -177,12 +187,20 @@ func (s *Server) solicitLeafNodeRemotes(remotes []*RemoteLeafOpts) {
return remote return remote
} }
for _, r := range remotes { 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) 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 { func (s *Server) remoteLeafNodeStillValid(remote *leafNodeCfg) bool {
if remote.Disabled {
return false
}
for _, ri := range s.getOpts().LeafNode.Remotes { for _, ri := range s.getOpts().LeafNode.Remotes {
// FIXME(dlc) - What about auth changes? // FIXME(dlc) - What about auth changes?
if reflect.DeepEqual(ri.URLs, remote.URLs) { if reflect.DeepEqual(ri.URLs, remote.URLs) {
@@ -748,6 +766,7 @@ func (s *Server) startLeafNodeAcceptLoop() {
Domain: opts.JetStreamDomain, Domain: opts.JetStreamDomain,
Proto: s.getServerProto(), Proto: s.getServerProto(),
InfoOnConnect: true, InfoOnConnect: true,
JSApiLevel: JSApiLevel,
} }
// If we have selected a random port... // If we have selected a random port...
if port == 0 { if port == 0 {
@@ -811,6 +830,7 @@ func (c *client) sendLeafConnect(clusterName string, headers bool) error {
Compression: c.leaf.compression, Compression: c.leaf.compression,
RemoteAccount: c.acc.GetName(), RemoteAccount: c.acc.GetName(),
Proto: c.srv.getServerProto(), Proto: c.srv.getServerProto(),
Isolate: c.leaf.remote.RequestIsolation,
} }
// If a signature callback is specified, this takes precedence over anything else. // 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 // Do not update the smap here, we need to do it in initLeafNodeSmapAndSendSubs
c.leaf = &leaf{} 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 // For accepted LN connections, ws will be != nil if it was accepted
// through the Websocket port. // through the Websocket port.
c.ws = ws 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 { if cm := opts.LeafNode.Compression.Mode; cm != CompressionNotSupported {
info.Compression = cm 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.generateNonce(nonce[:])
s.mu.Unlock() s.mu.Unlock()
} }
@@ -1794,9 +1823,13 @@ func (s *Server) removeLeafNodeConnection(c *client) {
c.leaf.gwSub = nil c.leaf.gwSub = nil
} }
} }
proxyKey := c.proxyKey
c.mu.Unlock() c.mu.Unlock()
s.mu.Lock() s.mu.Lock()
delete(s.leafs, cid) delete(s.leafs, cid)
if proxyKey != _EMPTY_ {
s.removeProxiedConn(proxyKey, cid)
}
s.mu.Unlock() s.mu.Unlock()
s.removeFromTempClients(cid) s.removeFromTempClients(cid)
} }
@@ -1818,6 +1851,7 @@ type leafConnectInfo struct {
Headers bool `json:"headers,omitempty"` Headers bool `json:"headers,omitempty"`
JetStream bool `json:"jetstream,omitempty"` JetStream bool `json:"jetstream,omitempty"`
DenyPub []string `json:"deny_pub,omitempty"` DenyPub []string `json:"deny_pub,omitempty"`
Isolate bool `json:"isolate,omitempty"`
// There was an existing field called: // There was an existing field called:
// >> Comp bool `json:"compression,omitempty"` // >> 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 c.leaf.remoteServer = proto.Name
// Remember the remote account name // Remember the remote account name
c.leaf.remoteAccName = proto.RemoteAccount 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 the other side has declared itself a hub, so we will take on the spoke role.
if proto.Hub { if proto.Hub {
@@ -2044,12 +2080,16 @@ func (c *client) remoteCluster() string {
// its permission settings for local enforcement. // its permission settings for local enforcement.
func (s *Server) sendPermsAndAccountInfo(c *client) { func (s *Server) sendPermsAndAccountInfo(c *client) {
// Copy // Copy
s.mu.Lock()
info := s.copyLeafNodeInfo() info := s.copyLeafNodeInfo()
s.mu.Unlock()
c.mu.Lock() c.mu.Lock()
info.CID = c.cid info.CID = c.cid
info.Import = c.opts.Import info.Import = c.opts.Import
info.Export = c.opts.Export info.Export = c.opts.Export
info.RemoteAccount = c.acc.Name 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 info.ConnectInfo = true
c.enqueueProto(generateInfoJSON(info)) c.enqueueProto(generateInfoJSON(info))
c.mu.Unlock() 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) c.Debugf("Not permitted to subscribe to %q on behalf of %s%s", sub.subject, accName, accNTag)
continue continue
} }
// Don't advertise interest from leafnodes to other isolated leafnodes.
if sub.client.kind == LEAF && c.isIsolatedLeafNode() {
continue
}
// We ignore ourselves here. // We ignore ourselves here.
// Also don't add the subscription if it has a origin cluster and the // 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. // 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 continue
} }
ln.mu.Lock() 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 // If `hubOnly` is true, it means that we want to update only leafnodes
// that connect to this server (so isHubLeafNode() would return `true`). // that connect to this server (so isHubLeafNode() would return `true`).
if hubOnly && !ln.isHubLeafNode() { if hubOnly && !ln.isHubLeafNode() {
+96 -10
View File
@@ -24,7 +24,6 @@ import (
"time" "time"
"github.com/nats-io/nats-server/v2/server/ats" "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/avl"
"github.com/nats-io/nats-server/v2/server/gsl" "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/stree"
@@ -42,11 +41,12 @@ type memStore struct {
maxp int64 maxp int64
scb StorageUpdateHandler scb StorageUpdateHandler
rmcb StorageRemoveMsgHandler rmcb StorageRemoveMsgHandler
sdmcb SubjectDeleteMarkerUpdateHandler pmsgcb ProcessJetStreamMsgHandler
ageChk *time.Timer ageChk *time.Timer
consumers int consumers int
receivedAny bool receivedAny bool
ttls *thw.HashWheel ttls *thw.HashWheel
scheduling *MsgScheduling
sdm *SDMMeta sdm *SDMMeta
} }
@@ -67,6 +67,9 @@ func newMemStore(cfg *StreamConfig) (*memStore, error) {
if cfg.AllowMsgTTL { if cfg.AllowMsgTTL {
ms.ttls = thw.NewHashWheel() ms.ttls = thw.NewHashWheel()
} }
if cfg.AllowMsgSchedules {
ms.scheduling = newMsgScheduling(ms.runMsgScheduling)
}
if cfg.FirstSeq > 0 { if cfg.FirstSeq > 0 {
if _, err := ms.purge(cfg.FirstSeq); err != nil { if _, err := ms.purge(cfg.FirstSeq); err != nil {
return nil, err return nil, err
@@ -95,6 +98,11 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error {
} else if !cfg.AllowMsgTTL && ms.ttls != nil { } else if !cfg.AllowMsgTTL && ms.ttls != nil {
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. // Limits checks and enforcement.
ms.enforceMsgLimit() ms.enforceMsgLimit()
ms.enforceBytesLimit() ms.enforceBytesLimit()
@@ -127,6 +135,9 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error {
if cfg.MaxAge != 0 || cfg.AllowMsgTTL { if cfg.MaxAge != 0 || cfg.AllowMsgTTL {
ms.expireMsgs() ms.expireMsgs()
} }
if cfg.AllowMsgSchedules {
ms.runMsgScheduling()
}
return nil 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. // Stores a raw message with expected sequence number and timestamp.
// Lock should be held. // Lock should be held.
func (ms *memStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) error { 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() 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 return nil
} }
@@ -360,6 +401,11 @@ func (ms *memStore) SkipMsgs(seq uint64, num uint64) error {
return nil 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. // RegisterStorageUpdates registers a callback for updates to storage changes.
// It will present number of messages and bytes as a signed integer and an // It will present number of messages and bytes as a signed integer and an
// optional sequence number of the message if a single. // optional sequence number of the message if a single.
@@ -377,10 +423,10 @@ func (ms *memStore) RegisterStorageRemoveMsg(cb StorageRemoveMsgHandler) {
ms.mu.Unlock() ms.mu.Unlock()
} }
// RegisterSubjectDeleteMarkerUpdates registers a callback for updates to new subject delete markers. // RegisterProcessJetStreamMsg registers a callback to process new JetStream messages.
func (ms *memStore) RegisterSubjectDeleteMarkerUpdates(cb SubjectDeleteMarkerUpdateHandler) { func (ms *memStore) RegisterProcessJetStreamMsg(cb ProcessJetStreamMsgHandler) {
ms.mu.Lock() ms.mu.Lock()
ms.sdmcb = cb ms.pmsgcb = cb
ms.mu.Unlock() ms.mu.Unlock()
} }
@@ -1084,11 +1130,11 @@ func (ms *memStore) expireMsgs() {
maxAge := int64(ms.cfg.MaxAge) maxAge := int64(ms.cfg.MaxAge)
minAge := time.Now().UnixNano() - maxAge minAge := time.Now().UnixNano() - maxAge
rmcb := ms.rmcb rmcb := ms.rmcb
sdmcb := ms.sdmcb pmsgcb := ms.pmsgcb
sdmTTL := int64(ms.cfg.SubjectDeleteMarkerTTL.Seconds()) sdmTTL := int64(ms.cfg.SubjectDeleteMarkerTTL.Seconds())
sdmEnabled := sdmTTL > 0 sdmEnabled := sdmTTL > 0
ms.mu.RUnlock() ms.mu.RUnlock()
if sdmEnabled && (rmcb == nil || sdmcb == nil) { if sdmEnabled && (rmcb == nil || pmsgcb == nil) {
return return
} }
@@ -1244,12 +1290,39 @@ func (ms *memStore) handleRemovalOrSdm(seq uint64, subj string, sdm bool, sdmTTL
subj: subj, subj: subj,
hdr: hdr, hdr: hdr,
} }
ms.sdmcb(msg) ms.pmsgcb(msg)
} else { } else {
ms.rmcb(seq) 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. // PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
// Will return the number of purged messages. // Will return the number of purged messages.
func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) { 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 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 { 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. // Delete is same as Stop for memory store.
func (ms *memStore) Delete() error { func (ms *memStore) Delete(_ bool) error {
return ms.Stop() return ms.Stop()
} }
+106 -29
View File
@@ -129,6 +129,7 @@ type ConnInfo struct {
OutMsgs int64 `json:"out_msgs"` OutMsgs int64 `json:"out_msgs"`
InBytes int64 `json:"in_bytes"` InBytes int64 `json:"in_bytes"`
OutBytes int64 `json:"out_bytes"` OutBytes int64 `json:"out_bytes"`
Stalls int64 `json:"stalls,omitempty"`
NumSubs uint32 `json:"subscriptions"` NumSubs uint32 `json:"subscriptions"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Lang string `json:"lang,omitempty"` Lang string `json:"lang,omitempty"`
@@ -146,11 +147,17 @@ type ConnInfo struct {
NameTag string `json:"name_tag,omitempty"` NameTag string `json:"name_tag,omitempty"`
Tags jwt.TagList `json:"tags,omitempty"` Tags jwt.TagList `json:"tags,omitempty"`
MQTTClient string `json:"mqtt_client,omitempty"` // This is the MQTT client id MQTTClient string `json:"mqtt_client,omitempty"` // This is the MQTT client id
Proxy *ProxyInfo `json:"proxy,omitempty"`
// Internal // Internal
rtt int64 // For fast sorting 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 // TLSPeerCert contains basic information about a TLS peer certificate
type TLSPeerCert struct { type TLSPeerCert struct {
Subject string `json:"subject,omitempty"` 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. // we need to use atomic here.
ci.InMsgs = atomic.LoadInt64(&client.inMsgs) ci.InMsgs = atomic.LoadInt64(&client.inMsgs)
ci.InBytes = atomic.LoadInt64(&client.inBytes) 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. // 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 // 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 { if conn, ok := nc.(*tls.Conn); ok {
cs := conn.ConnectionState() cs := conn.ConnectionState()
ci.TLSVersion = tlsVersion(cs.Version) ci.TLSVersion = tlsVersion(cs.Version)
ci.TLSCipher = tlsCipher(cs.CipherSuite) ci.TLSCipher = tls.CipherSuiteName(cs.CipherSuite)
if auth && len(cs.PeerCertificates) > 0 { if auth && len(cs.PeerCertificates) > 0 {
ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates) 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 { func makePeerCerts(pc []*x509.Certificate) []*TLSPeerCert {
res := make([]*TLSPeerCert, len(pc)) res := make([]*TLSPeerCert, len(pc))
for i, c := range pc { for i, c := range pc {
@@ -808,8 +828,10 @@ type RouteInfo struct {
// Routez returns a Routez struct containing information about routes. // Routez returns a Routez struct containing information about routes.
func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) { func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) {
rs := &Routez{Routes: []*RouteInfo{}} rs := &Routez{
rs.Now = time.Now().UTC() Now: time.Now().UTC(),
Routes: []*RouteInfo{},
}
if routezOpts == nil { if routezOpts == nil {
routezOpts = &RoutezOptions{} routezOpts = &RoutezOptions{}
@@ -826,7 +848,7 @@ func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) {
rs.Import = perms.Import rs.Import = perms.Import
rs.Export = perms.Export rs.Export = perms.Export
} }
rs.Name = s.getOpts().ServerName rs.Name = s.info.Name
addRoute := func(r *client) { addRoute := func(r *client) {
r.mu.Lock() r.mu.Lock()
@@ -1003,7 +1025,15 @@ func (s *Server) Subsz(opts *SubszOptions) (*Subsz, error) {
slStats := &SublistStats{} slStats := &SublistStats{}
// FIXME(dlc) - Make account aware. // 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 { if subdetail {
var raw [4096]*subscription var raw [4096]*subscription
@@ -1100,12 +1130,7 @@ func (s *Server) HandleSubsz(w http.ResponseWriter, r *http.Request) {
} }
var b []byte var b []byte
b, err = json.MarshalIndent(st, "", " ")
if len(st.Subs) == 0 {
b, err = json.MarshalIndent(st.SublistStats, "", " ")
} else {
b, err = json.MarshalIndent(st, "", " ")
}
if err != nil { if err != nil {
s.Errorf("Error marshaling response to /subscriptionsz request: %v", err) 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 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 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 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 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 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 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 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 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. 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 OCSPResponseCache *OCSPResponseCacheVarz `json:"ocsp_peer_cache,omitempty"` // OCSPResponseCache is the state of the OCSP cache
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats"` // SlowConsumersStats is statistics about all detected Slow Consumer 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 // 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 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(). // VarzOptions are the options passed to Varz().
// Currently, there are no options defined. // Currently, there are no options defined.
type VarzOptions struct{} type VarzOptions struct{}
@@ -1377,6 +1416,14 @@ type SlowConsumersStats struct {
Leafs uint64 `json:"leafs"` // Leafs is how many Leafnodes were slow consumers Leafs uint64 `json:"leafs"` // Leafs is how many Leafnodes were slow consumers
} }
// StaleConnectionStats contains information about the stale connections from different type of connections.
type StaleConnectionStats struct {
Clients uint64 `json:"clients"` // Clients is how many Client connections became stale connections
Routes uint64 `json:"routes"` // Routes is how many Route connections became stale connections
Gateways uint64 `json:"gateways"` // Gateways is how many Gateway connections became stale connections
Leafs uint64 `json:"leafs"` // Leafs is how many Leafnode connections became stale connections
}
func myUptime(d time.Duration) string { func myUptime(d time.Duration) string {
// Just use total seconds for uptime, and display days / years // Just use total seconds for uptime, and display days / years
tsecs := d / time.Second tsecs := d / time.Second
@@ -1615,7 +1662,6 @@ func (s *Server) createVarz(pcpu float64, rss int64) *Varz {
MaxSubs: opts.MaxSubs, MaxSubs: opts.MaxSubs,
Cores: runtime.NumCPU(), Cores: runtime.NumCPU(),
MaxProcs: runtime.GOMAXPROCS(0), MaxProcs: runtime.GOMAXPROCS(0),
Tags: opts.Tags,
TrustedOperatorsJwt: opts.operatorJWT, TrustedOperatorsJwt: opts.operatorJWT,
TrustedOperatorsClaim: opts.TrustedOperators, TrustedOperatorsClaim: opts.TrustedOperators,
} }
@@ -1702,6 +1748,8 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) {
v.WriteDeadline = opts.WriteDeadline v.WriteDeadline = opts.WriteDeadline
v.ConfigLoadTime = s.configTime.UTC() v.ConfigLoadTime = s.configTime.UTC()
v.ConfigDigest = opts.configDigest v.ConfigDigest = opts.configDigest
v.Tags = opts.Tags
v.Metadata = opts.Metadata
// Update route URLs if applicable // Update route URLs if applicable
if s.varzUpdateRouteURLs { if s.varzUpdateRouteURLs {
v.Cluster.URLs = urlsToStrings(opts.Routes) v.Cluster.URLs = urlsToStrings(opts.Routes)
@@ -1714,6 +1762,19 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) {
v.Websocket.TLSPinnedCerts = getPinnedCertsAsSlice(opts.Websocket.TLSPinnedCerts) v.Websocket.TLSPinnedCerts = getPinnedCertsAsSlice(opts.Websocket.TLSPinnedCerts)
v.TLSOCSPPeerVerify = s.ocspPeerVerify && v.TLSRequired && s.opts.tlsConfigOpts != nil && s.opts.tlsConfigOpts.OCSPPeerConfig != nil && s.opts.tlsConfigOpts.OCSPPeerConfig.Verify v.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 { 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.OutMsgs = atomic.LoadInt64(&s.outMsgs)
v.OutBytes = atomic.LoadInt64(&s.outBytes) v.OutBytes = atomic.LoadInt64(&s.outBytes)
v.SlowConsumers = atomic.LoadInt64(&s.slowConsumers) v.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
v.StalledClients = atomic.LoadInt64(&s.stalls)
v.SlowConsumersStats = &SlowConsumersStats{ v.SlowConsumersStats = &SlowConsumersStats{
Clients: s.NumSlowConsumersClients(), Clients: s.NumSlowConsumersClients(),
Routes: s.NumSlowConsumersRoutes(), Routes: s.NumSlowConsumersRoutes(),
Gateways: s.NumSlowConsumersGateways(), Gateways: s.NumSlowConsumersGateways(),
Leafs: s.NumSlowConsumersLeafs(), 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) v.PinnedAccountFail = atomic.LoadUint64(&s.pinnedAccFail)
// Make sure to reset in case we are re-using. // 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. // LeafInfo has detailed information on each remote leafnode connection.
type LeafInfo struct { type LeafInfo struct {
ID uint64 `json:"id"` ID uint64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
IsSpoke bool `json:"is_spoke"` IsSpoke bool `json:"is_spoke"`
Account string `json:"account"` IsIsolated bool `json:"is_isolated,omitempty"`
IP string `json:"ip"` Account string `json:"account"`
Port int `json:"port"` IP string `json:"ip"`
RTT string `json:"rtt,omitempty"` Port int `json:"port"`
InMsgs int64 `json:"in_msgs"` RTT string `json:"rtt,omitempty"`
OutMsgs int64 `json:"out_msgs"` InMsgs int64 `json:"in_msgs"`
InBytes int64 `json:"in_bytes"` OutMsgs int64 `json:"out_msgs"`
OutBytes int64 `json:"out_bytes"` InBytes int64 `json:"in_bytes"`
NumSubs uint32 `json:"subscriptions"` OutBytes int64 `json:"out_bytes"`
Subs []string `json:"subscriptions_list,omitempty"` NumSubs uint32 `json:"subscriptions"`
Compression string `json:"compression,omitempty"` 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. // Leafz returns a Leafz structure containing information about leafnodes.
@@ -2294,6 +2365,7 @@ func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) {
ID: ln.cid, ID: ln.cid,
Name: ln.leaf.remoteServer, Name: ln.leaf.remoteServer,
IsSpoke: ln.isSpokeLeafNode(), IsSpoke: ln.isSpokeLeafNode(),
IsIsolated: ln.leaf.isolated,
Account: ln.acc.Name, Account: ln.acc.Name,
IP: ln.host, IP: ln.host,
Port: int(ln.port), Port: int(ln.port),
@@ -2304,6 +2376,7 @@ func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) {
OutBytes: ln.outBytes, OutBytes: ln.outBytes,
NumSubs: uint32(len(ln.subs)), NumSubs: uint32(len(ln.subs)),
Compression: ln.leaf.compression, Compression: ln.leaf.compression,
Proxy: createProxyInfo(ln),
} }
if opts != nil && opts.Subscriptions { if opts != nil && opts.Subscriptions {
lni.Subs = make([]string, 0, len(ln.subs)) 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 { s.accounts.Range(func(key, a any) bool {
acc := a.(*Account) acc := a.(*Account)
acc.mu.RLock() acc.mu.RLock()
if opts.IncludeUnused || acc.numLocalConnections() != 0 { if (opts != nil && opts.IncludeUnused) || acc.numLocalConnections() != 0 {
stz.Accounts = append(stz.Accounts, acc.statz()) stz.Accounts = append(stz.Accounts, acc.statz())
} }
acc.mu.RUnlock() acc.mu.RUnlock()
@@ -2516,6 +2589,10 @@ func (reason ClosedState) String() string {
return "Cluster Names Identical" return "Cluster Names Identical"
case Kicked: case Kicked:
return "Kicked" return "Kicked"
case ProxyNotTrusted:
return "Proxy Not Trusted"
case ProxyRequired:
return "Proxy Required"
} }
return "Unknown State" return "Unknown State"
+2 -2
View File
@@ -514,7 +514,7 @@ func (s *Server) startMQTT() {
hp := net.JoinHostPort(o.Host, strconv.Itoa(port)) hp := net.JoinHostPort(o.Host, strconv.Itoa(port))
s.mu.Lock() s.mu.Lock()
s.mqtt.sessmgr.sessions = make(map[string]*mqttAccountSessionManager) s.mqtt.sessmgr.sessions = make(map[string]*mqttAccountSessionManager)
hl, err = net.Listen("tcp", hp) hl, err = natsListen("tcp", hp)
s.mqtt.listenerErr = err s.mqtt.listenerErr = err
if err != nil { if err != nil {
s.mu.Unlock() s.mu.Unlock()
@@ -647,7 +647,7 @@ func (s *Server) createMQTTClient(conn net.Conn, ws *websocket) *client {
if tlsRequired { if tlsRequired {
c.Debugf("TLS handshake complete") c.Debugf("TLS handshake complete")
cs := c.nc.(*tls.Conn).ConnectionState() 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() c.mu.Unlock()
+1 -1
View File
@@ -34,7 +34,7 @@ func (s *Server) NonceRequired() bool {
// nonceRequired tells us if we should send a nonce. // nonceRequired tells us if we should send a nonce.
// Lock should be held on entry. // Lock should be held on entry.
func (s *Server) nonceRequired() bool { 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. // Generate a nonce for INFO challenge.
+265 -23
View File
@@ -77,6 +77,7 @@ type ClusterOpts struct {
Advertise string `json:"-"` Advertise string `json:"-"`
NoAdvertise bool `json:"-"` NoAdvertise bool `json:"-"`
ConnectRetries int `json:"-"` ConnectRetries int `json:"-"`
ConnectBackoff bool `json:"-"`
PoolSize int `json:"-"` PoolSize int `json:"-"`
PinnedAccounts []string `json:"-"` PinnedAccounts []string `json:"-"`
Compression CompressionOpts `json:"-"` Compression CompressionOpts `json:"-"`
@@ -121,6 +122,7 @@ type GatewayOpts struct {
TLSPinnedCerts PinnedCertSet `json:"-"` TLSPinnedCerts PinnedCertSet `json:"-"`
Advertise string `json:"advertise,omitempty"` Advertise string `json:"advertise,omitempty"`
ConnectRetries int `json:"connect_retries,omitempty"` ConnectRetries int `json:"connect_retries,omitempty"`
ConnectBackoff bool `json:"connect_backoff,omitempty"`
Gateways []*RemoteGatewayOpts `json:"gateways,omitempty"` Gateways []*RemoteGatewayOpts `json:"gateways,omitempty"`
RejectUnknown bool `json:"reject_unknown,omitempty"` // config got renamed to reject_unknown_cluster 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"` Port int `json:"port,omitempty"`
Username string `json:"-"` Username string `json:"-"`
Password string `json:"-"` Password string `json:"-"`
ProxyRequired bool `json:"-"`
Nkey string `json:"-"` Nkey string `json:"-"`
Account string `json:"-"` Account string `json:"-"`
Users []*User `json:"-"` Users []*User `json:"-"`
@@ -186,6 +189,10 @@ type LeafNodeOpts struct {
// least" test). // least" test).
MinVersion string MinVersion string
// Isolate subject interest from other leafnode connections, preventing
// east-west propagation.
IsolateLeafnodeInterest bool `json:"-"`
// Not exported, for tests. // Not exported, for tests.
resolver netResolver resolver netResolver
dialTimeout time.Duration dialTimeout time.Duration
@@ -245,14 +252,30 @@ type RemoteLeafOpts struct {
// If JetStreamClusterMigrate is set to true, this is the time after which the leader // If JetStreamClusterMigrate is set to true, this is the time after which the leader
// will be migrated away from this server if still disconnected. // will be migrated away from this server if still disconnected.
JetStreamClusterMigrateDelay time.Duration `json:"jetstream_cluster_migrate_delay,omitempty"` 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 // JSLimitOpts are active limits for the meta cluster
type JSLimitOpts struct { type JSLimitOpts struct {
MaxRequestBatch int `json:"max_request_batch,omitempty"` // MaxRequestBatch is the maximum amount of updates that can be sent in a batch 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 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 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 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 { type JSTpmOpts struct {
@@ -291,7 +314,8 @@ type Options struct {
Trace bool `json:"-"` Trace bool `json:"-"`
Debug bool `json:"-"` Debug bool `json:"-"`
TraceVerbose 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:"-"` TraceHeaders bool `json:"-"`
NoLog bool `json:"-"` NoLog bool `json:"-"`
NoSigs bool `json:"-"` NoSigs bool `json:"-"`
@@ -312,6 +336,7 @@ type Options struct {
NoSystemAccount bool `json:"-"` NoSystemAccount bool `json:"-"`
Username string `json:"-"` Username string `json:"-"`
Password string `json:"-"` Password string `json:"-"`
ProxyRequired bool `json:"-"`
Authorization string `json:"-"` Authorization string `json:"-"`
AuthCallout *AuthCallout `json:"-"` AuthCallout *AuthCallout `json:"-"`
PingInterval time.Duration `json:"ping_interval"` PingInterval time.Duration `json:"ping_interval"`
@@ -329,7 +354,7 @@ type Options struct {
Gateway GatewayOpts `json:"gateway,omitempty"` Gateway GatewayOpts `json:"gateway,omitempty"`
LeafNode LeafNodeOpts `json:"leaf,omitempty"` LeafNode LeafNodeOpts `json:"leaf,omitempty"`
JetStream bool `json:"jetstream"` JetStream bool `json:"jetstream"`
JetStreamStrict bool `json:"-"` NoJetStreamStrict bool `json:"-"` // Strict by default.
JetStreamMaxMemory int64 `json:"-"` JetStreamMaxMemory int64 `json:"-"`
JetStreamMaxStore int64 `json:"-"` JetStreamMaxStore int64 `json:"-"`
JetStreamDomain string `json:"-"` JetStreamDomain string `json:"-"`
@@ -426,10 +451,16 @@ type Options struct {
// and used as a filter criteria for some system requests. // and used as a filter criteria for some system requests.
Tags jwt.TagList `json:"-"` 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 enables OCSP Stapling in the server.
OCSPConfig *OCSPConfig OCSPConfig *OCSPConfig
tlsConfigOpts *TLSConfigOpts tlsConfigOpts *TLSConfigOpts
// Proxies configuration.
Proxies *ProxiesConfig
// private fields, used to know if bool options are explicitly // private fields, used to know if bool options are explicitly
// defined in config and/or command line params. // defined in config and/or command line params.
inConfig map[string]bool inConfig map[string]bool
@@ -714,6 +745,8 @@ type authorization struct {
token string token string
nkey string nkey string
acc string acc string
// If connection must come through proxy
proxyRequired bool
// Multiple Nkeys/Users // Multiple Nkeys/Users
nkeys []*NkeyUser nkeys []*NkeyUser
users []*User 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. FallbackDelay time.Duration // Where supported, indicates how long to wait for the handshake before falling back to sending the INFO protocol first.
Timeout float64 Timeout float64
RateLimit int64 RateLimit int64
AllowInsecureCiphers bool
Ciphers []uint16 Ciphers []uint16
CurvePreferences []tls.CurveID CurvePreferences []tls.CurveID
PinnedCerts PinnedCertSet PinnedCerts PinnedCertSet
@@ -765,6 +799,17 @@ type OCSPConfig struct {
OverrideURLs []string 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 = ` var tlsUsage = `
TLS configuration is specified in the tls section of a configuration file: 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.authBlockDefined = true
o.Username = auth.user o.Username = auth.user
o.Password = auth.pass o.Password = auth.pass
o.ProxyRequired = auth.proxyRequired
o.Authorization = auth.token o.Authorization = auth.token
o.AuthTimeout = auth.timeout o.AuthTimeout = auth.timeout
o.AuthCallout = auth.callout o.AuthCallout = auth.callout
@@ -1649,6 +1695,24 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin
*errors = append(*errors, err) *errors = append(*errors, err)
return return
} }
case "server_metadata":
var err error
switch v := v.(type) {
case map[string]any:
for mk, mv := range v {
tk, mv = unwrapValue(mv, &lt)
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": case "default_js_domain":
vv, ok := v.(map[string]any) vv, ok := v.(map[string]any)
if !ok { if !ok {
@@ -1693,6 +1757,13 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin
o.NoFastProducerStall = v.(bool) o.NoFastProducerStall = v.(bool)
case "max_closed_clients": case "max_closed_clients":
o.MaxClosedClients = int(v.(int64)) o.MaxClosedClients = int(v.(int64))
case "proxies":
proxies, err := parseProxies(tk, errors)
if err != nil {
*errors = append(*errors, err)
return
}
o.Proxies = proxies
default: default:
if au := atomic.LoadInt32(&allowUnknownTopLevelField); au == 0 && !tk.IsUsedVariable() { if au := atomic.LoadInt32(&allowUnknownTopLevelField); au == 0 && !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{ 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) trackExplicitVal(&opts.inConfig, "Cluster.NoAdvertise", opts.Cluster.NoAdvertise)
case "connect_retries": case "connect_retries":
opts.Cluster.ConnectRetries = int(mv.(int64)) opts.Cluster.ConnectRetries = int(mv.(int64))
case "connect_backoff":
opts.Cluster.ConnectBackoff = mv.(bool)
case "permissions": case "permissions":
perms, err := parseUserPermissions(mv, errors) perms, err := parseUserPermissions(mv, errors)
if err != nil { if err != nil {
@@ -2091,6 +2164,8 @@ func parseGateway(v any, o *Options, errors *[]error, warnings *[]error) error {
o.Gateway.Advertise = mv.(string) o.Gateway.Advertise = mv.(string)
case "connect_retries": case "connect_retries":
o.Gateway.ConnectRetries = int(mv.(int64)) o.Gateway.ConnectRetries = int(mv.(int64))
case "connect_backoff":
o.Gateway.ConnectBackoff = mv.(bool)
case "gateways": case "gateways":
gateways, err := parseGateways(mv, errors, warnings) gateways, err := parseGateways(mv, errors, warnings)
if err != nil { if err != nil {
@@ -2265,7 +2340,7 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error {
var lt token var lt token
tk, v := unwrapValue(v, &lt) tk, v := unwrapValue(v, &lt)
lim := JSLimitOpts{} opts.JetStreamLimits = JSLimitOpts{}
vv, ok := v.(map[string]any) vv, ok := v.(map[string]any)
if !ok { if !ok {
@@ -2275,14 +2350,57 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error {
tk, mv = unwrapValue(mv, &lt) tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) { switch strings.ToLower(mk) {
case "max_ack_pending": case "max_ack_pending":
lim.MaxAckPending = int(mv.(int64)) opts.JetStreamLimits.MaxAckPending = int(mv.(int64))
case "max_ha_assets": case "max_ha_assets":
lim.MaxHAAssets = int(mv.(int64)) opts.JetStreamLimits.MaxHAAssets = int(mv.(int64))
case "max_request_batch": case "max_request_batch":
lim.MaxRequestBatch = int(mv.(int64)) opts.JetStreamLimits.MaxRequestBatch = int(mv.(int64))
case "duplicate_window": case "duplicate_window":
var err error 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, &lt)
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, &lt)
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 { if err != nil {
*errors = append(*errors, err) *errors = append(*errors, err)
} }
@@ -2299,7 +2417,6 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error {
} }
} }
} }
opts.JetStreamLimits = lim
return nil return nil
} }
@@ -2308,7 +2425,7 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error {
var lt token var lt token
tk, v := unwrapValue(v, &lt) tk, v := unwrapValue(v, &lt)
tpm := JSTpmOpts{} opts.JetStreamTpm = JSTpmOpts{}
vv, ok := v.(map[string]interface{}) vv, ok := v.(map[string]interface{})
if !ok { if !ok {
@@ -2318,13 +2435,13 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error {
tk, mv = unwrapValue(mv, &lt) tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) { switch strings.ToLower(mk) {
case "keys_file": case "keys_file":
tpm.KeysFile = mv.(string) opts.JetStreamTpm.KeysFile = mv.(string)
case "encryption_password": case "encryption_password":
tpm.KeyPassword = mv.(string) opts.JetStreamTpm.KeyPassword = mv.(string)
case "srk_password": case "srk_password":
tpm.SrkPassword = mv.(string) opts.JetStreamTpm.SrkPassword = mv.(string)
case "pcr": case "pcr":
tpm.Pcr = int(mv.(int64)) opts.JetStreamTpm.Pcr = int(mv.(int64))
case "cipher": case "cipher":
if err := setJetStreamEkCipher(opts, mv, tk); err != nil { if err := setJetStreamEkCipher(opts, mv, tk); err != nil {
return err return err
@@ -2342,7 +2459,6 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error {
} }
} }
} }
opts.JetStreamTpm = tpm
return nil return nil
} }
@@ -2384,7 +2500,7 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er
switch strings.ToLower(mk) { switch strings.ToLower(mk) {
case "strict": case "strict":
if v, ok := mv.(bool); ok { if v, ok := mv.(bool); ok {
opts.JetStreamStrict = v opts.NoJetStreamStrict = !v
} else { } else {
return &configErr{tk, fmt.Sprintf("Expected 'true' or 'false' for bool value, got '%s'", mv)} 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.Username = auth.user
opts.LeafNode.Password = auth.pass opts.LeafNode.Password = auth.pass
opts.LeafNode.ProxyRequired = auth.proxyRequired
opts.LeafNode.AuthTimeout = auth.timeout opts.LeafNode.AuthTimeout = auth.timeout
opts.LeafNode.Account = auth.acc opts.LeafNode.Account = auth.acc
opts.LeafNode.Users = auth.users opts.LeafNode.Users = auth.users
@@ -2575,6 +2692,8 @@ func parseLeafNodes(v any, opts *Options, errors *[]error, warnings *[]error) er
*errors = append(*errors, err) *errors = append(*errors, err)
continue continue
} }
case "isolate_leafnode_interest", "isolate":
opts.LeafNode.IsolateLeafnodeInterest = mv.(bool)
default: default:
if !tk.IsUsedVariable() { if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{ err := &unknownConfigFieldErr{
@@ -2647,6 +2766,8 @@ func parseLeafAuthorization(v any, errors, warnings *[]error) (*authorization, e
auth.users = users auth.users = users
case "account": case "account":
auth.acc = mv.(string) auth.acc = mv.(string)
case "proxy_required":
auth.proxyRequired = mv.(bool)
default: default:
if !tk.IsUsedVariable() { if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{ 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 // we need to create internal objects to store u/p and account
// name and have a server structure to hold that. // name and have a server structure to hold that.
user.Account = NewAccount(v.(string)) user.Account = NewAccount(v.(string))
case "proxy_required":
user.ProxyRequired = v.(bool)
default: default:
if !tk.IsUsedVariable() { if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{ err := &unknownConfigFieldErr{
@@ -2854,6 +2977,10 @@ func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteL
default: default:
*errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected boolean or map for jetstream_cluster_migrate, got %T", v)}) *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": case "compression":
if err := parseCompression(&remote.Compression, CompressionS2Auto, tk, k, v); err != nil { if err := parseCompression(&remote.Compression, CompressionS2Auto, tk, k, v); err != nil {
*errors = append(*errors, err) *errors = append(*errors, err)
@@ -2861,6 +2988,8 @@ func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteL
} }
case "first_info_timeout": case "first_info_timeout":
remote.FirstInfoTimeout = parseDuration(k, tk, v, errors, warnings) remote.FirstInfoTimeout = parseDuration(k, tk, v, errors, warnings)
case "disabled":
remote.Disabled = v.(bool)
default: default:
if !tk.IsUsedVariable() { if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{ err := &unknownConfigFieldErr{
@@ -4192,6 +4321,8 @@ func parseAuthorization(v any, errors, warnings *[]error) (*authorization, error
continue continue
} }
auth.callout = ac auth.callout = ac
case "proxy_required":
auth.proxyRequired = mv.(bool)
default: default:
if !tk.IsUsedVariable() { if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{ err := &unknownConfigFieldErr{
@@ -4264,6 +4395,9 @@ func parseUsers(mv any, errors *[]error) ([]*NkeyUser, []*User, error) {
cts := parseAllowedConnectionTypes(tk, &lt, v, errors) cts := parseAllowedConnectionTypes(tk, &lt, v, errors)
nkey.AllowedConnectionTypes = cts nkey.AllowedConnectionTypes = cts
user.AllowedConnectionTypes = cts user.AllowedConnectionTypes = cts
case "proxy_required":
nkey.ProxyRequired = v.(bool)
user.ProxyRequired = v.(bool)
default: default:
if !tk.IsUsedVariable() { if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{ err := &unknownConfigFieldErr{
@@ -4639,12 +4773,11 @@ func PrintTLSHelpAndDie() {
os.Exit(0) os.Exit(0)
} }
func parseCipher(cipherName string) (uint16, error) { func parseCipher(cipherName string) (*tls.CipherSuite, error) {
cipher, exists := cipherMap[cipherName] cipher, exists := cipherMap[cipherName]
if !exists { if !exists {
return 0, fmt.Errorf("unrecognized cipher %s", cipherName) return nil, fmt.Errorf("unrecognized cipher %s", cipherName)
} }
return cipher, nil return cipher, nil
} }
@@ -4680,6 +4813,7 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
tlsm map[string]any tlsm map[string]any
tc = TLSConfigOpts{} tc = TLSConfigOpts{}
lt token lt token
ics []*tls.CipherSuite // Insecure ciphers found
) )
defer convertPanicToError(&lt, &retErr) defer convertPanicToError(&lt, &retErr)
@@ -4739,6 +4873,12 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
tc.Verify = verify tc.Verify = verify
} }
tc.TLSCheckKnownURLs = 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": case "cipher_suites":
ra := mv.([]any) ra := mv.([]any)
if len(ra) == 0 { if len(ra) == 0 {
@@ -4751,7 +4891,10 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
if err != nil { if err != nil {
return nil, &configErr{tk, err.Error()} 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": case "curve_preferences":
ra := mv.([]any) ra := mv.([]any)
@@ -4969,6 +5112,16 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
tc.CurvePreferences = defaultCurvePreferences() 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 return &tc, nil
} }
@@ -5258,6 +5411,95 @@ func parseMQTT(v any, o *Options, errors *[]error, warnings *[]error) error {
return nil return nil
} }
func parseProxies(mv any, errors *[]error) (*ProxiesConfig, error) {
var (
tk token
lt token
proxies = &ProxiesConfig{}
)
defer convertPanicToErrorList(&lt, errors)
tk, mv = unwrapValue(mv, &lt)
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, &lt)
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(&lt, errors)
tk, mv = unwrapValue(mv, &lt)
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, &lt)
// 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, &lt)
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. // GenTLSConfig loads TLS related configuration parameters.
func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) { func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
// Create the tls.Config from our options before including the certs. // Create the tls.Config from our options before including the certs.
+202 -80
View File
@@ -45,6 +45,7 @@ type RaftNode interface {
SendSnapshot(snap []byte) error SendSnapshot(snap []byte) error
NeedSnapshot() bool NeedSnapshot() bool
Applied(index uint64) (entries uint64, bytes uint64) Applied(index uint64) (entries uint64, bytes uint64)
Processed(index uint64, applied uint64) (entries uint64, bytes uint64)
State() RaftState State() RaftState
Size() (entries, bytes uint64) Size() (entries, bytes uint64)
Progress() (index, commit, applied uint64) Progress() (index, commit, applied uint64)
@@ -98,7 +99,7 @@ type WAL interface {
State() StreamState State() StreamState
FastState(*StreamState) FastState(*StreamState)
Stop() error Stop() error
Delete() error Delete(inline bool) error
} }
type Peer struct { type Peer struct {
@@ -168,12 +169,13 @@ type raft struct {
llqrt time.Time // Last quorum lost time llqrt time.Time // Last quorum lost time
lsut time.Time // Last scale-up time lsut time.Time // Last scale-up time
term uint64 // The current vote term term uint64 // The current vote term
pterm uint64 // Previous term from the last snapshot pterm uint64 // Previous term from the last snapshot
pindex uint64 // Previous index from the last snapshot pindex uint64 // Previous index from the last snapshot
commit uint64 // Index of the most recent commit commit uint64 // Index of the most recent commit
applied uint64 // Index of the most recently applied commit processed uint64 // Index of the most recently processed commit
papplied uint64 // First sequence of our log, matches when we last installed a snapshot. 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. 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 leadc chan bool // Leader changes
quit chan struct{} // Raft group shutdown quit chan struct{} // Raft group shutdown
lxfer bool // Are we doing a leadership transfer? lxfer bool // Are we doing a leadership transfer?
hcbehind bool // Were we falling behind at the last health check? (see: isCurrent) 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. 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 paused bool // Whether or not applies are paused
observer bool // The node is observing, i.e. not able to become leader observer bool // The node is observing, i.e. not able to become leader
pobserver bool // Were we previously an observer? 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 { type proposedEntry struct {
@@ -281,6 +284,16 @@ type RaftConfig struct {
Log WAL Log WAL
Track bool Track bool
Observer 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 ( var (
@@ -539,6 +552,17 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel
n.Lock() n.Lock()
n.resetElectionTimeout() n.resetElectionTimeout()
n.llqrt = time.Now() 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() n.Unlock()
// Register the Raft group. // Register the Raft group.
@@ -842,12 +866,13 @@ func (s *Server) transferRaftLeaders() bool {
// Propose will propose a new entry to the group. // Propose will propose a new entry to the group.
// This should only be called on the leader. // This should only be called on the leader.
func (n *raft) Propose(data []byte) error { 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 { if state := n.State(); state != Leader {
n.debug("Proposal ignored, not leader (state: %v)", state) n.debug("Proposal ignored, not leader (state: %v)", state)
return errNotLeader return errNotLeader
} }
n.Lock()
defer n.Unlock()
// Error if we had a previous write error. // Error if we had a previous write error.
if werr := n.werr; werr != nil { if werr := n.werr; werr != nil {
@@ -857,15 +882,16 @@ func (n *raft) Propose(data []byte) error {
return nil return nil
} }
// ProposeDirect will propose multiple entries at once. // ProposeMulti will propose multiple entries at once.
// This should only be called on the leader. // This should only be called on the leader.
func (n *raft) ProposeMulti(entries []*Entry) error { 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() n.Lock()
defer n.Unlock() 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. // Error if we had a previous write error.
if werr := n.werr; werr != nil { 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. // ProposeAddPeer is called to add a peer to the group.
func (n *raft) ProposeAddPeer(peer string) error { func (n *raft) ProposeAddPeer(peer string) error {
n.RLock()
// Check state under lock, we might not be leader anymore.
if n.State() != Leader { if n.State() != Leader {
n.RUnlock()
return errNotLeader return errNotLeader
} }
n.RLock()
// Error if we had a previous write error. // Error if we had a previous write error.
if werr := n.werr; werr != nil { if werr := n.werr; werr != nil {
n.RUnlock() n.RUnlock()
@@ -984,10 +1012,13 @@ func (n *raft) AdjustBootClusterSize(csz int) error {
// AdjustClusterSize will change the cluster set size. // AdjustClusterSize will change the cluster set size.
// Must be the leader. // Must be the leader.
func (n *raft) AdjustClusterSize(csz int) error { 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 { if n.State() != Leader {
return errNotLeader return errNotLeader
} }
n.Lock()
// Same floor as bootstrap. // Same floor as bootstrap.
if csz < 2 { if csz < 2 {
csz = 2 csz = 2
@@ -997,7 +1028,6 @@ func (n *raft) AdjustClusterSize(csz int) error {
// a quorum. // a quorum.
n.csz = csz n.csz = csz
n.qn = n.csz/2 + 1 n.qn = n.csz/2 + 1
n.Unlock()
n.sendPeerState() n.sendPeerState()
return nil return nil
@@ -1023,7 +1053,6 @@ func (n *raft) PauseApply() error {
n.paused = true n.paused = true
n.hcommit = n.commit n.hcommit = n.commit
// Also prevent us from trying to become a leader while paused and catching up. // Also prevent us from trying to become a leader while paused and catching up.
n.pobserver, n.observer = n.observer, true
n.resetElect(observerModeInterval) n.resetElect(observerModeInterval)
return nil return nil
@@ -1066,8 +1095,7 @@ func (n *raft) ResumeApply() {
} }
} }
// Clear our observer and paused state after we apply. // Clear our paused state after we apply.
n.observer, n.pobserver = n.pobserver, false
n.paused = false n.paused = false
n.hcommit = 0 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 // apply queue. It will return the number of entries and an estimation of the
// byte size that could be removed with a snapshot/compact. // byte size that could be removed with a snapshot/compact.
func (n *raft) Applied(index uint64) (entries uint64, bytes uint64) { 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() n.Lock()
defer n.Unlock() defer n.Unlock()
@@ -1092,13 +1130,22 @@ func (n *raft) Applied(index uint64) (entries uint64, bytes uint64) {
return 0, 0 return 0, 0
} }
// Ignore if already applied. // Ignore if already processed.
if index > n.applied { if index > n.processed {
n.applied = index n.processed = index
} }
// If it was set, and we reached the minimum applied index, reset and send signal to upper layer. // Ignore if already applied.
if n.aflr > 0 && index >= n.aflr { 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 n.aflr = 0
// Quick sanity-check to confirm we're still leader. // Quick sanity-check to confirm we're still leader.
// In which case we must signal, since switchToLeader would not have done so already. // 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. // 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. // Used when restoring streams, moving a stream from R1 to R>1, etc.
func (n *raft) SendSnapshot(data []byte) error { 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 return nil
} }
@@ -1592,11 +1642,12 @@ func (n *raft) selectNextLeader() string {
// StepDown will have a leader stepdown and optionally do a leader transfer. // StepDown will have a leader stepdown and optionally do a leader transfer.
func (n *raft) StepDown(preferred ...string) error { func (n *raft) StepDown(preferred ...string) error {
n.Lock()
// Check state under lock, we might not be leader anymore.
if n.State() != Leader { if n.State() != Leader {
n.Unlock()
return errNotLeader return errNotLeader
} }
n.Lock()
if len(preferred) > 1 { if len(preferred) > 1 {
n.Unlock() n.Unlock()
return errTooManyPrefs return errTooManyPrefs
@@ -1686,6 +1737,7 @@ func (n *raft) CampaignImmediately() error {
n.Lock() n.Lock()
defer n.Unlock() defer n.Unlock()
n.maybeLeader = true n.maybeLeader = true
n.resetInitializing()
return n.campaign(minCampaignTimeout / 2) return n.campaign(minCampaignTimeout / 2)
} }
@@ -1775,21 +1827,27 @@ func (n *raft) Peers() []*Peer {
// Update and propose our known set of peers. // Update and propose our known set of peers.
func (n *raft) ProposeKnownPeers(knownPeers []string) { func (n *raft) ProposeKnownPeers(knownPeers []string) {
n.Lock()
defer n.Unlock()
// If we are the leader update and send this update out. // If we are the leader update and send this update out.
if n.State() != Leader { if n.State() != Leader {
return return
} }
n.UpdateKnownPeers(knownPeers) n.updateKnownPeersLocked(knownPeers)
n.sendPeerState() n.sendPeerState()
} }
// Update our known set of peers. // Update our known set of peers.
func (n *raft) UpdateKnownPeers(knownPeers []string) { func (n *raft) UpdateKnownPeers(knownPeers []string) {
n.Lock() n.Lock()
n.updateKnownPeersLocked(knownPeers)
n.Unlock()
}
func (n *raft) updateKnownPeersLocked(knownPeers []string) {
// Process like peer state update. // Process like peer state update.
ps := &peerState{knownPeers, len(knownPeers), n.extSt} ps := &peerState{knownPeers, len(knownPeers), n.extSt}
n.processPeerState(ps) n.processPeerState(ps)
n.Unlock()
} }
// ApplyQ returns the apply queue that new commits will be sent to for the // 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() defer n.Unlock()
if wal := n.wal; wal != nil { if wal := n.wal; wal != nil {
wal.Delete() wal.Delete(false)
} }
os.RemoveAll(n.sd) os.RemoveAll(n.sd)
n.debug("Deleted") n.debug("Deleted")
@@ -2082,15 +2140,10 @@ func (n *raft) SetObserver(isObserver bool) {
func (n *raft) setObserver(isObserver bool, extSt extensionState) { func (n *raft) setObserver(isObserver bool, extSt extensionState) {
n.Lock() n.Lock()
defer n.Unlock() defer n.Unlock()
n.setObserverLocked(isObserver, extSt)
}
if n.paused { func (n *raft) setObserverLocked(isObserver bool, extSt extensionState) {
// 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
}
wasObserver := n.observer wasObserver := n.observer
n.observer = isObserver n.observer = isObserver
n.extSt = extSt 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. // 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 { if len(msg) < appendEntryBaseLen {
return nil, errBadAppendEntry 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. // Track all peers we may have ever seen to use an string interns for appendEntryResponse decoding.
var peers sync.Map var peers sync.Map
func (n *raft) decodeAppendEntryResponse(msg []byte) *appendEntryResponse { func decodeAppendEntryResponse(msg []byte) *appendEntryResponse {
if len(msg) != appendEntryResponseLen { if len(msg) != appendEntryResponseLen {
return nil 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) { func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) {
n.debug("Received forwarded remove peer proposal: %q", msg) 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 { if len(msg) != idLen {
n.warn("Received invalid peer name for remove proposal: %q", msg) n.warn("Received invalid peer name for remove proposal: %q", msg)
return return
} }
n.RLock() 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 prop, werr := n.prop, n.werr
n.RUnlock() n.RUnlock()
@@ -2521,14 +2576,16 @@ func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _
// Called when a peer has forwarded a proposal. // Called when a peer has forwarded a proposal.
func (n *raft) handleForwardedProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { 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. // Need to copy since this is underlying client/route buffer.
msg = copyBytes(msg) msg = copyBytes(msg)
n.RLock() 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 prop, werr := n.prop, n.werr
n.RUnlock() n.RUnlock()
@@ -2564,7 +2621,6 @@ func (n *raft) runAsLeader() {
n.Unlock() n.Unlock()
return return
} }
n.Unlock()
// Cleanup our subscription when we leave. // Cleanup our subscription when we leave.
defer func() { defer func() {
@@ -2576,6 +2632,7 @@ func (n *raft) runAsLeader() {
// To send out our initial peer state. // To send out our initial peer state.
n.sendPeerState() n.sendPeerState()
n.Unlock()
hb := time.NewTicker(hbInterval) hb := time.NewTicker(hbInterval)
defer hb.Stop() defer hb.Stop()
@@ -2728,6 +2785,7 @@ func (n *raft) runCatchup(ar *appendEntryResponse, indexUpdatesQ *ipQueue[uint64
n.RLock() n.RLock()
s, reply := n.s, n.areply s, reply := n.s, n.areply
peer, subj, term, pterm, last := ar.peer, ar.reply, n.term, n.pterm, n.pindex 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() n.RUnlock()
defer s.grWG.Done() defer s.grWG.Done()
@@ -2749,6 +2807,10 @@ func (n *raft) runCatchup(ar *appendEntryResponse, indexUpdatesQ *ipQueue[uint64
indexUpdatesQ.unregister() 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) 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. const maxOutstanding = 2 * 1024 * 1024 // 2MB for now.
@@ -2935,7 +2997,7 @@ func (n *raft) loadEntry(index uint64) (*appendEntry, error) {
if err != nil { if err != nil {
return nil, err 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. // 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{}{ votes := map[string]struct{}{
n.ID(): {}, n.ID(): {},
} }
emptyVotes := map[string]struct{}{}
for n.State() == Candidate { for n.State() == Candidate {
elect := n.electTimer() elect := n.electTimer()
@@ -3216,16 +3279,28 @@ func (n *raft) runAsCandidate() {
} }
n.RLock() n.RLock()
nterm := n.term nterm := n.term
csz := n.csz
n.RUnlock() n.RUnlock()
if vresp.granted && nterm == vresp.term { if vresp.granted && nterm == vresp.term {
// only track peers that would be our followers // only track peers that would be our followers
n.trackPeer(vresp.peer) 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)) { if n.wonElection(len(votes)) {
// Become LEADER if we have won and gotten a quorum with everyone we should hear from. // Become LEADER if we have won and gotten a quorum with everyone we should hear from.
n.switchToLeader() n.switchToLeader()
return 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 { } else if vresp.term > nterm {
// if we observe a bigger term, we should start over again or risk forming a quorum fully knowing // 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. // is an internal callback from the "asubj" append entry subscription.
func (n *raft) handleAppendEntry(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) { func (n *raft) handleAppendEntry(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) {
msg = copyBytes(msg) 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 // Push to the new entry channel. From here one of the worker
// goroutines (runAsLeader, runAsFollower, runAsCandidate) will // goroutines (runAsLeader, runAsFollower, runAsCandidate) will
// pick it up. // pick it up.
@@ -3327,10 +3402,13 @@ func (n *raft) truncateWAL(term, index uint64) {
} }
if index < n.commit { if index < n.commit {
assert.Unreachable("WAL truncate lost commits", map[string]any{ assert.Unreachable("WAL truncate lost commits", map[string]any{
"term": term, "n.accName": n.accName,
"index": index, "n.group": n.group,
"commit": n.commit, "n.id": n.id,
"applied": n.applied, "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 { if n.commit > n.pindex {
n.commit = n.pindex n.commit = n.pindex
} }
if n.applied > n.commit { if n.processed > n.commit {
n.applied = n.commit n.processed = n.commit
}
if n.applied > n.processed {
n.applied = n.processed
} }
if n.papplied > n.applied { if n.papplied > n.applied {
n.papplied = n.applied n.papplied = n.applied
@@ -3432,11 +3513,13 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) {
assert.Unreachable( assert.Unreachable(
"Two leaders using the same term", "Two leaders using the same term",
map[string]any{ map[string]any{
"Node id": n.id, "n.accName": n.accName,
"Node term": n.term, "n.group": n.group,
"AppendEntry id": ae.leader, "n.id": n.id,
"AppendEntry term": ae.term, "n.term": n.term,
"AppendEntry lterm": ae.lterm, "ae.leader": ae.leader,
"ae.term": ae.term,
"ae.lterm": ae.lterm,
}) })
} }
n.debug("Received append entry from another leader, stepping down to %q", ae.leader) n.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() n.Unlock()
return return
} }
n.resetInitializing()
// Now send snapshot to upper levels. Only send the snapshot, not the peerstate entry. // Now send snapshot to upper levels. Only send the snapshot, not the peerstate entry.
n.apply.push(newCommittedEntry(n.commit, ae.entries[:1])) n.apply.push(newCommittedEntry(n.commit, ae.entries[:1]))
@@ -3690,6 +3774,7 @@ CONTINUE:
return return
} }
n.cachePendingEntry(ae) n.cachePendingEntry(ae)
n.resetInitializing()
} else { } else {
// This is a replay on startup so just take the appendEntry version. // This is a replay on startup so just take the appendEntry version.
n.pterm = ae.term n.pterm = ae.term
@@ -3710,7 +3795,7 @@ CONTINUE:
if !n.observer && !n.paused { if !n.observer && !n.paused {
n.lxfer = true n.lxfer = true
n.xferCampaign() 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. // Here we can become a leader but need to wait for resume of the apply queue.
n.lxfer = true 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 // processPeerState is called when a peer state entry is received
// over the wire or when we're updating known peers. // over the wire or when we're updating known peers.
// Lock should be held. // Lock should be held.
@@ -3828,7 +3924,7 @@ func (n *raft) processAppendEntryResponse(ar *appendEntryResponse) {
// handleAppendEntryResponse processes responses to append entries. // handleAppendEntryResponse processes responses to append entries.
func (n *raft) handleAppendEntryResponse(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) { func (n *raft) handleAppendEntryResponse(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) {
ar := n.decodeAppendEntryResponse(msg) ar := decodeAppendEntryResponse(msg)
ar.reply = reply ar.reply = reply
n.resp.push(ar) n.resp.push(ar)
} }
@@ -3892,6 +3988,15 @@ const (
func (n *raft) sendAppendEntry(entries []*Entry) { func (n *raft) sendAppendEntry(entries []*Entry) {
n.Lock() n.Lock()
defer n.Unlock() 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) ae := n.buildAppendEntry(entries)
var err error var err error
@@ -3998,14 +4103,19 @@ func (n *raft) peerNames() []string {
func (n *raft) currentPeerState() *peerState { func (n *raft) currentPeerState() *peerState {
n.RLock() n.RLock()
ps := &peerState{n.peerNames(), n.csz, n.extSt} ps := n.currentPeerStateLocked()
n.RUnlock() n.RUnlock()
return ps 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. // sendPeerState will send our current peer state to the cluster.
// Lock should be held.
func (n *raft) sendPeerState() { func (n *raft) sendPeerState() {
n.sendAppendEntry([]*Entry{{EntryPeerState, encodePeerState(n.currentPeerState())}}) n.sendAppendEntryLocked([]*Entry{{EntryPeerState, encodePeerState(n.currentPeerStateLocked())}}, true)
} }
// Send a heartbeat. // Send a heartbeat.
@@ -4198,6 +4308,7 @@ type voteResponse struct {
term uint64 term uint64
peer string peer string
granted bool granted bool
empty bool // "Empty vote", whether this peer's log is empty.
} }
const voteResponseLen = 8 + 8 + 1 const voteResponseLen = 8 + 8 + 1
@@ -4208,9 +4319,10 @@ func (vr *voteResponse) encode() []byte {
le.PutUint64(buf[0:], vr.term) le.PutUint64(buf[0:], vr.term)
copy(buf[8:], vr.peer) copy(buf[8:], vr.peer)
if vr.granted { if vr.granted {
buf[16] = 1 buf[16] |= 1
} else { }
buf[16] = 0 if vr.empty {
buf[16] |= 2
} }
return buf[:voteResponseLen] return buf[:voteResponseLen]
} }
@@ -4221,7 +4333,8 @@ func decodeVoteResponse(msg []byte) *voteResponse {
} }
var le = binary.LittleEndian var le = binary.LittleEndian
vr := &voteResponse{term: le.Uint64(msg[0:]), peer: string(msg[8:16])} 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 return vr
} }
@@ -4255,7 +4368,7 @@ func (n *raft) processVoteRequest(vr *voteRequest) error {
n.Lock() 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) 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 // 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. // Only way we get to yes is through here.
voteOk := n.vote == noVote || n.vote == vr.candidate 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) { if voteOk && (vr.lastTerm > n.pterm || vr.lastTerm == n.pterm && vr.lastIndex >= n.pindex) {
vresp.granted = true vresp.granted = true
n.term = vr.term 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. // 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. // 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) n.resetElect(minElectionTimeout / 4)
return return
} }
+123 -10
View File
@@ -369,6 +369,19 @@ func (u *tagsOption) IsStatszChange() bool {
return true 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` // usersOption implements the option interface for the authorization `users`
// setting. // setting.
type usersOption struct { type usersOption struct {
@@ -862,6 +875,7 @@ type leafNodeOption struct {
noopOption noopOption
tlsFirstChanged bool tlsFirstChanged bool
compressionChanged bool compressionChanged bool
disabledChanged bool
} }
func (l *leafNodeOption) Apply(s *Server) { 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) 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 leafs []*client
var solicit []*leafNodeCfg
acceptSideCompOpts := &opts.LeafNode.Compression acceptSideCompOpts := &opts.LeafNode.Compression
s.mu.RLock() s.mu.RLock()
@@ -887,10 +902,15 @@ func (l *leafNodeOption) Apply(s *Server) {
if l := len(s.leafRemoteCfgs); l < max { if l := len(s.leafRemoteCfgs); l < max {
max = l max = l
} }
for i := 0; i < max; i++ { for i := range max {
lr := s.leafRemoteCfgs[i] lr := s.leafRemoteCfgs[i]
or := opts.LeafNode.Remotes[i]
lr.Lock() 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() lr.Unlock()
} }
@@ -899,6 +919,13 @@ func (l *leafNodeOption) Apply(s *Server) {
l.mu.Lock() l.mu.Lock()
if r := l.leaf.remote; r != nil { 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 co = &r.Compression
} else { } else {
co = acceptSideCompOpts co = acceptSideCompOpts
@@ -929,11 +956,25 @@ func (l *leafNodeOption) Apply(s *Server) {
l.mu.Unlock() l.mu.Unlock()
} }
s.mu.RUnlock() 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 { for _, l := range leafs {
l.closeConnection(ClientClosed) 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 // 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 // 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. // 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) slices.Sort(value.AllowedOrigins)
case string, bool, uint8, uint16, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, PinnedCertSet, 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, *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 // explicitly skipped types
case *AuthCallout: case *AuthCallout:
case JSTpmOpts: case JSTpmOpts:
@@ -1283,6 +1356,8 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
diffOpts = append(diffOpts, &passwordOption{}) diffOpts = append(diffOpts, &passwordOption{})
case "tags": case "tags":
diffOpts = append(diffOpts, &tagsOption{}) diffOpts = append(diffOpts, &tagsOption{})
case "metadata":
diffOpts = append(diffOpts, &metadataOption{})
case "authorization": case "authorization":
diffOpts = append(diffOpts, &authorizationOption{}) diffOpts = append(diffOpts, &authorizationOption{})
case "authtimeout": case "authtimeout":
@@ -1300,7 +1375,7 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
co := &clusterOption{ co := &clusterOption{
newValue: newClusterOpts, newValue: newClusterOpts,
permsChanged: !reflect.DeepEqual(newClusterOpts.Permissions, oldClusterOpts.Permissions), permsChanged: !reflect.DeepEqual(newClusterOpts.Permissions, oldClusterOpts.Permissions),
compressChanged: !reflect.DeepEqual(oldClusterOpts.Compression, newClusterOpts.Compression), compressChanged: !compressOptsEqual(&oldClusterOpts.Compression, &newClusterOpts.Compression),
} }
co.diffPoolAndAccounts(&oldClusterOpts) co.diffPoolAndAccounts(&oldClusterOpts)
// If there are added accounts, first make sure that we can look them up. // 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 // We also support config reload for compression. Check if it changed before
// blanking them out for the deep-equal check at the end. // 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 { if compressionChanged {
tmpOld.Compression, tmpNew.Compression = CompressionOpts{}, CompressionOpts{} tmpOld.Compression, tmpNew.Compression = CompressionOpts{}, CompressionOpts{}
} else if len(tmpOld.Remotes) == len(tmpNew.Remotes) { } else if len(tmpOld.Remotes) == len(tmpNew.Remotes) {
// Same that for tls first check, do the remotes now. // Same that for tls first check, do the remotes now.
for i := 0; i < len(tmpOld.Remotes); i++ { for i := range len(tmpOld.Remotes) {
if !reflect.DeepEqual(tmpOld.Remotes[i].Compression, tmpNew.Remotes[i].Compression) { if !compressOptsEqual(&tmpOld.Remotes[i].Compression, &tmpNew.Remotes[i].Compression) {
compressionChanged = true compressionChanged = true
break 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. // Need to do the same for remote leafnodes' TLS configs.
// But we can't just set remotes' TLSConfig to nil otherwise this // 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{ diffOpts = append(diffOpts, &leafNodeOption{
tlsFirstChanged: handshakeFirstChanged, tlsFirstChanged: handshakeFirstChanged,
compressionChanged: compressionChanged, compressionChanged: compressionChanged,
disabledChanged: disabledChanged,
}) })
case "jetstream": case "jetstream":
new := newValue.(bool) new := newValue.(bool)
@@ -1670,6 +1754,12 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
continue continue
case "nofastproducerstall": case "nofastproducerstall":
diffOpts = append(diffOpts, &noFastProdStallReload{noStall: newValue.(bool)}) 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: default:
// TODO(ik): Implement String() on those options to have a nice print. // 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 // %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 cp.DenyImports, cp.DenyExports = nil, nil
// Remove compression mode // Remove compression mode
cp.Compression = CompressionOpts{} cp.Compression = CompressionOpts{}
// Reset disabled status
cp.Disabled = false
rlns = append(rlns, &cp) rlns = append(rlns, &cp)
} }
return rlns return rlns
@@ -2535,3 +2627,24 @@ addLoop:
return add, remove 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
}
+11 -2
View File
@@ -143,6 +143,7 @@ const (
// Can be changed for tests // Can be changed for tests
var ( var (
routeConnectDelay = DEFAULT_ROUTE_CONNECT routeConnectDelay = DEFAULT_ROUTE_CONNECT
routeConnectMaxDelay = DEFAULT_ROUTE_CONNECT_MAX
routeMaxPingInterval = defaultRouteMaxPingInterval routeMaxPingInterval = defaultRouteMaxPingInterval
) )
@@ -2054,7 +2055,7 @@ func (s *Server) createRoute(conn net.Conn, rURL *url.URL, rtype RouteType, goss
if tlsRequired { if tlsRequired {
c.Debugf("TLS handshake complete") c.Debugf("TLS handshake complete")
cs := c.nc.(*tls.Conn).ConnectionState() 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. // 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 excludedAddresses := s.routesToSelf
s.mu.RUnlock() s.mu.RUnlock()
attemptDelay := routeConnectDelay
for attempts := 0; s.isRunning(); { for attempts := 0; s.isRunning(); {
if tryForEver { if tryForEver {
if !s.routeStillValid(rURL) { if !s.routeStillValid(rURL) {
@@ -2935,7 +2937,14 @@ func (s *Server) connectToRoute(rURL *url.URL, rtype RouteType, firstConnect boo
select { select {
case <-s.quitCh: case <-s.quitCh:
return 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 continue
} }
} }
+239
View File
@@ -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
}
+111 -23
View File
@@ -30,6 +30,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"reflect"
"regexp" "regexp"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
@@ -129,6 +130,10 @@ type Info struct {
WSConnectURLs []string `json:"ws_connect_urls,omitempty"` // Contains URLs a ws client can connect to. WSConnectURLs []string `json:"ws_connect_urls,omitempty"` // Contains URLs a ws client can connect to.
LameDuckMode bool `json:"ldm,omitempty"` LameDuckMode bool `json:"ldm,omitempty"`
Compression string `json:"compression,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 // Route Specific
Import *SubjectPermission `json:"import,omitempty"` Import *SubjectPermission `json:"import,omitempty"`
@@ -136,7 +141,6 @@ type Info struct {
LNOC bool `json:"lnoc,omitempty"` LNOC bool `json:"lnoc,omitempty"`
LNOCU bool `json:"lnocu,omitempty"` LNOCU bool `json:"lnocu,omitempty"`
InfoOnConnect bool `json:"info_on_connect,omitempty"` // When true the server will respond to CONNECT with an INFO 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"` RoutePoolSize int `json:"route_pool_size,omitempty"`
RoutePoolIdx int `json:"route_pool_idx,omitempty"` RoutePoolIdx int `json:"route_pool_idx,omitempty"`
RouteAccount string `json:"route_account,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" GatewayIOM bool `json:"gateway_iom,omitempty"` // Indicate that all accounts will be switched to InterestOnly mode "right away"
// LeafNode Specific // LeafNode Specific
LeafNodeURLs []string `json:"leafnode_urls,omitempty"` // LeafNode URLs that the server can reconnect to. 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.
XKey string `json:"xkey,omitempty"` // Public server's x25519 key. XKey string `json:"xkey,omitempty"` // Public server's x25519 key.
} }
@@ -167,6 +170,7 @@ type Server struct {
pinnedAccFail uint64 pinnedAccFail uint64
stats stats
scStats scStats
staleStats
mu sync.RWMutex mu sync.RWMutex
reloadMu sync.RWMutex // Write-locked when a config reload is taking place ONLY reloadMu sync.RWMutex // Write-locked when a config reload is taking place ONLY
kp nkeys.KeyPair kp nkeys.KeyPair
@@ -259,8 +263,6 @@ type Server struct {
// Used internally for quick look-ups. // Used internally for quick look-ups.
clientConnectURLsMap refCountedUrlSet clientConnectURLsMap refCountedUrlSet
lastCURLsUpdate int64
// For Gateways // For Gateways
gatewayListener net.Listener // Accept listener gatewayListener net.Listener // Accept listener
gatewayListenerErr error gatewayListenerErr error
@@ -371,6 +373,11 @@ type Server struct {
// Controls whether or not the account NRG capability is set in statsz. // Controls whether or not the account NRG capability is set in statsz.
// Currently used by unit tests to simulate nodes not supporting account NRG. // Currently used by unit tests to simulate nodes not supporting account NRG.
accountNRGAllowed atomic.Bool 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. // For tracking JS nodes.
@@ -390,11 +397,13 @@ type nodeInfo struct {
} }
type stats struct { type stats struct {
inMsgs int64 inMsgs int64
outMsgs int64 outMsgs int64
inBytes int64 inBytes int64
outBytes int64 outBytes int64
slowConsumers int64 slowConsumers int64
staleConnections int64
stalls int64
} }
// scStats includes the total and per connection counters of Slow Consumers. // scStats includes the total and per connection counters of Slow Consumers.
@@ -405,6 +414,14 @@ type scStats struct {
gateways atomic.Uint64 gateways atomic.Uint64
} }
// staleStats includes the total and per connection counters of Stale Connections.
type staleStats struct {
clients atomic.Uint64
routes atomic.Uint64
leafs atomic.Uint64
gateways atomic.Uint64
}
// This is used by tests so we can run all server tests with a default route // This is used by tests so we can run all server tests with a default route
// or leafnode compression mode. For instance: // or leafnode compression mode. For instance:
// go test -race -v ./server -cluster_compression=fast // go test -race -v ./server -cluster_compression=fast
@@ -625,6 +642,32 @@ func selectS2AutoModeBasedOnRTT(rtt time.Duration, rttThresholds []time.Duration
return CompressionS2Best 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. // 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() // 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 // 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, Headers: !opts.NoHeaderSupport,
Cluster: opts.Cluster.Name, Cluster: opts.Cluster.Name,
Domain: opts.JetStreamDomain, Domain: opts.JetStreamDomain,
JSApiLevel: JSApiLevel,
} }
if tlsReq && !info.TLSRequired { if tlsReq && !info.TLSRequired {
@@ -774,6 +818,11 @@ func NewServer(opts *Options) (*Server, error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() 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. // Place ourselves in the JetStream nodeInfo if needed.
if opts.JetStream { if opts.JetStream {
ourNode := getHash(serverName) ourNode := getHash(serverName)
@@ -1099,6 +1148,10 @@ func validateOptions(o *Options) error {
if err := validateAuth(o); err != nil { if err := validateAuth(o); err != nil {
return err 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 // Check that gateway is properly configured. Returns no error
// if there is no gateway defined. // if there is no gateway defined.
if err := validateGatewayOptions(o); err != nil { if err := validateGatewayOptions(o); err != nil {
@@ -2352,7 +2405,7 @@ func (s *Server) Start() {
StoreDir: opts.StoreDir, StoreDir: opts.StoreDir,
SyncInterval: opts.SyncInterval, SyncInterval: opts.SyncInterval,
SyncAlways: opts.SyncAlways, SyncAlways: opts.SyncAlways,
Strict: opts.JetStreamStrict, Strict: !opts.NoJetStreamStrict,
MaxMemory: opts.JetStreamMaxMemory, MaxMemory: opts.JetStreamMaxMemory,
MaxStore: opts.JetStreamMaxStore, MaxStore: opts.JetStreamMaxStore,
Domain: opts.JetStreamDomain, Domain: opts.JetStreamDomain,
@@ -3396,7 +3449,7 @@ func (s *Server) createClientEx(conn net.Conn, inProcess bool) *client {
if tlsRequired { if tlsRequired {
c.Debugf("TLS handshake complete") c.Debugf("TLS handshake complete")
cs := c.nc.(*tls.Conn).ConnectionState() 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() 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) updateInfo(&s.info.WSConnectURLs, s.websocket.connectURLs, s.websocket.connectURLsMap)
} }
if cliUpdated || wsUpdated { if cliUpdated || wsUpdated {
// Update the time of this update
s.lastCURLsUpdate = time.Now().UnixNano()
// Send to all registered clients that support async INFO protocols. // Send to all registered clients that support async INFO protocols.
s.sendAsyncInfoToClients(cliUpdated, wsUpdated) s.sendAsyncInfoToClients(cliUpdated, wsUpdated)
} }
@@ -3554,15 +3605,6 @@ func tlsVersionFromString(ver string) (uint16, error) {
return 0, fmt.Errorf("unknown version: %v", ver) 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. // Remove a client or route from our internal accounting.
func (s *Server) removeClient(c *client) { func (s *Server) removeClient(c *client) {
// kind is immutable, so can check without lock // 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 { if c.kind == CLIENT && c.opts.Protocol >= ClientProtoInfo {
updateProtoInfoCount = true updateProtoInfoCount = true
} }
proxyKey := c.proxyKey
c.mu.Unlock() c.mu.Unlock()
s.mu.Lock() s.mu.Lock()
@@ -3581,6 +3624,9 @@ func (s *Server) removeClient(c *client) {
if updateProtoInfoCount { if updateProtoInfoCount {
s.cproto-- s.cproto--
} }
if proxyKey != _EMPTY_ {
s.removeProxiedConn(proxyKey, cid)
}
s.mu.Unlock() s.mu.Unlock()
case ROUTER: case ROUTER:
s.removeRoute(c) 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) { func (s *Server) removeFromTempClients(cid uint64) {
s.grMu.Lock() s.grMu.Lock()
delete(s.grTmpClients, cid) delete(s.grTmpClients, cid)
@@ -3699,6 +3757,11 @@ func (s *Server) NumSlowConsumers() int64 {
return atomic.LoadInt64(&s.slowConsumers) 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. // NumSlowConsumersClients will report the number of slow consumers clients.
func (s *Server) NumSlowConsumersClients() uint64 { func (s *Server) NumSlowConsumersClients() uint64 {
return s.scStats.clients.Load() return s.scStats.clients.Load()
@@ -3719,6 +3782,31 @@ func (s *Server) NumSlowConsumersLeafs() uint64 {
return s.scStats.leafs.Load() 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. // ConfigTime will report the last time the server configuration was loaded.
func (s *Server) ConfigTime() time.Time { func (s *Server) ConfigTime() time.Time {
s.mu.RLock() s.mu.RLock()
+9 -15
View File
@@ -35,8 +35,6 @@ const (
FileStorage = StorageType(22) FileStorage = StorageType(22)
// MemoryStorage specifies in memory only. // MemoryStorage specifies in memory only.
MemoryStorage = StorageType(33) MemoryStorage = StorageType(33)
// Any is for internals.
AnyStorage = StorageType(44)
) )
var ( 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. // Used to call back into the upper layers to remove a message.
type StorageRemoveMsgHandler func(seq uint64) type StorageRemoveMsgHandler func(seq uint64)
// Used to call back into the upper layers to report on newly created subject delete markers. // Used to call back into the upper layers to process a JetStream message.
type SubjectDeleteMarkerUpdateHandler func(*inMsg) // Will propose the message if the stream is replicated.
type ProcessJetStreamMsgHandler func(*inMsg)
type StreamStore interface { type StreamStore interface {
StoreMsg(subject string, hdr, msg []byte, ttl int64) (uint64, int64, error) StoreMsg(subject string, hdr, msg []byte, ttl int64) (uint64, int64, error)
StoreRawMsg(subject string, hdr, msg []byte, seq uint64, ts int64, ttl int64) error StoreRawMsg(subject string, hdr, msg []byte, seq uint64, ts int64, ttl int64) error
SkipMsg() uint64 SkipMsg() uint64
SkipMsgs(seq uint64, num uint64) error SkipMsgs(seq uint64, num uint64) error
FlushAllPending()
LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error) LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error)
LoadNextMsg(filter string, wc bool, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err 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) LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error)
@@ -120,16 +120,17 @@ type StreamStore interface {
SyncDeleted(dbs DeleteBlocks) SyncDeleted(dbs DeleteBlocks)
Type() StorageType Type() StorageType
RegisterStorageUpdates(StorageUpdateHandler) RegisterStorageUpdates(StorageUpdateHandler)
RegisterStorageRemoveMsg(handler StorageRemoveMsgHandler) RegisterStorageRemoveMsg(StorageRemoveMsgHandler)
RegisterSubjectDeleteMarkerUpdates(SubjectDeleteMarkerUpdateHandler) RegisterProcessJetStreamMsg(ProcessJetStreamMsgHandler)
UpdateConfig(cfg *StreamConfig) error UpdateConfig(cfg *StreamConfig) error
Delete() error Delete(inline bool) error
Stop() error Stop() error
ConsumerStore(name string, cfg *ConsumerConfig) (ConsumerStore, error) ConsumerStore(name string, cfg *ConsumerConfig) (ConsumerStore, error)
AddConsumer(o ConsumerStore) error AddConsumer(o ConsumerStore) error
RemoveConsumer(o ConsumerStore) error RemoveConsumer(o ConsumerStore) error
Snapshot(deadline time.Duration, includeConsumers, checkMsgs bool) (*SnapshotResult, error) Snapshot(deadline time.Duration, includeConsumers, checkMsgs bool) (*SnapshotResult, error)
Utilization() (total, reported uint64, err error) Utilization() (total, reported uint64, err error)
ResetState()
} }
// RetentionPolicy determines how messages in a set are retained. // RetentionPolicy determines how messages in a set are retained.
@@ -461,6 +462,7 @@ type Pending struct {
} }
// TemplateStore stores templates. // TemplateStore stores templates.
// Deprecated: stream templates are deprecated and will be removed in a future version.
type TemplateStore interface { type TemplateStore interface {
Store(*streamTemplate) error Store(*streamTemplate) error
Delete(*streamTemplate) error Delete(*streamTemplate) error
@@ -555,13 +557,11 @@ func (dp *DiscardPolicy) UnmarshalJSON(data []byte) error {
const ( const (
memoryStorageJSONString = `"memory"` memoryStorageJSONString = `"memory"`
fileStorageJSONString = `"file"` fileStorageJSONString = `"file"`
anyStorageJSONString = `"any"`
) )
var ( var (
memoryStorageJSONBytes = []byte(memoryStorageJSONString) memoryStorageJSONBytes = []byte(memoryStorageJSONString)
fileStorageJSONBytes = []byte(fileStorageJSONString) fileStorageJSONBytes = []byte(fileStorageJSONString)
anyStorageJSONBytes = []byte(anyStorageJSONString)
) )
func (st StorageType) String() string { func (st StorageType) String() string {
@@ -570,8 +570,6 @@ func (st StorageType) String() string {
return "Memory" return "Memory"
case FileStorage: case FileStorage:
return "File" return "File"
case AnyStorage:
return "Any"
default: default:
return "Unknown Storage Type" return "Unknown Storage Type"
} }
@@ -583,8 +581,6 @@ func (st StorageType) MarshalJSON() ([]byte, error) {
return memoryStorageJSONBytes, nil return memoryStorageJSONBytes, nil
case FileStorage: case FileStorage:
return fileStorageJSONBytes, nil return fileStorageJSONBytes, nil
case AnyStorage:
return anyStorageJSONBytes, nil
default: default:
return nil, fmt.Errorf("can not marshal %v", st) return nil, fmt.Errorf("can not marshal %v", st)
} }
@@ -596,8 +592,6 @@ func (st *StorageType) UnmarshalJSON(data []byte) error {
*st = MemoryStorage *st = MemoryStorage
case fileStorageJSONString: case fileStorageJSONString:
*st = FileStorage *st = FileStorage
case anyStorageJSONString:
*st = AnyStorage
default: default:
return fmt.Errorf("can not unmarshal %q", data) return fmt.Errorf("can not unmarshal %q", data)
} }
File diff suppressed because it is too large Load Diff
+67 -12
View File
@@ -16,6 +16,8 @@ package server
import ( import (
"fmt" "fmt"
"hash/fnv" "hash/fnv"
"math"
"math/rand"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -33,6 +35,7 @@ var (
splitMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit\s*\((.*)\)\s*}}`) splitMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit\s*\((.*)\)\s*}}`)
leftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[lL]eft\s*\((.*)\)\s*}}`) leftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[lL]eft\s*\((.*)\)\s*}}`)
rightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[rR]ight\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 // Enum for the subject mapping subjectTransform function types
@@ -48,6 +51,7 @@ const (
Split Split
Left Left
Right Right
Random
) )
// Transforms for arbitrarily mapping subjects from one to another for maps, tees and filters. // 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 { if tranformType == NoTransform {
dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform) dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform)
dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1}) dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1})
dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1) dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1)
dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_) 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 { } else {
nphs += len(transformArgWildcardIndexes) nphs += len(transformArgWildcardIndexes)
// Now build up our runtime mapping from dest to source tokens. // Now build up our runtime mapping from dest to source tokens.
@@ -158,12 +161,22 @@ func NewSubjectTransformWithStrict(src, dest string, strict bool) (*subjectTrans
} else { } else {
// no wildcards used in the source: check that no transform functions are used in the destination // no wildcards used in the source: check that no transform functions are used in the destination
for _, token := range dtokens { for _, token := range dtokens {
tranformType, _, _, _, err := indexPlaceHolders(token) tranformType, _, transfomArgInt, _, err := indexPlaceHolders(token)
if err != nil { if err != nil {
return nil, err 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} return nil, &mappingDestinationErr{token, ErrMappingDestinationIndexOutOfRange}
} }
} }
@@ -255,12 +268,19 @@ func indexPlaceHolders(token string) (int16, []int, int32, string, error) {
// partition(number of partitions, token1, token2, ...) // partition(number of partitions, token1, token2, ...)
args = getMappingFunctionArgs(partitionMappingFunctionRegEx, token) args = getMappingFunctionArgs(partitionMappingFunctionRegEx, token)
if args != nil { if args != nil {
if len(args) < 2 { if len(args) < 1 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs} 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 { if len(args) >= 2 {
mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " ")) 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} return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
} }
var numPositions = len(args[1:]) 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 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} return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction}
} }
} }
@@ -423,7 +456,21 @@ func (tr *subjectTransform) TransformSubject(subject string) string {
return tr.TransformTokenizedSubject(tokenizeSubject(subject)) 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 { 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 := fnv.New32a()
_, _ = h.Write(key) _, _ = h.Write(key)
@@ -454,8 +501,14 @@ func (tr *subjectTransform) TransformTokenizedSubject(tokens []string) string {
_buffer [64]byte _buffer [64]byte
keyForHashing = _buffer[:0] keyForHashing = _buffer[:0]
) )
for _, sourceToken := range tr.dtokmftokindexesargs[i] { if len(tr.dtokmftokindexesargs[i]) > 0 {
keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...) // 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]))) b.WriteString(tr.getHashPartition(keyForHashing, int(tr.dtokmfintargs[i])))
case Wildcard: // simple substitution 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 } else { // too small to slice at the requested size: don't slice
b.WriteString(sourceToken) b.WriteString(sourceToken)
} }
case Random:
b.WriteString(tr.getRandomPartition(int(tr.dtokmfintargs[i])))
} }
} }
+2 -1
View File
@@ -1292,7 +1292,8 @@ func ValidateMapping(src string, dest string) error {
!splitFromRightMappingFunctionRegEx.MatchString(t) && !splitFromRightMappingFunctionRegEx.MatchString(t) &&
!sliceFromLeftMappingFunctionRegEx.MatchString(t) && !sliceFromLeftMappingFunctionRegEx.MatchString(t) &&
!sliceFromRightMappingFunctionRegEx.MatchString(t) && !sliceFromRightMappingFunctionRegEx.MatchString(t) &&
!splitMappingFunctionRegEx.MatchString(t) { !splitMappingFunctionRegEx.MatchString(t) &&
!randomMappingFunctionRegEx.MatchString(t) {
return &mappingDestinationErr{t, ErrUnknownMappingDestinationFunction} return &mappingDestinationErr{t, ErrUnknownMappingDestinationFunction}
} else { } else {
continue continue
+1 -1
View File
@@ -211,7 +211,7 @@ func (hw *HashWheel) Count() uint64 {
return hw.count 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 // and returns it. The high seq number is included in the snapshot and will
// be returned on decode. // be returned on decode.
func (hw *HashWheel) Encode(highSeq uint64) []byte { func (hw *HashWheel) Encode(highSeq uint64) []byte {
+1 -1
View File
@@ -312,7 +312,7 @@ func getURLsAsString(urls []*url.URL) []string {
return a 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` // If `src` is nil or its length is 0, then this returns `nil`
func copyBytes(src []byte) []byte { func copyBytes(src []byte) []byte {
if len(src) == 0 { if len(src) == 0 {
+6 -3
View File
@@ -1147,21 +1147,24 @@ func (s *Server) startWebsocketServer() {
// regardless of NoTLS. If we don't have a TLS config, it means that the // 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 // user has configured NoTLS because otherwise the server would have failed
// to start due to options validation. // to start due to options validation.
var config *tls.Config
if o.TLSConfig != nil { if o.TLSConfig != nil {
proto = wsSchemePrefixTLS proto = wsSchemePrefixTLS
config := o.TLSConfig.Clone() config = o.TLSConfig.Clone()
config.GetConfigForClient = s.wsGetTLSConfig config.GetConfigForClient = s.wsGetTLSConfig
hl, err = tls.Listen("tcp", hp, config)
} else { } else {
proto = wsSchemePrefix proto = wsSchemePrefix
hl, err = net.Listen("tcp", hp)
} }
hl, err = natsListen("tcp", hp)
s.websocket.listenerErr = err s.websocket.listenerErr = err
if err != nil { if err != nil {
s.mu.Unlock() s.mu.Unlock()
s.Fatalf("Unable to listen for websocket connections: %v", err) s.Fatalf("Unable to listen for websocket connections: %v", err)
return return
} }
if config != nil {
hl = tls.NewListener(hl, config)
}
if port == 0 { if port == 0 {
o.Port = hl.Addr().(*net.TCPAddr).Port o.Port = hl.Addr().(*net.TCPAddr).Port
} }
+3 -2
View File
@@ -1105,10 +1105,10 @@ github.com/mschoch/smat
# github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 # github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
## explicit ## explicit
github.com/munnerz/goautoneg 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 ## explicit; go 1.23.0
github.com/nats-io/jwt/v2 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 ## explicit; go 1.24.0
github.com/nats-io/nats-server/v2/conf github.com/nats-io/nats-server/v2/conf
github.com/nats-io/nats-server/v2/internal/fastrand 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/avl
github.com/nats-io/nats-server/v2/server/certidp 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/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/gsl
github.com/nats-io/nats-server/v2/server/pse github.com/nats-io/nats-server/v2/server/pse
github.com/nats-io/nats-server/v2/server/stree github.com/nats-io/nats-server/v2/server/stree