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:
committed by
Ralf Haferkamp
parent
4a0cc1004f
commit
703b8dd084
@@ -56,7 +56,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/mna/pigeon v1.3.0
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
github.com/nats-io/nats-server/v2 v2.11.9
|
||||
github.com/nats-io/nats-server/v2 v2.12.0
|
||||
github.com/nats-io/nats.go v1.46.0
|
||||
github.com/oklog/run v1.2.0
|
||||
github.com/olekukonko/tablewriter v1.0.9
|
||||
@@ -296,7 +296,7 @@ require (
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.7.4 // indirect
|
||||
github.com/nats-io/jwt/v2 v2.8.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
|
||||
@@ -881,10 +881,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
|
||||
github.com/nats-io/jwt/v2 v2.7.4 h1:jXFuDDxs/GQjGDZGhNgH4tXzSUK6WQi2rsj4xmsNOtI=
|
||||
github.com/nats-io/jwt/v2 v2.7.4/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
|
||||
github.com/nats-io/nats-server/v2 v2.11.9 h1:k7nzHZjUf51W1b08xiQih63Rdxh0yr5O4K892Mx5gQA=
|
||||
github.com/nats-io/nats-server/v2 v2.11.9/go.mod h1:1MQgsAQX1tVjpf3Yzrk3x2pzdsZiNL/TVP3Amhp3CR8=
|
||||
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
|
||||
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
|
||||
github.com/nats-io/nats-server/v2 v2.12.0 h1:OIwe8jZUqJFrh+hhiyKu8snNib66qsx806OslqJuo74=
|
||||
github.com/nats-io/nats-server/v2 v2.12.0/go.mod h1:nr8dhzqkP5E/lDwmn+A2CvQPMd1yDKXQI7iGg3lAvww=
|
||||
github.com/nats-io/nats.go v1.46.0 h1:iUcX+MLT0HHXskGkz+Sg20sXrPtJLsOojMDTDzOHSb8=
|
||||
github.com/nats-io/nats.go v1.46.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ type UserPermissionLimits struct {
|
||||
Permissions
|
||||
Limits
|
||||
BearerToken bool `json:"bearer_token,omitempty"`
|
||||
ProxyRequired bool `json:"proxy_required,omitempty"`
|
||||
AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
+171
-13
@@ -25,6 +25,7 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -65,6 +66,7 @@ type NkeyUser struct {
|
||||
Account *Account `json:"account,omitempty"`
|
||||
SigningKey string `json:"signing_key,omitempty"`
|
||||
AllowedConnectionTypes map[string]struct{} `json:"connection_types,omitempty"`
|
||||
ProxyRequired bool `json:"proxy_required,omitempty"`
|
||||
}
|
||||
|
||||
// User is for multiple accounts/users.
|
||||
@@ -75,6 +77,7 @@ type User struct {
|
||||
Account *Account `json:"account,omitempty"`
|
||||
ConnectionDeadline time.Time `json:"connection_deadline,omitempty"`
|
||||
AllowedConnectionTypes map[string]struct{} `json:"connection_types,omitempty"`
|
||||
ProxyRequired bool `json:"proxy_required,omitempty"`
|
||||
}
|
||||
|
||||
// clone performs a deep copy of the User struct, returning a new clone with
|
||||
@@ -593,10 +596,30 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
ao bool // auth override
|
||||
)
|
||||
|
||||
// Little helper that will log the error as a debug statement, set the auth error in
|
||||
// the connection and return false to indicate authentication failure.
|
||||
setProxyAuthError := func(err error) bool {
|
||||
c.Debugf(err.Error())
|
||||
c.setAuthError(err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Indicate if this connection came from a trusted proxy. Note that if
|
||||
// trustedProxy could be false even if the connection is proxied, but it
|
||||
// means that there was no trusted proxy configured.
|
||||
trustedProxy, ok := s.proxyCheck(c, opts)
|
||||
if trustedProxy && !ok {
|
||||
return setProxyAuthError(ErrAuthProxyNotTrusted)
|
||||
}
|
||||
|
||||
var proxyRequired bool
|
||||
// Check if we have auth callouts enabled at the server level or in the bound account.
|
||||
defer func() {
|
||||
// Default reason
|
||||
reason := AuthenticationViolation.String()
|
||||
authErr := c.getAuthError()
|
||||
if authErr == nil {
|
||||
authErr = ErrAuthentication
|
||||
}
|
||||
reason := getAuthErrClosedState(authErr).String()
|
||||
// No-op
|
||||
if juc == nil && opts.AuthCallout == nil {
|
||||
if !authorized {
|
||||
@@ -656,7 +679,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
// If we are here we have an auth callout defined and we have failed auth so far
|
||||
// so we will callout to our auth backend for processing.
|
||||
if !skip {
|
||||
authorized, reason = s.processClientOrLeafCallout(c, opts)
|
||||
authorized, reason = s.processClientOrLeafCallout(c, opts, proxyRequired, trustedProxy)
|
||||
}
|
||||
// Check if we are authorized and in the auth callout account, and if so add in deny publish permissions for the auth subject.
|
||||
if authorized {
|
||||
@@ -697,6 +720,11 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
if !authRequired {
|
||||
// TODO(dlc) - If they send us credentials should we fail?
|
||||
s.mu.Unlock()
|
||||
if c.kind == LEAF {
|
||||
// Auth is not required, register the leaf node with the selected account.
|
||||
// Otherwise, auth needs to match client auth.
|
||||
return s.registerLeafWithAccount(c, opts.LeafNode.Account)
|
||||
}
|
||||
return true
|
||||
}
|
||||
var (
|
||||
@@ -769,6 +797,10 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
c.Debugf("User JWT not valid: %v", err)
|
||||
return false
|
||||
}
|
||||
if proxyRequired = juc.ProxyRequired; proxyRequired && !trustedProxy {
|
||||
s.mu.Unlock()
|
||||
return setProxyAuthError(ErrAuthProxyRequired)
|
||||
}
|
||||
vr := jwt.CreateValidationResults()
|
||||
juc.Validate(vr)
|
||||
if vr.IsBlocking(true) {
|
||||
@@ -1045,6 +1077,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
}
|
||||
|
||||
if nkey != nil {
|
||||
if proxyRequired = nkey.ProxyRequired; proxyRequired && !trustedProxy {
|
||||
return setProxyAuthError(ErrAuthProxyRequired)
|
||||
}
|
||||
// If we did not match noAuthUser check signature which is required.
|
||||
if nkey.Nkey != noAuthUser {
|
||||
if c.opts.Sig == _EMPTY_ {
|
||||
@@ -1076,6 +1111,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
return true
|
||||
}
|
||||
if user != nil {
|
||||
if proxyRequired = user.ProxyRequired; proxyRequired && !trustedProxy {
|
||||
return setProxyAuthError(ErrAuthProxyRequired)
|
||||
}
|
||||
ok = comparePasswords(user.Password, c.opts.Password)
|
||||
// If we are authorized, register the user which will properly setup any permissions
|
||||
// for pub/sub authorizations.
|
||||
@@ -1086,6 +1124,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
}
|
||||
|
||||
if c.kind == CLIENT {
|
||||
if proxyRequired = opts.ProxyRequired; proxyRequired && !trustedProxy {
|
||||
return setProxyAuthError(ErrAuthProxyRequired)
|
||||
}
|
||||
if token != _EMPTY_ {
|
||||
return comparePasswords(token, c.opts.Token)
|
||||
} else if username != _EMPTY_ {
|
||||
@@ -1094,16 +1135,65 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
|
||||
}
|
||||
return comparePasswords(password, c.opts.Password)
|
||||
}
|
||||
} else if c.kind == LEAF {
|
||||
// There is no required username/password to connect and
|
||||
// there was no u/p in the CONNECT or none that matches the
|
||||
// know users. Register the leaf connection with global account
|
||||
// or the one specified in config (if provided).
|
||||
return s.registerLeafWithAccount(c, opts.LeafNode.Account)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// If there are configured trusted proxies and this connection comes
|
||||
// from a proxy whose signature can be verified by one of the known
|
||||
// trusted key, this function will return `true, true`. If the signature
|
||||
// cannot be verified by any, it will return `true, false`.
|
||||
// If the connectio is not proxied, or there are no configured trusted
|
||||
// proxies, then this function returns `false, false`.
|
||||
//
|
||||
// Server lock MUST NOT be held on entry since this function will grab
|
||||
// the read lock to extract the list of proxy trusted keys. The signature
|
||||
// verification process will be done outside of the lock.
|
||||
func (s *Server) proxyCheck(c *client, opts *Options) (bool, bool) {
|
||||
// If there is no signature or no configured trusted proxy, return false.
|
||||
psig := c.opts.ProxySig
|
||||
if psig == _EMPTY_ || opts.Proxies == nil || len(opts.Proxies.Trusted) == 0 {
|
||||
return false, false
|
||||
}
|
||||
// Decode the signature.
|
||||
sig, err := base64.RawURLEncoding.DecodeString(psig)
|
||||
if err != nil {
|
||||
c.Debugf("Proxy signature not valid base64")
|
||||
return true, false
|
||||
}
|
||||
// Go through the trusted keys and verify the signature.
|
||||
s.mu.RLock()
|
||||
keys := slices.Clone(s.proxiesKeyPairs)
|
||||
s.mu.RUnlock()
|
||||
for _, kp := range keys {
|
||||
// We stop at the first that is valid.
|
||||
if err := kp.Verify(c.nonce, sig); err == nil {
|
||||
pub, _ := kp.PublicKey()
|
||||
// Track which proxy public key is used by this connection.
|
||||
c.mu.Lock()
|
||||
c.proxyKey = pub
|
||||
cid := c.cid
|
||||
c.mu.Unlock()
|
||||
// Track this proxied connection so that it can be closed
|
||||
// if the trusted key is removed on configuration reload.
|
||||
s.mu.Lock()
|
||||
if s.proxiedConns == nil {
|
||||
s.proxiedConns = make(map[string]map[uint64]*client)
|
||||
}
|
||||
clients := s.proxiedConns[pub]
|
||||
if clients == nil {
|
||||
clients = make(map[uint64]*client)
|
||||
}
|
||||
clients[cid] = c
|
||||
s.proxiedConns[pub] = clients
|
||||
s.mu.Unlock()
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
// We could not verify the signature, so indicate failure.
|
||||
return true, false
|
||||
}
|
||||
|
||||
func getTLSAuthDCs(rdns *pkix.RDNSequence) string {
|
||||
dcOID := asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}
|
||||
dcs := []string{}
|
||||
@@ -1352,7 +1442,25 @@ func (s *Server) registerLeafWithAccount(c *client, account string) bool {
|
||||
func (s *Server) isLeafNodeAuthorized(c *client) bool {
|
||||
opts := s.getOpts()
|
||||
|
||||
isAuthorized := func(username, password, account string) bool {
|
||||
setProxyAuthError := func(err error) bool {
|
||||
c.Debugf(err.Error())
|
||||
c.setAuthError(err)
|
||||
return false
|
||||
}
|
||||
|
||||
isAuthorized := func(username, password, account string, proxyRequired bool) bool {
|
||||
trustedProxy, ok := s.proxyCheck(c, opts)
|
||||
if trustedProxy && !ok {
|
||||
return setProxyAuthError(ErrAuthProxyNotTrusted)
|
||||
}
|
||||
// A given user may not be required, but if the boolean is set at the
|
||||
// authorization top-level, then override.
|
||||
if !proxyRequired && opts.LeafNode.ProxyRequired {
|
||||
proxyRequired = true
|
||||
}
|
||||
if proxyRequired && !trustedProxy {
|
||||
return setProxyAuthError(ErrAuthProxyRequired)
|
||||
}
|
||||
if username != c.opts.Username {
|
||||
return false
|
||||
}
|
||||
@@ -1366,8 +1474,16 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool {
|
||||
// The user in CONNECT must match. We will bind to the account associated
|
||||
// with that user (from the leafnode's authorization{} config).
|
||||
if opts.LeafNode.Username != _EMPTY_ {
|
||||
return isAuthorized(opts.LeafNode.Username, opts.LeafNode.Password, opts.LeafNode.Account)
|
||||
return isAuthorized(opts.LeafNode.Username, opts.LeafNode.Password, opts.LeafNode.Account,
|
||||
opts.LeafNode.ProxyRequired)
|
||||
} else if opts.LeafNode.Nkey != _EMPTY_ {
|
||||
trustedProxy, ok := s.proxyCheck(c, opts)
|
||||
if trustedProxy && !ok {
|
||||
return false
|
||||
}
|
||||
if opts.LeafNode.ProxyRequired && !trustedProxy {
|
||||
return setProxyAuthError(ErrAuthProxyRequired)
|
||||
}
|
||||
if c.opts.Nkey != opts.LeafNode.Nkey {
|
||||
return false
|
||||
}
|
||||
@@ -1421,7 +1537,7 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool {
|
||||
}
|
||||
// This will authorize since are using an existing user,
|
||||
// but it will also register with proper account.
|
||||
return isAuthorized(user.Username, user.Password, accName)
|
||||
return isAuthorized(user.Username, user.Password, accName, user.ProxyRequired)
|
||||
}
|
||||
|
||||
// This is expected to be a very small array.
|
||||
@@ -1431,7 +1547,7 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool {
|
||||
if u.Account != nil {
|
||||
accName = u.Account.Name
|
||||
}
|
||||
return isAuthorized(u.Username, u.Password, accName)
|
||||
return isAuthorized(u.Username, u.Password, accName, u.ProxyRequired)
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -1536,3 +1652,45 @@ func validateNoAuthUser(o *Options, noAuthUser string) error {
|
||||
`no_auth_user: "%s" not present as user or nkey in authorization block or account configuration`,
|
||||
noAuthUser)
|
||||
}
|
||||
|
||||
func validateProxies(o *Options) error {
|
||||
if o.Proxies == nil {
|
||||
return nil
|
||||
}
|
||||
for _, p := range o.Proxies.Trusted {
|
||||
if !nkeys.IsValidPublicKey(p.Key) {
|
||||
return fmt.Errorf("proxy trusted key %q is invalid", p.Key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a list of nkeys.KeyPair corresponding to the public keys
|
||||
// of the Proxies.TrustedKeys list.
|
||||
// Server lock must be held on entry.
|
||||
func (s *Server) processProxiesTrustedKeys() {
|
||||
// We could be here on reload.
|
||||
if s.proxiesKeyPairs != nil {
|
||||
s.proxiesKeyPairs = s.proxiesKeyPairs[:0]
|
||||
}
|
||||
if opts := s.getOpts(); opts.Proxies == nil {
|
||||
return
|
||||
}
|
||||
for _, p := range s.getOpts().Proxies.Trusted {
|
||||
// Can't fail since we have already checked that it was a valid key.
|
||||
kp, _ := nkeys.FromPublicKey(p.Key)
|
||||
s.proxiesKeyPairs = append(s.proxiesKeyPairs, kp)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the connection's `ClosedState` for the given authenication error.
|
||||
func getAuthErrClosedState(authErr error) ClosedState {
|
||||
switch authErr {
|
||||
case ErrAuthProxyNotTrusted:
|
||||
return ProxyNotTrusted
|
||||
case ErrAuthProxyRequired:
|
||||
return ProxyRequired
|
||||
default:
|
||||
return AuthenticationViolation
|
||||
}
|
||||
}
|
||||
|
||||
+12
-2
@@ -33,7 +33,7 @@ const (
|
||||
)
|
||||
|
||||
// Process a callout on this client's behalf.
|
||||
func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorized bool, errStr string) {
|
||||
func (s *Server) processClientOrLeafCallout(c *client, opts *Options, proxyRequired, trustedProxy bool) (authorized bool, errStr string) {
|
||||
isOperatorMode := len(opts.TrustedKeys) > 0
|
||||
|
||||
// this is the account the user connected in, or the one running the callout
|
||||
@@ -245,6 +245,16 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
|
||||
respCh <- titleCase(err.Error())
|
||||
return
|
||||
}
|
||||
// If the caller had established that the user should go through a proxy,
|
||||
// or if the `arc` JWT requires it, and we don't have a trusted proxy,
|
||||
// reject the connection.
|
||||
if (proxyRequired || arc.ProxyRequired) && !trustedProxy {
|
||||
err = ErrAuthProxyRequired
|
||||
c.setAuthError(err)
|
||||
c.authViolation()
|
||||
respCh <- titleCase(err.Error())
|
||||
return
|
||||
}
|
||||
vr := jwt.CreateValidationResults()
|
||||
arc.Validate(vr)
|
||||
if len(vr.Issues) > 0 {
|
||||
@@ -369,7 +379,7 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
|
||||
conn := c.nc.(*tls.Conn)
|
||||
cs := conn.ConnectionState()
|
||||
ct.Version = tlsVersion(cs.Version)
|
||||
ct.Cipher = tlsCipher(cs.CipherSuite)
|
||||
ct.Cipher = tls.CipherSuiteName(cs.CipherSuite)
|
||||
// Check verified chains.
|
||||
for _, vs := range cs.VerifiedChains {
|
||||
var certs []string
|
||||
|
||||
+23
-64
@@ -17,85 +17,44 @@ import (
|
||||
"crypto/tls"
|
||||
)
|
||||
|
||||
// Where we maintain all of the available ciphers
|
||||
var cipherMap = map[string]uint16{
|
||||
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
|
||||
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
"TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256,
|
||||
"TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384,
|
||||
"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
func init() {
|
||||
for _, cs := range tls.CipherSuites() {
|
||||
cipherMap[cs.Name] = cs
|
||||
cipherMapByID[cs.ID] = cs
|
||||
}
|
||||
for _, cs := range tls.InsecureCipherSuites() {
|
||||
cipherMap[cs.Name] = cs
|
||||
cipherMapByID[cs.ID] = cs
|
||||
}
|
||||
}
|
||||
|
||||
var cipherMapByID = map[uint16]string{
|
||||
tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA",
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA",
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA256: "TLS_RSA_WITH_AES_128_CBC_SHA256",
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA",
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384",
|
||||
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
|
||||
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
tls.TLS_AES_128_GCM_SHA256: "TLS_AES_128_GCM_SHA256",
|
||||
tls.TLS_AES_256_GCM_SHA384: "TLS_AES_256_GCM_SHA384",
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256: "TLS_CHACHA20_POLY1305_SHA256",
|
||||
}
|
||||
var cipherMap = map[string]*tls.CipherSuite{}
|
||||
var cipherMapByID = map[uint16]*tls.CipherSuite{}
|
||||
|
||||
func defaultCipherSuites() []uint16 {
|
||||
return []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
ciphers := tls.CipherSuites()
|
||||
defaults := make([]uint16, 0, len(ciphers))
|
||||
for _, cs := range ciphers {
|
||||
defaults = append(defaults, cs.ID)
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
// Where we maintain available curve preferences
|
||||
var curvePreferenceMap = map[string]tls.CurveID{
|
||||
"X25519": tls.X25519,
|
||||
"CurveP256": tls.CurveP256,
|
||||
"CurveP384": tls.CurveP384,
|
||||
"CurveP521": tls.CurveP521,
|
||||
"X25519MLKEM768": tls.X25519MLKEM768,
|
||||
"X25519": tls.X25519,
|
||||
"CurveP256": tls.CurveP256,
|
||||
"CurveP384": tls.CurveP384,
|
||||
"CurveP521": tls.CurveP521,
|
||||
}
|
||||
|
||||
// reorder to default to the highest level of security. See:
|
||||
// https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
|
||||
func defaultCurvePreferences() []tls.CurveID {
|
||||
return []tls.CurveID{
|
||||
tls.X25519, // faster than P256, arguably more secure
|
||||
tls.X25519MLKEM768, // post-quantum
|
||||
tls.X25519, // faster than P256, arguably more secure
|
||||
tls.CurveP256,
|
||||
tls.CurveP384,
|
||||
tls.CurveP521,
|
||||
|
||||
+322
-130
@@ -35,6 +35,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
|
||||
"github.com/klauspost/compress/s2"
|
||||
"github.com/nats-io/jwt/v2"
|
||||
"github.com/nats-io/nats-server/v2/internal/fastrand"
|
||||
@@ -222,6 +224,8 @@ const (
|
||||
MinimumVersionRequired
|
||||
ClusterNamesIdentical
|
||||
Kicked
|
||||
ProxyNotTrusted
|
||||
ProxyRequired
|
||||
)
|
||||
|
||||
// Some flags passed to processMsgResults
|
||||
@@ -255,6 +259,8 @@ type client struct {
|
||||
pubKey string
|
||||
nc net.Conn
|
||||
ncs atomic.Value
|
||||
ncsAcc atomic.Value
|
||||
ncsUser atomic.Value
|
||||
out outbound
|
||||
user *NkeyUser
|
||||
host string
|
||||
@@ -270,6 +276,7 @@ type client struct {
|
||||
msgb [msgScratchSize]byte
|
||||
last time.Time
|
||||
lastIn time.Time
|
||||
proxyKey string
|
||||
|
||||
repliesSincePrune uint16
|
||||
lastReplyPrune time.Time
|
||||
@@ -298,6 +305,13 @@ type client struct {
|
||||
nameTag string
|
||||
|
||||
tlsTo *time.Timer
|
||||
|
||||
// Authentication error override. This is used because the authentication
|
||||
// stack is simply returning a boolean, and the only authentication error
|
||||
// reported is the generic `ErrAuthentication`. In the authentication code,
|
||||
// if we want to report a different error, we can now set this field
|
||||
// and `authViolation()` will use that one.
|
||||
authErr error
|
||||
}
|
||||
|
||||
type rrTracking struct {
|
||||
@@ -494,15 +508,13 @@ func (rcf readCacheFlag) isSet(c readCacheFlag) bool {
|
||||
}
|
||||
|
||||
const (
|
||||
defaultMaxPerAccountCacheSize = 8192
|
||||
defaultPrunePerAccountCacheSize = 1024
|
||||
defaultClosedSubsCheckInterval = 5 * time.Minute
|
||||
defaultMaxPerAccountCacheSize = 8192
|
||||
defaultClosedSubsCheckInterval = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
maxPerAccountCacheSize = defaultMaxPerAccountCacheSize
|
||||
prunePerAccountCacheSize = defaultPrunePerAccountCacheSize
|
||||
closedSubsCheckInterval = defaultClosedSubsCheckInterval
|
||||
maxPerAccountCacheSize = defaultMaxPerAccountCacheSize
|
||||
closedSubsCheckInterval = defaultClosedSubsCheckInterval
|
||||
)
|
||||
|
||||
// perAccountCache is for L1 semantics for inbound messages from a route or gateway to mimic the performance of clients.
|
||||
@@ -652,6 +664,9 @@ type ClientOpts struct {
|
||||
|
||||
// Leafnodes
|
||||
RemoteAccount string `json:"remote_account,omitempty"`
|
||||
|
||||
// Proxy would include its own nonce signature.
|
||||
ProxySig string `json:"proxy_sig,omitempty"`
|
||||
}
|
||||
|
||||
var defaultOpts = ClientOpts{Verbose: true, Pedantic: true, Echo: true}
|
||||
@@ -1854,6 +1869,19 @@ func (c *client) markConnAsClosed(reason ClosedState) {
|
||||
case ReadError, WriteError, SlowConsumerPendingBytes, SlowConsumerWriteDeadline, TLSHandshakeError:
|
||||
c.flags.set(skipFlushOnClose)
|
||||
skipFlush = true
|
||||
case StaleConnection:
|
||||
// Track stale connections statistics.
|
||||
atomic.AddInt64(&c.srv.staleConnections, 1)
|
||||
switch c.kind {
|
||||
case CLIENT:
|
||||
c.srv.staleStats.clients.Add(1)
|
||||
case ROUTER:
|
||||
c.srv.staleStats.routes.Add(1)
|
||||
case GATEWAY:
|
||||
c.srv.staleStats.gateways.Add(1)
|
||||
case LEAF:
|
||||
c.srv.staleStats.leafs.Add(1)
|
||||
}
|
||||
}
|
||||
if c.flags.isSet(connMarkedClosed) {
|
||||
return
|
||||
@@ -1867,14 +1895,25 @@ func (c *client) markConnAsClosed(reason ClosedState) {
|
||||
// Be consistent with the creation: for routes, gateways and leaf,
|
||||
// we use Noticef on create, so use that too for delete.
|
||||
if c.srv != nil {
|
||||
if c.kind == LEAF {
|
||||
if c.acc != nil {
|
||||
c.Noticef("%s connection closed: %s - Account: %s", c.kindString(), reason, c.acc.traceLabel())
|
||||
if c.kind == LEAF || c.kind == ROUTER || c.kind == GATEWAY {
|
||||
var tags []string
|
||||
var remoteName string
|
||||
switch {
|
||||
case c.kind == LEAF && c.leaf != nil:
|
||||
remoteName = c.leaf.remoteServer
|
||||
case c.kind == ROUTER && c.route != nil:
|
||||
remoteName = c.route.remoteName
|
||||
case c.kind == GATEWAY && c.gw != nil:
|
||||
remoteName = c.gw.remoteName
|
||||
}
|
||||
if remoteName != _EMPTY_ {
|
||||
tags = append(tags, fmt.Sprintf("Remote: %s", remoteName))
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
c.Noticef("%s connection closed: %s - %s", c.kindString(), reason, strings.Join(tags, ", "))
|
||||
} else {
|
||||
c.Noticef("%s connection closed: %s", c.kindString(), reason)
|
||||
}
|
||||
} else if c.kind == ROUTER || c.kind == GATEWAY {
|
||||
c.Noticef("%s connection closed: %s", c.kindString(), reason)
|
||||
} else { // Client, System, Jetstream, and Account connections.
|
||||
c.Debugf("%s connection closed: %s", c.kindString(), reason)
|
||||
}
|
||||
@@ -1917,7 +1956,7 @@ func (c *client) flushSignal() {
|
||||
|
||||
// Traces a message.
|
||||
// Will NOT check if tracing is enabled, does NOT need the client lock.
|
||||
func (c *client) traceMsg(msg []byte) {
|
||||
func (c *client) traceMsgInternal(msg []byte, delivered bool, hdrSize int) {
|
||||
opts := c.srv.getOpts()
|
||||
maxTrace := opts.MaxTracedMsgLen
|
||||
headersOnly := opts.TraceHeaders
|
||||
@@ -1926,8 +1965,13 @@ func (c *client) traceMsg(msg []byte) {
|
||||
// If TraceHeaders is enabled, extract only the header portion of the msg.
|
||||
// If a header is present, it ends with an additional trailing CRLF.
|
||||
if headersOnly {
|
||||
msg, _ = c.msgParts(msg)
|
||||
suffix += LEN_CR_LF
|
||||
if hdrSize > 0 && len(msg) >= hdrSize {
|
||||
msg = msg[:hdrSize]
|
||||
suffix += LEN_CR_LF
|
||||
} else {
|
||||
// No headers present, so nothing to trace.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Do not emit a log line for zero-length payloads.
|
||||
@@ -1936,14 +1980,32 @@ func (c *client) traceMsg(msg []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
traceInPrefix string = "<<-"
|
||||
traceOutPrefix string = "->>"
|
||||
)
|
||||
var prefix string
|
||||
if delivered {
|
||||
prefix = traceOutPrefix
|
||||
} else {
|
||||
prefix = traceInPrefix
|
||||
}
|
||||
if maxTrace > 0 && l > maxTrace {
|
||||
tm := fmt.Sprintf("%q", msg[:maxTrace])
|
||||
c.Tracef("<<- MSG_PAYLOAD: [\"%s...\"]", tm[1:len(tm)-1])
|
||||
c.Tracef("%s MSG_PAYLOAD: [\"%s...\"]", prefix, tm[1:len(tm)-1])
|
||||
} else {
|
||||
c.Tracef("<<- MSG_PAYLOAD: [%q]", msg[:l])
|
||||
c.Tracef("%s MSG_PAYLOAD: [%q]", prefix, msg[:l])
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) traceMsg(msg []byte) {
|
||||
c.traceMsgInternal(msg, false, c.pa.hdr)
|
||||
}
|
||||
|
||||
func (c *client) traceMsgDelivery(msg []byte, hdrSize int) {
|
||||
c.traceMsgInternal(msg, true, hdrSize)
|
||||
}
|
||||
|
||||
// Traces an incoming operation.
|
||||
// Will NOT check if tracing is enabled, does NOT need the client lock.
|
||||
func (c *client) traceInOp(op string, arg []byte) {
|
||||
@@ -2094,6 +2156,7 @@ func (c *client) processConnect(arg []byte) error {
|
||||
}
|
||||
// Indicate that the CONNECT protocol has been received, and that the
|
||||
// server now knows which protocol this client supports.
|
||||
firstConnect := !c.flags.isSet(connectReceived)
|
||||
c.flags.set(connectReceived)
|
||||
// Capture these under lock
|
||||
c.echo = c.opts.Echo
|
||||
@@ -2103,30 +2166,6 @@ func (c *client) processConnect(arg []byte) error {
|
||||
account := c.opts.Account
|
||||
accountNew := c.opts.AccountNew
|
||||
|
||||
if c.kind == CLIENT {
|
||||
var ncs string
|
||||
if c.opts.Version != _EMPTY_ {
|
||||
ncs = fmt.Sprintf("v%s", c.opts.Version)
|
||||
}
|
||||
if c.opts.Lang != _EMPTY_ {
|
||||
if c.opts.Version == _EMPTY_ {
|
||||
ncs = c.opts.Lang
|
||||
} else {
|
||||
ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Lang)
|
||||
}
|
||||
}
|
||||
if c.opts.Name != _EMPTY_ {
|
||||
if c.opts.Version == _EMPTY_ && c.opts.Lang == _EMPTY_ {
|
||||
ncs = c.opts.Name
|
||||
} else {
|
||||
ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Name)
|
||||
}
|
||||
}
|
||||
if ncs != _EMPTY_ {
|
||||
c.ncs.CompareAndSwap(nil, fmt.Sprintf("%s - %q", c, ncs))
|
||||
}
|
||||
}
|
||||
|
||||
// if websocket client, maybe some options through cookies
|
||||
if ws := c.ws; ws != nil {
|
||||
// if JWT not in the CONNECT, use the cookie JWT (possibly empty).
|
||||
@@ -2200,6 +2239,55 @@ func (c *client) processConnect(arg []byte) error {
|
||||
// By default register with the global account.
|
||||
c.registerWithAccount(srv.globalAccount())
|
||||
}
|
||||
|
||||
// Initialize user info used in logs.
|
||||
c.mu.Lock()
|
||||
acc := c.acc
|
||||
if c.getRawAuthUser() != _EMPTY_ {
|
||||
c.ncsUser.Store(c.getAuthUserLabel())
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if acc != nil {
|
||||
acc.mu.RLock()
|
||||
c.ncsAcc.Store(acc.traceLabel())
|
||||
acc.mu.RUnlock()
|
||||
}
|
||||
|
||||
// Enable logging connection details and auth info for this client.
|
||||
if c.kind == CLIENT && firstConnect && c.srv != nil {
|
||||
var ncs string
|
||||
if c.opts.Version != _EMPTY_ {
|
||||
ncs = fmt.Sprintf("v%s", c.opts.Version)
|
||||
}
|
||||
if c.opts.Lang != _EMPTY_ {
|
||||
if c.opts.Version == _EMPTY_ {
|
||||
ncs = c.opts.Lang
|
||||
} else {
|
||||
ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Lang)
|
||||
}
|
||||
}
|
||||
if c.opts.Name != _EMPTY_ {
|
||||
if c.opts.Version == _EMPTY_ && c.opts.Lang == _EMPTY_ {
|
||||
ncs = c.opts.Name
|
||||
} else {
|
||||
ncs = fmt.Sprintf("%s:%s", ncs, c.opts.Name)
|
||||
}
|
||||
}
|
||||
var acs string
|
||||
accl := c.ncsAcc.Load()
|
||||
authUser := c.ncsUser.Load()
|
||||
if accl != nil && authUser != nil {
|
||||
acs = fmt.Sprintf("%s/%s", accl, authUser)
|
||||
}
|
||||
switch {
|
||||
case ncs != _EMPTY_ && acs != _EMPTY_:
|
||||
c.ncs.Store(fmt.Sprintf("%s - %q - %q", c, ncs, acs))
|
||||
case ncs != _EMPTY_:
|
||||
c.ncs.Store(fmt.Sprintf("%s - %q", c, ncs))
|
||||
case acs != _EMPTY_:
|
||||
c.ncs.Store(fmt.Sprintf("%s - %q", c, acs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch kind {
|
||||
@@ -2238,12 +2326,12 @@ func (c *client) processConnect(arg []byte) error {
|
||||
|
||||
func (c *client) sendErrAndErr(err string) {
|
||||
c.sendErr(err)
|
||||
c.Errorf(err)
|
||||
c.RateLimitErrorf(err)
|
||||
}
|
||||
|
||||
func (c *client) sendErrAndDebug(err string) {
|
||||
c.sendErr(err)
|
||||
c.Debugf(err)
|
||||
c.RateLimitDebugf(err)
|
||||
}
|
||||
|
||||
func (c *client) authTimeout() {
|
||||
@@ -2262,38 +2350,28 @@ func (c *client) accountAuthExpired() {
|
||||
}
|
||||
|
||||
func (c *client) authViolation() {
|
||||
var s *Server
|
||||
var hasTrustedNkeys, hasNkeys, hasUsers bool
|
||||
if s = c.srv; s != nil {
|
||||
s.mu.RLock()
|
||||
hasTrustedNkeys = s.trustedKeys != nil
|
||||
hasNkeys = s.nkeys != nil
|
||||
hasUsers = s.users != nil
|
||||
s.mu.RUnlock()
|
||||
defer s.sendAuthErrorEvent(c)
|
||||
authErr := c.getAuthError()
|
||||
if authErr == nil {
|
||||
authErr = ErrAuthentication
|
||||
}
|
||||
reason := getAuthErrClosedState(authErr)
|
||||
|
||||
if hasTrustedNkeys {
|
||||
c.Errorf("%v", ErrAuthentication)
|
||||
} else if hasNkeys {
|
||||
c.Errorf("%s - Nkey %q",
|
||||
ErrAuthentication.Error(),
|
||||
c.opts.Nkey)
|
||||
} else if hasUsers {
|
||||
c.Errorf("%s - User %q",
|
||||
ErrAuthentication.Error(),
|
||||
c.opts.Username)
|
||||
} else {
|
||||
if c.srv != nil {
|
||||
var s *Server
|
||||
if s = c.srv; s != nil {
|
||||
defer s.sendAuthErrorEvent(c, reason.String())
|
||||
if c.getRawAuthUser() != _EMPTY_ {
|
||||
c.Errorf("%v - %s", ErrAuthentication, c.getAuthUser())
|
||||
} else {
|
||||
c.Errorf(ErrAuthentication.Error())
|
||||
}
|
||||
}
|
||||
if c.isMqtt() {
|
||||
c.mqttEnqueueConnAck(mqttConnAckRCNotAuthorized, false)
|
||||
} else {
|
||||
// Send this to client, regardless of the authErr override.
|
||||
c.sendErr("Authorization Violation")
|
||||
}
|
||||
c.closeConnection(AuthenticationViolation)
|
||||
c.closeConnection(reason)
|
||||
}
|
||||
|
||||
func (c *client) maxAccountConnExceeded() {
|
||||
@@ -2523,36 +2601,29 @@ func (c *client) processPing() {
|
||||
// If we are here, the CONNECT has been received so we know
|
||||
// if this client supports async INFO or not.
|
||||
var (
|
||||
checkInfoChange bool
|
||||
sendConnectInfo bool
|
||||
srv = c.srv
|
||||
)
|
||||
// For older clients, just flip the firstPongSent flag if not already
|
||||
// set and we are done.
|
||||
if c.opts.Protocol < ClientProtoInfo || srv == nil {
|
||||
c.flags.setIfNotSet(firstPongSent)
|
||||
} else {
|
||||
// This is a client that supports async INFO protocols.
|
||||
// If this is the first PING (so firstPongSent is not set yet),
|
||||
// we will need to check if there was a change in cluster topology
|
||||
// or we have a different max payload. We will send this first before
|
||||
// pong since most clients do flush after connect call.
|
||||
checkInfoChange = !c.flags.isSet(firstPongSent)
|
||||
// For the first PING (so firstPongSet is false) and for clients
|
||||
// that support async INFO protocols, we will send one with ConnectInfo=true,
|
||||
// the name of the account the client is bound to, and if the
|
||||
// account is the system account.
|
||||
if !c.flags.isSet(firstPongSent) {
|
||||
// Flip the flag.
|
||||
c.flags.set(firstPongSent)
|
||||
// Evaluate if we should send the INFO protocol.
|
||||
sendConnectInfo = srv != nil && c.opts.Protocol >= ClientProtoInfo
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if checkInfoChange {
|
||||
opts := srv.getOpts()
|
||||
if sendConnectInfo {
|
||||
srv.mu.Lock()
|
||||
info := srv.copyInfo()
|
||||
c.mu.Lock()
|
||||
// Now that we are under both locks, we can flip the flag.
|
||||
// This prevents sendAsyncInfoToClients() and code here to
|
||||
// send a double INFO protocol.
|
||||
c.flags.set(firstPongSent)
|
||||
// If there was a cluster update since this client was created,
|
||||
// send an updated INFO protocol now.
|
||||
if srv.lastCURLsUpdate >= c.start.UnixNano() || c.mpay != int32(opts.MaxPayload) {
|
||||
c.enqueueProto(c.generateClientInfoJSON(srv.copyInfo()))
|
||||
}
|
||||
info.RemoteAccount = c.acc.Name
|
||||
info.IsSystemAccount = c.acc == srv.SystemAccount()
|
||||
info.ConnectInfo = true
|
||||
c.enqueueProto(c.generateClientInfoJSON(info))
|
||||
c.mu.Unlock()
|
||||
srv.mu.Unlock()
|
||||
}
|
||||
@@ -3469,6 +3540,12 @@ func (c *client) stalledWait(producer *client) {
|
||||
c.mu.Unlock()
|
||||
defer c.mu.Lock()
|
||||
|
||||
// Track per client and total client stalls.
|
||||
atomic.AddInt64(&c.stalls, 1)
|
||||
if c.srv != nil {
|
||||
atomic.AddInt64(&c.srv.stalls, 1)
|
||||
}
|
||||
|
||||
// Now check if we are close to total allowed.
|
||||
if producer.in.tst+ttl > stallTotalAllowed {
|
||||
ttl = stallTotalAllowed - producer.in.tst
|
||||
@@ -3621,6 +3698,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su
|
||||
// support we need to strip the headers from the payload.
|
||||
// The actual header would have been processed correctly for us, so just
|
||||
// need to update payload.
|
||||
hdrSize := c.pa.hdr
|
||||
if c.pa.hdr > 0 && !sub.client.headers {
|
||||
msg = msg[c.pa.hdr:]
|
||||
}
|
||||
@@ -3762,6 +3840,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su
|
||||
|
||||
if client.trace {
|
||||
client.traceOutOp(bytesToString(mh[:len(mh)-LEN_CR_LF]), nil)
|
||||
client.traceMsgDelivery(msg, hdrSize)
|
||||
}
|
||||
|
||||
client.mu.Unlock()
|
||||
@@ -4197,7 +4276,8 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
|
||||
c.mu.Lock()
|
||||
if c.opts.NoResponders {
|
||||
if sub := c.subForReply(c.pa.reply); sub != nil {
|
||||
proto := fmt.Sprintf("HMSG %s %s 16 16\r\nNATS/1.0 503\r\n\r\n\r\n", c.pa.reply, sub.sid)
|
||||
hdrLen := 32 /* header without the subject */ + len(c.pa.subject)
|
||||
proto := fmt.Sprintf("HMSG %s %s %d %d\r\nNATS/1.0 503\r\nNats-Subject: %s\r\n\r\n\r\n", c.pa.reply, sub.sid, hdrLen, hdrLen, c.pa.subject)
|
||||
c.queueOutbound([]byte(proto))
|
||||
c.addToPCD(c)
|
||||
}
|
||||
@@ -4257,7 +4337,7 @@ func (c *client) setupResponseServiceImport(acc *Account, si *serviceImport, tra
|
||||
|
||||
// Will remove a header if present.
|
||||
func removeHeaderIfPresent(hdr []byte, key string) []byte {
|
||||
start := bytes.Index(hdr, []byte(key))
|
||||
start := bytes.Index(hdr, []byte(key+":"))
|
||||
// key can't be first and we want to check that it is preceded by a '\n'
|
||||
if start < 1 || hdr[start-1] != '\n' {
|
||||
return hdr
|
||||
@@ -4406,6 +4486,27 @@ func sliceHeader(key string, hdr []byte) []byte {
|
||||
return hdr[start:index:index]
|
||||
}
|
||||
|
||||
func setHeader(key, val string, hdr []byte) []byte {
|
||||
prefix := []byte(key + ": ")
|
||||
start := bytes.Index(hdr, prefix)
|
||||
if start >= 0 {
|
||||
valStart := start + len(prefix)
|
||||
valEnd := bytes.Index(hdr[valStart:], []byte("\r"))
|
||||
if valEnd < 0 {
|
||||
return hdr // malformed headers
|
||||
}
|
||||
valEnd += valStart
|
||||
suffix := slices.Clone(hdr[valEnd:])
|
||||
newHdr := append(hdr[:valStart], val...)
|
||||
return append(newHdr, suffix...)
|
||||
}
|
||||
if len(hdr) > 0 && bytes.HasSuffix(hdr, []byte("\r\n")) {
|
||||
hdr = hdr[:len(hdr)-2]
|
||||
val += "\r\n"
|
||||
}
|
||||
return fmt.Appendf(hdr, "%s: %s\r\n", key, val)
|
||||
}
|
||||
|
||||
// For bytes.HasPrefix below.
|
||||
var (
|
||||
jsRequestNextPreB = []byte(jsRequestNextPre)
|
||||
@@ -5272,18 +5373,16 @@ func (c *client) pubPermissionViolation(subject []byte) {
|
||||
mt.setIngressError(errTxt)
|
||||
}
|
||||
c.sendErr(errTxt)
|
||||
c.Errorf("Publish Violation - %s, Subject %q", c.getAuthUser(), subject)
|
||||
c.Errorf("Publish Violation - Subject %q", subject)
|
||||
}
|
||||
|
||||
func (c *client) subPermissionViolation(sub *subscription) {
|
||||
errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q", sub.subject)
|
||||
logTxt := fmt.Sprintf("Subscription Violation - %s, Subject %q, SID %s",
|
||||
c.getAuthUser(), sub.subject, sub.sid)
|
||||
logTxt := fmt.Sprintf("Subscription Violation - Subject %q, SID %s", sub.subject, sub.sid)
|
||||
|
||||
if sub.queue != nil {
|
||||
errTxt = fmt.Sprintf("Permissions Violation for Subscription to %q using queue %q", sub.subject, sub.queue)
|
||||
logTxt = fmt.Sprintf("Subscription Violation - %s, Subject %q, Queue: %q, SID %s",
|
||||
c.getAuthUser(), sub.subject, sub.queue, sub.sid)
|
||||
logTxt = fmt.Sprintf("Subscription Violation - Subject %q, Queue: %q, SID %s", sub.subject, sub.queue, sub.sid)
|
||||
}
|
||||
|
||||
c.sendErr(errTxt)
|
||||
@@ -5296,13 +5395,12 @@ func (c *client) replySubjectViolation(reply []byte) {
|
||||
mt.setIngressError(errTxt)
|
||||
}
|
||||
c.sendErr(errTxt)
|
||||
c.Errorf("Publish Violation - %s, Reply %q", c.getAuthUser(), reply)
|
||||
c.Errorf("Publish Violation - Reply %q", reply)
|
||||
}
|
||||
|
||||
func (c *client) maxTokensViolation(sub *subscription) {
|
||||
errTxt := fmt.Sprintf("Permissions Violation for Subscription to %q, too many tokens", sub.subject)
|
||||
logTxt := fmt.Sprintf("Subscription Violation Too Many Tokens - %s, Subject %q, SID %s",
|
||||
c.getAuthUser(), sub.subject, sub.sid)
|
||||
logTxt := fmt.Sprintf("Subscription Violation Too Many Tokens - Subject %q, SID %s", sub.subject, sub.sid)
|
||||
c.sendErr(errTxt)
|
||||
c.Errorf(logTxt)
|
||||
}
|
||||
@@ -5621,10 +5719,8 @@ func (c *client) processSubsOnConfigReload(awcsti map[string]struct{}) {
|
||||
// Unsubscribe all that need to be removed and report back to client and logs.
|
||||
for _, sub := range removed {
|
||||
c.unsubscribe(acc, sub, true, true)
|
||||
c.sendErr(fmt.Sprintf("Permissions Violation for Subscription to %q (sid %q)",
|
||||
sub.subject, sub.sid))
|
||||
srv.Noticef("Removed sub %q (sid %q) for %s - not authorized",
|
||||
sub.subject, sub.sid, c.getAuthUser())
|
||||
c.sendErr(fmt.Sprintf("Permissions Violation for Subscription to %q (sid %q)", sub.subject, sub.sid))
|
||||
srv.Noticef("Removed sub %q (sid %q) for %s - not authorized", sub.subject, sub.sid, c.getAuthUser())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5914,7 +6010,7 @@ func (c *client) getAccAndResultFromCache() (*Account, *SublistResult) {
|
||||
|
||||
if genid := atomic.LoadUint64(&sl.genid); genid != pac.genid {
|
||||
ok = false
|
||||
c.in.pacache = make(map[string]*perAccountCache)
|
||||
clear(c.in.pacache)
|
||||
} else {
|
||||
acc = pac.acc
|
||||
r = pac.results
|
||||
@@ -5937,13 +6033,33 @@ func (c *client) getAccAndResultFromCache() (*Account, *SublistResult) {
|
||||
// Match against the account sublist.
|
||||
r = sl.MatchBytes(c.pa.subject)
|
||||
|
||||
// Check if we need to prune.
|
||||
// Check if we need to prune. This should give us a perAccountCache struct
|
||||
// to reuse instead of having to allocate a new one.
|
||||
// Previously we would have removed multiple entries but now we will only
|
||||
// prune the minimum number required to maintain the cache size, so that
|
||||
// we reduce the amount of GC pressure and maintain cache stability as best
|
||||
// as possible.
|
||||
if len(c.in.pacache) >= maxPerAccountCacheSize {
|
||||
c.prunePerAccountCache()
|
||||
for cacheKey, p := range c.in.pacache {
|
||||
delete(c.in.pacache, cacheKey)
|
||||
pac = p
|
||||
if len(c.in.pacache) < maxPerAccountCacheSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we can reuse the pac from earlier (i.e. we loaded one but it was an
|
||||
// old generation or we pruned the cache) then do so.
|
||||
if pac == nil {
|
||||
pac = &perAccountCache{}
|
||||
}
|
||||
pac.acc = acc
|
||||
pac.results = r
|
||||
pac.genid = atomic.LoadUint64(&sl.genid)
|
||||
|
||||
// Store in our cache,make sure to do so after we prune.
|
||||
c.in.pacache[string(c.pa.pacache)] = &perAccountCache{acc, r, atomic.LoadUint64(&sl.genid)}
|
||||
c.in.pacache[string(c.pa.pacache)] = pac
|
||||
}
|
||||
return acc, r
|
||||
}
|
||||
@@ -5959,17 +6075,6 @@ func (c *client) Account() *Account {
|
||||
return acc
|
||||
}
|
||||
|
||||
// prunePerAccountCache will prune off a random number of cache entries.
|
||||
func (c *client) prunePerAccountCache() {
|
||||
n := 0
|
||||
for cacheKey := range c.in.pacache {
|
||||
delete(c.in.pacache, cacheKey)
|
||||
if n++; n > prunePerAccountCacheSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pruneClosedSubFromPerAccountCache remove entries that contain subscriptions
|
||||
// that have been closed.
|
||||
func (c *client) pruneClosedSubFromPerAccountCache() {
|
||||
@@ -6225,6 +6330,22 @@ func (c *client) getAuthUser() string {
|
||||
}
|
||||
}
|
||||
|
||||
// getAuthUserLabel returns a label for the auth user for the client.
|
||||
func (c *client) getAuthUserLabel() string {
|
||||
switch {
|
||||
case c.opts.Nkey != _EMPTY_:
|
||||
return fmt.Sprintf("nkey:%s", c.opts.Nkey)
|
||||
case c.opts.Username != _EMPTY_:
|
||||
return fmt.Sprintf("user:%s", c.opts.Username)
|
||||
case c.opts.JWT != _EMPTY_:
|
||||
return fmt.Sprintf("jwt:%s", c.pubKey)
|
||||
case c.opts.Token != _EMPTY_:
|
||||
return "token"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Given an array of strings, this function converts it to a map as long
|
||||
// as all the content (converted to upper-case) matches some constants.
|
||||
|
||||
@@ -6300,51 +6421,107 @@ func (c *client) isClosed() bool {
|
||||
return c.flags.isSet(closeConnection) || c.flags.isSet(connMarkedClosed) || c.nc == nil
|
||||
}
|
||||
|
||||
func (c *client) format(format string) string {
|
||||
if s := c.String(); s != _EMPTY_ {
|
||||
return fmt.Sprintf("%s - %s", s, format)
|
||||
} else {
|
||||
return format
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) formatNoClientInfo(format string) string {
|
||||
acc := c.ncsAcc.Load()
|
||||
if acc != nil {
|
||||
return fmt.Sprintf("%s - Account:%s", format, acc)
|
||||
} else {
|
||||
return format
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) formatClientSuffix() string {
|
||||
user := c.ncsUser.Load()
|
||||
if user == nil || user.(string) == _EMPTY_ {
|
||||
return _EMPTY_
|
||||
}
|
||||
return fmt.Sprintf(" - %s", user)
|
||||
}
|
||||
|
||||
// Logging functionality scoped to a client or route.
|
||||
func (c *client) Error(err error) {
|
||||
c.srv.Errors(c, err)
|
||||
c.srv.Errorf(c.format(err.Error()))
|
||||
}
|
||||
|
||||
func (c *client) Errorf(format string, v ...any) {
|
||||
format = fmt.Sprintf("%s - %s", c, format)
|
||||
c.srv.Errorf(format, v...)
|
||||
c.srv.Errorf(c.format(format), v...)
|
||||
}
|
||||
|
||||
func (c *client) Debugf(format string, v ...any) {
|
||||
format = fmt.Sprintf("%s - %s", c, format)
|
||||
c.srv.Debugf(format, v...)
|
||||
c.srv.Debugf(c.format(format), v...)
|
||||
}
|
||||
|
||||
func (c *client) Noticef(format string, v ...any) {
|
||||
format = fmt.Sprintf("%s - %s", c, format)
|
||||
c.srv.Noticef(format, v...)
|
||||
c.srv.Noticef(c.format(format), v...)
|
||||
}
|
||||
|
||||
func (c *client) Tracef(format string, v ...any) {
|
||||
format = fmt.Sprintf("%s - %s", c, format)
|
||||
c.srv.Tracef(format, v...)
|
||||
c.srv.Tracef(c.format(format), v...)
|
||||
}
|
||||
|
||||
func (c *client) Warnf(format string, v ...any) {
|
||||
format = fmt.Sprintf("%s - %s", c, format)
|
||||
c.srv.Warnf(format, v...)
|
||||
c.srv.Warnf(c.format(format), v...)
|
||||
}
|
||||
|
||||
func (c *client) RateLimitErrorf(format string, v ...any) {
|
||||
// Do the check before adding the client info to the format...
|
||||
statement := fmt.Sprintf(c.formatNoClientInfo(format), v...)
|
||||
if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
|
||||
return
|
||||
}
|
||||
if s := c.String(); s != _EMPTY_ {
|
||||
c.srv.Errorf("%s - %s%s", c, statement, c.formatClientSuffix())
|
||||
} else {
|
||||
c.srv.Errorf("%s%s", statement, c.formatClientSuffix())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) rateLimitFormatWarnf(format string, v ...any) {
|
||||
// Do the check before adding the client info to the format...
|
||||
format = c.formatNoClientInfo(format)
|
||||
if _, loaded := c.srv.rateLimitLogging.LoadOrStore(format, time.Now()); loaded {
|
||||
return
|
||||
}
|
||||
statement := fmt.Sprintf(format, v...)
|
||||
c.Warnf("%s", statement)
|
||||
if s := c.String(); s != _EMPTY_ {
|
||||
c.srv.Warnf("%s - %s%s", c, statement, c.formatClientSuffix())
|
||||
} else {
|
||||
c.srv.Warnf("%s%s", statement, c.formatClientSuffix())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) RateLimitWarnf(format string, v ...any) {
|
||||
// Do the check before adding the client info to the format...
|
||||
statement := fmt.Sprintf(format, v...)
|
||||
statement := fmt.Sprintf(c.formatNoClientInfo(format), v...)
|
||||
if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
|
||||
return
|
||||
}
|
||||
c.Warnf("%s", statement)
|
||||
if s := c.String(); s != _EMPTY_ {
|
||||
c.srv.Warnf("%s - %s%s", c, statement, c.formatClientSuffix())
|
||||
} else {
|
||||
c.srv.Warnf("%s%s", statement, c.formatClientSuffix())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) RateLimitDebugf(format string, v ...any) {
|
||||
// Do the check before adding the client info to the format...
|
||||
statement := fmt.Sprintf(c.formatNoClientInfo(format), v...)
|
||||
if _, loaded := c.srv.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
|
||||
return
|
||||
}
|
||||
if s := c.String(); s != _EMPTY_ {
|
||||
c.srv.Debugf("%s - %s%s", c, statement, c.formatClientSuffix())
|
||||
} else {
|
||||
c.srv.Debugf("%s%s", statement, c.formatClientSuffix())
|
||||
}
|
||||
}
|
||||
|
||||
// Set the very first PING to a lower interval to capture the initial RTT.
|
||||
@@ -6383,3 +6560,18 @@ func (c *client) setFirstPingTimer() {
|
||||
}
|
||||
c.ping.tmr = time.AfterFunc(d, c.processPingTimer)
|
||||
}
|
||||
|
||||
// Sets this error as the authentication error. To be used in authViolation()
|
||||
// to report an error different of `ErrAuthentication`.
|
||||
func (c *client) setAuthError(err error) {
|
||||
c.mu.Lock()
|
||||
c.authErr = err
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Returns the authentication error set in the connection, possibly nil.
|
||||
func (c *client) getAuthError() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.authErr
|
||||
}
|
||||
|
||||
+5
-2
@@ -66,7 +66,7 @@ func init() {
|
||||
|
||||
const (
|
||||
// VERSION is the current version for the server.
|
||||
VERSION = "2.11.9"
|
||||
VERSION = "2.12.0"
|
||||
|
||||
// PROTO is the currently supported protocol.
|
||||
// 0 was the original
|
||||
@@ -146,7 +146,10 @@ const (
|
||||
// DEFAULT_ROUTE_CONNECT Route solicitation intervals.
|
||||
DEFAULT_ROUTE_CONNECT = 1 * time.Second
|
||||
|
||||
// DEFAULT_ROUTE_RECONNECT Route reconnect intervals.
|
||||
// DEFAULT_ROUTE_CONNECT_MAX Route solicitation intervals (max).
|
||||
DEFAULT_ROUTE_CONNECT_MAX = 30 * time.Second
|
||||
|
||||
// DEFAULT_ROUTE_RECONNECT Route reconnect delay.
|
||||
DEFAULT_ROUTE_RECONNECT = 1 * time.Second
|
||||
|
||||
// DEFAULT_ROUTE_DIAL Route dial timeout.
|
||||
|
||||
+249
-37
@@ -19,6 +19,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -227,18 +228,22 @@ const (
|
||||
PriorityOverflow
|
||||
// Single client takes over handling of the messages, while others are on standby.
|
||||
PriorityPinnedClient
|
||||
// Clients with lowest priority will be selected first.
|
||||
PriorityPrioritized
|
||||
)
|
||||
|
||||
const (
|
||||
PriorityNoneJSONString = `"none"`
|
||||
PriorityOverflowJSONString = `"overflow"`
|
||||
PriorityPinnedClientJSONString = `"pinned_client"`
|
||||
PriorityPrioritizedJSONString = `"prioritized"`
|
||||
)
|
||||
|
||||
var (
|
||||
PriorityNoneJSONBytes = []byte(PriorityNoneJSONString)
|
||||
PriorityOverflowJSONBytes = []byte(PriorityOverflowJSONString)
|
||||
PriorityPinnedClientJSONBytes = []byte(PriorityPinnedClientJSONString)
|
||||
PriorityPrioritizedJSONBytes = []byte(PriorityPrioritizedJSONString)
|
||||
)
|
||||
|
||||
func (pp PriorityPolicy) String() string {
|
||||
@@ -247,6 +252,8 @@ func (pp PriorityPolicy) String() string {
|
||||
return PriorityOverflowJSONString
|
||||
case PriorityPinnedClient:
|
||||
return PriorityPinnedClientJSONString
|
||||
case PriorityPrioritized:
|
||||
return PriorityPrioritizedJSONString
|
||||
default:
|
||||
return PriorityNoneJSONString
|
||||
}
|
||||
@@ -258,6 +265,8 @@ func (pp PriorityPolicy) MarshalJSON() ([]byte, error) {
|
||||
return PriorityOverflowJSONBytes, nil
|
||||
case PriorityPinnedClient:
|
||||
return PriorityPinnedClientJSONBytes, nil
|
||||
case PriorityPrioritized:
|
||||
return PriorityPrioritizedJSONBytes, nil
|
||||
case PriorityNone:
|
||||
return PriorityNoneJSONBytes, nil
|
||||
default:
|
||||
@@ -271,6 +280,8 @@ func (pp *PriorityPolicy) UnmarshalJSON(data []byte) error {
|
||||
*pp = PriorityOverflow
|
||||
case PriorityPinnedClientJSONString:
|
||||
*pp = PriorityPinnedClient
|
||||
case PriorityPrioritizedJSONString:
|
||||
*pp = PriorityPrioritized
|
||||
case PriorityNoneJSONString:
|
||||
*pp = PriorityNone
|
||||
default:
|
||||
@@ -554,6 +565,63 @@ const (
|
||||
|
||||
// Helper function to set consumer config defaults from above.
|
||||
func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig, lim *JSLimitOpts, accLim *JetStreamAccountLimits, pedantic bool) *ApiError {
|
||||
// Setup default of -1, meaning no limit for MaxDeliver.
|
||||
if config.MaxDeliver == 0 || config.MaxDeliver < -1 {
|
||||
if pedantic && config.MaxDeliver < -1 {
|
||||
return NewJSPedanticError(errors.New("max_deliver must be set to -1"))
|
||||
}
|
||||
config.MaxDeliver = -1
|
||||
}
|
||||
// Setup zero defaults.
|
||||
if config.MaxWaiting < 0 {
|
||||
if pedantic {
|
||||
return NewJSPedanticError(errors.New("max_waiting must not be negative"))
|
||||
}
|
||||
config.MaxWaiting = 0
|
||||
}
|
||||
if config.MaxAckPending < -1 {
|
||||
if pedantic {
|
||||
return NewJSPedanticError(errors.New("max_ack_pending must be set to -1"))
|
||||
}
|
||||
config.MaxAckPending = -1
|
||||
}
|
||||
if config.MaxRequestBatch < 0 {
|
||||
if pedantic {
|
||||
return NewJSPedanticError(errors.New("max_batch must not be negative"))
|
||||
}
|
||||
config.MaxRequestBatch = 0
|
||||
}
|
||||
if config.MaxRequestExpires < 0 {
|
||||
if pedantic {
|
||||
return NewJSPedanticError(errors.New("max_expires must not be negative"))
|
||||
}
|
||||
config.MaxRequestExpires = 0
|
||||
}
|
||||
if config.MaxRequestMaxBytes < 0 {
|
||||
if pedantic {
|
||||
return NewJSPedanticError(errors.New("max_bytes must not be negative"))
|
||||
}
|
||||
config.MaxRequestMaxBytes = 0
|
||||
}
|
||||
if config.Heartbeat < 0 {
|
||||
if pedantic {
|
||||
return NewJSPedanticError(errors.New("idle_heartbeat must not be negative"))
|
||||
}
|
||||
config.Heartbeat = 0
|
||||
}
|
||||
if config.InactiveThreshold < 0 {
|
||||
if pedantic {
|
||||
return NewJSPedanticError(errors.New("inactive_threshold must not be negative"))
|
||||
}
|
||||
config.InactiveThreshold = 0
|
||||
}
|
||||
if config.PinnedTTL < 0 {
|
||||
if pedantic {
|
||||
return NewJSPedanticError(errors.New("priority_timeout must not be negative"))
|
||||
}
|
||||
config.PinnedTTL = 0
|
||||
}
|
||||
|
||||
// Set to default if not specified.
|
||||
if config.DeliverSubject == _EMPTY_ && config.MaxWaiting == 0 {
|
||||
config.MaxWaiting = JSWaitQueueDefaultMax
|
||||
@@ -562,10 +630,6 @@ func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig,
|
||||
if config.AckWait == 0 && (config.AckPolicy == AckExplicit || config.AckPolicy == AckAll) {
|
||||
config.AckWait = JsAckWaitDefault
|
||||
}
|
||||
// Setup default of -1, meaning no limit for MaxDeliver.
|
||||
if config.MaxDeliver == 0 {
|
||||
config.MaxDeliver = -1
|
||||
}
|
||||
// If BackOff was specified that will override the AckWait and the MaxDeliver.
|
||||
if len(config.BackOff) > 0 {
|
||||
if pedantic && config.AckWait != config.BackOff[0] {
|
||||
@@ -587,14 +651,14 @@ func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig,
|
||||
}
|
||||
// Set proper default for max ack pending if we are ack explicit and none has been set.
|
||||
if (config.AckPolicy == AckExplicit || config.AckPolicy == AckAll) && config.MaxAckPending == 0 {
|
||||
accPending := JsDefaultMaxAckPending
|
||||
if lim.MaxAckPending > 0 && lim.MaxAckPending < accPending {
|
||||
accPending = lim.MaxAckPending
|
||||
ackPending := JsDefaultMaxAckPending
|
||||
if lim.MaxAckPending > 0 && lim.MaxAckPending < ackPending {
|
||||
ackPending = lim.MaxAckPending
|
||||
}
|
||||
if accLim.MaxAckPending > 0 && accLim.MaxAckPending < accPending {
|
||||
accPending = accLim.MaxAckPending
|
||||
if accLim.MaxAckPending > 0 && accLim.MaxAckPending < ackPending {
|
||||
ackPending = accLim.MaxAckPending
|
||||
}
|
||||
config.MaxAckPending = accPending
|
||||
config.MaxAckPending = ackPending
|
||||
}
|
||||
// if applicable set max request batch size
|
||||
if config.DeliverSubject == _EMPTY_ && config.MaxRequestBatch == 0 && lim.MaxRequestBatch > 0 {
|
||||
@@ -640,6 +704,23 @@ func checkConsumerCfg(
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := config.AckPolicy.MarshalJSON(); err != nil {
|
||||
return NewJSConsumerAckPolicyInvalidError()
|
||||
}
|
||||
if _, err := config.ReplayPolicy.MarshalJSON(); err != nil {
|
||||
return NewJSConsumerReplayPolicyInvalidError()
|
||||
}
|
||||
|
||||
// Check not negative AckWait/BackOff
|
||||
for _, backoff := range config.BackOff {
|
||||
if backoff < 0 {
|
||||
return NewJSConsumerBackOffNegativeError()
|
||||
}
|
||||
}
|
||||
if config.AckWait < 0 {
|
||||
return NewJSConsumerAckWaitNegativeError()
|
||||
}
|
||||
|
||||
// Check if we have a BackOff defined that MaxDeliver is within range etc.
|
||||
if lbo := len(config.BackOff); lbo > 0 && config.MaxDeliver != -1 && lbo > config.MaxDeliver {
|
||||
return NewJSConsumerMaxDeliverBackoffError()
|
||||
@@ -690,7 +771,7 @@ func checkConsumerCfg(
|
||||
return NewJSConsumerMaxRequestBatchNegativeError()
|
||||
}
|
||||
if config.MaxRequestExpires != 0 && config.MaxRequestExpires < time.Millisecond {
|
||||
return NewJSConsumerMaxRequestExpiresToSmallError()
|
||||
return NewJSConsumerMaxRequestExpiresTooSmallError()
|
||||
}
|
||||
if srvLim.MaxRequestBatch > 0 && config.MaxRequestBatch > srvLim.MaxRequestBatch {
|
||||
return NewJSConsumerMaxRequestBatchExceededError(srvLim.MaxRequestBatch)
|
||||
@@ -849,6 +930,14 @@ func checkConsumerCfg(
|
||||
return NewJSConsumerInvalidGroupNameError()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If PriorityPolicy is None or not set, reject if PriorityGroups or PinnedTTL are set
|
||||
if len(config.PriorityGroups) > 0 {
|
||||
return NewJSConsumerPriorityGroupWithPolicyNoneError()
|
||||
}
|
||||
if config.PinnedTTL > 0 {
|
||||
return NewJSConsumerPinnedTTLWithoutPriorityPolicyNoneError()
|
||||
}
|
||||
}
|
||||
|
||||
// For now don't allow preferred server in placement.
|
||||
@@ -3445,6 +3534,7 @@ type PriorityGroup struct {
|
||||
MinPending int64 `json:"min_pending,omitempty"`
|
||||
MinAckPending int64 `json:"min_ack_pending,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
}
|
||||
|
||||
// Used in nextReqFromMsg, since the json.Unmarshal causes the request
|
||||
@@ -3574,6 +3664,33 @@ var (
|
||||
errWaitQueueNil = errors.New("wait queue is nil")
|
||||
)
|
||||
|
||||
// insertSorted inserts wr at the correct position based on priority
|
||||
func (wq *waitQueue) insertSorted(wr *waitingRequest) {
|
||||
|
||||
// Handle empty queue
|
||||
if wq.head == nil {
|
||||
wq.head = wr
|
||||
wq.tail = wr
|
||||
wr.next = nil
|
||||
return
|
||||
}
|
||||
insertAtPosition(wr, wq)
|
||||
}
|
||||
|
||||
func (wq *waitQueue) addPrioritized(wr *waitingRequest) error {
|
||||
if wq == nil {
|
||||
return errWaitQueueNil
|
||||
}
|
||||
if wq.isFull() {
|
||||
return errWaitQueueFull
|
||||
}
|
||||
|
||||
wq.insertSorted(wr)
|
||||
wq.n++
|
||||
wq.last = wr.received
|
||||
return nil
|
||||
}
|
||||
|
||||
// Adds in a new request.
|
||||
func (wq *waitQueue) add(wr *waitingRequest) error {
|
||||
if wq == nil {
|
||||
@@ -3637,6 +3754,17 @@ func (wq *waitQueue) cycle() {
|
||||
}
|
||||
}
|
||||
|
||||
func (wq *waitQueue) popOrPopAndRequeue(priority PriorityPolicy) *waitingRequest {
|
||||
if wq == nil || wq.head == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if priority == PriorityPrioritized {
|
||||
return wq.popAndRequeue()
|
||||
}
|
||||
return wq.pop()
|
||||
}
|
||||
|
||||
// pop will return the next request and move the read cursor.
|
||||
// This will now place a request that still has pending items at the ends of the list.
|
||||
func (wq *waitQueue) pop() *waitingRequest {
|
||||
@@ -3656,6 +3784,74 @@ func (wq *waitQueue) pop() *waitingRequest {
|
||||
return wr
|
||||
}
|
||||
|
||||
// popAndRequeue pops the head element and requeues it at the end of its priority group.
|
||||
// This maintains FIFO order within the same priority level.
|
||||
func (wq *waitQueue) popAndRequeue() *waitingRequest {
|
||||
if wq == nil || wq.head == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save the head
|
||||
wr := wq.head
|
||||
|
||||
if wr == nil {
|
||||
return wr
|
||||
}
|
||||
|
||||
wr.d++
|
||||
wr.n--
|
||||
|
||||
if wr.n > 0 && wq.n > 1 {
|
||||
if wr.next == nil {
|
||||
return wr
|
||||
}
|
||||
wq.head = wq.head.next
|
||||
wr.next = nil
|
||||
|
||||
insertAtPosition(wr, wq)
|
||||
|
||||
} else if wr.n <= 0 {
|
||||
wq.removeCurrent()
|
||||
return wr
|
||||
|
||||
}
|
||||
|
||||
return wr
|
||||
}
|
||||
|
||||
func insertAtPosition(wr *waitingRequest, wq *waitQueue) {
|
||||
priority := math.MaxInt32
|
||||
if wr.priorityGroup != nil {
|
||||
priority = wr.priorityGroup.Priority
|
||||
}
|
||||
|
||||
var prev *waitingRequest
|
||||
current := wq.head
|
||||
for current != nil {
|
||||
currentPriority := math.MaxInt32
|
||||
if current.priorityGroup != nil {
|
||||
currentPriority = current.priorityGroup.Priority
|
||||
}
|
||||
if currentPriority > priority {
|
||||
break
|
||||
}
|
||||
prev = current
|
||||
current = current.next
|
||||
}
|
||||
|
||||
if prev == nil {
|
||||
// All remaining elements have higher priority
|
||||
wr.next = wq.head
|
||||
wq.head = wr
|
||||
} else {
|
||||
wr.next = prev.next
|
||||
prev.next = wr
|
||||
if wr.next == nil {
|
||||
wq.tail = wr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removes the current read pointer (head FIFO) entry.
|
||||
func (wq *waitQueue) removeCurrent() {
|
||||
wq.remove(nil, wq.head)
|
||||
@@ -3817,17 +4013,17 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
|
||||
if needNewPin {
|
||||
o.sendPinnedAdvisoryLocked(priorityGroup)
|
||||
}
|
||||
return o.waiting.pop()
|
||||
return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy)
|
||||
} else if time.Since(wr.received) < defaultGatewayRecentSubExpiration && (o.srv.leafNodeEnabled || o.srv.gateway.enabled) {
|
||||
if needNewPin {
|
||||
o.sendPinnedAdvisoryLocked(priorityGroup)
|
||||
}
|
||||
return o.waiting.pop()
|
||||
return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy)
|
||||
} else if o.srv.gateway.enabled && o.srv.hasGatewayInterest(wr.acc.Name, wr.interest) {
|
||||
if needNewPin {
|
||||
o.sendPinnedAdvisoryLocked(priorityGroup)
|
||||
}
|
||||
return o.waiting.pop()
|
||||
return o.waiting.popOrPopAndRequeue(o.cfg.PriorityPolicy)
|
||||
}
|
||||
} else {
|
||||
// We do check for expiration in `processWaiting`, but it is possible to hit the expiry here, and not there.
|
||||
@@ -3902,7 +4098,7 @@ func (nmr *nextMsgReq) returnToPool() {
|
||||
// processNextMsgReq will process a request for the next message available. A nil message payload means deliver
|
||||
// a single message. If the payload is a formal request or a number parseable with Atoi(), then we will send a
|
||||
// batch of messages without requiring another request to this endpoint, or an ACK.
|
||||
func (o *consumer) processNextMsgReq(_ *subscription, c *client, _ *Account, _, reply string, msg []byte) {
|
||||
func (o *consumer) processNextMsgReq(_ *subscription, c *client, _ *Account, _, reply string, rmsg []byte) {
|
||||
if reply == _EMPTY_ {
|
||||
return
|
||||
}
|
||||
@@ -3914,7 +4110,12 @@ func (o *consumer) processNextMsgReq(_ *subscription, c *client, _ *Account, _,
|
||||
return
|
||||
}
|
||||
|
||||
_, msg = c.msgParts(msg)
|
||||
hdr, msg := c.msgParts(rmsg)
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
hdr = []byte("NATS/1.0 412 Required Api Level\r\n\r\n")
|
||||
o.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
||||
return
|
||||
}
|
||||
o.nextMsgReqs.push(newNextMsgReq(reply, copyBytes(msg)))
|
||||
}
|
||||
|
||||
@@ -3968,6 +4169,10 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) {
|
||||
if priorityGroup.Id != _EMPTY_ && o.cfg.PriorityPolicy != PriorityPinnedClient {
|
||||
sendErr(400, "Bad Request - Not a Pinned Client Priority consumer")
|
||||
}
|
||||
if priorityGroup.Priority < 0 || priorityGroup.Priority > 9 {
|
||||
sendErr(400, "Bad Request - Priority must be between 0 and 9")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if priorityGroup != nil && o.cfg.PriorityPolicy != PriorityNone {
|
||||
@@ -4038,15 +4243,25 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) {
|
||||
wr.b = maxBytes
|
||||
wr.received = time.Now()
|
||||
|
||||
if err := o.waiting.add(wr); err != nil {
|
||||
// If the client has a heartbeat interval set, don't bother responding with a 409,
|
||||
// otherwise we can end up in a hot loop with the client re-requesting instead of
|
||||
// waiting for the missing heartbeats instead and retrying.
|
||||
if hb == 0 {
|
||||
sendErr(409, "Exceeded MaxWaiting")
|
||||
if o.cfg.PriorityPolicy == PriorityPrioritized {
|
||||
if err := o.waiting.addPrioritized(wr); err != nil {
|
||||
if hb == 0 {
|
||||
sendErr(409, "Exceeded MaxWaiting")
|
||||
}
|
||||
wr.recycle()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := o.waiting.add(wr); err != nil {
|
||||
// If the client has a heartbeat interval set, don't bother responding with a 409,
|
||||
// otherwise we can end up in a hot loop with the client re-requesting instead of
|
||||
// waiting for the missing heartbeats instead and retrying.
|
||||
if hb == 0 {
|
||||
sendErr(409, "Exceeded MaxWaiting")
|
||||
}
|
||||
wr.recycle()
|
||||
return
|
||||
}
|
||||
wr.recycle()
|
||||
return
|
||||
}
|
||||
o.signalNewMessages()
|
||||
// If we are clustered update our followers about this request.
|
||||
@@ -4243,9 +4458,6 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
|
||||
return pmsg, 1, err
|
||||
}
|
||||
|
||||
// Hold onto this since we release the lock.
|
||||
store := o.mset.store
|
||||
|
||||
var sseq uint64
|
||||
var err error
|
||||
var sm *StoreMsg
|
||||
@@ -4255,13 +4467,13 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
|
||||
filters, subjf, fseq := o.filters, o.subjf, o.sseq
|
||||
// Check if we are multi-filtered or not.
|
||||
if filters != nil {
|
||||
sm, sseq, err = store.LoadNextMsgMulti(filters, fseq, &pmsg.StoreMsg)
|
||||
sm, sseq, err = o.mset.store.LoadNextMsgMulti(filters, fseq, &pmsg.StoreMsg)
|
||||
} else if len(subjf) > 0 { // Means single filtered subject since o.filters means > 1.
|
||||
filter, wc := subjf[0].subject, subjf[0].hasWildcard
|
||||
sm, sseq, err = store.LoadNextMsg(filter, wc, fseq, &pmsg.StoreMsg)
|
||||
sm, sseq, err = o.mset.store.LoadNextMsg(filter, wc, fseq, &pmsg.StoreMsg)
|
||||
} else {
|
||||
// No filter here.
|
||||
sm, sseq, err = store.LoadNextMsg(_EMPTY_, false, fseq, &pmsg.StoreMsg)
|
||||
sm, sseq, err = o.mset.store.LoadNextMsg(_EMPTY_, false, fseq, &pmsg.StoreMsg)
|
||||
}
|
||||
if sm == nil {
|
||||
pmsg.returnToPool()
|
||||
@@ -4670,14 +4882,14 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
|
||||
}
|
||||
if err == ErrStoreMsgNotFound || err == errDeletedMsg || err == ErrStoreEOF || err == errMaxAckPending {
|
||||
goto waitForMsgs
|
||||
} else if err == errPartialCache {
|
||||
s.Warnf("Unexpected partial cache error looking up message for consumer '%s > %s > %s'",
|
||||
o.mset.acc, stream, o.cfg.Name)
|
||||
goto waitForMsgs
|
||||
|
||||
} else {
|
||||
s.Errorf("Received an error looking up message for consumer '%s > %s > %s': %v",
|
||||
o.mset.acc, stream, o.cfg.Name, err)
|
||||
if pmsg != nil {
|
||||
s.Errorf("Received an error looking up message with sequence %d for consumer '%s > %s > %s': %v",
|
||||
pmsg.seq, o.mset.acc, stream, o.cfg.Name, err)
|
||||
} else {
|
||||
s.Errorf("Received an error looking up message for consumer '%s > %s > %s': %v",
|
||||
o.mset.acc, stream, o.cfg.Name, err)
|
||||
}
|
||||
goto waitForMsgs
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -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
@@ -31,6 +31,14 @@ var (
|
||||
// ErrAuthExpired represents an expired authorization due to timeout.
|
||||
ErrAuthExpired = errors.New("authentication expired")
|
||||
|
||||
// ErrAuthProxyNotTrusted represents an error condition on failed authentication
|
||||
// due to a connection from a proxy not in the list of trusted proxies.
|
||||
ErrAuthProxyNotTrusted = errors.New("proxy is not trusted")
|
||||
|
||||
// ErrAuthProxyRequired represents an error condition on failed authentication
|
||||
// due to a connection not coming from a proxy.
|
||||
ErrAuthProxyRequired = errors.New("proxy connection required")
|
||||
|
||||
// ErrMaxPayload represents an error condition when the payload is too big.
|
||||
ErrMaxPayload = errors.New("maximum payload exceeded")
|
||||
|
||||
|
||||
+291
-1
@@ -1140,7 +1140,7 @@
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSConsumerMaxRequestExpiresToSmall",
|
||||
"constant": "JSConsumerMaxRequestExpiresTooSmall",
|
||||
"code": 400,
|
||||
"error_code": 10115,
|
||||
"description": "consumer max request expires needs to be \u003e= 1ms",
|
||||
@@ -1659,6 +1659,106 @@
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageIncrDisabledErr",
|
||||
"code": 400,
|
||||
"error_code": 10168,
|
||||
"description": "message counters is disabled",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageIncrMissingErr",
|
||||
"code": 400,
|
||||
"error_code": 10169,
|
||||
"description": "message counter increment is missing",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageIncrPayloadErr",
|
||||
"code": 400,
|
||||
"error_code": 10170,
|
||||
"description": "message counter has payload",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageIncrInvalidErr",
|
||||
"code": 400,
|
||||
"error_code": 10171,
|
||||
"description": "message counter increment is invalid",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageCounterBrokenErr",
|
||||
"code": 400,
|
||||
"error_code": 10172,
|
||||
"description": "message counter is broken",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMirrorWithCountersErr",
|
||||
"code": 400,
|
||||
"error_code": 10173,
|
||||
"description": "stream mirrors can not also calculate counters",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSAtomicPublishDisabledErr",
|
||||
"code": 400,
|
||||
"error_code": 10174,
|
||||
"description": "atomic publish is disabled",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSAtomicPublishMissingSeqErr",
|
||||
"code": 400,
|
||||
"error_code": 10175,
|
||||
"description": "atomic publish sequence is missing",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSAtomicPublishIncompleteBatchErr",
|
||||
"code": 400,
|
||||
"error_code": 10176,
|
||||
"description": "atomic publish batch is incomplete",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSAtomicPublishUnsupportedHeaderBatchErr",
|
||||
"code": 400,
|
||||
"error_code": 10177,
|
||||
"description": "atomic publish unsupported header used: {header}",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSConsumerPushWithPriorityGroupErr",
|
||||
"code": 400,
|
||||
@@ -1669,6 +1769,156 @@
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSAtomicPublishInvalidBatchIDErr",
|
||||
"code": 400,
|
||||
"error_code": 10179,
|
||||
"description": "atomic publish batch ID is invalid",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSStreamMinLastSeqErr",
|
||||
"code": 412,
|
||||
"error_code": 10180,
|
||||
"description": "min last sequence",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSConsumerAckPolicyInvalidErr",
|
||||
"code": 400,
|
||||
"error_code": 10181,
|
||||
"description": "consumer ack policy invalid",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSConsumerReplayPolicyInvalidErr",
|
||||
"code": 400,
|
||||
"error_code": 10182,
|
||||
"description": "consumer replay policy invalid",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSConsumerAckWaitNegativeErr",
|
||||
"code": 400,
|
||||
"error_code": 10183,
|
||||
"description": "consumer ack wait needs to be positive",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSConsumerBackOffNegativeErr",
|
||||
"code": 400,
|
||||
"error_code": 10184,
|
||||
"description": "consumer backoff needs to be positive",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSRequiredApiLevelErr",
|
||||
"code": 412,
|
||||
"error_code": 10185,
|
||||
"description": "JetStream minimum api level required",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMirrorWithMsgSchedulesErr",
|
||||
"code": 400,
|
||||
"error_code": 10186,
|
||||
"description": "stream mirrors can not also schedule messages",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSSourceWithMsgSchedulesErr",
|
||||
"code": 400,
|
||||
"error_code": 10187,
|
||||
"description": "stream source can not also schedule messages",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageSchedulesDisabledErr",
|
||||
"code": 400,
|
||||
"error_code": 10188,
|
||||
"description": "message schedules is disabled",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageSchedulesPatternInvalidErr",
|
||||
"code": 400,
|
||||
"error_code": 10189,
|
||||
"description": "message schedules pattern is invalid",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageSchedulesTargetInvalidErr",
|
||||
"code": 400,
|
||||
"error_code": 10190,
|
||||
"description": "message schedules target is invalid",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageSchedulesTTLInvalidErr",
|
||||
"code": 400,
|
||||
"error_code": 10191,
|
||||
"description": "message schedules invalid per-message TTL",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMessageSchedulesRollupInvalidErr",
|
||||
"code": 400,
|
||||
"error_code": 10192,
|
||||
"description": "message schedules invalid rollup",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSStreamExpectedLastSeqPerSubjectInvalid",
|
||||
"code": 400,
|
||||
"error_code": 10193,
|
||||
"description": "missing sequence for expected last sequence per subject",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSStreamOfflineReasonErrF",
|
||||
"code": 500,
|
||||
@@ -1688,5 +1938,45 @@
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSConsumerPriorityGroupWithPolicyNone",
|
||||
"code": 400,
|
||||
"error_code": 10196,
|
||||
"description": "consumer can not have priority groups when policy is none",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSConsumerPinnedTTLWithoutPriorityPolicyNone",
|
||||
"code": 400,
|
||||
"error_code": 10197,
|
||||
"description": "PinnedTTL cannot be set when PriorityPolicy is none",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSMirrorWithAtomicPublishErr",
|
||||
"code": 400,
|
||||
"error_code": 10198,
|
||||
"description": "stream mirrors can not also use atomic publishing",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
},
|
||||
{
|
||||
"constant": "JSAtomicPublishTooLargeBatchErrF",
|
||||
"code": 400,
|
||||
"error_code": 10199,
|
||||
"description": "atomic publish batch is too large: {size}",
|
||||
"comment": "",
|
||||
"help": "",
|
||||
"url": "",
|
||||
"deprecates": ""
|
||||
}
|
||||
]
|
||||
|
||||
+87
-71
@@ -247,13 +247,14 @@ type ServerCapability uint64
|
||||
|
||||
// ServerInfo identifies remote servers.
|
||||
type ServerInfo struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
ID string `json:"id"`
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Version string `json:"ver"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
ID string `json:"id"`
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Version string `json:"ver"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
// Whether JetStream is enabled (deprecated in favor of the `ServerCapability`).
|
||||
JetStream bool `json:"jetstream"`
|
||||
// Generic capability flags
|
||||
@@ -362,24 +363,27 @@ func (ci *ClientInfo) forAdvisory() *ClientInfo {
|
||||
|
||||
// ServerStats hold various statistics that we will periodically send out.
|
||||
type ServerStats struct {
|
||||
Start time.Time `json:"start"`
|
||||
Mem int64 `json:"mem"`
|
||||
Cores int `json:"cores"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Connections int `json:"connections"`
|
||||
TotalConnections uint64 `json:"total_connections"`
|
||||
ActiveAccounts int `json:"active_accounts"`
|
||||
NumSubs uint32 `json:"subscriptions"`
|
||||
Sent DataStats `json:"sent"`
|
||||
Received DataStats `json:"received"`
|
||||
SlowConsumers int64 `json:"slow_consumers"`
|
||||
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"`
|
||||
Routes []*RouteStat `json:"routes,omitempty"`
|
||||
Gateways []*GatewayStat `json:"gateways,omitempty"`
|
||||
ActiveServers int `json:"active_servers,omitempty"`
|
||||
JetStream *JetStreamVarz `json:"jetstream,omitempty"`
|
||||
MemLimit int64 `json:"gomemlimit,omitempty"`
|
||||
MaxProcs int `json:"gomaxprocs,omitempty"`
|
||||
Start time.Time `json:"start"`
|
||||
Mem int64 `json:"mem"`
|
||||
Cores int `json:"cores"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Connections int `json:"connections"`
|
||||
TotalConnections uint64 `json:"total_connections"`
|
||||
ActiveAccounts int `json:"active_accounts"`
|
||||
NumSubs uint32 `json:"subscriptions"`
|
||||
Sent DataStats `json:"sent"`
|
||||
Received DataStats `json:"received"`
|
||||
SlowConsumers int64 `json:"slow_consumers"`
|
||||
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"`
|
||||
StaleConnections int64 `json:"stale_connections,omitempty"`
|
||||
StaleConnectionStats *StaleConnectionStats `json:"stale_connection_stats,omitempty"`
|
||||
StalledClients int64 `json:"stalled_clients,omitempty"`
|
||||
Routes []*RouteStat `json:"routes,omitempty"`
|
||||
Gateways []*GatewayStat `json:"gateways,omitempty"`
|
||||
ActiveServers int `json:"active_servers,omitempty"`
|
||||
JetStream *JetStreamVarz `json:"jetstream,omitempty"`
|
||||
MemLimit int64 `json:"gomemlimit,omitempty"`
|
||||
MaxProcs int `json:"gomaxprocs,omitempty"`
|
||||
}
|
||||
|
||||
// RouteStat holds route statistics.
|
||||
@@ -400,17 +404,17 @@ type GatewayStat struct {
|
||||
NumInbound int `json:"inbound_connections"`
|
||||
}
|
||||
|
||||
type dataStats struct {
|
||||
type MsgBytes struct {
|
||||
Msgs int64 `json:"msgs"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
}
|
||||
|
||||
// DataStats reports how may msg and bytes. Applicable for both sent and received.
|
||||
type DataStats struct {
|
||||
dataStats
|
||||
Gateways dataStats `json:"gateways,omitempty"`
|
||||
Routes dataStats `json:"routes,omitempty"`
|
||||
Leafs dataStats `json:"leafs,omitempty"`
|
||||
MsgBytes
|
||||
Gateways *MsgBytes `json:"gateways,omitempty"`
|
||||
Routes *MsgBytes `json:"routes,omitempty"`
|
||||
Leafs *MsgBytes `json:"leafs,omitempty"`
|
||||
}
|
||||
|
||||
// Used for internally queueing up messages that the server wants to send.
|
||||
@@ -512,8 +516,9 @@ RESET:
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
// Grab tags.
|
||||
tags := s.getOpts().Tags
|
||||
// Grab tags and metadata.
|
||||
opts := s.getOpts()
|
||||
tags, metadata := opts.Tags, opts.Metadata
|
||||
|
||||
for s.eventsRunning() {
|
||||
select {
|
||||
@@ -530,6 +535,7 @@ RESET:
|
||||
si.Version = VERSION
|
||||
si.Time = time.Now().UTC()
|
||||
si.Tags = tags
|
||||
si.Metadata = metadata
|
||||
si.Flags = 0
|
||||
if js {
|
||||
// New capability based flags.
|
||||
@@ -860,13 +866,13 @@ func routeStat(r *client) *RouteStat {
|
||||
rs := &RouteStat{
|
||||
ID: r.cid,
|
||||
Sent: DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: r.outMsgs,
|
||||
Bytes: r.outBytes,
|
||||
},
|
||||
},
|
||||
Received: DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: atomic.LoadInt64(&r.inMsgs),
|
||||
Bytes: atomic.LoadInt64(&r.inBytes),
|
||||
},
|
||||
@@ -952,6 +958,17 @@ func (s *Server) sendStatsz(subj string) {
|
||||
if scs.Clients != 0 || scs.Routes != 0 || scs.Gateways != 0 || scs.Leafs != 0 {
|
||||
m.Stats.SlowConsumersStats = scs
|
||||
}
|
||||
m.Stats.StaleConnections = atomic.LoadInt64(&s.staleConnections)
|
||||
m.Stats.StalledClients = atomic.LoadInt64(&s.stalls)
|
||||
stcs := &StaleConnectionStats{
|
||||
Clients: s.NumStaleConnectionsClients(),
|
||||
Routes: s.NumStaleConnectionsRoutes(),
|
||||
Gateways: s.NumStaleConnectionsGateways(),
|
||||
Leafs: s.NumStaleConnectionsLeafs(),
|
||||
}
|
||||
if stcs.Clients != 0 || stcs.Routes != 0 || stcs.Gateways != 0 || stcs.Leafs != 0 {
|
||||
m.Stats.StaleConnectionStats = stcs
|
||||
}
|
||||
m.Stats.NumSubs = s.numSubscriptions()
|
||||
// Routes
|
||||
s.forEachRoute(func(r *client) {
|
||||
@@ -968,7 +985,7 @@ func (s *Server) sendStatsz(subj string) {
|
||||
// Note that *client.out[Msgs|Bytes] are not set using atomic,
|
||||
// unlike the in[Msgs|bytes].
|
||||
gs.Sent = DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: c.outMsgs,
|
||||
Bytes: c.outBytes,
|
||||
},
|
||||
@@ -1927,11 +1944,12 @@ func (s *Server) leafNodeConnected(sub *subscription, _ *client, _ *Account, sub
|
||||
|
||||
// Common filter options for system requests STATSZ VARZ SUBSZ CONNZ ROUTEZ GATEWAYZ LEAFZ
|
||||
type EventFilterOptions struct {
|
||||
Name string `json:"server_name,omitempty"` // filter by server name
|
||||
Cluster string `json:"cluster,omitempty"` // filter by cluster name
|
||||
Host string `json:"host,omitempty"` // filter by host name
|
||||
Tags []string `json:"tags,omitempty"` // filter by tags (must match all tags)
|
||||
Domain string `json:"domain,omitempty"` // filter by JS domain
|
||||
Name string `json:"server_name,omitempty"` // filter by server name
|
||||
Cluster string `json:"cluster,omitempty"` // filter by cluster name
|
||||
Host string `json:"host,omitempty"` // filter by host name
|
||||
ExactMatch bool `json:"exact_match,omitempty"` // if the above filters should use exact matching or only "contains"
|
||||
Tags []string `json:"tags,omitempty"` // filter by tags (must match all tags)
|
||||
Domain string `json:"domain,omitempty"` // filter by JS domain
|
||||
}
|
||||
|
||||
// StatszEventOptions are options passed to Statsz
|
||||
@@ -2030,18 +2048,21 @@ type RaftzEventOptions struct {
|
||||
}
|
||||
|
||||
// returns true if the request does NOT apply to this server and can be ignored.
|
||||
// DO NOT hold the server lock when
|
||||
// DO NOT hold the server lock when calling this.
|
||||
func (s *Server) filterRequest(fOpts *EventFilterOptions) bool {
|
||||
if fOpts.Name != _EMPTY_ && !strings.Contains(s.info.Name, fOpts.Name) {
|
||||
return true
|
||||
if fOpts == nil {
|
||||
return false
|
||||
}
|
||||
if fOpts.Host != _EMPTY_ && !strings.Contains(s.info.Host, fOpts.Host) {
|
||||
return true
|
||||
}
|
||||
if fOpts.Cluster != _EMPTY_ {
|
||||
if !strings.Contains(s.ClusterName(), fOpts.Cluster) {
|
||||
if fOpts.ExactMatch {
|
||||
if (fOpts.Name != _EMPTY_ && fOpts.Name != s.info.Name) ||
|
||||
(fOpts.Host != _EMPTY_ && fOpts.Host != s.info.Host) ||
|
||||
(fOpts.Cluster != _EMPTY_ && fOpts.Cluster != s.ClusterName()) {
|
||||
return true
|
||||
}
|
||||
} else if (fOpts.Name != _EMPTY_ && !strings.Contains(s.info.Name, fOpts.Name)) ||
|
||||
(fOpts.Host != _EMPTY_ && !strings.Contains(s.info.Host, fOpts.Host)) ||
|
||||
(fOpts.Cluster != _EMPTY_ && !strings.Contains(s.ClusterName(), fOpts.Cluster)) {
|
||||
return true
|
||||
}
|
||||
if len(fOpts.Tags) > 0 {
|
||||
opts := s.getOpts()
|
||||
@@ -2428,37 +2449,37 @@ func (a *Account) statz() *AccountStat {
|
||||
|
||||
a.stats.Lock()
|
||||
received := DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: a.stats.inMsgs,
|
||||
Bytes: a.stats.inBytes,
|
||||
},
|
||||
Gateways: dataStats{
|
||||
Gateways: &MsgBytes{
|
||||
Msgs: a.stats.gw.inMsgs,
|
||||
Bytes: a.stats.gw.inBytes,
|
||||
},
|
||||
Routes: dataStats{
|
||||
Routes: &MsgBytes{
|
||||
Msgs: a.stats.rt.inMsgs,
|
||||
Bytes: a.stats.rt.inBytes,
|
||||
},
|
||||
Leafs: dataStats{
|
||||
Leafs: &MsgBytes{
|
||||
Msgs: a.stats.ln.inMsgs,
|
||||
Bytes: a.stats.ln.inBytes,
|
||||
},
|
||||
}
|
||||
sent := DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: a.stats.outMsgs,
|
||||
Bytes: a.stats.outBytes,
|
||||
},
|
||||
Gateways: dataStats{
|
||||
Gateways: &MsgBytes{
|
||||
Msgs: a.stats.gw.outMsgs,
|
||||
Bytes: a.stats.gw.outBytes,
|
||||
},
|
||||
Routes: dataStats{
|
||||
Routes: &MsgBytes{
|
||||
Msgs: a.stats.rt.outMsgs,
|
||||
Bytes: a.stats.rt.outBytes,
|
||||
},
|
||||
Leafs: dataStats{
|
||||
Leafs: &MsgBytes{
|
||||
Msgs: a.stats.ln.outMsgs,
|
||||
Bytes: a.stats.ln.outBytes,
|
||||
},
|
||||
@@ -2504,13 +2525,11 @@ func (s *Server) accountConnectEvent(c *client) {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
gacc := s.gacc
|
||||
eid := s.nextEventID()
|
||||
s.mu.Unlock()
|
||||
|
||||
c.mu.Lock()
|
||||
// Ignore global account activity
|
||||
if c.acc == nil || c.acc == gacc {
|
||||
if c.acc == nil {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -2553,18 +2572,15 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string)
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
gacc := s.gacc
|
||||
eid := s.nextEventID()
|
||||
s.mu.Unlock()
|
||||
|
||||
c.mu.Lock()
|
||||
|
||||
// Ignore global account activity
|
||||
if c.acc == nil || c.acc == gacc {
|
||||
if c.acc == nil {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
m := DisconnectEventMsg{
|
||||
TypedEvent: TypedEvent{
|
||||
Type: DisconnectEventMsgType,
|
||||
@@ -2591,13 +2607,13 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string)
|
||||
MQTTClient: c.getMQTTClientID(),
|
||||
},
|
||||
Sent: DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: atomic.LoadInt64(&c.inMsgs),
|
||||
Bytes: atomic.LoadInt64(&c.inBytes),
|
||||
},
|
||||
},
|
||||
Received: DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: c.outMsgs,
|
||||
Bytes: c.outBytes,
|
||||
},
|
||||
@@ -2612,7 +2628,7 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string)
|
||||
}
|
||||
|
||||
// This is the system level event sent to the system account for operators.
|
||||
func (s *Server) sendAuthErrorEvent(c *client) {
|
||||
func (s *Server) sendAuthErrorEvent(c *client, reason string) {
|
||||
s.mu.Lock()
|
||||
if !s.eventsEnabled() {
|
||||
s.mu.Unlock()
|
||||
@@ -2649,18 +2665,18 @@ func (s *Server) sendAuthErrorEvent(c *client) {
|
||||
MQTTClient: c.getMQTTClientID(),
|
||||
},
|
||||
Sent: DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: c.inMsgs,
|
||||
Bytes: c.inBytes,
|
||||
},
|
||||
},
|
||||
Received: DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: c.outMsgs,
|
||||
Bytes: c.outBytes,
|
||||
},
|
||||
},
|
||||
Reason: AuthenticationViolation.String(),
|
||||
Reason: reason,
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -2711,13 +2727,13 @@ func (s *Server) sendAccountAuthErrorEvent(c *client, acc *Account, reason strin
|
||||
MQTTClient: c.getMQTTClientID(),
|
||||
},
|
||||
Sent: DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: c.inMsgs,
|
||||
Bytes: c.inBytes,
|
||||
},
|
||||
},
|
||||
Received: DataStats{
|
||||
dataStats: dataStats{
|
||||
MsgBytes: MsgBytes{
|
||||
Msgs: c.outMsgs,
|
||||
Bytes: c.outBytes,
|
||||
},
|
||||
|
||||
+633
-217
File diff suppressed because it is too large
Load Diff
+16
-6
@@ -35,6 +35,7 @@ import (
|
||||
const (
|
||||
defaultSolicitGatewaysDelay = time.Second
|
||||
defaultGatewayConnectDelay = time.Second
|
||||
defaultGatewayConnectMaxDelay = 30 * time.Second
|
||||
defaultGatewayReconnectDelay = time.Second
|
||||
defaultGatewayRecentSubExpiration = 2 * time.Second
|
||||
defaultGatewayMaxRUnsubBeforeSwitch = 1000
|
||||
@@ -59,6 +60,7 @@ const (
|
||||
|
||||
var (
|
||||
gatewayConnectDelay = defaultGatewayConnectDelay
|
||||
gatewayConnectMaxDelay = defaultGatewayConnectMaxDelay
|
||||
gatewayReconnectDelay = defaultGatewayReconnectDelay
|
||||
gatewayMaxRUnsubBeforeSwitch = defaultGatewayMaxRUnsubBeforeSwitch
|
||||
gatewaySolicitDelay = int64(defaultSolicitGatewaysDelay)
|
||||
@@ -703,10 +705,11 @@ func (s *Server) reconnectGateway(cfg *gatewayCfg) {
|
||||
// to the given Gateway. It will return once a connection has been created.
|
||||
func (s *Server) solicitGateway(cfg *gatewayCfg, firstConnect bool) {
|
||||
var (
|
||||
opts = s.getOpts()
|
||||
isImplicit = cfg.isImplicit()
|
||||
attempts int
|
||||
typeStr string
|
||||
opts = s.getOpts()
|
||||
isImplicit = cfg.isImplicit()
|
||||
attemptDelay = gatewayConnectDelay
|
||||
attempts int
|
||||
typeStr string
|
||||
)
|
||||
if isImplicit {
|
||||
typeStr = "implicit"
|
||||
@@ -769,7 +772,14 @@ func (s *Server) solicitGateway(cfg *gatewayCfg, firstConnect bool) {
|
||||
select {
|
||||
case <-s.quitCh:
|
||||
return
|
||||
case <-time.After(gatewayConnectDelay):
|
||||
case <-time.After(attemptDelay):
|
||||
if opts.Gateway.ConnectBackoff {
|
||||
// Use exponential backoff for connection attempts.
|
||||
attemptDelay *= 2
|
||||
if attemptDelay > gatewayConnectMaxDelay {
|
||||
attemptDelay = gatewayConnectMaxDelay
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -923,7 +933,7 @@ func (s *Server) createGateway(cfg *gatewayCfg, url *url.URL, conn net.Conn) {
|
||||
if tlsRequired {
|
||||
c.Debugf("TLS handshake complete")
|
||||
cs := c.nc.(*tls.Conn).ConnectionState()
|
||||
c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite))
|
||||
c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite))
|
||||
}
|
||||
|
||||
// For outbound, we can't set the normal ping timer yet since the other
|
||||
|
||||
+75
-6
@@ -156,8 +156,8 @@ type jsAccount struct {
|
||||
storeDir string
|
||||
inflight sync.Map
|
||||
streams map[string]*stream
|
||||
templates map[string]*streamTemplate
|
||||
store TemplateStore
|
||||
templates map[string]*streamTemplate // Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
store TemplateStore // Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
|
||||
// From server
|
||||
sendq *ipQueue[*pubMsg]
|
||||
@@ -560,7 +560,7 @@ func (s *Server) restartJetStream() error {
|
||||
MaxMemory: opts.JetStreamMaxMemory,
|
||||
MaxStore: opts.JetStreamMaxStore,
|
||||
Domain: opts.JetStreamDomain,
|
||||
Strict: opts.JetStreamStrict,
|
||||
Strict: !opts.NoJetStreamStrict,
|
||||
}
|
||||
s.Noticef("Restarting JetStream")
|
||||
err := s.EnableJetStream(&cfg)
|
||||
@@ -1451,6 +1451,61 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
|
||||
mset.setCreatedTime(cfg.Created)
|
||||
}
|
||||
|
||||
// Might need to recover from a partial batch write, but only if a single replica stream.
|
||||
if cfg.AllowAtomicPublish && cfg.Replicas == 1 {
|
||||
var (
|
||||
ok bool
|
||||
smv StoreMsg
|
||||
batchId string
|
||||
batchSeq uint64
|
||||
commit bool
|
||||
batchStoreDir string
|
||||
store StreamStore
|
||||
state StreamState
|
||||
)
|
||||
// Check if the last message was part of a batch.
|
||||
sm, err := mset.store.LoadLastMsg(fwcs, &smv)
|
||||
if err != nil || sm == nil {
|
||||
goto SKIP
|
||||
}
|
||||
batchId = getBatchId(sm.hdr)
|
||||
batchSeq, ok = getBatchSequence(sm.hdr)
|
||||
commit = len(sliceHeader(JSBatchCommit, sm.hdr)) != 0
|
||||
if batchId == _EMPTY_ || !ok || commit {
|
||||
goto SKIP
|
||||
}
|
||||
// We've observed a partial batch write. Write the remainder of the batch.
|
||||
batchSeq++
|
||||
_, batchStoreDir = getBatchStoreDir(mset, batchId)
|
||||
if _, err = os.Stat(batchStoreDir); err != nil {
|
||||
s.Errorf(" Failed restoring partial batch write for stream '%s > %s' at sequence %d: %v",
|
||||
mset.accName(), mset.name(), batchSeq, err)
|
||||
goto SKIP
|
||||
}
|
||||
store, err = newBatchStore(mset, batchId)
|
||||
if err != nil {
|
||||
s.Errorf(" Failed restoring partial batch write for stream '%s > %s' at sequence %d: %v",
|
||||
mset.accName(), mset.name(), batchSeq, err)
|
||||
goto SKIP
|
||||
}
|
||||
store.FastState(&state)
|
||||
s.Noticef(" Restoring partial batch write for stream '%s > %s' (seq %d to %d)",
|
||||
mset.accName(), mset.name(), batchSeq, state.LastSeq)
|
||||
// Loop through items that weren't persisted yet.
|
||||
for seq := batchSeq; seq <= state.LastSeq; seq++ {
|
||||
sm, err = store.LoadMsg(seq, &smv)
|
||||
if err != nil || sm == nil {
|
||||
s.Errorf(" Failed restoring partial batch write for stream '%s > %s' at sequence %d: %v",
|
||||
mset.accName(), mset.name(), seq, err)
|
||||
break
|
||||
}
|
||||
mset.processJetStreamMsg(sm.subj, _EMPTY_, sm.hdr, sm.msg, 0, 0, nil, false, true)
|
||||
}
|
||||
store.Delete(true)
|
||||
SKIP:
|
||||
os.RemoveAll(filepath.Join(sdir, fi.Name(), batchesDir))
|
||||
}
|
||||
|
||||
state := mset.state()
|
||||
s.Noticef(" Restored %s messages for stream '%s > %s' in %v",
|
||||
comma(int64(state.Msgs)), mset.accName(), mset.name(), time.Since(rt).Round(time.Millisecond))
|
||||
@@ -2640,7 +2695,7 @@ func (s *Server) dynJetStreamConfig(storeDir string, maxStore, maxMem int64) *Je
|
||||
opts := s.getOpts()
|
||||
|
||||
// Strict mode.
|
||||
jsc.Strict = opts.JetStreamStrict
|
||||
jsc.Strict = !opts.NoJetStreamStrict
|
||||
|
||||
// Sync options.
|
||||
jsc.SyncInterval = opts.SyncInterval
|
||||
@@ -2687,6 +2742,7 @@ func (a *Account) checkForJetStream() (*Server, *jsAccount, error) {
|
||||
|
||||
// StreamTemplateConfig allows a configuration to auto-create streams based on this template when a message
|
||||
// is received that matches. Each new stream will use the config as the template config to create them.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type StreamTemplateConfig struct {
|
||||
Name string `json:"name"`
|
||||
Config *StreamConfig `json:"config"`
|
||||
@@ -2694,12 +2750,14 @@ type StreamTemplateConfig struct {
|
||||
}
|
||||
|
||||
// StreamTemplateInfo
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type StreamTemplateInfo struct {
|
||||
Config *StreamTemplateConfig `json:"config"`
|
||||
Streams []string `json:"streams"`
|
||||
}
|
||||
|
||||
// streamTemplate
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type streamTemplate struct {
|
||||
mu sync.Mutex
|
||||
tc *client
|
||||
@@ -2708,6 +2766,7 @@ type streamTemplate struct {
|
||||
streams []string
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (t *StreamTemplateConfig) deepCopy() *StreamTemplateConfig {
|
||||
copy := *t
|
||||
cfg := *t.Config
|
||||
@@ -2716,6 +2775,7 @@ func (t *StreamTemplateConfig) deepCopy() *StreamTemplateConfig {
|
||||
}
|
||||
|
||||
// addStreamTemplate will add a stream template to this account that allows auto-creation of streams.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (a *Account) addStreamTemplate(tc *StreamTemplateConfig) (*streamTemplate, error) {
|
||||
s, jsa, err := a.checkForJetStream()
|
||||
if err != nil {
|
||||
@@ -2772,6 +2832,7 @@ func (a *Account) addStreamTemplate(tc *StreamTemplateConfig) (*streamTemplate,
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (t *streamTemplate) createTemplateSubscriptions() error {
|
||||
if t == nil {
|
||||
return fmt.Errorf("no template")
|
||||
@@ -2795,6 +2856,7 @@ func (t *streamTemplate) createTemplateSubscriptions() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (t *streamTemplate) processInboundTemplateMsg(_ *subscription, pc *client, acc *Account, subject, reply string, msg []byte) {
|
||||
if t == nil || t.jsa == nil {
|
||||
return
|
||||
@@ -2842,6 +2904,7 @@ func (t *streamTemplate) processInboundTemplateMsg(_ *subscription, pc *client,
|
||||
}
|
||||
|
||||
// lookupStreamTemplate looks up the names stream template.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (a *Account) lookupStreamTemplate(name string) (*streamTemplate, error) {
|
||||
_, jsa, err := a.checkForJetStream()
|
||||
if err != nil {
|
||||
@@ -2860,6 +2923,7 @@ func (a *Account) lookupStreamTemplate(name string) (*streamTemplate, error) {
|
||||
}
|
||||
|
||||
// This function will check all named streams and make sure they are valid.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (a *Account) validateStreams(t *streamTemplate) {
|
||||
t.mu.Lock()
|
||||
var vstreams []string
|
||||
@@ -2872,6 +2936,7 @@ func (a *Account) validateStreams(t *streamTemplate) {
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (t *streamTemplate) delete() error {
|
||||
if t == nil {
|
||||
return fmt.Errorf("nil stream template")
|
||||
@@ -2930,6 +2995,7 @@ func (t *streamTemplate) delete() error {
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (a *Account) deleteStreamTemplate(name string) error {
|
||||
t, err := a.lookupStreamTemplate(name)
|
||||
if err != nil {
|
||||
@@ -2938,6 +3004,7 @@ func (a *Account) deleteStreamTemplate(name string) error {
|
||||
return t.delete()
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (a *Account) templates() []*streamTemplate {
|
||||
var ts []*streamTemplate
|
||||
_, jsa, err := a.checkForJetStream()
|
||||
@@ -2956,6 +3023,7 @@ func (a *Account) templates() []*streamTemplate {
|
||||
}
|
||||
|
||||
// Will add a stream to a template, this is for recovery.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (jsa *jsAccount) addStreamNameToTemplate(tname, mname string) error {
|
||||
if jsa.templates == nil {
|
||||
return fmt.Errorf("template not found")
|
||||
@@ -2973,6 +3041,7 @@ func (jsa *jsAccount) addStreamNameToTemplate(tname, mname string) error {
|
||||
|
||||
// This will check if a template owns this stream.
|
||||
// jsAccount lock should be held
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (jsa *jsAccount) checkTemplateOwnership(tname, sname string) bool {
|
||||
if jsa.templates == nil {
|
||||
return false
|
||||
@@ -3022,12 +3091,12 @@ func canonicalName(name string) string {
|
||||
}
|
||||
|
||||
// To throttle the out of resources errors.
|
||||
func (s *Server) resourcesExceededError() {
|
||||
func (s *Server) resourcesExceededError(storeType StorageType) {
|
||||
var didAlert bool
|
||||
|
||||
s.rerrMu.Lock()
|
||||
if now := time.Now(); now.Sub(s.rerrLast) > 10*time.Second {
|
||||
s.Errorf("JetStream resource limits exceeded for server")
|
||||
s.Errorf("JetStream %s resource limits exceeded for server", strings.ToLower(storeType.String()))
|
||||
s.rerrLast = now
|
||||
didAlert = true
|
||||
}
|
||||
|
||||
+231
-56
@@ -49,20 +49,24 @@ const (
|
||||
|
||||
// JSApiTemplateCreate is the endpoint to create new stream templates.
|
||||
// Will return JSON response.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
JSApiTemplateCreate = "$JS.API.STREAM.TEMPLATE.CREATE.*"
|
||||
JSApiTemplateCreateT = "$JS.API.STREAM.TEMPLATE.CREATE.%s"
|
||||
|
||||
// JSApiTemplates is the endpoint to list all stream template names for this account.
|
||||
// Will return JSON response.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
JSApiTemplates = "$JS.API.STREAM.TEMPLATE.NAMES"
|
||||
|
||||
// JSApiTemplateInfo is for obtaining general information about a named stream template.
|
||||
// Will return JSON response.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
JSApiTemplateInfo = "$JS.API.STREAM.TEMPLATE.INFO.*"
|
||||
JSApiTemplateInfoT = "$JS.API.STREAM.TEMPLATE.INFO.%s"
|
||||
|
||||
// JSApiTemplateDelete is the endpoint to delete stream templates.
|
||||
// Will return JSON response.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
JSApiTemplateDelete = "$JS.API.STREAM.TEMPLATE.DELETE.*"
|
||||
JSApiTemplateDeleteT = "$JS.API.STREAM.TEMPLATE.DELETE.%s"
|
||||
|
||||
@@ -305,6 +309,9 @@ const (
|
||||
// JSAdvisoryStreamQuorumLostPre notification that a stream and its consumers are stalled.
|
||||
JSAdvisoryStreamQuorumLostPre = "$JS.EVENT.ADVISORY.STREAM.QUORUM_LOST"
|
||||
|
||||
// JSAdvisoryStreamBatchAbandonedPre notification that a stream's batch was abandoned.
|
||||
JSAdvisoryStreamBatchAbandonedPre = "$JS.EVENT.ADVISORY.STREAM.BATCH_ABANDONED"
|
||||
|
||||
// JSAdvisoryConsumerLeaderElectedPre notification that a replicated consumer has elected a leader.
|
||||
JSAdvisoryConsumerLeaderElectedPre = "$JS.EVENT.ADVISORY.CONSUMER.LEADER_ELECTED"
|
||||
|
||||
@@ -325,6 +332,12 @@ const (
|
||||
JSAuditAdvisory = "$JS.EVENT.ADVISORY.API"
|
||||
)
|
||||
|
||||
// Headers used in $JS.API.> requests.
|
||||
const (
|
||||
// JSRequiredApiLevel requires the API level of the responding server to have the specified minimum value.
|
||||
JSRequiredApiLevel = "Nats-Required-Api-Level"
|
||||
)
|
||||
|
||||
var denyAllClientJs = []string{jsAllAPI, "$KV.>", "$OBJ.>"}
|
||||
var denyAllJs = []string{jscAllSubj, raftAllSubj, jsAllAPI, "$KV.>", "$OBJ.>"}
|
||||
|
||||
@@ -672,7 +685,7 @@ type JSApiMsgGetRequest struct {
|
||||
LastFor string `json:"last_by_subj,omitempty"`
|
||||
NextFor string `json:"next_by_subj,omitempty"`
|
||||
|
||||
// Batch support. Used to request more then one msg at a time.
|
||||
// Batch support. Used to request more than one msg at a time.
|
||||
// Can be used with simple starting seq, but also NextFor with wildcards.
|
||||
Batch int `json:"batch,omitempty"`
|
||||
// This will make sure we limit how much data we blast out. If not set we will
|
||||
@@ -687,6 +700,8 @@ type JSApiMsgGetRequest struct {
|
||||
UpToSeq uint64 `json:"up_to_seq,omitempty"`
|
||||
// Only return messages up to this time.
|
||||
UpToTime *time.Time `json:"up_to_time,omitempty"`
|
||||
// Only return the message payload, excluding headers if present.
|
||||
NoHeaders bool `json:"no_hdr,omitempty"`
|
||||
}
|
||||
|
||||
type JSApiMsgGetResponse struct {
|
||||
@@ -766,44 +781,52 @@ type JSApiConsumerGetNextRequest struct {
|
||||
}
|
||||
|
||||
// JSApiStreamTemplateCreateResponse for creating templates.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type JSApiStreamTemplateCreateResponse struct {
|
||||
ApiResponse
|
||||
*StreamTemplateInfo
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
const JSApiStreamTemplateCreateResponseType = "io.nats.jetstream.api.v1.stream_template_create_response"
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type JSApiStreamTemplateDeleteResponse struct {
|
||||
ApiResponse
|
||||
Success bool `json:"success,omitempty"`
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
const JSApiStreamTemplateDeleteResponseType = "io.nats.jetstream.api.v1.stream_template_delete_response"
|
||||
|
||||
// JSApiStreamTemplateInfoResponse for information about stream templates.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type JSApiStreamTemplateInfoResponse struct {
|
||||
ApiResponse
|
||||
*StreamTemplateInfo
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
const JSApiStreamTemplateInfoResponseType = "io.nats.jetstream.api.v1.stream_template_info_response"
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type JSApiStreamTemplatesRequest struct {
|
||||
ApiPagedRequest
|
||||
}
|
||||
|
||||
// JSApiStreamTemplateNamesResponse list of templates
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type JSApiStreamTemplateNamesResponse struct {
|
||||
ApiResponse
|
||||
ApiPaged
|
||||
Templates []string `json:"streams"`
|
||||
}
|
||||
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
const JSApiStreamTemplateNamesResponseType = "io.nats.jetstream.api.v1.stream_template_names_response"
|
||||
|
||||
// Structure that holds state for a JetStream API request that is processed
|
||||
// in a separate long-lived go routine. This is to avoid possibly blocking
|
||||
// ROUTE and GATEWAY connections.
|
||||
// in a separate long-lived go routine. This is to avoid blocking connections.
|
||||
type jsAPIRoutedReq struct {
|
||||
jsub *subscription
|
||||
sub *subscription
|
||||
@@ -872,17 +895,6 @@ func (js *jetStream) apiDispatch(sub *subscription, c *client, acc *Account, sub
|
||||
}
|
||||
jsub := rr.psubs[0]
|
||||
|
||||
// If this is directly from a client connection ok to do in place.
|
||||
if c.kind != ROUTER && c.kind != GATEWAY && c.kind != LEAF {
|
||||
start := time.Now()
|
||||
jsub.icb(sub, c, acc, subject, reply, rmsg)
|
||||
if dur := time.Since(start); dur >= readLoopReportThreshold {
|
||||
s.Warnf("Internal subscription on %q took too long: %v", subject, dur)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If we are here we have received this request over a non-client connection.
|
||||
// We need to make sure not to block. We will send the request to a long-lived
|
||||
// pool of go routines.
|
||||
|
||||
@@ -1277,13 +1289,18 @@ func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, _ *Account,
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiAccountInfoResponse{ApiResponse: ApiResponse{Type: JSApiAccountInfoResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -1333,17 +1350,23 @@ func consumerNameFromSubject(subject string) string {
|
||||
}
|
||||
|
||||
// Request to create a new template.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamTemplateCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateCreateResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
if !acc.JetStreamEnabled() {
|
||||
resp.Error = NewJSNotEnabledForAccountError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
@@ -1388,17 +1411,23 @@ func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, _ *Accoun
|
||||
}
|
||||
|
||||
// Request for the list of all template names.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamTemplateNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateNamesResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
if !acc.JetStreamEnabled() {
|
||||
resp.Error = NewJSNotEnabledForAccountError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
@@ -1452,17 +1481,23 @@ func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, _ *Account
|
||||
}
|
||||
|
||||
// Request for information about a stream template.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (s *Server) jsTemplateInfoRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamTemplateInfoResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateInfoResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
if !acc.JetStreamEnabled() {
|
||||
resp.Error = NewJSNotEnabledForAccountError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
@@ -1493,17 +1528,23 @@ func (s *Server) jsTemplateInfoRequest(sub *subscription, c *client, _ *Account,
|
||||
}
|
||||
|
||||
// Request to delete a stream template.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
func (s *Server) jsTemplateDeleteRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamTemplateDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateDeleteResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
if !acc.JetStreamEnabled() {
|
||||
resp.Error = NewJSNotEnabledForAccountError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
@@ -1564,13 +1605,18 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, _ *Account,
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -1676,13 +1722,18 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account,
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -1731,8 +1782,7 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account,
|
||||
|
||||
// Handle clustered version here.
|
||||
if s.JetStreamIsClustered() {
|
||||
// Always do in separate Go routine.
|
||||
go s.jsClusteredStreamUpdateRequest(ci, acc, subject, reply, copyBytes(rmsg), &cfg, nil, ncfg.Pedantic)
|
||||
s.jsClusteredStreamUpdateRequest(ci, acc, subject, reply, copyBytes(rmsg), &cfg, nil, ncfg.Pedantic)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1775,13 +1825,18 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, _ *Account,
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamNamesResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -1902,7 +1957,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -1912,6 +1967,11 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s
|
||||
ApiResponse: ApiResponse{Type: JSApiStreamListResponseType},
|
||||
Streams: []*StreamInfo{},
|
||||
}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -2030,6 +2090,11 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, a *Account, s
|
||||
if rt := getHeader(JSResponseType, hdr); len(rt) > 0 && string(rt) == jsCreateResponse {
|
||||
resp.ApiResponse.Type = JSApiStreamCreateResponseType
|
||||
}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
var clusterWideConsCount int
|
||||
|
||||
@@ -2234,7 +2299,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, _ *
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -2244,6 +2309,11 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, _ *
|
||||
name := tokenAt(subject, 6)
|
||||
|
||||
var resp = JSApiStreamLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiStreamLeaderStepDownResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// If we are not in clustered mode this is a failed request.
|
||||
if !s.JetStreamIsClustered() {
|
||||
@@ -2344,13 +2414,18 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, _
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiConsumerLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiConsumerLeaderStepDownResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// If we are not in clustered mode this is a failed request.
|
||||
if !s.JetStreamIsClustered() {
|
||||
@@ -2462,7 +2537,7 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, _ *Acco
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -2472,6 +2547,11 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, _ *Acco
|
||||
name := tokenAt(subject, 6)
|
||||
|
||||
var resp = JSApiStreamRemovePeerResponse{ApiResponse: ApiResponse{Type: JSApiStreamRemovePeerResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// If we are not in clustered mode this is a failed request.
|
||||
if !s.JetStreamIsClustered() {
|
||||
@@ -2565,7 +2645,7 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, _ *Ac
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -2589,6 +2669,11 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, _ *Ac
|
||||
}
|
||||
|
||||
var resp = JSApiMetaServerRemoveResponse{ApiResponse: ApiResponse{Type: JSApiMetaServerRemoveResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
if isEmptyRequest(msg) {
|
||||
resp.Error = NewJSBadRequestError()
|
||||
@@ -2674,7 +2759,7 @@ func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -2702,6 +2787,11 @@ func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _
|
||||
}
|
||||
|
||||
var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
var req JSApiMetaServerStreamMoveRequest
|
||||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil {
|
||||
@@ -2833,7 +2923,7 @@ func (s *Server) jsLeaderServerStreamCancelMoveRequest(sub *subscription, c *cli
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -2854,6 +2944,11 @@ func (s *Server) jsLeaderServerStreamCancelMoveRequest(sub *subscription, c *cli
|
||||
}
|
||||
|
||||
var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
accName := tokenAt(subject, 6)
|
||||
streamName := tokenAt(subject, 7)
|
||||
@@ -2943,7 +3038,7 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -2960,6 +3055,11 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac
|
||||
accName := tokenAt(subject, 5)
|
||||
|
||||
var resp = JSApiAccountPurgeResponse{ApiResponse: ApiResponse{Type: JSApiAccountPurgeResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
if !s.JetStreamIsClustered() {
|
||||
var streams []*stream
|
||||
@@ -3030,7 +3130,7 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Accoun
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -3058,6 +3158,11 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Accoun
|
||||
|
||||
var preferredLeader string
|
||||
var resp = JSApiLeaderStepDownResponse{ApiResponse: ApiResponse{Type: JSApiLeaderStepDownResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
if isJSONObjectOrArray(msg) {
|
||||
var req JSApiLeaderStepdownRequest
|
||||
@@ -3203,13 +3308,18 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, _ *Account,
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamDeleteResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -3271,7 +3381,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -3280,6 +3390,11 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su
|
||||
stream := tokenAt(subject, 6)
|
||||
|
||||
var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// If we are in clustered mode we need to be the stream leader to proceed.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -3390,7 +3505,7 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -3399,6 +3514,11 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje
|
||||
stream := tokenAt(subject, 6)
|
||||
|
||||
var resp = JSApiMsgGetResponse{ApiResponse: ApiResponse{Type: JSApiMsgGetResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// If we are in clustered mode we need to be the stream leader to proceed.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -3499,6 +3619,10 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje
|
||||
var svp StoreMsg
|
||||
var sm *StoreMsg
|
||||
|
||||
// Ensure this read request is isolated and doesn't interleave with writes.
|
||||
mset.mu.RLock()
|
||||
defer mset.mu.RUnlock()
|
||||
|
||||
// If AsOfTime is set, perform this first to get the sequence.
|
||||
var seq uint64
|
||||
if req.StartTime != nil {
|
||||
@@ -3522,10 +3646,12 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje
|
||||
resp.Message = &StoredMsg{
|
||||
Subject: sm.subj,
|
||||
Sequence: sm.seq,
|
||||
Header: sm.hdr,
|
||||
Data: sm.msg,
|
||||
Time: time.Unix(0, sm.ts).UTC(),
|
||||
}
|
||||
if !req.NoHeaders {
|
||||
resp.Message.Header = sm.hdr
|
||||
}
|
||||
|
||||
// Don't send response through API layer for this call.
|
||||
s.sendInternalAccountMsg(nil, reply, s.jsonResponse(resp))
|
||||
@@ -3536,7 +3662,7 @@ func (s *Server) jsConsumerUnpinRequest(sub *subscription, c *client, _ *Account
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -3547,6 +3673,11 @@ func (s *Server) jsConsumerUnpinRequest(sub *subscription, c *client, _ *Account
|
||||
|
||||
var req JSApiConsumerUnpinRequest
|
||||
var resp = JSApiConsumerUnpinResponse{ApiResponse: ApiResponse{Type: JSApiConsumerUnpinResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(msg, &req); err != nil {
|
||||
resp.Error = NewJSInvalidJSONError(err)
|
||||
@@ -3670,7 +3801,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account,
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -3679,6 +3810,11 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account,
|
||||
stream := streamNameFromSubject(subject)
|
||||
|
||||
var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// If we are in clustered mode we need to be the stream leader to proceed.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -3812,13 +3948,18 @@ func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, _ *Account
|
||||
if c == nil || !s.JetStreamIsLeader() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
if !acc.JetStreamEnabled() {
|
||||
resp.Error = NewJSNotEnabledForAccountError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
@@ -3964,7 +4105,7 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC
|
||||
// TODO(dlc) - We could check apriori and cancel initial request if we know it won't fit.
|
||||
total += len(msg)
|
||||
if js.wouldExceedLimits(FileStorage, total) {
|
||||
s.resourcesExceededError()
|
||||
s.resourcesExceededError(FileStorage)
|
||||
resultCh <- result{NewJSInsufficientResourcesError(), reply}
|
||||
return
|
||||
}
|
||||
@@ -4092,7 +4233,7 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -4107,6 +4248,11 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun
|
||||
}
|
||||
|
||||
var resp = JSApiStreamSnapshotResponse{ApiResponse: ApiResponse{Type: JSApiStreamSnapshotResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
if !acc.JetStreamEnabled() {
|
||||
resp.Error = NewJSNotEnabledForAccountError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp))
|
||||
@@ -4328,13 +4474,18 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateConsumerRequest
|
||||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil {
|
||||
@@ -4479,14 +4630,7 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun
|
||||
}
|
||||
|
||||
if isClustered && !req.Config.Direct {
|
||||
// If we are inline with client, we still may need to do a callout for consumer info
|
||||
// during this call, so place in Go routine to not block client.
|
||||
// Router and Gateway API calls already in separate context.
|
||||
if c.kind != ROUTER && c.kind != GATEWAY {
|
||||
go s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action, req.Pedantic)
|
||||
} else {
|
||||
s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action, req.Pedantic)
|
||||
}
|
||||
s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action, req.Pedantic)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4552,7 +4696,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, _ *Account
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -4562,6 +4706,11 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, _ *Account
|
||||
ApiResponse: ApiResponse{Type: JSApiConsumerNamesResponseType},
|
||||
Consumers: []string{},
|
||||
}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -4674,7 +4823,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account,
|
||||
return
|
||||
}
|
||||
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -4684,6 +4833,11 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account,
|
||||
ApiResponse: ApiResponse{Type: JSApiConsumerListResponseType},
|
||||
Consumers: []*ConsumerInfo{},
|
||||
}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -4702,6 +4856,12 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account,
|
||||
}
|
||||
}
|
||||
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSClusterNotAvailError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
if hasJS, doErr := acc.checkJetStream(); !hasJS {
|
||||
if doErr {
|
||||
resp.Error = NewJSNotEnabledForAccountError()
|
||||
@@ -4777,7 +4937,7 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, _ *Account,
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -4787,6 +4947,11 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, _ *Account,
|
||||
consumerName := consumerNameFromSubject(subject)
|
||||
|
||||
var resp = JSApiConsumerInfoResponse{ApiResponse: ApiResponse{Type: JSApiConsumerInfoResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
if !isEmptyRequest(msg) {
|
||||
resp.Error = NewJSNotEmptyRequestError()
|
||||
@@ -4973,13 +5138,18 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, _ *Accoun
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
}
|
||||
|
||||
var resp = JSApiConsumerDeleteResponse{ApiResponse: ApiResponse{Type: JSApiConsumerDeleteResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if we should proceed here when we are in clustered mode.
|
||||
if s.JetStreamIsClustered() {
|
||||
@@ -5045,7 +5215,7 @@ func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account
|
||||
if c == nil || !s.JetStreamEnabled() {
|
||||
return
|
||||
}
|
||||
ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
|
||||
ci, acc, hdr, msg, err := s.getRequestInfo(c, rmsg)
|
||||
if err != nil {
|
||||
s.Warnf(badAPIRequestT, msg)
|
||||
return
|
||||
@@ -5053,6 +5223,11 @@ func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account
|
||||
|
||||
var req JSApiConsumerPauseRequest
|
||||
var resp = JSApiConsumerPauseResponse{ApiResponse: ApiResponse{Type: JSApiConsumerPauseResponseType}}
|
||||
if errorOnRequiredApiLevel(hdr) {
|
||||
resp.Error = NewJSRequiredApiLevelError()
|
||||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
|
||||
return
|
||||
}
|
||||
|
||||
if isJSONObjectOrArray(msg) {
|
||||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil {
|
||||
|
||||
+653
@@ -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
|
||||
}
|
||||
+706
-404
File diff suppressed because it is too large
Load Diff
+592
-174
@@ -8,6 +8,24 @@ const (
|
||||
// JSAccountResourcesExceededErr resource limits exceeded for account
|
||||
JSAccountResourcesExceededErr ErrorIdentifier = 10002
|
||||
|
||||
// JSAtomicPublishDisabledErr atomic publish is disabled
|
||||
JSAtomicPublishDisabledErr ErrorIdentifier = 10174
|
||||
|
||||
// JSAtomicPublishIncompleteBatchErr atomic publish batch is incomplete
|
||||
JSAtomicPublishIncompleteBatchErr ErrorIdentifier = 10176
|
||||
|
||||
// JSAtomicPublishInvalidBatchIDErr atomic publish batch ID is invalid
|
||||
JSAtomicPublishInvalidBatchIDErr ErrorIdentifier = 10179
|
||||
|
||||
// JSAtomicPublishMissingSeqErr atomic publish sequence is missing
|
||||
JSAtomicPublishMissingSeqErr ErrorIdentifier = 10175
|
||||
|
||||
// JSAtomicPublishTooLargeBatchErrF atomic publish batch is too large: {size}
|
||||
JSAtomicPublishTooLargeBatchErrF ErrorIdentifier = 10199
|
||||
|
||||
// JSAtomicPublishUnsupportedHeaderBatchErr atomic publish unsupported header used: {header}
|
||||
JSAtomicPublishUnsupportedHeaderBatchErr ErrorIdentifier = 10177
|
||||
|
||||
// JSBadRequestErr bad request
|
||||
JSBadRequestErr ErrorIdentifier = 10003
|
||||
|
||||
@@ -44,9 +62,18 @@ const (
|
||||
// JSClusterUnSupportFeatureErr not currently supported in clustered mode
|
||||
JSClusterUnSupportFeatureErr ErrorIdentifier = 10036
|
||||
|
||||
// JSConsumerAckPolicyInvalidErr consumer ack policy invalid
|
||||
JSConsumerAckPolicyInvalidErr ErrorIdentifier = 10181
|
||||
|
||||
// JSConsumerAckWaitNegativeErr consumer ack wait needs to be positive
|
||||
JSConsumerAckWaitNegativeErr ErrorIdentifier = 10183
|
||||
|
||||
// JSConsumerAlreadyExists action CREATE is used for a existing consumer with a different config (consumer already exists)
|
||||
JSConsumerAlreadyExists ErrorIdentifier = 10148
|
||||
|
||||
// JSConsumerBackOffNegativeErr consumer backoff needs to be positive
|
||||
JSConsumerBackOffNegativeErr ErrorIdentifier = 10184
|
||||
|
||||
// JSConsumerBadDurableNameErr durable name can not contain '.', '*', '>'
|
||||
JSConsumerBadDurableNameErr ErrorIdentifier = 10103
|
||||
|
||||
@@ -149,8 +176,8 @@ const (
|
||||
// JSConsumerMaxRequestBatchNegativeErr consumer max request batch needs to be > 0
|
||||
JSConsumerMaxRequestBatchNegativeErr ErrorIdentifier = 10114
|
||||
|
||||
// JSConsumerMaxRequestExpiresToSmall consumer max request expires needs to be >= 1ms
|
||||
JSConsumerMaxRequestExpiresToSmall ErrorIdentifier = 10115
|
||||
// JSConsumerMaxRequestExpiresTooSmall consumer max request expires needs to be >= 1ms
|
||||
JSConsumerMaxRequestExpiresTooSmall ErrorIdentifier = 10115
|
||||
|
||||
// JSConsumerMaxWaitingNegativeErr consumer max waiting needs to be positive
|
||||
JSConsumerMaxWaitingNegativeErr ErrorIdentifier = 10087
|
||||
@@ -185,6 +212,12 @@ const (
|
||||
// JSConsumerOverlappingSubjectFilters consumer subject filters cannot overlap
|
||||
JSConsumerOverlappingSubjectFilters ErrorIdentifier = 10138
|
||||
|
||||
// JSConsumerPinnedTTLWithoutPriorityPolicyNone PinnedTTL cannot be set when PriorityPolicy is none
|
||||
JSConsumerPinnedTTLWithoutPriorityPolicyNone ErrorIdentifier = 10197
|
||||
|
||||
// JSConsumerPriorityGroupWithPolicyNone consumer can not have priority groups when policy is none
|
||||
JSConsumerPriorityGroupWithPolicyNone ErrorIdentifier = 10196
|
||||
|
||||
// JSConsumerPriorityPolicyWithoutGroup Setting PriorityPolicy requires at least one PriorityGroup to be set
|
||||
JSConsumerPriorityPolicyWithoutGroup ErrorIdentifier = 10159
|
||||
|
||||
@@ -206,6 +239,9 @@ const (
|
||||
// JSConsumerReplacementWithDifferentNameErr consumer replacement durable config not the same
|
||||
JSConsumerReplacementWithDifferentNameErr ErrorIdentifier = 10106
|
||||
|
||||
// JSConsumerReplayPolicyInvalidErr consumer replay policy invalid
|
||||
JSConsumerReplayPolicyInvalidErr ErrorIdentifier = 10182
|
||||
|
||||
// JSConsumerReplicasExceedsStream consumer config replica count exceeds parent stream
|
||||
JSConsumerReplicasExceedsStream ErrorIdentifier = 10126
|
||||
|
||||
@@ -248,6 +284,36 @@ const (
|
||||
// JSMemoryResourcesExceededErr insufficient memory resources available
|
||||
JSMemoryResourcesExceededErr ErrorIdentifier = 10028
|
||||
|
||||
// JSMessageCounterBrokenErr message counter is broken
|
||||
JSMessageCounterBrokenErr ErrorIdentifier = 10172
|
||||
|
||||
// JSMessageIncrDisabledErr message counters is disabled
|
||||
JSMessageIncrDisabledErr ErrorIdentifier = 10168
|
||||
|
||||
// JSMessageIncrInvalidErr message counter increment is invalid
|
||||
JSMessageIncrInvalidErr ErrorIdentifier = 10171
|
||||
|
||||
// JSMessageIncrMissingErr message counter increment is missing
|
||||
JSMessageIncrMissingErr ErrorIdentifier = 10169
|
||||
|
||||
// JSMessageIncrPayloadErr message counter has payload
|
||||
JSMessageIncrPayloadErr ErrorIdentifier = 10170
|
||||
|
||||
// JSMessageSchedulesDisabledErr message schedules is disabled
|
||||
JSMessageSchedulesDisabledErr ErrorIdentifier = 10188
|
||||
|
||||
// JSMessageSchedulesPatternInvalidErr message schedules pattern is invalid
|
||||
JSMessageSchedulesPatternInvalidErr ErrorIdentifier = 10189
|
||||
|
||||
// JSMessageSchedulesRollupInvalidErr message schedules invalid rollup
|
||||
JSMessageSchedulesRollupInvalidErr ErrorIdentifier = 10192
|
||||
|
||||
// JSMessageSchedulesTTLInvalidErr message schedules invalid per-message TTL
|
||||
JSMessageSchedulesTTLInvalidErr ErrorIdentifier = 10191
|
||||
|
||||
// JSMessageSchedulesTargetInvalidErr message schedules target is invalid
|
||||
JSMessageSchedulesTargetInvalidErr ErrorIdentifier = 10190
|
||||
|
||||
// JSMessageTTLDisabledErr per-message TTL is disabled
|
||||
JSMessageTTLDisabledErr ErrorIdentifier = 10166
|
||||
|
||||
@@ -275,9 +341,18 @@ const (
|
||||
// JSMirrorOverlappingSubjectFilters mirror subject filters can not overlap
|
||||
JSMirrorOverlappingSubjectFilters ErrorIdentifier = 10152
|
||||
|
||||
// JSMirrorWithAtomicPublishErr stream mirrors can not also use atomic publishing
|
||||
JSMirrorWithAtomicPublishErr ErrorIdentifier = 10198
|
||||
|
||||
// JSMirrorWithCountersErr stream mirrors can not also calculate counters
|
||||
JSMirrorWithCountersErr ErrorIdentifier = 10173
|
||||
|
||||
// JSMirrorWithFirstSeqErr stream mirrors can not have first sequence configured
|
||||
JSMirrorWithFirstSeqErr ErrorIdentifier = 10143
|
||||
|
||||
// JSMirrorWithMsgSchedulesErr stream mirrors can not also schedule messages
|
||||
JSMirrorWithMsgSchedulesErr ErrorIdentifier = 10186
|
||||
|
||||
// JSMirrorWithSourcesErr stream mirrors can not also contain other sources
|
||||
JSMirrorWithSourcesErr ErrorIdentifier = 10031
|
||||
|
||||
@@ -320,6 +395,9 @@ const (
|
||||
// JSReplicasCountCannotBeNegative replicas count cannot be negative
|
||||
JSReplicasCountCannotBeNegative ErrorIdentifier = 10133
|
||||
|
||||
// JSRequiredApiLevelErr JetStream minimum api level required
|
||||
JSRequiredApiLevelErr ErrorIdentifier = 10185
|
||||
|
||||
// JSRestoreSubscribeFailedErrF JetStream unable to subscribe to restore snapshot {subject}: {err}
|
||||
JSRestoreSubscribeFailedErrF ErrorIdentifier = 10042
|
||||
|
||||
@@ -353,6 +431,9 @@ const (
|
||||
// JSSourceOverlappingSubjectFilters source filters can not overlap
|
||||
JSSourceOverlappingSubjectFilters ErrorIdentifier = 10147
|
||||
|
||||
// JSSourceWithMsgSchedulesErr stream source can not also schedule messages
|
||||
JSSourceWithMsgSchedulesErr ErrorIdentifier = 10187
|
||||
|
||||
// JSStorageResourcesExceededErr insufficient storage resources available
|
||||
JSStorageResourcesExceededErr ErrorIdentifier = 10047
|
||||
|
||||
@@ -368,6 +449,9 @@ const (
|
||||
// JSStreamDuplicateMessageConflict duplicate message id is in process
|
||||
JSStreamDuplicateMessageConflict ErrorIdentifier = 10158
|
||||
|
||||
// JSStreamExpectedLastSeqPerSubjectInvalid missing sequence for expected last sequence per subject
|
||||
JSStreamExpectedLastSeqPerSubjectInvalid ErrorIdentifier = 10193
|
||||
|
||||
// JSStreamExpectedLastSeqPerSubjectNotReady expected last sequence per subject temporarily unavailable
|
||||
JSStreamExpectedLastSeqPerSubjectNotReady ErrorIdentifier = 10163
|
||||
|
||||
@@ -407,6 +491,9 @@ const (
|
||||
// JSStreamMessageExceedsMaximumErr message size exceeds maximum allowed
|
||||
JSStreamMessageExceedsMaximumErr ErrorIdentifier = 10054
|
||||
|
||||
// JSStreamMinLastSeqErr min last sequence
|
||||
JSStreamMinLastSeqErr ErrorIdentifier = 10180
|
||||
|
||||
// JSStreamMirrorNotUpdatableErr stream mirror configuration can not be updated
|
||||
JSStreamMirrorNotUpdatableErr ErrorIdentifier = 10055
|
||||
|
||||
@@ -515,175 +602,204 @@ const (
|
||||
|
||||
var (
|
||||
ApiErrors = map[ErrorIdentifier]*ApiError{
|
||||
JSAccountResourcesExceededErr: {Code: 400, ErrCode: 10002, Description: "resource limits exceeded for account"},
|
||||
JSBadRequestErr: {Code: 400, ErrCode: 10003, Description: "bad request"},
|
||||
JSClusterIncompleteErr: {Code: 503, ErrCode: 10004, Description: "incomplete results"},
|
||||
JSClusterNoPeersErrF: {Code: 400, ErrCode: 10005, Description: "{err}"},
|
||||
JSClusterNotActiveErr: {Code: 500, ErrCode: 10006, Description: "JetStream not in clustered mode"},
|
||||
JSClusterNotAssignedErr: {Code: 500, ErrCode: 10007, Description: "JetStream cluster not assigned to this server"},
|
||||
JSClusterNotAvailErr: {Code: 503, ErrCode: 10008, Description: "JetStream system temporarily unavailable"},
|
||||
JSClusterNotLeaderErr: {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"},
|
||||
JSClusterPeerNotMemberErr: {Code: 400, ErrCode: 10040, Description: "peer not a member"},
|
||||
JSClusterRequiredErr: {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"},
|
||||
JSClusterServerNotMemberErr: {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"},
|
||||
JSClusterTagsErr: {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"},
|
||||
JSClusterUnSupportFeatureErr: {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"},
|
||||
JSConsumerAlreadyExists: {Code: 400, ErrCode: 10148, Description: "consumer already exists"},
|
||||
JSConsumerBadDurableNameErr: {Code: 400, ErrCode: 10103, Description: "durable name can not contain '.', '*', '>'"},
|
||||
JSConsumerConfigRequiredErr: {Code: 400, ErrCode: 10078, Description: "consumer config required"},
|
||||
JSConsumerCreateDurableAndNameMismatch: {Code: 400, ErrCode: 10132, Description: "Consumer Durable and Name have to be equal if both are provided"},
|
||||
JSConsumerCreateErrF: {Code: 500, ErrCode: 10012, Description: "{err}"},
|
||||
JSConsumerCreateFilterSubjectMismatchErr: {Code: 400, ErrCode: 10131, Description: "Consumer create request did not match filtered subject from create subject"},
|
||||
JSConsumerDeliverCycleErr: {Code: 400, ErrCode: 10081, Description: "consumer deliver subject forms a cycle"},
|
||||
JSConsumerDeliverToWildcardsErr: {Code: 400, ErrCode: 10079, Description: "consumer deliver subject has wildcards"},
|
||||
JSConsumerDescriptionTooLongErrF: {Code: 400, ErrCode: 10107, Description: "consumer description is too long, maximum allowed is {max}"},
|
||||
JSConsumerDirectRequiresEphemeralErr: {Code: 400, ErrCode: 10091, Description: "consumer direct requires an ephemeral consumer"},
|
||||
JSConsumerDirectRequiresPushErr: {Code: 400, ErrCode: 10090, Description: "consumer direct requires a push based consumer"},
|
||||
JSConsumerDoesNotExist: {Code: 400, ErrCode: 10149, Description: "consumer does not exist"},
|
||||
JSConsumerDuplicateFilterSubjects: {Code: 400, ErrCode: 10136, Description: "consumer cannot have both FilterSubject and FilterSubjects specified"},
|
||||
JSConsumerDurableNameNotInSubjectErr: {Code: 400, ErrCode: 10016, Description: "consumer expected to be durable but no durable name set in subject"},
|
||||
JSConsumerDurableNameNotMatchSubjectErr: {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"},
|
||||
JSConsumerDurableNameNotSetErr: {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"},
|
||||
JSConsumerEmptyFilter: {Code: 400, ErrCode: 10139, Description: "consumer filter in FilterSubjects cannot be empty"},
|
||||
JSConsumerEmptyGroupName: {Code: 400, ErrCode: 10161, Description: "Group name cannot be an empty string"},
|
||||
JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"},
|
||||
JSConsumerEphemeralWithDurableNameErr: {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"},
|
||||
JSConsumerExistingActiveErr: {Code: 400, ErrCode: 10105, Description: "consumer already exists and is still active"},
|
||||
JSConsumerFCRequiresPushErr: {Code: 400, ErrCode: 10089, Description: "consumer flow control requires a push based consumer"},
|
||||
JSConsumerFilterNotSubsetErr: {Code: 400, ErrCode: 10093, Description: "consumer filter subject is not a valid subset of the interest subjects"},
|
||||
JSConsumerHBRequiresPushErr: {Code: 400, ErrCode: 10088, Description: "consumer idle heartbeat requires a push based consumer"},
|
||||
JSConsumerInactiveThresholdExcess: {Code: 400, ErrCode: 10153, Description: "consumer inactive threshold exceeds system limit of {limit}"},
|
||||
JSConsumerInvalidDeliverSubject: {Code: 400, ErrCode: 10112, Description: "invalid push consumer deliver subject"},
|
||||
JSConsumerInvalidGroupNameErr: {Code: 400, ErrCode: 10162, Description: "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters"},
|
||||
JSConsumerInvalidPolicyErrF: {Code: 400, ErrCode: 10094, Description: "{err}"},
|
||||
JSConsumerInvalidPriorityGroupErr: {Code: 400, ErrCode: 10160, Description: "Provided priority group does not exist for this consumer"},
|
||||
JSConsumerInvalidSamplingErrF: {Code: 400, ErrCode: 10095, Description: "failed to parse consumer sampling configuration: {err}"},
|
||||
JSConsumerMaxDeliverBackoffErr: {Code: 400, ErrCode: 10116, Description: "max deliver is required to be > length of backoff values"},
|
||||
JSConsumerMaxPendingAckExcessErrF: {Code: 400, ErrCode: 10121, Description: "consumer max ack pending exceeds system limit of {limit}"},
|
||||
JSConsumerMaxPendingAckPolicyRequiredErr: {Code: 400, ErrCode: 10082, Description: "consumer requires ack policy for max ack pending"},
|
||||
JSConsumerMaxRequestBatchExceededF: {Code: 400, ErrCode: 10125, Description: "consumer max request batch exceeds server limit of {limit}"},
|
||||
JSConsumerMaxRequestBatchNegativeErr: {Code: 400, ErrCode: 10114, Description: "consumer max request batch needs to be > 0"},
|
||||
JSConsumerMaxRequestExpiresToSmall: {Code: 400, ErrCode: 10115, Description: "consumer max request expires needs to be >= 1ms"},
|
||||
JSConsumerMaxWaitingNegativeErr: {Code: 400, ErrCode: 10087, Description: "consumer max waiting needs to be positive"},
|
||||
JSConsumerMetadataLengthErrF: {Code: 400, ErrCode: 10135, Description: "consumer metadata exceeds maximum size of {limit}"},
|
||||
JSConsumerMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10137, Description: "consumer with multiple subject filters cannot use subject based API"},
|
||||
JSConsumerNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10127, Description: "Consumer name can not contain path separators"},
|
||||
JSConsumerNameExistErr: {Code: 400, ErrCode: 10013, Description: "consumer name already in use"},
|
||||
JSConsumerNameTooLongErrF: {Code: 400, ErrCode: 10102, Description: "consumer name is too long, maximum allowed is {max}"},
|
||||
JSConsumerNotFoundErr: {Code: 404, ErrCode: 10014, Description: "consumer not found"},
|
||||
JSConsumerOfflineErr: {Code: 500, ErrCode: 10119, Description: "consumer is offline"},
|
||||
JSConsumerOfflineReasonErrF: {Code: 500, ErrCode: 10195, Description: "consumer is offline: {err}"},
|
||||
JSConsumerOnMappedErr: {Code: 400, ErrCode: 10092, Description: "consumer direct on a mapped consumer"},
|
||||
JSConsumerOverlappingSubjectFilters: {Code: 400, ErrCode: 10138, Description: "consumer subject filters cannot overlap"},
|
||||
JSConsumerPriorityPolicyWithoutGroup: {Code: 400, ErrCode: 10159, Description: "Setting PriorityPolicy requires at least one PriorityGroup to be set"},
|
||||
JSConsumerPullNotDurableErr: {Code: 400, ErrCode: 10085, Description: "consumer in pull mode requires a durable name"},
|
||||
JSConsumerPullRequiresAckErr: {Code: 400, ErrCode: 10084, Description: "consumer in pull mode requires explicit ack policy on workqueue stream"},
|
||||
JSConsumerPullWithRateLimitErr: {Code: 400, ErrCode: 10086, Description: "consumer in pull mode can not have rate limit set"},
|
||||
JSConsumerPushMaxWaitingErr: {Code: 400, ErrCode: 10080, Description: "consumer in push mode can not set max waiting"},
|
||||
JSConsumerPushWithPriorityGroupErr: {Code: 400, ErrCode: 10178, Description: "priority groups can not be used with push consumers"},
|
||||
JSConsumerReplacementWithDifferentNameErr: {Code: 400, ErrCode: 10106, Description: "consumer replacement durable config not the same"},
|
||||
JSConsumerReplicasExceedsStream: {Code: 400, ErrCode: 10126, Description: "consumer config replica count exceeds parent stream"},
|
||||
JSConsumerReplicasShouldMatchStream: {Code: 400, ErrCode: 10134, Description: "consumer config replicas must match interest retention stream's replicas"},
|
||||
JSConsumerSmallHeartbeatErr: {Code: 400, ErrCode: 10083, Description: "consumer idle heartbeat needs to be >= 100ms"},
|
||||
JSConsumerStoreFailedErrF: {Code: 500, ErrCode: 10104, Description: "error creating store for consumer: {err}"},
|
||||
JSConsumerWQConsumerNotDeliverAllErr: {Code: 400, ErrCode: 10101, Description: "consumer must be deliver all on workqueue stream"},
|
||||
JSConsumerWQConsumerNotUniqueErr: {Code: 400, ErrCode: 10100, Description: "filtered consumer not unique on workqueue stream"},
|
||||
JSConsumerWQMultipleUnfilteredErr: {Code: 400, ErrCode: 10099, Description: "multiple non-filtered consumers not allowed on workqueue stream"},
|
||||
JSConsumerWQRequiresExplicitAckErr: {Code: 400, ErrCode: 10098, Description: "workqueue stream requires explicit ack"},
|
||||
JSConsumerWithFlowControlNeedsHeartbeats: {Code: 400, ErrCode: 10108, Description: "consumer with flow control also needs heartbeats"},
|
||||
JSInsufficientResourcesErr: {Code: 503, ErrCode: 10023, Description: "insufficient resources"},
|
||||
JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON: {err}"},
|
||||
JSMaximumConsumersLimitErr: {Code: 400, ErrCode: 10026, Description: "maximum consumers limit reached"},
|
||||
JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"},
|
||||
JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"},
|
||||
JSMessageTTLDisabledErr: {Code: 400, ErrCode: 10166, Description: "per-message TTL is disabled"},
|
||||
JSMessageTTLInvalidErr: {Code: 400, ErrCode: 10165, Description: "invalid per-message TTL"},
|
||||
JSMirrorConsumerSetupFailedErrF: {Code: 500, ErrCode: 10029, Description: "{err}"},
|
||||
JSMirrorInvalidStreamName: {Code: 400, ErrCode: 10142, Description: "mirrored stream name is invalid"},
|
||||
JSMirrorInvalidSubjectFilter: {Code: 400, ErrCode: 10151, Description: "mirror transform source: {err}"},
|
||||
JSMirrorInvalidTransformDestination: {Code: 400, ErrCode: 10154, Description: "mirror transform: {err}"},
|
||||
JSMirrorMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"},
|
||||
JSMirrorMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10150, Description: "mirror with multiple subject transforms cannot also have a single subject filter"},
|
||||
JSMirrorOverlappingSubjectFilters: {Code: 400, ErrCode: 10152, Description: "mirror subject filters can not overlap"},
|
||||
JSMirrorWithFirstSeqErr: {Code: 400, ErrCode: 10143, Description: "stream mirrors can not have first sequence configured"},
|
||||
JSMirrorWithSourcesErr: {Code: 400, ErrCode: 10031, Description: "stream mirrors can not also contain other sources"},
|
||||
JSMirrorWithStartSeqAndTimeErr: {Code: 400, ErrCode: 10032, Description: "stream mirrors can not have both start seq and start time configured"},
|
||||
JSMirrorWithSubjectFiltersErr: {Code: 400, ErrCode: 10033, Description: "stream mirrors can not contain filtered subjects"},
|
||||
JSMirrorWithSubjectsErr: {Code: 400, ErrCode: 10034, Description: "stream mirrors can not contain subjects"},
|
||||
JSNoAccountErr: {Code: 503, ErrCode: 10035, Description: "account not found"},
|
||||
JSNoLimitsErr: {Code: 400, ErrCode: 10120, Description: "no JetStream default or applicable tiered limit present"},
|
||||
JSNoMessageFoundErr: {Code: 404, ErrCode: 10037, Description: "no message found"},
|
||||
JSNotEmptyRequestErr: {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"},
|
||||
JSNotEnabledErr: {Code: 503, ErrCode: 10076, Description: "JetStream not enabled"},
|
||||
JSNotEnabledForAccountErr: {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account"},
|
||||
JSPedanticErrF: {Code: 400, ErrCode: 10157, Description: "pedantic mode: {err}"},
|
||||
JSPeerRemapErr: {Code: 503, ErrCode: 10075, Description: "peer remap failed"},
|
||||
JSRaftGeneralErrF: {Code: 500, ErrCode: 10041, Description: "{err}"},
|
||||
JSReplicasCountCannotBeNegative: {Code: 400, ErrCode: 10133, Description: "replicas count cannot be negative"},
|
||||
JSRestoreSubscribeFailedErrF: {Code: 500, ErrCode: 10042, Description: "JetStream unable to subscribe to restore snapshot {subject}: {err}"},
|
||||
JSSequenceNotFoundErrF: {Code: 400, ErrCode: 10043, Description: "sequence {seq} not found"},
|
||||
JSSnapshotDeliverSubjectInvalidErr: {Code: 400, ErrCode: 10015, Description: "deliver subject not valid"},
|
||||
JSSourceConsumerSetupFailedErrF: {Code: 500, ErrCode: 10045, Description: "{err}"},
|
||||
JSSourceDuplicateDetected: {Code: 400, ErrCode: 10140, Description: "duplicate source configuration detected"},
|
||||
JSSourceInvalidStreamName: {Code: 400, ErrCode: 10141, Description: "sourced stream name is invalid"},
|
||||
JSSourceInvalidSubjectFilter: {Code: 400, ErrCode: 10145, Description: "source transform source: {err}"},
|
||||
JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform: {err}"},
|
||||
JSSourceMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"},
|
||||
JSSourceMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10144, Description: "source with multiple subject transforms cannot also have a single subject filter"},
|
||||
JSSourceOverlappingSubjectFilters: {Code: 400, ErrCode: 10147, Description: "source filters can not overlap"},
|
||||
JSStorageResourcesExceededErr: {Code: 500, ErrCode: 10047, Description: "insufficient storage resources available"},
|
||||
JSStreamAssignmentErrF: {Code: 500, ErrCode: 10048, Description: "{err}"},
|
||||
JSStreamCreateErrF: {Code: 500, ErrCode: 10049, Description: "{err}"},
|
||||
JSStreamDeleteErrF: {Code: 500, ErrCode: 10050, Description: "{err}"},
|
||||
JSStreamDuplicateMessageConflict: {Code: 409, ErrCode: 10158, Description: "duplicate message id is in process"},
|
||||
JSStreamExpectedLastSeqPerSubjectNotReady: {Code: 503, ErrCode: 10163, Description: "expected last sequence per subject temporarily unavailable"},
|
||||
JSStreamExternalApiOverlapErrF: {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"},
|
||||
JSStreamExternalDelPrefixOverlapsErrF: {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"},
|
||||
JSStreamGeneralErrorF: {Code: 500, ErrCode: 10051, Description: "{err}"},
|
||||
JSStreamHeaderExceedsMaximumErr: {Code: 400, ErrCode: 10097, Description: "header size exceeds maximum allowed of 64k"},
|
||||
JSStreamInfoMaxSubjectsErr: {Code: 500, ErrCode: 10117, Description: "subject details would exceed maximum allowed"},
|
||||
JSStreamInvalidConfigF: {Code: 500, ErrCode: 10052, Description: "{err}"},
|
||||
JSStreamInvalidErr: {Code: 500, ErrCode: 10096, Description: "stream not valid"},
|
||||
JSStreamInvalidExternalDeliverySubjErrF: {Code: 400, ErrCode: 10024, Description: "stream external delivery prefix {prefix} must not contain wildcards"},
|
||||
JSStreamLimitsErrF: {Code: 500, ErrCode: 10053, Description: "{err}"},
|
||||
JSStreamMaxBytesRequired: {Code: 400, ErrCode: 10113, Description: "account requires a stream config to have max bytes set"},
|
||||
JSStreamMaxStreamBytesExceeded: {Code: 400, ErrCode: 10122, Description: "stream max bytes exceeds account limit max stream bytes"},
|
||||
JSStreamMessageExceedsMaximumErr: {Code: 400, ErrCode: 10054, Description: "message size exceeds maximum allowed"},
|
||||
JSStreamMirrorNotUpdatableErr: {Code: 400, ErrCode: 10055, Description: "stream mirror configuration can not be updated"},
|
||||
JSStreamMismatchErr: {Code: 400, ErrCode: 10056, Description: "stream name in subject does not match request"},
|
||||
JSStreamMoveAndScaleErr: {Code: 400, ErrCode: 10123, Description: "can not move and scale a stream in a single update"},
|
||||
JSStreamMoveInProgressF: {Code: 400, ErrCode: 10124, Description: "stream move already in progress: {msg}"},
|
||||
JSStreamMoveNotInProgress: {Code: 400, ErrCode: 10129, Description: "stream move not in progress"},
|
||||
JSStreamMsgDeleteFailedF: {Code: 500, ErrCode: 10057, Description: "{err}"},
|
||||
JSStreamNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10128, Description: "Stream name can not contain path separators"},
|
||||
JSStreamNameExistErr: {Code: 400, ErrCode: 10058, Description: "stream name already in use with a different configuration"},
|
||||
JSStreamNameExistRestoreFailedErr: {Code: 400, ErrCode: 10130, Description: "stream name already in use, cannot restore"},
|
||||
JSStreamNotFoundErr: {Code: 404, ErrCode: 10059, Description: "stream not found"},
|
||||
JSStreamNotMatchErr: {Code: 400, ErrCode: 10060, Description: "expected stream does not match"},
|
||||
JSStreamOfflineErr: {Code: 500, ErrCode: 10118, Description: "stream is offline"},
|
||||
JSStreamOfflineReasonErrF: {Code: 500, ErrCode: 10194, Description: "stream is offline: {err}"},
|
||||
JSStreamPurgeFailedF: {Code: 500, ErrCode: 10110, Description: "{err}"},
|
||||
JSStreamReplicasNotSupportedErr: {Code: 500, ErrCode: 10074, Description: "replicas > 1 not supported in non-clustered mode"},
|
||||
JSStreamReplicasNotUpdatableErr: {Code: 400, ErrCode: 10061, Description: "Replicas configuration can not be updated"},
|
||||
JSStreamRestoreErrF: {Code: 500, ErrCode: 10062, Description: "restore failed: {err}"},
|
||||
JSStreamRollupFailedF: {Code: 500, ErrCode: 10111, Description: "{err}"},
|
||||
JSStreamSealedErr: {Code: 400, ErrCode: 10109, Description: "invalid operation on sealed stream"},
|
||||
JSStreamSequenceNotMatchErr: {Code: 503, ErrCode: 10063, Description: "expected stream sequence does not match"},
|
||||
JSStreamSnapshotErrF: {Code: 500, ErrCode: 10064, Description: "snapshot failed: {err}"},
|
||||
JSStreamStoreFailedF: {Code: 503, ErrCode: 10077, Description: "{err}"},
|
||||
JSStreamSubjectOverlapErr: {Code: 400, ErrCode: 10065, Description: "subjects overlap with an existing stream"},
|
||||
JSStreamTemplateCreateErrF: {Code: 500, ErrCode: 10066, Description: "{err}"},
|
||||
JSStreamTemplateDeleteErrF: {Code: 500, ErrCode: 10067, Description: "{err}"},
|
||||
JSStreamTemplateNotFoundErr: {Code: 404, ErrCode: 10068, Description: "template not found"},
|
||||
JSStreamTooManyRequests: {Code: 429, ErrCode: 10167, Description: "too many requests"},
|
||||
JSStreamTransformInvalidDestination: {Code: 400, ErrCode: 10156, Description: "stream transform: {err}"},
|
||||
JSStreamTransformInvalidSource: {Code: 400, ErrCode: 10155, Description: "stream transform source: {err}"},
|
||||
JSStreamUpdateErrF: {Code: 500, ErrCode: 10069, Description: "{err}"},
|
||||
JSStreamWrongLastMsgIDErrF: {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"},
|
||||
JSStreamWrongLastSequenceConstantErr: {Code: 400, ErrCode: 10164, Description: "wrong last sequence"},
|
||||
JSStreamWrongLastSequenceErrF: {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"},
|
||||
JSTempStorageFailedErr: {Code: 500, ErrCode: 10072, Description: "JetStream unable to open temp storage for restore"},
|
||||
JSTemplateNameNotMatchSubjectErr: {Code: 400, ErrCode: 10073, Description: "template name in subject does not match request"},
|
||||
JSAccountResourcesExceededErr: {Code: 400, ErrCode: 10002, Description: "resource limits exceeded for account"},
|
||||
JSAtomicPublishDisabledErr: {Code: 400, ErrCode: 10174, Description: "atomic publish is disabled"},
|
||||
JSAtomicPublishIncompleteBatchErr: {Code: 400, ErrCode: 10176, Description: "atomic publish batch is incomplete"},
|
||||
JSAtomicPublishInvalidBatchIDErr: {Code: 400, ErrCode: 10179, Description: "atomic publish batch ID is invalid"},
|
||||
JSAtomicPublishMissingSeqErr: {Code: 400, ErrCode: 10175, Description: "atomic publish sequence is missing"},
|
||||
JSAtomicPublishTooLargeBatchErrF: {Code: 400, ErrCode: 10199, Description: "atomic publish batch is too large: {size}"},
|
||||
JSAtomicPublishUnsupportedHeaderBatchErr: {Code: 400, ErrCode: 10177, Description: "atomic publish unsupported header used: {header}"},
|
||||
JSBadRequestErr: {Code: 400, ErrCode: 10003, Description: "bad request"},
|
||||
JSClusterIncompleteErr: {Code: 503, ErrCode: 10004, Description: "incomplete results"},
|
||||
JSClusterNoPeersErrF: {Code: 400, ErrCode: 10005, Description: "{err}"},
|
||||
JSClusterNotActiveErr: {Code: 500, ErrCode: 10006, Description: "JetStream not in clustered mode"},
|
||||
JSClusterNotAssignedErr: {Code: 500, ErrCode: 10007, Description: "JetStream cluster not assigned to this server"},
|
||||
JSClusterNotAvailErr: {Code: 503, ErrCode: 10008, Description: "JetStream system temporarily unavailable"},
|
||||
JSClusterNotLeaderErr: {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"},
|
||||
JSClusterPeerNotMemberErr: {Code: 400, ErrCode: 10040, Description: "peer not a member"},
|
||||
JSClusterRequiredErr: {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"},
|
||||
JSClusterServerNotMemberErr: {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"},
|
||||
JSClusterTagsErr: {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"},
|
||||
JSClusterUnSupportFeatureErr: {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"},
|
||||
JSConsumerAckPolicyInvalidErr: {Code: 400, ErrCode: 10181, Description: "consumer ack policy invalid"},
|
||||
JSConsumerAckWaitNegativeErr: {Code: 400, ErrCode: 10183, Description: "consumer ack wait needs to be positive"},
|
||||
JSConsumerAlreadyExists: {Code: 400, ErrCode: 10148, Description: "consumer already exists"},
|
||||
JSConsumerBackOffNegativeErr: {Code: 400, ErrCode: 10184, Description: "consumer backoff needs to be positive"},
|
||||
JSConsumerBadDurableNameErr: {Code: 400, ErrCode: 10103, Description: "durable name can not contain '.', '*', '>'"},
|
||||
JSConsumerConfigRequiredErr: {Code: 400, ErrCode: 10078, Description: "consumer config required"},
|
||||
JSConsumerCreateDurableAndNameMismatch: {Code: 400, ErrCode: 10132, Description: "Consumer Durable and Name have to be equal if both are provided"},
|
||||
JSConsumerCreateErrF: {Code: 500, ErrCode: 10012, Description: "{err}"},
|
||||
JSConsumerCreateFilterSubjectMismatchErr: {Code: 400, ErrCode: 10131, Description: "Consumer create request did not match filtered subject from create subject"},
|
||||
JSConsumerDeliverCycleErr: {Code: 400, ErrCode: 10081, Description: "consumer deliver subject forms a cycle"},
|
||||
JSConsumerDeliverToWildcardsErr: {Code: 400, ErrCode: 10079, Description: "consumer deliver subject has wildcards"},
|
||||
JSConsumerDescriptionTooLongErrF: {Code: 400, ErrCode: 10107, Description: "consumer description is too long, maximum allowed is {max}"},
|
||||
JSConsumerDirectRequiresEphemeralErr: {Code: 400, ErrCode: 10091, Description: "consumer direct requires an ephemeral consumer"},
|
||||
JSConsumerDirectRequiresPushErr: {Code: 400, ErrCode: 10090, Description: "consumer direct requires a push based consumer"},
|
||||
JSConsumerDoesNotExist: {Code: 400, ErrCode: 10149, Description: "consumer does not exist"},
|
||||
JSConsumerDuplicateFilterSubjects: {Code: 400, ErrCode: 10136, Description: "consumer cannot have both FilterSubject and FilterSubjects specified"},
|
||||
JSConsumerDurableNameNotInSubjectErr: {Code: 400, ErrCode: 10016, Description: "consumer expected to be durable but no durable name set in subject"},
|
||||
JSConsumerDurableNameNotMatchSubjectErr: {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"},
|
||||
JSConsumerDurableNameNotSetErr: {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"},
|
||||
JSConsumerEmptyFilter: {Code: 400, ErrCode: 10139, Description: "consumer filter in FilterSubjects cannot be empty"},
|
||||
JSConsumerEmptyGroupName: {Code: 400, ErrCode: 10161, Description: "Group name cannot be an empty string"},
|
||||
JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"},
|
||||
JSConsumerEphemeralWithDurableNameErr: {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"},
|
||||
JSConsumerExistingActiveErr: {Code: 400, ErrCode: 10105, Description: "consumer already exists and is still active"},
|
||||
JSConsumerFCRequiresPushErr: {Code: 400, ErrCode: 10089, Description: "consumer flow control requires a push based consumer"},
|
||||
JSConsumerFilterNotSubsetErr: {Code: 400, ErrCode: 10093, Description: "consumer filter subject is not a valid subset of the interest subjects"},
|
||||
JSConsumerHBRequiresPushErr: {Code: 400, ErrCode: 10088, Description: "consumer idle heartbeat requires a push based consumer"},
|
||||
JSConsumerInactiveThresholdExcess: {Code: 400, ErrCode: 10153, Description: "consumer inactive threshold exceeds system limit of {limit}"},
|
||||
JSConsumerInvalidDeliverSubject: {Code: 400, ErrCode: 10112, Description: "invalid push consumer deliver subject"},
|
||||
JSConsumerInvalidGroupNameErr: {Code: 400, ErrCode: 10162, Description: "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters"},
|
||||
JSConsumerInvalidPolicyErrF: {Code: 400, ErrCode: 10094, Description: "{err}"},
|
||||
JSConsumerInvalidPriorityGroupErr: {Code: 400, ErrCode: 10160, Description: "Provided priority group does not exist for this consumer"},
|
||||
JSConsumerInvalidSamplingErrF: {Code: 400, ErrCode: 10095, Description: "failed to parse consumer sampling configuration: {err}"},
|
||||
JSConsumerMaxDeliverBackoffErr: {Code: 400, ErrCode: 10116, Description: "max deliver is required to be > length of backoff values"},
|
||||
JSConsumerMaxPendingAckExcessErrF: {Code: 400, ErrCode: 10121, Description: "consumer max ack pending exceeds system limit of {limit}"},
|
||||
JSConsumerMaxPendingAckPolicyRequiredErr: {Code: 400, ErrCode: 10082, Description: "consumer requires ack policy for max ack pending"},
|
||||
JSConsumerMaxRequestBatchExceededF: {Code: 400, ErrCode: 10125, Description: "consumer max request batch exceeds server limit of {limit}"},
|
||||
JSConsumerMaxRequestBatchNegativeErr: {Code: 400, ErrCode: 10114, Description: "consumer max request batch needs to be > 0"},
|
||||
JSConsumerMaxRequestExpiresTooSmall: {Code: 400, ErrCode: 10115, Description: "consumer max request expires needs to be >= 1ms"},
|
||||
JSConsumerMaxWaitingNegativeErr: {Code: 400, ErrCode: 10087, Description: "consumer max waiting needs to be positive"},
|
||||
JSConsumerMetadataLengthErrF: {Code: 400, ErrCode: 10135, Description: "consumer metadata exceeds maximum size of {limit}"},
|
||||
JSConsumerMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10137, Description: "consumer with multiple subject filters cannot use subject based API"},
|
||||
JSConsumerNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10127, Description: "Consumer name can not contain path separators"},
|
||||
JSConsumerNameExistErr: {Code: 400, ErrCode: 10013, Description: "consumer name already in use"},
|
||||
JSConsumerNameTooLongErrF: {Code: 400, ErrCode: 10102, Description: "consumer name is too long, maximum allowed is {max}"},
|
||||
JSConsumerNotFoundErr: {Code: 404, ErrCode: 10014, Description: "consumer not found"},
|
||||
JSConsumerOfflineErr: {Code: 500, ErrCode: 10119, Description: "consumer is offline"},
|
||||
JSConsumerOfflineReasonErrF: {Code: 500, ErrCode: 10195, Description: "consumer is offline: {err}"},
|
||||
JSConsumerOnMappedErr: {Code: 400, ErrCode: 10092, Description: "consumer direct on a mapped consumer"},
|
||||
JSConsumerOverlappingSubjectFilters: {Code: 400, ErrCode: 10138, Description: "consumer subject filters cannot overlap"},
|
||||
JSConsumerPinnedTTLWithoutPriorityPolicyNone: {Code: 400, ErrCode: 10197, Description: "PinnedTTL cannot be set when PriorityPolicy is none"},
|
||||
JSConsumerPriorityGroupWithPolicyNone: {Code: 400, ErrCode: 10196, Description: "consumer can not have priority groups when policy is none"},
|
||||
JSConsumerPriorityPolicyWithoutGroup: {Code: 400, ErrCode: 10159, Description: "Setting PriorityPolicy requires at least one PriorityGroup to be set"},
|
||||
JSConsumerPullNotDurableErr: {Code: 400, ErrCode: 10085, Description: "consumer in pull mode requires a durable name"},
|
||||
JSConsumerPullRequiresAckErr: {Code: 400, ErrCode: 10084, Description: "consumer in pull mode requires explicit ack policy on workqueue stream"},
|
||||
JSConsumerPullWithRateLimitErr: {Code: 400, ErrCode: 10086, Description: "consumer in pull mode can not have rate limit set"},
|
||||
JSConsumerPushMaxWaitingErr: {Code: 400, ErrCode: 10080, Description: "consumer in push mode can not set max waiting"},
|
||||
JSConsumerPushWithPriorityGroupErr: {Code: 400, ErrCode: 10178, Description: "priority groups can not be used with push consumers"},
|
||||
JSConsumerReplacementWithDifferentNameErr: {Code: 400, ErrCode: 10106, Description: "consumer replacement durable config not the same"},
|
||||
JSConsumerReplayPolicyInvalidErr: {Code: 400, ErrCode: 10182, Description: "consumer replay policy invalid"},
|
||||
JSConsumerReplicasExceedsStream: {Code: 400, ErrCode: 10126, Description: "consumer config replica count exceeds parent stream"},
|
||||
JSConsumerReplicasShouldMatchStream: {Code: 400, ErrCode: 10134, Description: "consumer config replicas must match interest retention stream's replicas"},
|
||||
JSConsumerSmallHeartbeatErr: {Code: 400, ErrCode: 10083, Description: "consumer idle heartbeat needs to be >= 100ms"},
|
||||
JSConsumerStoreFailedErrF: {Code: 500, ErrCode: 10104, Description: "error creating store for consumer: {err}"},
|
||||
JSConsumerWQConsumerNotDeliverAllErr: {Code: 400, ErrCode: 10101, Description: "consumer must be deliver all on workqueue stream"},
|
||||
JSConsumerWQConsumerNotUniqueErr: {Code: 400, ErrCode: 10100, Description: "filtered consumer not unique on workqueue stream"},
|
||||
JSConsumerWQMultipleUnfilteredErr: {Code: 400, ErrCode: 10099, Description: "multiple non-filtered consumers not allowed on workqueue stream"},
|
||||
JSConsumerWQRequiresExplicitAckErr: {Code: 400, ErrCode: 10098, Description: "workqueue stream requires explicit ack"},
|
||||
JSConsumerWithFlowControlNeedsHeartbeats: {Code: 400, ErrCode: 10108, Description: "consumer with flow control also needs heartbeats"},
|
||||
JSInsufficientResourcesErr: {Code: 503, ErrCode: 10023, Description: "insufficient resources"},
|
||||
JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON: {err}"},
|
||||
JSMaximumConsumersLimitErr: {Code: 400, ErrCode: 10026, Description: "maximum consumers limit reached"},
|
||||
JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"},
|
||||
JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"},
|
||||
JSMessageCounterBrokenErr: {Code: 400, ErrCode: 10172, Description: "message counter is broken"},
|
||||
JSMessageIncrDisabledErr: {Code: 400, ErrCode: 10168, Description: "message counters is disabled"},
|
||||
JSMessageIncrInvalidErr: {Code: 400, ErrCode: 10171, Description: "message counter increment is invalid"},
|
||||
JSMessageIncrMissingErr: {Code: 400, ErrCode: 10169, Description: "message counter increment is missing"},
|
||||
JSMessageIncrPayloadErr: {Code: 400, ErrCode: 10170, Description: "message counter has payload"},
|
||||
JSMessageSchedulesDisabledErr: {Code: 400, ErrCode: 10188, Description: "message schedules is disabled"},
|
||||
JSMessageSchedulesPatternInvalidErr: {Code: 400, ErrCode: 10189, Description: "message schedules pattern is invalid"},
|
||||
JSMessageSchedulesRollupInvalidErr: {Code: 400, ErrCode: 10192, Description: "message schedules invalid rollup"},
|
||||
JSMessageSchedulesTTLInvalidErr: {Code: 400, ErrCode: 10191, Description: "message schedules invalid per-message TTL"},
|
||||
JSMessageSchedulesTargetInvalidErr: {Code: 400, ErrCode: 10190, Description: "message schedules target is invalid"},
|
||||
JSMessageTTLDisabledErr: {Code: 400, ErrCode: 10166, Description: "per-message TTL is disabled"},
|
||||
JSMessageTTLInvalidErr: {Code: 400, ErrCode: 10165, Description: "invalid per-message TTL"},
|
||||
JSMirrorConsumerSetupFailedErrF: {Code: 500, ErrCode: 10029, Description: "{err}"},
|
||||
JSMirrorInvalidStreamName: {Code: 400, ErrCode: 10142, Description: "mirrored stream name is invalid"},
|
||||
JSMirrorInvalidSubjectFilter: {Code: 400, ErrCode: 10151, Description: "mirror transform source: {err}"},
|
||||
JSMirrorInvalidTransformDestination: {Code: 400, ErrCode: 10154, Description: "mirror transform: {err}"},
|
||||
JSMirrorMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"},
|
||||
JSMirrorMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10150, Description: "mirror with multiple subject transforms cannot also have a single subject filter"},
|
||||
JSMirrorOverlappingSubjectFilters: {Code: 400, ErrCode: 10152, Description: "mirror subject filters can not overlap"},
|
||||
JSMirrorWithAtomicPublishErr: {Code: 400, ErrCode: 10198, Description: "stream mirrors can not also use atomic publishing"},
|
||||
JSMirrorWithCountersErr: {Code: 400, ErrCode: 10173, Description: "stream mirrors can not also calculate counters"},
|
||||
JSMirrorWithFirstSeqErr: {Code: 400, ErrCode: 10143, Description: "stream mirrors can not have first sequence configured"},
|
||||
JSMirrorWithMsgSchedulesErr: {Code: 400, ErrCode: 10186, Description: "stream mirrors can not also schedule messages"},
|
||||
JSMirrorWithSourcesErr: {Code: 400, ErrCode: 10031, Description: "stream mirrors can not also contain other sources"},
|
||||
JSMirrorWithStartSeqAndTimeErr: {Code: 400, ErrCode: 10032, Description: "stream mirrors can not have both start seq and start time configured"},
|
||||
JSMirrorWithSubjectFiltersErr: {Code: 400, ErrCode: 10033, Description: "stream mirrors can not contain filtered subjects"},
|
||||
JSMirrorWithSubjectsErr: {Code: 400, ErrCode: 10034, Description: "stream mirrors can not contain subjects"},
|
||||
JSNoAccountErr: {Code: 503, ErrCode: 10035, Description: "account not found"},
|
||||
JSNoLimitsErr: {Code: 400, ErrCode: 10120, Description: "no JetStream default or applicable tiered limit present"},
|
||||
JSNoMessageFoundErr: {Code: 404, ErrCode: 10037, Description: "no message found"},
|
||||
JSNotEmptyRequestErr: {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"},
|
||||
JSNotEnabledErr: {Code: 503, ErrCode: 10076, Description: "JetStream not enabled"},
|
||||
JSNotEnabledForAccountErr: {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account"},
|
||||
JSPedanticErrF: {Code: 400, ErrCode: 10157, Description: "pedantic mode: {err}"},
|
||||
JSPeerRemapErr: {Code: 503, ErrCode: 10075, Description: "peer remap failed"},
|
||||
JSRaftGeneralErrF: {Code: 500, ErrCode: 10041, Description: "{err}"},
|
||||
JSReplicasCountCannotBeNegative: {Code: 400, ErrCode: 10133, Description: "replicas count cannot be negative"},
|
||||
JSRequiredApiLevelErr: {Code: 412, ErrCode: 10185, Description: "JetStream minimum api level required"},
|
||||
JSRestoreSubscribeFailedErrF: {Code: 500, ErrCode: 10042, Description: "JetStream unable to subscribe to restore snapshot {subject}: {err}"},
|
||||
JSSequenceNotFoundErrF: {Code: 400, ErrCode: 10043, Description: "sequence {seq} not found"},
|
||||
JSSnapshotDeliverSubjectInvalidErr: {Code: 400, ErrCode: 10015, Description: "deliver subject not valid"},
|
||||
JSSourceConsumerSetupFailedErrF: {Code: 500, ErrCode: 10045, Description: "{err}"},
|
||||
JSSourceDuplicateDetected: {Code: 400, ErrCode: 10140, Description: "duplicate source configuration detected"},
|
||||
JSSourceInvalidStreamName: {Code: 400, ErrCode: 10141, Description: "sourced stream name is invalid"},
|
||||
JSSourceInvalidSubjectFilter: {Code: 400, ErrCode: 10145, Description: "source transform source: {err}"},
|
||||
JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform: {err}"},
|
||||
JSSourceMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"},
|
||||
JSSourceMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10144, Description: "source with multiple subject transforms cannot also have a single subject filter"},
|
||||
JSSourceOverlappingSubjectFilters: {Code: 400, ErrCode: 10147, Description: "source filters can not overlap"},
|
||||
JSSourceWithMsgSchedulesErr: {Code: 400, ErrCode: 10187, Description: "stream source can not also schedule messages"},
|
||||
JSStorageResourcesExceededErr: {Code: 500, ErrCode: 10047, Description: "insufficient storage resources available"},
|
||||
JSStreamAssignmentErrF: {Code: 500, ErrCode: 10048, Description: "{err}"},
|
||||
JSStreamCreateErrF: {Code: 500, ErrCode: 10049, Description: "{err}"},
|
||||
JSStreamDeleteErrF: {Code: 500, ErrCode: 10050, Description: "{err}"},
|
||||
JSStreamDuplicateMessageConflict: {Code: 409, ErrCode: 10158, Description: "duplicate message id is in process"},
|
||||
JSStreamExpectedLastSeqPerSubjectInvalid: {Code: 400, ErrCode: 10193, Description: "missing sequence for expected last sequence per subject"},
|
||||
JSStreamExpectedLastSeqPerSubjectNotReady: {Code: 503, ErrCode: 10163, Description: "expected last sequence per subject temporarily unavailable"},
|
||||
JSStreamExternalApiOverlapErrF: {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"},
|
||||
JSStreamExternalDelPrefixOverlapsErrF: {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"},
|
||||
JSStreamGeneralErrorF: {Code: 500, ErrCode: 10051, Description: "{err}"},
|
||||
JSStreamHeaderExceedsMaximumErr: {Code: 400, ErrCode: 10097, Description: "header size exceeds maximum allowed of 64k"},
|
||||
JSStreamInfoMaxSubjectsErr: {Code: 500, ErrCode: 10117, Description: "subject details would exceed maximum allowed"},
|
||||
JSStreamInvalidConfigF: {Code: 500, ErrCode: 10052, Description: "{err}"},
|
||||
JSStreamInvalidErr: {Code: 500, ErrCode: 10096, Description: "stream not valid"},
|
||||
JSStreamInvalidExternalDeliverySubjErrF: {Code: 400, ErrCode: 10024, Description: "stream external delivery prefix {prefix} must not contain wildcards"},
|
||||
JSStreamLimitsErrF: {Code: 500, ErrCode: 10053, Description: "{err}"},
|
||||
JSStreamMaxBytesRequired: {Code: 400, ErrCode: 10113, Description: "account requires a stream config to have max bytes set"},
|
||||
JSStreamMaxStreamBytesExceeded: {Code: 400, ErrCode: 10122, Description: "stream max bytes exceeds account limit max stream bytes"},
|
||||
JSStreamMessageExceedsMaximumErr: {Code: 400, ErrCode: 10054, Description: "message size exceeds maximum allowed"},
|
||||
JSStreamMinLastSeqErr: {Code: 412, ErrCode: 10180, Description: "min last sequence"},
|
||||
JSStreamMirrorNotUpdatableErr: {Code: 400, ErrCode: 10055, Description: "stream mirror configuration can not be updated"},
|
||||
JSStreamMismatchErr: {Code: 400, ErrCode: 10056, Description: "stream name in subject does not match request"},
|
||||
JSStreamMoveAndScaleErr: {Code: 400, ErrCode: 10123, Description: "can not move and scale a stream in a single update"},
|
||||
JSStreamMoveInProgressF: {Code: 400, ErrCode: 10124, Description: "stream move already in progress: {msg}"},
|
||||
JSStreamMoveNotInProgress: {Code: 400, ErrCode: 10129, Description: "stream move not in progress"},
|
||||
JSStreamMsgDeleteFailedF: {Code: 500, ErrCode: 10057, Description: "{err}"},
|
||||
JSStreamNameContainsPathSeparatorsErr: {Code: 400, ErrCode: 10128, Description: "Stream name can not contain path separators"},
|
||||
JSStreamNameExistErr: {Code: 400, ErrCode: 10058, Description: "stream name already in use with a different configuration"},
|
||||
JSStreamNameExistRestoreFailedErr: {Code: 400, ErrCode: 10130, Description: "stream name already in use, cannot restore"},
|
||||
JSStreamNotFoundErr: {Code: 404, ErrCode: 10059, Description: "stream not found"},
|
||||
JSStreamNotMatchErr: {Code: 400, ErrCode: 10060, Description: "expected stream does not match"},
|
||||
JSStreamOfflineErr: {Code: 500, ErrCode: 10118, Description: "stream is offline"},
|
||||
JSStreamOfflineReasonErrF: {Code: 500, ErrCode: 10194, Description: "stream is offline: {err}"},
|
||||
JSStreamPurgeFailedF: {Code: 500, ErrCode: 10110, Description: "{err}"},
|
||||
JSStreamReplicasNotSupportedErr: {Code: 500, ErrCode: 10074, Description: "replicas > 1 not supported in non-clustered mode"},
|
||||
JSStreamReplicasNotUpdatableErr: {Code: 400, ErrCode: 10061, Description: "Replicas configuration can not be updated"},
|
||||
JSStreamRestoreErrF: {Code: 500, ErrCode: 10062, Description: "restore failed: {err}"},
|
||||
JSStreamRollupFailedF: {Code: 500, ErrCode: 10111, Description: "{err}"},
|
||||
JSStreamSealedErr: {Code: 400, ErrCode: 10109, Description: "invalid operation on sealed stream"},
|
||||
JSStreamSequenceNotMatchErr: {Code: 503, ErrCode: 10063, Description: "expected stream sequence does not match"},
|
||||
JSStreamSnapshotErrF: {Code: 500, ErrCode: 10064, Description: "snapshot failed: {err}"},
|
||||
JSStreamStoreFailedF: {Code: 503, ErrCode: 10077, Description: "{err}"},
|
||||
JSStreamSubjectOverlapErr: {Code: 400, ErrCode: 10065, Description: "subjects overlap with an existing stream"},
|
||||
JSStreamTemplateCreateErrF: {Code: 500, ErrCode: 10066, Description: "{err}"},
|
||||
JSStreamTemplateDeleteErrF: {Code: 500, ErrCode: 10067, Description: "{err}"},
|
||||
JSStreamTemplateNotFoundErr: {Code: 404, ErrCode: 10068, Description: "template not found"},
|
||||
JSStreamTooManyRequests: {Code: 429, ErrCode: 10167, Description: "too many requests"},
|
||||
JSStreamTransformInvalidDestination: {Code: 400, ErrCode: 10156, Description: "stream transform: {err}"},
|
||||
JSStreamTransformInvalidSource: {Code: 400, ErrCode: 10155, Description: "stream transform source: {err}"},
|
||||
JSStreamUpdateErrF: {Code: 500, ErrCode: 10069, Description: "{err}"},
|
||||
JSStreamWrongLastMsgIDErrF: {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"},
|
||||
JSStreamWrongLastSequenceConstantErr: {Code: 400, ErrCode: 10164, Description: "wrong last sequence"},
|
||||
JSStreamWrongLastSequenceErrF: {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"},
|
||||
JSTempStorageFailedErr: {Code: 500, ErrCode: 10072, Description: "JetStream unable to open temp storage for restore"},
|
||||
JSTemplateNameNotMatchSubjectErr: {Code: 400, ErrCode: 10073, Description: "template name in subject does not match request"},
|
||||
}
|
||||
// ErrJetStreamNotClustered Deprecated by JSClusterNotActiveErr ApiError, use IsNatsError() for comparisons
|
||||
ErrJetStreamNotClustered = ApiErrors[JSClusterNotActiveErr]
|
||||
@@ -719,6 +835,78 @@ func NewJSAccountResourcesExceededError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSAccountResourcesExceededErr]
|
||||
}
|
||||
|
||||
// NewJSAtomicPublishDisabledError creates a new JSAtomicPublishDisabledErr error: "atomic publish is disabled"
|
||||
func NewJSAtomicPublishDisabledError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSAtomicPublishDisabledErr]
|
||||
}
|
||||
|
||||
// NewJSAtomicPublishIncompleteBatchError creates a new JSAtomicPublishIncompleteBatchErr error: "atomic publish batch is incomplete"
|
||||
func NewJSAtomicPublishIncompleteBatchError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSAtomicPublishIncompleteBatchErr]
|
||||
}
|
||||
|
||||
// NewJSAtomicPublishInvalidBatchIDError creates a new JSAtomicPublishInvalidBatchIDErr error: "atomic publish batch ID is invalid"
|
||||
func NewJSAtomicPublishInvalidBatchIDError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSAtomicPublishInvalidBatchIDErr]
|
||||
}
|
||||
|
||||
// NewJSAtomicPublishMissingSeqError creates a new JSAtomicPublishMissingSeqErr error: "atomic publish sequence is missing"
|
||||
func NewJSAtomicPublishMissingSeqError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSAtomicPublishMissingSeqErr]
|
||||
}
|
||||
|
||||
// NewJSAtomicPublishTooLargeBatchError creates a new JSAtomicPublishTooLargeBatchErrF error: "atomic publish batch is too large: {size}"
|
||||
func NewJSAtomicPublishTooLargeBatchError(size interface{}, opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
e := ApiErrors[JSAtomicPublishTooLargeBatchErrF]
|
||||
args := e.toReplacerArgs([]interface{}{"{size}", size})
|
||||
return &ApiError{
|
||||
Code: e.Code,
|
||||
ErrCode: e.ErrCode,
|
||||
Description: strings.NewReplacer(args...).Replace(e.Description),
|
||||
}
|
||||
}
|
||||
|
||||
// NewJSAtomicPublishUnsupportedHeaderBatchError creates a new JSAtomicPublishUnsupportedHeaderBatchErr error: "atomic publish unsupported header used: {header}"
|
||||
func NewJSAtomicPublishUnsupportedHeaderBatchError(header interface{}, opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
e := ApiErrors[JSAtomicPublishUnsupportedHeaderBatchErr]
|
||||
args := e.toReplacerArgs([]interface{}{"{header}", header})
|
||||
return &ApiError{
|
||||
Code: e.Code,
|
||||
ErrCode: e.ErrCode,
|
||||
Description: strings.NewReplacer(args...).Replace(e.Description),
|
||||
}
|
||||
}
|
||||
|
||||
// NewJSBadRequestError creates a new JSBadRequestErr error: "bad request"
|
||||
func NewJSBadRequestError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -845,6 +1033,26 @@ func NewJSClusterUnSupportFeatureError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSClusterUnSupportFeatureErr]
|
||||
}
|
||||
|
||||
// NewJSConsumerAckPolicyInvalidError creates a new JSConsumerAckPolicyInvalidErr error: "consumer ack policy invalid"
|
||||
func NewJSConsumerAckPolicyInvalidError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSConsumerAckPolicyInvalidErr]
|
||||
}
|
||||
|
||||
// NewJSConsumerAckWaitNegativeError creates a new JSConsumerAckWaitNegativeErr error: "consumer ack wait needs to be positive"
|
||||
func NewJSConsumerAckWaitNegativeError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSConsumerAckWaitNegativeErr]
|
||||
}
|
||||
|
||||
// NewJSConsumerAlreadyExistsError creates a new JSConsumerAlreadyExists error: "consumer already exists"
|
||||
func NewJSConsumerAlreadyExistsError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -855,6 +1063,16 @@ func NewJSConsumerAlreadyExistsError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSConsumerAlreadyExists]
|
||||
}
|
||||
|
||||
// NewJSConsumerBackOffNegativeError creates a new JSConsumerBackOffNegativeErr error: "consumer backoff needs to be positive"
|
||||
func NewJSConsumerBackOffNegativeError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSConsumerBackOffNegativeErr]
|
||||
}
|
||||
|
||||
// NewJSConsumerBadDurableNameError creates a new JSConsumerBadDurableNameErr error: "durable name can not contain '.', '*', '>'"
|
||||
func NewJSConsumerBadDurableNameError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -1237,14 +1455,14 @@ func NewJSConsumerMaxRequestBatchNegativeError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSConsumerMaxRequestBatchNegativeErr]
|
||||
}
|
||||
|
||||
// NewJSConsumerMaxRequestExpiresToSmallError creates a new JSConsumerMaxRequestExpiresToSmall error: "consumer max request expires needs to be >= 1ms"
|
||||
func NewJSConsumerMaxRequestExpiresToSmallError(opts ...ErrorOption) *ApiError {
|
||||
// NewJSConsumerMaxRequestExpiresTooSmallError creates a new JSConsumerMaxRequestExpiresTooSmall error: "consumer max request expires needs to be >= 1ms"
|
||||
func NewJSConsumerMaxRequestExpiresTooSmallError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSConsumerMaxRequestExpiresToSmall]
|
||||
return ApiErrors[JSConsumerMaxRequestExpiresTooSmall]
|
||||
}
|
||||
|
||||
// NewJSConsumerMaxWaitingNegativeError creates a new JSConsumerMaxWaitingNegativeErr error: "consumer max waiting needs to be positive"
|
||||
@@ -1375,6 +1593,26 @@ func NewJSConsumerOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError
|
||||
return ApiErrors[JSConsumerOverlappingSubjectFilters]
|
||||
}
|
||||
|
||||
// NewJSConsumerPinnedTTLWithoutPriorityPolicyNoneError creates a new JSConsumerPinnedTTLWithoutPriorityPolicyNone error: "PinnedTTL cannot be set when PriorityPolicy is none"
|
||||
func NewJSConsumerPinnedTTLWithoutPriorityPolicyNoneError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSConsumerPinnedTTLWithoutPriorityPolicyNone]
|
||||
}
|
||||
|
||||
// NewJSConsumerPriorityGroupWithPolicyNoneError creates a new JSConsumerPriorityGroupWithPolicyNone error: "consumer can not have priority groups when policy is none"
|
||||
func NewJSConsumerPriorityGroupWithPolicyNoneError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSConsumerPriorityGroupWithPolicyNone]
|
||||
}
|
||||
|
||||
// NewJSConsumerPriorityPolicyWithoutGroupError creates a new JSConsumerPriorityPolicyWithoutGroup error: "Setting PriorityPolicy requires at least one PriorityGroup to be set"
|
||||
func NewJSConsumerPriorityPolicyWithoutGroupError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -1445,6 +1683,16 @@ func NewJSConsumerReplacementWithDifferentNameError(opts ...ErrorOption) *ApiErr
|
||||
return ApiErrors[JSConsumerReplacementWithDifferentNameErr]
|
||||
}
|
||||
|
||||
// NewJSConsumerReplayPolicyInvalidError creates a new JSConsumerReplayPolicyInvalidErr error: "consumer replay policy invalid"
|
||||
func NewJSConsumerReplayPolicyInvalidError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSConsumerReplayPolicyInvalidErr]
|
||||
}
|
||||
|
||||
// NewJSConsumerReplicasExceedsStreamError creates a new JSConsumerReplicasExceedsStream error: "consumer config replica count exceeds parent stream"
|
||||
func NewJSConsumerReplicasExceedsStreamError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -1597,6 +1845,106 @@ func NewJSMemoryResourcesExceededError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSMemoryResourcesExceededErr]
|
||||
}
|
||||
|
||||
// NewJSMessageCounterBrokenError creates a new JSMessageCounterBrokenErr error: "message counter is broken"
|
||||
func NewJSMessageCounterBrokenError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageCounterBrokenErr]
|
||||
}
|
||||
|
||||
// NewJSMessageIncrDisabledError creates a new JSMessageIncrDisabledErr error: "message counters is disabled"
|
||||
func NewJSMessageIncrDisabledError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageIncrDisabledErr]
|
||||
}
|
||||
|
||||
// NewJSMessageIncrInvalidError creates a new JSMessageIncrInvalidErr error: "message counter increment is invalid"
|
||||
func NewJSMessageIncrInvalidError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageIncrInvalidErr]
|
||||
}
|
||||
|
||||
// NewJSMessageIncrMissingError creates a new JSMessageIncrMissingErr error: "message counter increment is missing"
|
||||
func NewJSMessageIncrMissingError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageIncrMissingErr]
|
||||
}
|
||||
|
||||
// NewJSMessageIncrPayloadError creates a new JSMessageIncrPayloadErr error: "message counter has payload"
|
||||
func NewJSMessageIncrPayloadError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageIncrPayloadErr]
|
||||
}
|
||||
|
||||
// NewJSMessageSchedulesDisabledError creates a new JSMessageSchedulesDisabledErr error: "message schedules is disabled"
|
||||
func NewJSMessageSchedulesDisabledError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageSchedulesDisabledErr]
|
||||
}
|
||||
|
||||
// NewJSMessageSchedulesPatternInvalidError creates a new JSMessageSchedulesPatternInvalidErr error: "message schedules pattern is invalid"
|
||||
func NewJSMessageSchedulesPatternInvalidError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageSchedulesPatternInvalidErr]
|
||||
}
|
||||
|
||||
// NewJSMessageSchedulesRollupInvalidError creates a new JSMessageSchedulesRollupInvalidErr error: "message schedules invalid rollup"
|
||||
func NewJSMessageSchedulesRollupInvalidError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageSchedulesRollupInvalidErr]
|
||||
}
|
||||
|
||||
// NewJSMessageSchedulesTTLInvalidError creates a new JSMessageSchedulesTTLInvalidErr error: "message schedules invalid per-message TTL"
|
||||
func NewJSMessageSchedulesTTLInvalidError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageSchedulesTTLInvalidErr]
|
||||
}
|
||||
|
||||
// NewJSMessageSchedulesTargetInvalidError creates a new JSMessageSchedulesTargetInvalidErr error: "message schedules target is invalid"
|
||||
func NewJSMessageSchedulesTargetInvalidError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMessageSchedulesTargetInvalidErr]
|
||||
}
|
||||
|
||||
// NewJSMessageTTLDisabledError creates a new JSMessageTTLDisabledErr error: "per-message TTL is disabled"
|
||||
func NewJSMessageTTLDisabledError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -1705,6 +2053,26 @@ func NewJSMirrorOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSMirrorOverlappingSubjectFilters]
|
||||
}
|
||||
|
||||
// NewJSMirrorWithAtomicPublishError creates a new JSMirrorWithAtomicPublishErr error: "stream mirrors can not also use atomic publishing"
|
||||
func NewJSMirrorWithAtomicPublishError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMirrorWithAtomicPublishErr]
|
||||
}
|
||||
|
||||
// NewJSMirrorWithCountersError creates a new JSMirrorWithCountersErr error: "stream mirrors can not also calculate counters"
|
||||
func NewJSMirrorWithCountersError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMirrorWithCountersErr]
|
||||
}
|
||||
|
||||
// NewJSMirrorWithFirstSeqError creates a new JSMirrorWithFirstSeqErr error: "stream mirrors can not have first sequence configured"
|
||||
func NewJSMirrorWithFirstSeqError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -1715,6 +2083,16 @@ func NewJSMirrorWithFirstSeqError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSMirrorWithFirstSeqErr]
|
||||
}
|
||||
|
||||
// NewJSMirrorWithMsgSchedulesError creates a new JSMirrorWithMsgSchedulesErr error: "stream mirrors can not also schedule messages"
|
||||
func NewJSMirrorWithMsgSchedulesError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSMirrorWithMsgSchedulesErr]
|
||||
}
|
||||
|
||||
// NewJSMirrorWithSourcesError creates a new JSMirrorWithSourcesErr error: "stream mirrors can not also contain other sources"
|
||||
func NewJSMirrorWithSourcesError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -1867,6 +2245,16 @@ func NewJSReplicasCountCannotBeNegativeError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSReplicasCountCannotBeNegative]
|
||||
}
|
||||
|
||||
// NewJSRequiredApiLevelError creates a new JSRequiredApiLevelErr error: "JetStream minimum api level required"
|
||||
func NewJSRequiredApiLevelError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSRequiredApiLevelErr]
|
||||
}
|
||||
|
||||
// NewJSRestoreSubscribeFailedError creates a new JSRestoreSubscribeFailedErrF error: "JetStream unable to subscribe to restore snapshot {subject}: {err}"
|
||||
func NewJSRestoreSubscribeFailedError(err error, subject interface{}, opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -2007,6 +2395,16 @@ func NewJSSourceOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSSourceOverlappingSubjectFilters]
|
||||
}
|
||||
|
||||
// NewJSSourceWithMsgSchedulesError creates a new JSSourceWithMsgSchedulesErr error: "stream source can not also schedule messages"
|
||||
func NewJSSourceWithMsgSchedulesError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSSourceWithMsgSchedulesErr]
|
||||
}
|
||||
|
||||
// NewJSStorageResourcesExceededError creates a new JSStorageResourcesExceededErr error: "insufficient storage resources available"
|
||||
func NewJSStorageResourcesExceededError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -2075,6 +2473,16 @@ func NewJSStreamDuplicateMessageConflictError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSStreamDuplicateMessageConflict]
|
||||
}
|
||||
|
||||
// NewJSStreamExpectedLastSeqPerSubjectInvalidError creates a new JSStreamExpectedLastSeqPerSubjectInvalid error: "missing sequence for expected last sequence per subject"
|
||||
func NewJSStreamExpectedLastSeqPerSubjectInvalidError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSStreamExpectedLastSeqPerSubjectInvalid]
|
||||
}
|
||||
|
||||
// NewJSStreamExpectedLastSeqPerSubjectNotReadyError creates a new JSStreamExpectedLastSeqPerSubjectNotReady error: "expected last sequence per subject temporarily unavailable"
|
||||
func NewJSStreamExpectedLastSeqPerSubjectNotReadyError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
@@ -2241,6 +2649,16 @@ func NewJSStreamMessageExceedsMaximumError(opts ...ErrorOption) *ApiError {
|
||||
return ApiErrors[JSStreamMessageExceedsMaximumErr]
|
||||
}
|
||||
|
||||
// NewJSStreamMinLastSeqError creates a new JSStreamMinLastSeqErr error: "min last sequence"
|
||||
func NewJSStreamMinLastSeqError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
if ae, ok := eopts.err.(*ApiError); ok {
|
||||
return ae
|
||||
}
|
||||
|
||||
return ApiErrors[JSStreamMinLastSeqErr]
|
||||
}
|
||||
|
||||
// NewJSStreamMirrorNotUpdatableError creates a new JSStreamMirrorNotUpdatableErr error: "stream mirror configuration can not be updated"
|
||||
func NewJSStreamMirrorNotUpdatableError(opts ...ErrorOption) *ApiError {
|
||||
eopts := parseOpts(opts)
|
||||
|
||||
+22
-1
@@ -73,7 +73,7 @@ type JSStreamActionAdvisory struct {
|
||||
TypedEvent
|
||||
Stream string `json:"stream"`
|
||||
Action ActionAdvisoryType `json:"action"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Template string `json:"template,omitempty"` // Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
@@ -253,6 +253,27 @@ type JSStreamQuorumLostAdvisory struct {
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
// JSStreamBatchAbandonedAdvisoryType is sent when a stream's atomic batch is abandoned.
|
||||
const JSStreamBatchAbandonedAdvisoryType = "io.nats.jetstream.advisory.v1.stream_batch_abandoned"
|
||||
|
||||
// JSStreamBatchAbandonedAdvisory indicates that a stream's batch was abandoned.
|
||||
type JSStreamBatchAbandonedAdvisory struct {
|
||||
TypedEvent
|
||||
Account string `json:"account,omitempty"`
|
||||
Stream string `json:"stream"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
BatchId string `json:"batch"`
|
||||
Reason BatchAbandonReason `json:"reason"`
|
||||
}
|
||||
|
||||
type BatchAbandonReason string
|
||||
|
||||
var (
|
||||
BatchTimeout BatchAbandonReason = "timeout"
|
||||
BatchLarge BatchAbandonReason = "large"
|
||||
BatchIncomplete BatchAbandonReason = "incomplete"
|
||||
)
|
||||
|
||||
// JSConsumerLeaderElectedAdvisoryType is sent when the system elects a leader for a consumer.
|
||||
const JSConsumerLeaderElectedAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_leader_elected"
|
||||
|
||||
|
||||
+31
-1
@@ -17,7 +17,7 @@ import "strconv"
|
||||
|
||||
const (
|
||||
// JSApiLevel is the maximum supported JetStream API level for this server.
|
||||
JSApiLevel int = 1
|
||||
JSApiLevel int = 2
|
||||
|
||||
JSRequiredLevelMetadataKey = "_nats.req.level"
|
||||
JSServerVersionMetadataKey = "_nats.ver"
|
||||
@@ -62,6 +62,26 @@ func setStaticStreamMetadata(cfg *StreamConfig) {
|
||||
requires(1)
|
||||
}
|
||||
|
||||
// Counter CRDTs were added in v2.12 and require API level 2.
|
||||
if cfg.AllowMsgCounter {
|
||||
requires(2)
|
||||
}
|
||||
|
||||
// Atomic batch publishing was added in v2.12 and require API level 2.
|
||||
if cfg.AllowAtomicPublish {
|
||||
requires(2)
|
||||
}
|
||||
|
||||
// Message scheduling was added in v2.12 and require API level 2.
|
||||
if cfg.AllowMsgSchedules {
|
||||
requires(2)
|
||||
}
|
||||
|
||||
// Async persist mode was added in v2.12 and requires API level 2.
|
||||
if cfg.PersistMode == AsyncPersistMode {
|
||||
requires(2)
|
||||
}
|
||||
|
||||
cfg.Metadata[JSRequiredLevelMetadataKey] = strconv.Itoa(requiredApiLevel)
|
||||
}
|
||||
|
||||
@@ -204,3 +224,13 @@ func deleteDynamicMetadata(metadata map[string]string) {
|
||||
delete(metadata, JSServerVersionMetadataKey)
|
||||
delete(metadata, JSServerLevelMetadataKey)
|
||||
}
|
||||
|
||||
// errorOnRequiredApiLevel returns whether a request should be rejected based on the JSRequiredApiLevel header.
|
||||
func errorOnRequiredApiLevel(hdr []byte) bool {
|
||||
reqApiLevel := sliceHeader(JSRequiredApiLevel, hdr)
|
||||
if len(reqApiLevel) == 0 {
|
||||
return false
|
||||
}
|
||||
minLevel, err := strconv.Atoi(string(reqApiLevel))
|
||||
return err != nil || JSApiLevel < minLevel
|
||||
}
|
||||
|
||||
+50
-1
@@ -82,6 +82,8 @@ type leaf struct {
|
||||
remoteDomain string
|
||||
// account name of remote server
|
||||
remoteAccName string
|
||||
// Whether or not we want to propagate east-west interest from other LNs.
|
||||
isolated bool
|
||||
// Used to suppress sub and unsub interest. Same as routes but our audience
|
||||
// here is tied to this leaf node. This will hold all subscriptions except this
|
||||
// leaf nodes. This represents all the interest we want to send to the other side.
|
||||
@@ -130,6 +132,14 @@ func (c *client) isHubLeafNode() bool {
|
||||
return c.kind == LEAF && !c.leaf.isSpoke
|
||||
}
|
||||
|
||||
func (c *client) isIsolatedLeafNode() bool {
|
||||
// TODO(nat): In future we may want to pass in and consider an isolation
|
||||
// group name here, which the hub and/or leaf could provide, so that we
|
||||
// can isolate away certain LNs but not others on an opt-in basis. For
|
||||
// now we will just isolate all LN interest until then.
|
||||
return c.kind == LEAF && c.leaf.isolated
|
||||
}
|
||||
|
||||
// This will spin up go routines to solicit the remote leaf node connections.
|
||||
func (s *Server) solicitLeafNodeRemotes(remotes []*RemoteLeafOpts) {
|
||||
sysAccName := _EMPTY_
|
||||
@@ -177,12 +187,20 @@ func (s *Server) solicitLeafNodeRemotes(remotes []*RemoteLeafOpts) {
|
||||
return remote
|
||||
}
|
||||
for _, r := range remotes {
|
||||
// We need to call this, even if the leaf is disabled. This is so that
|
||||
// the number of internal configuration matches the options' remote leaf
|
||||
// configuration required for configuration reload.
|
||||
remote := addRemote(r, r.LocalAccount == sysAccName)
|
||||
s.startGoRoutine(func() { s.connectToRemoteLeafNode(remote, true) })
|
||||
if !r.Disabled {
|
||||
s.startGoRoutine(func() { s.connectToRemoteLeafNode(remote, true) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) remoteLeafNodeStillValid(remote *leafNodeCfg) bool {
|
||||
if remote.Disabled {
|
||||
return false
|
||||
}
|
||||
for _, ri := range s.getOpts().LeafNode.Remotes {
|
||||
// FIXME(dlc) - What about auth changes?
|
||||
if reflect.DeepEqual(ri.URLs, remote.URLs) {
|
||||
@@ -748,6 +766,7 @@ func (s *Server) startLeafNodeAcceptLoop() {
|
||||
Domain: opts.JetStreamDomain,
|
||||
Proto: s.getServerProto(),
|
||||
InfoOnConnect: true,
|
||||
JSApiLevel: JSApiLevel,
|
||||
}
|
||||
// If we have selected a random port...
|
||||
if port == 0 {
|
||||
@@ -811,6 +830,7 @@ func (c *client) sendLeafConnect(clusterName string, headers bool) error {
|
||||
Compression: c.leaf.compression,
|
||||
RemoteAccount: c.acc.GetName(),
|
||||
Proto: c.srv.getServerProto(),
|
||||
Isolate: c.leaf.remote.RequestIsolation,
|
||||
}
|
||||
|
||||
// If a signature callback is specified, this takes precedence over anything else.
|
||||
@@ -977,6 +997,13 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
|
||||
// Do not update the smap here, we need to do it in initLeafNodeSmapAndSendSubs
|
||||
c.leaf = &leaf{}
|
||||
|
||||
// If the leafnode subject interest should be isolated, flag it here.
|
||||
s.optsMu.RLock()
|
||||
if c.leaf.isolated = s.opts.LeafNode.IsolateLeafnodeInterest; !c.leaf.isolated && remote != nil {
|
||||
c.leaf.isolated = remote.LocalIsolation
|
||||
}
|
||||
s.optsMu.RUnlock()
|
||||
|
||||
// For accepted LN connections, ws will be != nil if it was accepted
|
||||
// through the Websocket port.
|
||||
c.ws = ws
|
||||
@@ -1057,6 +1084,8 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
|
||||
if cm := opts.LeafNode.Compression.Mode; cm != CompressionNotSupported {
|
||||
info.Compression = cm
|
||||
}
|
||||
// We always send a nonce for LEAF connections. Do not change that without
|
||||
// taking into account presence of proxy trusted keys.
|
||||
s.generateNonce(nonce[:])
|
||||
s.mu.Unlock()
|
||||
}
|
||||
@@ -1794,9 +1823,13 @@ func (s *Server) removeLeafNodeConnection(c *client) {
|
||||
c.leaf.gwSub = nil
|
||||
}
|
||||
}
|
||||
proxyKey := c.proxyKey
|
||||
c.mu.Unlock()
|
||||
s.mu.Lock()
|
||||
delete(s.leafs, cid)
|
||||
if proxyKey != _EMPTY_ {
|
||||
s.removeProxiedConn(proxyKey, cid)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.removeFromTempClients(cid)
|
||||
}
|
||||
@@ -1818,6 +1851,7 @@ type leafConnectInfo struct {
|
||||
Headers bool `json:"headers,omitempty"`
|
||||
JetStream bool `json:"jetstream,omitempty"`
|
||||
DenyPub []string `json:"deny_pub,omitempty"`
|
||||
Isolate bool `json:"isolate,omitempty"`
|
||||
|
||||
// There was an existing field called:
|
||||
// >> Comp bool `json:"compression,omitempty"`
|
||||
@@ -1928,6 +1962,8 @@ func (c *client) processLeafNodeConnect(s *Server, arg []byte, lang string) erro
|
||||
c.leaf.remoteServer = proto.Name
|
||||
// Remember the remote account name
|
||||
c.leaf.remoteAccName = proto.RemoteAccount
|
||||
// Remember if the leafnode requested isolation.
|
||||
c.leaf.isolated = c.leaf.isolated || proto.Isolate
|
||||
|
||||
// If the other side has declared itself a hub, so we will take on the spoke role.
|
||||
if proto.Hub {
|
||||
@@ -2044,12 +2080,16 @@ func (c *client) remoteCluster() string {
|
||||
// its permission settings for local enforcement.
|
||||
func (s *Server) sendPermsAndAccountInfo(c *client) {
|
||||
// Copy
|
||||
s.mu.Lock()
|
||||
info := s.copyLeafNodeInfo()
|
||||
s.mu.Unlock()
|
||||
c.mu.Lock()
|
||||
info.CID = c.cid
|
||||
info.Import = c.opts.Import
|
||||
info.Export = c.opts.Export
|
||||
info.RemoteAccount = c.acc.Name
|
||||
// s.SystemAccount() uses an atomic operation and does not get the server lock, so this is safe.
|
||||
info.IsSystemAccount = c.acc == s.SystemAccount()
|
||||
info.ConnectInfo = true
|
||||
c.enqueueProto(generateInfoJSON(info))
|
||||
c.mu.Unlock()
|
||||
@@ -2159,6 +2199,10 @@ func (s *Server) initLeafNodeSmapAndSendSubs(c *client) {
|
||||
c.Debugf("Not permitted to subscribe to %q on behalf of %s%s", sub.subject, accName, accNTag)
|
||||
continue
|
||||
}
|
||||
// Don't advertise interest from leafnodes to other isolated leafnodes.
|
||||
if sub.client.kind == LEAF && c.isIsolatedLeafNode() {
|
||||
continue
|
||||
}
|
||||
// We ignore ourselves here.
|
||||
// Also don't add the subscription if it has a origin cluster and the
|
||||
// cluster name matches the one of the client we are sending to.
|
||||
@@ -2280,6 +2324,11 @@ func (acc *Account) updateLeafNodesEx(sub *subscription, delta int32, hubOnly bo
|
||||
continue
|
||||
}
|
||||
ln.mu.Lock()
|
||||
// Don't advertise interest from leafnodes to other isolated leafnodes.
|
||||
if sub.client.kind == LEAF && ln.isIsolatedLeafNode() {
|
||||
ln.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
// If `hubOnly` is true, it means that we want to update only leafnodes
|
||||
// that connect to this server (so isHubLeafNode() would return `true`).
|
||||
if hubOnly && !ln.isHubLeafNode() {
|
||||
|
||||
+96
-10
@@ -24,7 +24,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server/ats"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server/avl"
|
||||
"github.com/nats-io/nats-server/v2/server/gsl"
|
||||
"github.com/nats-io/nats-server/v2/server/stree"
|
||||
@@ -42,11 +41,12 @@ type memStore struct {
|
||||
maxp int64
|
||||
scb StorageUpdateHandler
|
||||
rmcb StorageRemoveMsgHandler
|
||||
sdmcb SubjectDeleteMarkerUpdateHandler
|
||||
pmsgcb ProcessJetStreamMsgHandler
|
||||
ageChk *time.Timer
|
||||
consumers int
|
||||
receivedAny bool
|
||||
ttls *thw.HashWheel
|
||||
scheduling *MsgScheduling
|
||||
sdm *SDMMeta
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ func newMemStore(cfg *StreamConfig) (*memStore, error) {
|
||||
if cfg.AllowMsgTTL {
|
||||
ms.ttls = thw.NewHashWheel()
|
||||
}
|
||||
if cfg.AllowMsgSchedules {
|
||||
ms.scheduling = newMsgScheduling(ms.runMsgScheduling)
|
||||
}
|
||||
if cfg.FirstSeq > 0 {
|
||||
if _, err := ms.purge(cfg.FirstSeq); err != nil {
|
||||
return nil, err
|
||||
@@ -95,6 +98,11 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error {
|
||||
} else if !cfg.AllowMsgTTL && ms.ttls != nil {
|
||||
ms.ttls = nil
|
||||
}
|
||||
if cfg.AllowMsgSchedules && ms.scheduling == nil {
|
||||
ms.recoverMsgSchedulingState()
|
||||
} else if !cfg.AllowMsgSchedules && ms.scheduling != nil {
|
||||
ms.scheduling = nil
|
||||
}
|
||||
// Limits checks and enforcement.
|
||||
ms.enforceMsgLimit()
|
||||
ms.enforceBytesLimit()
|
||||
@@ -127,6 +135,9 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error {
|
||||
if cfg.MaxAge != 0 || cfg.AllowMsgTTL {
|
||||
ms.expireMsgs()
|
||||
}
|
||||
if cfg.AllowMsgSchedules {
|
||||
ms.runMsgScheduling()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -154,6 +165,29 @@ func (ms *memStore) recoverTTLState() {
|
||||
}
|
||||
}
|
||||
|
||||
// Lock should be held.
|
||||
func (ms *memStore) recoverMsgSchedulingState() {
|
||||
ms.scheduling = newMsgScheduling(ms.runMsgScheduling)
|
||||
if ms.state.Msgs == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
seq uint64
|
||||
smv StoreMsg
|
||||
sm *StoreMsg
|
||||
)
|
||||
defer ms.scheduling.resetTimer()
|
||||
for sm, seq, _ = ms.loadNextMsgLocked(fwcs, true, 0, &smv); sm != nil; sm, seq, _ = ms.loadNextMsgLocked(fwcs, true, seq+1, &smv) {
|
||||
if len(sm.hdr) == 0 {
|
||||
continue
|
||||
}
|
||||
if schedule, ok := getMessageSchedule(sm.hdr); ok && !schedule.IsZero() {
|
||||
ms.scheduling.init(seq, sm.subj, schedule.UnixNano())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stores a raw message with expected sequence number and timestamp.
|
||||
// Lock should be held.
|
||||
func (ms *memStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) error {
|
||||
@@ -271,6 +305,13 @@ func (ms *memStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, tt
|
||||
ms.startAgeChk()
|
||||
}
|
||||
|
||||
// Message scheduling.
|
||||
if ms.scheduling != nil {
|
||||
if schedule, ok := getMessageSchedule(hdr); ok && !schedule.IsZero() {
|
||||
ms.scheduling.add(seq, subj, schedule.UnixNano())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -360,6 +401,11 @@ func (ms *memStore) SkipMsgs(seq uint64, num uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlushAllPending flushes all data that was still pending to be written.
|
||||
func (ms *memStore) FlushAllPending() {
|
||||
// Noop, in-memory store doesn't use async applying.
|
||||
}
|
||||
|
||||
// RegisterStorageUpdates registers a callback for updates to storage changes.
|
||||
// It will present number of messages and bytes as a signed integer and an
|
||||
// optional sequence number of the message if a single.
|
||||
@@ -377,10 +423,10 @@ func (ms *memStore) RegisterStorageRemoveMsg(cb StorageRemoveMsgHandler) {
|
||||
ms.mu.Unlock()
|
||||
}
|
||||
|
||||
// RegisterSubjectDeleteMarkerUpdates registers a callback for updates to new subject delete markers.
|
||||
func (ms *memStore) RegisterSubjectDeleteMarkerUpdates(cb SubjectDeleteMarkerUpdateHandler) {
|
||||
// RegisterProcessJetStreamMsg registers a callback to process new JetStream messages.
|
||||
func (ms *memStore) RegisterProcessJetStreamMsg(cb ProcessJetStreamMsgHandler) {
|
||||
ms.mu.Lock()
|
||||
ms.sdmcb = cb
|
||||
ms.pmsgcb = cb
|
||||
ms.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -1084,11 +1130,11 @@ func (ms *memStore) expireMsgs() {
|
||||
maxAge := int64(ms.cfg.MaxAge)
|
||||
minAge := time.Now().UnixNano() - maxAge
|
||||
rmcb := ms.rmcb
|
||||
sdmcb := ms.sdmcb
|
||||
pmsgcb := ms.pmsgcb
|
||||
sdmTTL := int64(ms.cfg.SubjectDeleteMarkerTTL.Seconds())
|
||||
sdmEnabled := sdmTTL > 0
|
||||
ms.mu.RUnlock()
|
||||
if sdmEnabled && (rmcb == nil || sdmcb == nil) {
|
||||
if sdmEnabled && (rmcb == nil || pmsgcb == nil) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1244,12 +1290,39 @@ func (ms *memStore) handleRemovalOrSdm(seq uint64, subj string, sdm bool, sdmTTL
|
||||
subj: subj,
|
||||
hdr: hdr,
|
||||
}
|
||||
ms.sdmcb(msg)
|
||||
ms.pmsgcb(msg)
|
||||
} else {
|
||||
ms.rmcb(seq)
|
||||
}
|
||||
}
|
||||
|
||||
// Will run through scheduled messages.
|
||||
func (ms *memStore) runMsgScheduling() {
|
||||
// TODO: Not great that we're holding the lock here, but the timed hash wheel and message scheduling isn't thread-safe.
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
|
||||
if ms.scheduling == nil || ms.pmsgcb == nil {
|
||||
return
|
||||
}
|
||||
|
||||
scheduledMsgs := ms.scheduling.getScheduledMessages(func(seq uint64, smv *StoreMsg) *StoreMsg {
|
||||
sm, _ := ms.loadMsgLocked(seq, smv, false)
|
||||
return sm
|
||||
})
|
||||
if len(scheduledMsgs) > 0 {
|
||||
ms.mu.Unlock()
|
||||
for _, msg := range scheduledMsgs {
|
||||
ms.pmsgcb(msg)
|
||||
}
|
||||
ms.mu.Lock()
|
||||
}
|
||||
|
||||
if ms.scheduling != nil {
|
||||
ms.scheduling.resetTimer()
|
||||
}
|
||||
}
|
||||
|
||||
// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
|
||||
// Will return the number of purged messages.
|
||||
func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) {
|
||||
@@ -1952,12 +2025,25 @@ func (ms *memStore) Utilization() (total, reported uint64, err error) {
|
||||
return ms.state.Bytes, ms.state.Bytes, nil
|
||||
}
|
||||
|
||||
func memStoreMsgSizeRaw(slen, hlen, mlen int) uint64 {
|
||||
return uint64(slen + hlen + mlen + 16) // 8*2 for seq + age
|
||||
}
|
||||
|
||||
func memStoreMsgSize(subj string, hdr, msg []byte) uint64 {
|
||||
return uint64(len(subj) + len(hdr) + len(msg) + 16) // 8*2 for seq + age
|
||||
return memStoreMsgSizeRaw(len(subj), len(hdr), len(msg))
|
||||
}
|
||||
|
||||
// ResetState resets any state that's temporary. For example when changing leaders.
|
||||
func (ms *memStore) ResetState() {
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
if ms.scheduling != nil {
|
||||
ms.scheduling.clearInflight()
|
||||
}
|
||||
}
|
||||
|
||||
// Delete is same as Stop for memory store.
|
||||
func (ms *memStore) Delete() error {
|
||||
func (ms *memStore) Delete(_ bool) error {
|
||||
return ms.Stop()
|
||||
}
|
||||
|
||||
|
||||
+106
-29
@@ -129,6 +129,7 @@ type ConnInfo struct {
|
||||
OutMsgs int64 `json:"out_msgs"`
|
||||
InBytes int64 `json:"in_bytes"`
|
||||
OutBytes int64 `json:"out_bytes"`
|
||||
Stalls int64 `json:"stalls,omitempty"`
|
||||
NumSubs uint32 `json:"subscriptions"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Lang string `json:"lang,omitempty"`
|
||||
@@ -146,11 +147,17 @@ type ConnInfo struct {
|
||||
NameTag string `json:"name_tag,omitempty"`
|
||||
Tags jwt.TagList `json:"tags,omitempty"`
|
||||
MQTTClient string `json:"mqtt_client,omitempty"` // This is the MQTT client id
|
||||
Proxy *ProxyInfo `json:"proxy,omitempty"`
|
||||
|
||||
// Internal
|
||||
rtt int64 // For fast sorting
|
||||
}
|
||||
|
||||
// ProxyInfo represents the information about this proxied connection.
|
||||
type ProxyInfo struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// TLSPeerCert contains basic information about a TLS peer certificate
|
||||
type TLSPeerCert struct {
|
||||
Subject string `json:"subject,omitempty"`
|
||||
@@ -571,6 +578,8 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool)
|
||||
// we need to use atomic here.
|
||||
ci.InMsgs = atomic.LoadInt64(&client.inMsgs)
|
||||
ci.InBytes = atomic.LoadInt64(&client.inBytes)
|
||||
ci.Stalls = atomic.LoadInt64(&client.stalls)
|
||||
ci.Proxy = createProxyInfo(client)
|
||||
|
||||
// If the connection is gone, too bad, we won't set TLSVersion and TLSCipher.
|
||||
// Exclude clients that are still doing handshake so we don't block in
|
||||
@@ -579,7 +588,7 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool)
|
||||
if conn, ok := nc.(*tls.Conn); ok {
|
||||
cs := conn.ConnectionState()
|
||||
ci.TLSVersion = tlsVersion(cs.Version)
|
||||
ci.TLSCipher = tlsCipher(cs.CipherSuite)
|
||||
ci.TLSCipher = tls.CipherSuiteName(cs.CipherSuite)
|
||||
if auth && len(cs.PeerCertificates) > 0 {
|
||||
ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates)
|
||||
}
|
||||
@@ -593,6 +602,17 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool)
|
||||
}
|
||||
}
|
||||
|
||||
// If this client came from a trusted proxy, this will return a ProxyInfo
|
||||
// to be used in ConnInfo or LeafInfo.
|
||||
//
|
||||
// Client lock must be held on entry.
|
||||
func createProxyInfo(c *client) *ProxyInfo {
|
||||
if c.proxyKey == _EMPTY_ {
|
||||
return nil
|
||||
}
|
||||
return &ProxyInfo{Key: c.proxyKey}
|
||||
}
|
||||
|
||||
func makePeerCerts(pc []*x509.Certificate) []*TLSPeerCert {
|
||||
res := make([]*TLSPeerCert, len(pc))
|
||||
for i, c := range pc {
|
||||
@@ -808,8 +828,10 @@ type RouteInfo struct {
|
||||
|
||||
// Routez returns a Routez struct containing information about routes.
|
||||
func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) {
|
||||
rs := &Routez{Routes: []*RouteInfo{}}
|
||||
rs.Now = time.Now().UTC()
|
||||
rs := &Routez{
|
||||
Now: time.Now().UTC(),
|
||||
Routes: []*RouteInfo{},
|
||||
}
|
||||
|
||||
if routezOpts == nil {
|
||||
routezOpts = &RoutezOptions{}
|
||||
@@ -826,7 +848,7 @@ func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) {
|
||||
rs.Import = perms.Import
|
||||
rs.Export = perms.Export
|
||||
}
|
||||
rs.Name = s.getOpts().ServerName
|
||||
rs.Name = s.info.Name
|
||||
|
||||
addRoute := func(r *client) {
|
||||
r.mu.Lock()
|
||||
@@ -1003,7 +1025,15 @@ func (s *Server) Subsz(opts *SubszOptions) (*Subsz, error) {
|
||||
slStats := &SublistStats{}
|
||||
|
||||
// FIXME(dlc) - Make account aware.
|
||||
sz := &Subsz{s.info.ID, time.Now().UTC(), slStats, 0, offset, limit, nil}
|
||||
sz := &Subsz{
|
||||
ID: s.info.ID,
|
||||
Now: time.Now().UTC(),
|
||||
SublistStats: slStats,
|
||||
Total: 0,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Subs: nil,
|
||||
}
|
||||
|
||||
if subdetail {
|
||||
var raw [4096]*subscription
|
||||
@@ -1100,12 +1130,7 @@ func (s *Server) HandleSubsz(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
if len(st.Subs) == 0 {
|
||||
b, err = json.MarshalIndent(st.SublistStats, "", " ")
|
||||
} else {
|
||||
b, err = json.MarshalIndent(st, "", " ")
|
||||
}
|
||||
b, err = json.MarshalIndent(st, "", " ")
|
||||
if err != nil {
|
||||
s.Errorf("Error marshaling response to /subscriptionsz request: %v", err)
|
||||
}
|
||||
@@ -1237,6 +1262,8 @@ type Varz struct {
|
||||
InBytes int64 `json:"in_bytes"` // InBytes is the number of bytes this server received
|
||||
OutBytes int64 `json:"out_bytes"` // OutMsgs is the number of bytes this server sent
|
||||
SlowConsumers int64 `json:"slow_consumers"` // SlowConsumers is the total count of clients that were disconnected since start due to being slow consumers
|
||||
StaleConnections int64 `json:"stale_connections"` // StaleConnections is the total count of stale connections that were detected
|
||||
StalledClients int64 `json:"stalled_clients"` // StalledClients is the total number of times that clients have been stalled.
|
||||
Subscriptions uint32 `json:"subscriptions"` // Subscriptions is the count of active subscriptions
|
||||
HTTPReqStats map[string]uint64 `json:"http_req_stats"` // HTTPReqStats is the number of requests each HTTP endpoint received
|
||||
ConfigLoadTime time.Time `json:"config_load_time"` // ConfigLoadTime is the time the configuration was loaded or reloaded
|
||||
@@ -1247,8 +1274,10 @@ type Varz struct {
|
||||
TrustedOperatorsClaim []*jwt.OperatorClaims `json:"trusted_operators_claim,omitempty"` // TrustedOperatorsClaim is the decoded claims for each trusted operator
|
||||
SystemAccount string `json:"system_account,omitempty"` // SystemAccount is the name of the System account
|
||||
PinnedAccountFail uint64 `json:"pinned_account_fails,omitempty"` // PinnedAccountFail is how often user logon fails due to the issuer account not being pinned.
|
||||
OCSPResponseCache *OCSPResponseCacheVarz `json:"ocsp_peer_cache,omitempty"` // OCSPResponseCache is the state of the OCSP cache // OCSPResponseCache holds information about
|
||||
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats"` // SlowConsumersStats is statistics about all detected Slow Consumer
|
||||
OCSPResponseCache *OCSPResponseCacheVarz `json:"ocsp_peer_cache,omitempty"` // OCSPResponseCache is the state of the OCSP cache
|
||||
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats"` // SlowConsumersStats are statistics about all detected Slow Consumer
|
||||
StaleConnectionStats *StaleConnectionStats `json:"stale_connection_stats,omitempty"` // StaleConnectionStats are statistics about all detected Stale Connections
|
||||
Proxies *ProxiesOptsVarz `json:"proxies,omitempty"` // Proxies hold information about network proxy devices
|
||||
}
|
||||
|
||||
// JetStreamVarz contains basic runtime information about jetstream
|
||||
@@ -1365,6 +1394,16 @@ type OCSPResponseCacheVarz struct {
|
||||
Unknowns int64 `json:"cached_unknown_responses,omitempty"` // Unknowns is how many of the stored cache entries are unknown responses
|
||||
}
|
||||
|
||||
// ProxiesOptsVarz contains proxies information
|
||||
type ProxiesOptsVarz struct {
|
||||
Trusted []*ProxyOptsVarz `json:"trusted,omitempty"` // Trusted holds a list of trusted proxies
|
||||
}
|
||||
|
||||
// ProxyOptsVarz contains proxy information
|
||||
type ProxyOptsVarz struct {
|
||||
Key string `json:"key"` // Key is the public key of the proxy
|
||||
}
|
||||
|
||||
// VarzOptions are the options passed to Varz().
|
||||
// Currently, there are no options defined.
|
||||
type VarzOptions struct{}
|
||||
@@ -1377,6 +1416,14 @@ type SlowConsumersStats struct {
|
||||
Leafs uint64 `json:"leafs"` // Leafs is how many Leafnodes were slow consumers
|
||||
}
|
||||
|
||||
// StaleConnectionStats contains information about the stale connections from different type of connections.
|
||||
type StaleConnectionStats struct {
|
||||
Clients uint64 `json:"clients"` // Clients is how many Client connections became stale connections
|
||||
Routes uint64 `json:"routes"` // Routes is how many Route connections became stale connections
|
||||
Gateways uint64 `json:"gateways"` // Gateways is how many Gateway connections became stale connections
|
||||
Leafs uint64 `json:"leafs"` // Leafs is how many Leafnode connections became stale connections
|
||||
}
|
||||
|
||||
func myUptime(d time.Duration) string {
|
||||
// Just use total seconds for uptime, and display days / years
|
||||
tsecs := d / time.Second
|
||||
@@ -1615,7 +1662,6 @@ func (s *Server) createVarz(pcpu float64, rss int64) *Varz {
|
||||
MaxSubs: opts.MaxSubs,
|
||||
Cores: runtime.NumCPU(),
|
||||
MaxProcs: runtime.GOMAXPROCS(0),
|
||||
Tags: opts.Tags,
|
||||
TrustedOperatorsJwt: opts.operatorJWT,
|
||||
TrustedOperatorsClaim: opts.TrustedOperators,
|
||||
}
|
||||
@@ -1702,6 +1748,8 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) {
|
||||
v.WriteDeadline = opts.WriteDeadline
|
||||
v.ConfigLoadTime = s.configTime.UTC()
|
||||
v.ConfigDigest = opts.configDigest
|
||||
v.Tags = opts.Tags
|
||||
v.Metadata = opts.Metadata
|
||||
// Update route URLs if applicable
|
||||
if s.varzUpdateRouteURLs {
|
||||
v.Cluster.URLs = urlsToStrings(opts.Routes)
|
||||
@@ -1714,6 +1762,19 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) {
|
||||
v.Websocket.TLSPinnedCerts = getPinnedCertsAsSlice(opts.Websocket.TLSPinnedCerts)
|
||||
|
||||
v.TLSOCSPPeerVerify = s.ocspPeerVerify && v.TLSRequired && s.opts.tlsConfigOpts != nil && s.opts.tlsConfigOpts.OCSPPeerConfig != nil && s.opts.tlsConfigOpts.OCSPPeerConfig.Verify
|
||||
|
||||
if opts.Proxies != nil {
|
||||
if v.Proxies == nil {
|
||||
v.Proxies = &ProxiesOptsVarz{}
|
||||
}
|
||||
trusted := make([]*ProxyOptsVarz, 0, len(opts.Proxies.Trusted))
|
||||
for _, t := range opts.Proxies.Trusted {
|
||||
trusted = append(trusted, &ProxyOptsVarz{Key: t.Key})
|
||||
}
|
||||
v.Proxies.Trusted = trusted
|
||||
} else {
|
||||
v.Proxies = nil
|
||||
}
|
||||
}
|
||||
|
||||
func getPinnedCertsAsSlice(certs PinnedCertSet) []string {
|
||||
@@ -1752,12 +1813,20 @@ func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64
|
||||
v.OutMsgs = atomic.LoadInt64(&s.outMsgs)
|
||||
v.OutBytes = atomic.LoadInt64(&s.outBytes)
|
||||
v.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
|
||||
v.StalledClients = atomic.LoadInt64(&s.stalls)
|
||||
v.SlowConsumersStats = &SlowConsumersStats{
|
||||
Clients: s.NumSlowConsumersClients(),
|
||||
Routes: s.NumSlowConsumersRoutes(),
|
||||
Gateways: s.NumSlowConsumersGateways(),
|
||||
Leafs: s.NumSlowConsumersLeafs(),
|
||||
}
|
||||
v.StaleConnections = atomic.LoadInt64(&s.staleConnections)
|
||||
v.StaleConnectionStats = &StaleConnectionStats{
|
||||
Clients: s.NumStaleConnectionsClients(),
|
||||
Routes: s.NumStaleConnectionsRoutes(),
|
||||
Gateways: s.NumStaleConnectionsGateways(),
|
||||
Leafs: s.NumStaleConnectionsLeafs(),
|
||||
}
|
||||
v.PinnedAccountFail = atomic.LoadUint64(&s.pinnedAccFail)
|
||||
|
||||
// Make sure to reset in case we are re-using.
|
||||
@@ -2248,20 +2317,22 @@ type LeafzOptions struct {
|
||||
|
||||
// LeafInfo has detailed information on each remote leafnode connection.
|
||||
type LeafInfo struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsSpoke bool `json:"is_spoke"`
|
||||
Account string `json:"account"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
RTT string `json:"rtt,omitempty"`
|
||||
InMsgs int64 `json:"in_msgs"`
|
||||
OutMsgs int64 `json:"out_msgs"`
|
||||
InBytes int64 `json:"in_bytes"`
|
||||
OutBytes int64 `json:"out_bytes"`
|
||||
NumSubs uint32 `json:"subscriptions"`
|
||||
Subs []string `json:"subscriptions_list,omitempty"`
|
||||
Compression string `json:"compression,omitempty"`
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsSpoke bool `json:"is_spoke"`
|
||||
IsIsolated bool `json:"is_isolated,omitempty"`
|
||||
Account string `json:"account"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
RTT string `json:"rtt,omitempty"`
|
||||
InMsgs int64 `json:"in_msgs"`
|
||||
OutMsgs int64 `json:"out_msgs"`
|
||||
InBytes int64 `json:"in_bytes"`
|
||||
OutBytes int64 `json:"out_bytes"`
|
||||
NumSubs uint32 `json:"subscriptions"`
|
||||
Subs []string `json:"subscriptions_list,omitempty"`
|
||||
Compression string `json:"compression,omitempty"`
|
||||
Proxy *ProxyInfo `json:"proxy,omitempty"`
|
||||
}
|
||||
|
||||
// Leafz returns a Leafz structure containing information about leafnodes.
|
||||
@@ -2294,6 +2365,7 @@ func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) {
|
||||
ID: ln.cid,
|
||||
Name: ln.leaf.remoteServer,
|
||||
IsSpoke: ln.isSpokeLeafNode(),
|
||||
IsIsolated: ln.leaf.isolated,
|
||||
Account: ln.acc.Name,
|
||||
IP: ln.host,
|
||||
Port: int(ln.port),
|
||||
@@ -2304,6 +2376,7 @@ func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) {
|
||||
OutBytes: ln.outBytes,
|
||||
NumSubs: uint32(len(ln.subs)),
|
||||
Compression: ln.leaf.compression,
|
||||
Proxy: createProxyInfo(ln),
|
||||
}
|
||||
if opts != nil && opts.Subscriptions {
|
||||
lni.Subs = make([]string, 0, len(ln.subs))
|
||||
@@ -2373,7 +2446,7 @@ func (s *Server) AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error)
|
||||
s.accounts.Range(func(key, a any) bool {
|
||||
acc := a.(*Account)
|
||||
acc.mu.RLock()
|
||||
if opts.IncludeUnused || acc.numLocalConnections() != 0 {
|
||||
if (opts != nil && opts.IncludeUnused) || acc.numLocalConnections() != 0 {
|
||||
stz.Accounts = append(stz.Accounts, acc.statz())
|
||||
}
|
||||
acc.mu.RUnlock()
|
||||
@@ -2516,6 +2589,10 @@ func (reason ClosedState) String() string {
|
||||
return "Cluster Names Identical"
|
||||
case Kicked:
|
||||
return "Kicked"
|
||||
case ProxyNotTrusted:
|
||||
return "Proxy Not Trusted"
|
||||
case ProxyRequired:
|
||||
return "Proxy Required"
|
||||
}
|
||||
|
||||
return "Unknown State"
|
||||
|
||||
+2
-2
@@ -514,7 +514,7 @@ func (s *Server) startMQTT() {
|
||||
hp := net.JoinHostPort(o.Host, strconv.Itoa(port))
|
||||
s.mu.Lock()
|
||||
s.mqtt.sessmgr.sessions = make(map[string]*mqttAccountSessionManager)
|
||||
hl, err = net.Listen("tcp", hp)
|
||||
hl, err = natsListen("tcp", hp)
|
||||
s.mqtt.listenerErr = err
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
@@ -647,7 +647,7 @@ func (s *Server) createMQTTClient(conn net.Conn, ws *websocket) *client {
|
||||
if tlsRequired {
|
||||
c.Debugf("TLS handshake complete")
|
||||
cs := c.nc.(*tls.Conn).ConnectionState()
|
||||
c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite))
|
||||
c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite))
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ func (s *Server) NonceRequired() bool {
|
||||
// nonceRequired tells us if we should send a nonce.
|
||||
// Lock should be held on entry.
|
||||
func (s *Server) nonceRequired() bool {
|
||||
return s.getOpts().AlwaysEnableNonce || len(s.nkeys) > 0 || s.trustedKeys != nil
|
||||
return s.getOpts().AlwaysEnableNonce || len(s.nkeys) > 0 || s.trustedKeys != nil || len(s.proxiesKeyPairs) > 0
|
||||
}
|
||||
|
||||
// Generate a nonce for INFO challenge.
|
||||
|
||||
+265
-23
@@ -77,6 +77,7 @@ type ClusterOpts struct {
|
||||
Advertise string `json:"-"`
|
||||
NoAdvertise bool `json:"-"`
|
||||
ConnectRetries int `json:"-"`
|
||||
ConnectBackoff bool `json:"-"`
|
||||
PoolSize int `json:"-"`
|
||||
PinnedAccounts []string `json:"-"`
|
||||
Compression CompressionOpts `json:"-"`
|
||||
@@ -121,6 +122,7 @@ type GatewayOpts struct {
|
||||
TLSPinnedCerts PinnedCertSet `json:"-"`
|
||||
Advertise string `json:"advertise,omitempty"`
|
||||
ConnectRetries int `json:"connect_retries,omitempty"`
|
||||
ConnectBackoff bool `json:"connect_backoff,omitempty"`
|
||||
Gateways []*RemoteGatewayOpts `json:"gateways,omitempty"`
|
||||
RejectUnknown bool `json:"reject_unknown,omitempty"` // config got renamed to reject_unknown_cluster
|
||||
|
||||
@@ -149,6 +151,7 @@ type LeafNodeOpts struct {
|
||||
Port int `json:"port,omitempty"`
|
||||
Username string `json:"-"`
|
||||
Password string `json:"-"`
|
||||
ProxyRequired bool `json:"-"`
|
||||
Nkey string `json:"-"`
|
||||
Account string `json:"-"`
|
||||
Users []*User `json:"-"`
|
||||
@@ -186,6 +189,10 @@ type LeafNodeOpts struct {
|
||||
// least" test).
|
||||
MinVersion string
|
||||
|
||||
// Isolate subject interest from other leafnode connections, preventing
|
||||
// east-west propagation.
|
||||
IsolateLeafnodeInterest bool `json:"-"`
|
||||
|
||||
// Not exported, for tests.
|
||||
resolver netResolver
|
||||
dialTimeout time.Duration
|
||||
@@ -245,14 +252,30 @@ type RemoteLeafOpts struct {
|
||||
// If JetStreamClusterMigrate is set to true, this is the time after which the leader
|
||||
// will be migrated away from this server if still disconnected.
|
||||
JetStreamClusterMigrateDelay time.Duration `json:"jetstream_cluster_migrate_delay,omitempty"`
|
||||
|
||||
// LocalIsolation isolates this remote from east-west subject interest originating locally.
|
||||
LocalIsolation bool `json:"local_isolation,omitempty"`
|
||||
|
||||
// RequestIsolation asks the remote side to isolate us from their east-west subject interest.
|
||||
RequestIsolation bool `json:"request_isolation,omitempty"`
|
||||
|
||||
// If this is set to true, the connection to this remote will not be solicited.
|
||||
// During a configuration reload, if this is changed from `false` to `true`, the
|
||||
// existing connection will be closed and not solicited again (until it is changed
|
||||
// to `false` again.
|
||||
Disabled bool `json:"-"`
|
||||
}
|
||||
|
||||
// JSLimitOpts are active limits for the meta cluster
|
||||
type JSLimitOpts struct {
|
||||
MaxRequestBatch int `json:"max_request_batch,omitempty"` // MaxRequestBatch is the maximum amount of updates that can be sent in a batch
|
||||
MaxAckPending int `json:"max_ack_pending,omitempty"` // MaxAckPending is the server limit for maximum amount of outstanding Acks
|
||||
MaxHAAssets int `json:"max_ha_assets,omitempty"` // MaxHAAssets is the maximum of Streams and Consumers that may have more than 1 replica
|
||||
Duplicates time.Duration `json:"max_duplicate_window,omitempty"` // Duplicates is the maximum value for duplicate tracking on Streams
|
||||
MaxRequestBatch int `json:"max_request_batch,omitempty"` // MaxRequestBatch is the maximum amount of updates that can be sent in a batch
|
||||
MaxAckPending int `json:"max_ack_pending,omitempty"` // MaxAckPending is the server limit for maximum amount of outstanding Acks
|
||||
MaxHAAssets int `json:"max_ha_assets,omitempty"` // MaxHAAssets is the maximum of Streams and Consumers that may have more than 1 replica
|
||||
Duplicates time.Duration `json:"max_duplicate_window,omitempty"` // Duplicates is the maximum value for duplicate tracking on Streams
|
||||
MaxBatchInflightPerStream int `json:"max_batch_inflight_per_stream,omitempty"` // MaxBatchInflightPerStream is the maximum amount of open batches per stream
|
||||
MaxBatchInflightTotal int `json:"max_batch_inflight_total,omitempty"` // MaxBatchInflightTotal is the maximum amount of total open batches per server
|
||||
MaxBatchSize int `json:"max_batch_size,omitempty"` // MaxBatchSize is the maximum amount of messages allowed in a batch publish to a Stream
|
||||
MaxBatchTimeout time.Duration `json:"max_batch_timeout,omitempty"` // MaxBatchTimeout is the maximum time to receive the commit message after receiving the first message of a batch
|
||||
}
|
||||
|
||||
type JSTpmOpts struct {
|
||||
@@ -291,7 +314,8 @@ type Options struct {
|
||||
Trace bool `json:"-"`
|
||||
Debug bool `json:"-"`
|
||||
TraceVerbose bool `json:"-"`
|
||||
// TraceHeaders if true will only trace message headers, not the payload
|
||||
|
||||
// TraceHeaders if true will only trace message headers, not the payload.
|
||||
TraceHeaders bool `json:"-"`
|
||||
NoLog bool `json:"-"`
|
||||
NoSigs bool `json:"-"`
|
||||
@@ -312,6 +336,7 @@ type Options struct {
|
||||
NoSystemAccount bool `json:"-"`
|
||||
Username string `json:"-"`
|
||||
Password string `json:"-"`
|
||||
ProxyRequired bool `json:"-"`
|
||||
Authorization string `json:"-"`
|
||||
AuthCallout *AuthCallout `json:"-"`
|
||||
PingInterval time.Duration `json:"ping_interval"`
|
||||
@@ -329,7 +354,7 @@ type Options struct {
|
||||
Gateway GatewayOpts `json:"gateway,omitempty"`
|
||||
LeafNode LeafNodeOpts `json:"leaf,omitempty"`
|
||||
JetStream bool `json:"jetstream"`
|
||||
JetStreamStrict bool `json:"-"`
|
||||
NoJetStreamStrict bool `json:"-"` // Strict by default.
|
||||
JetStreamMaxMemory int64 `json:"-"`
|
||||
JetStreamMaxStore int64 `json:"-"`
|
||||
JetStreamDomain string `json:"-"`
|
||||
@@ -426,10 +451,16 @@ type Options struct {
|
||||
// and used as a filter criteria for some system requests.
|
||||
Tags jwt.TagList `json:"-"`
|
||||
|
||||
// Metadata describing the server. They will be included in 'Z' responses.
|
||||
Metadata map[string]string `json:"-"`
|
||||
|
||||
// OCSPConfig enables OCSP Stapling in the server.
|
||||
OCSPConfig *OCSPConfig
|
||||
tlsConfigOpts *TLSConfigOpts
|
||||
|
||||
// Proxies configuration.
|
||||
Proxies *ProxiesConfig
|
||||
|
||||
// private fields, used to know if bool options are explicitly
|
||||
// defined in config and/or command line params.
|
||||
inConfig map[string]bool
|
||||
@@ -714,6 +745,8 @@ type authorization struct {
|
||||
token string
|
||||
nkey string
|
||||
acc string
|
||||
// If connection must come through proxy
|
||||
proxyRequired bool
|
||||
// Multiple Nkeys/Users
|
||||
nkeys []*NkeyUser
|
||||
users []*User
|
||||
@@ -737,6 +770,7 @@ type TLSConfigOpts struct {
|
||||
FallbackDelay time.Duration // Where supported, indicates how long to wait for the handshake before falling back to sending the INFO protocol first.
|
||||
Timeout float64
|
||||
RateLimit int64
|
||||
AllowInsecureCiphers bool
|
||||
Ciphers []uint16
|
||||
CurvePreferences []tls.CurveID
|
||||
PinnedCerts PinnedCertSet
|
||||
@@ -765,6 +799,17 @@ type OCSPConfig struct {
|
||||
OverrideURLs []string
|
||||
}
|
||||
|
||||
// ProxiesConfig represents the options of Proxies.
|
||||
type ProxiesConfig struct {
|
||||
Trusted []*ProxyConfig
|
||||
}
|
||||
|
||||
// ProxyConfig represents the options of Proxy.
|
||||
type ProxyConfig struct {
|
||||
// Public key.
|
||||
Key string
|
||||
}
|
||||
|
||||
var tlsUsage = `
|
||||
TLS configuration is specified in the tls section of a configuration file:
|
||||
|
||||
@@ -1047,6 +1092,7 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin
|
||||
o.authBlockDefined = true
|
||||
o.Username = auth.user
|
||||
o.Password = auth.pass
|
||||
o.ProxyRequired = auth.proxyRequired
|
||||
o.Authorization = auth.token
|
||||
o.AuthTimeout = auth.timeout
|
||||
o.AuthCallout = auth.callout
|
||||
@@ -1649,6 +1695,24 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin
|
||||
*errors = append(*errors, err)
|
||||
return
|
||||
}
|
||||
case "server_metadata":
|
||||
var err error
|
||||
switch v := v.(type) {
|
||||
case map[string]any:
|
||||
for mk, mv := range v {
|
||||
tk, mv = unwrapValue(mv, <)
|
||||
if o.Metadata == nil {
|
||||
o.Metadata = make(map[string]string)
|
||||
}
|
||||
o.Metadata[mk] = mv.(string)
|
||||
}
|
||||
default:
|
||||
err = &configErr{tk, fmt.Sprintf("error parsing metadata: unsupported type %T", v)}
|
||||
}
|
||||
if err != nil {
|
||||
*errors = append(*errors, err)
|
||||
return
|
||||
}
|
||||
case "default_js_domain":
|
||||
vv, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
@@ -1693,6 +1757,13 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin
|
||||
o.NoFastProducerStall = v.(bool)
|
||||
case "max_closed_clients":
|
||||
o.MaxClosedClients = int(v.(int64))
|
||||
case "proxies":
|
||||
proxies, err := parseProxies(tk, errors)
|
||||
if err != nil {
|
||||
*errors = append(*errors, err)
|
||||
return
|
||||
}
|
||||
o.Proxies = proxies
|
||||
default:
|
||||
if au := atomic.LoadInt32(&allowUnknownTopLevelField); au == 0 && !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
@@ -1883,6 +1954,8 @@ func parseCluster(v any, opts *Options, errors *[]error, warnings *[]error) erro
|
||||
trackExplicitVal(&opts.inConfig, "Cluster.NoAdvertise", opts.Cluster.NoAdvertise)
|
||||
case "connect_retries":
|
||||
opts.Cluster.ConnectRetries = int(mv.(int64))
|
||||
case "connect_backoff":
|
||||
opts.Cluster.ConnectBackoff = mv.(bool)
|
||||
case "permissions":
|
||||
perms, err := parseUserPermissions(mv, errors)
|
||||
if err != nil {
|
||||
@@ -2091,6 +2164,8 @@ func parseGateway(v any, o *Options, errors *[]error, warnings *[]error) error {
|
||||
o.Gateway.Advertise = mv.(string)
|
||||
case "connect_retries":
|
||||
o.Gateway.ConnectRetries = int(mv.(int64))
|
||||
case "connect_backoff":
|
||||
o.Gateway.ConnectBackoff = mv.(bool)
|
||||
case "gateways":
|
||||
gateways, err := parseGateways(mv, errors, warnings)
|
||||
if err != nil {
|
||||
@@ -2265,7 +2340,7 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error {
|
||||
var lt token
|
||||
tk, v := unwrapValue(v, <)
|
||||
|
||||
lim := JSLimitOpts{}
|
||||
opts.JetStreamLimits = JSLimitOpts{}
|
||||
|
||||
vv, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
@@ -2275,14 +2350,57 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error {
|
||||
tk, mv = unwrapValue(mv, <)
|
||||
switch strings.ToLower(mk) {
|
||||
case "max_ack_pending":
|
||||
lim.MaxAckPending = int(mv.(int64))
|
||||
opts.JetStreamLimits.MaxAckPending = int(mv.(int64))
|
||||
case "max_ha_assets":
|
||||
lim.MaxHAAssets = int(mv.(int64))
|
||||
opts.JetStreamLimits.MaxHAAssets = int(mv.(int64))
|
||||
case "max_request_batch":
|
||||
lim.MaxRequestBatch = int(mv.(int64))
|
||||
opts.JetStreamLimits.MaxRequestBatch = int(mv.(int64))
|
||||
case "duplicate_window":
|
||||
var err error
|
||||
lim.Duplicates, err = time.ParseDuration(mv.(string))
|
||||
opts.JetStreamLimits.Duplicates, err = time.ParseDuration(mv.(string))
|
||||
if err != nil {
|
||||
*errors = append(*errors, err)
|
||||
}
|
||||
case "batch":
|
||||
if err := parseJetStreamLimitsBatch(tk, opts, errors); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
field: mk,
|
||||
configErr: configErr{
|
||||
token: tk,
|
||||
},
|
||||
}
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseJetStreamLimitsBatch(v any, opts *Options, errors *[]error) error {
|
||||
var lt token
|
||||
tk, v := unwrapValue(v, <)
|
||||
|
||||
vv, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return &configErr{tk, fmt.Sprintf("Expected a map to define batch limits, got %T", v)}
|
||||
}
|
||||
for mk, mv := range vv {
|
||||
tk, mv = unwrapValue(mv, <)
|
||||
switch strings.ToLower(mk) {
|
||||
case "max_inflight_per_stream":
|
||||
opts.JetStreamLimits.MaxBatchInflightPerStream = int(mv.(int64))
|
||||
case "max_inflight_total":
|
||||
opts.JetStreamLimits.MaxBatchInflightTotal = int(mv.(int64))
|
||||
case "max_msgs":
|
||||
opts.JetStreamLimits.MaxBatchSize = int(mv.(int64))
|
||||
case "timeout":
|
||||
var err error
|
||||
opts.JetStreamLimits.MaxBatchTimeout, err = time.ParseDuration(mv.(string))
|
||||
if err != nil {
|
||||
*errors = append(*errors, err)
|
||||
}
|
||||
@@ -2299,7 +2417,6 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
opts.JetStreamLimits = lim
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2308,7 +2425,7 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error {
|
||||
var lt token
|
||||
tk, v := unwrapValue(v, <)
|
||||
|
||||
tpm := JSTpmOpts{}
|
||||
opts.JetStreamTpm = JSTpmOpts{}
|
||||
|
||||
vv, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
@@ -2318,13 +2435,13 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error {
|
||||
tk, mv = unwrapValue(mv, <)
|
||||
switch strings.ToLower(mk) {
|
||||
case "keys_file":
|
||||
tpm.KeysFile = mv.(string)
|
||||
opts.JetStreamTpm.KeysFile = mv.(string)
|
||||
case "encryption_password":
|
||||
tpm.KeyPassword = mv.(string)
|
||||
opts.JetStreamTpm.KeyPassword = mv.(string)
|
||||
case "srk_password":
|
||||
tpm.SrkPassword = mv.(string)
|
||||
opts.JetStreamTpm.SrkPassword = mv.(string)
|
||||
case "pcr":
|
||||
tpm.Pcr = int(mv.(int64))
|
||||
opts.JetStreamTpm.Pcr = int(mv.(int64))
|
||||
case "cipher":
|
||||
if err := setJetStreamEkCipher(opts, mv, tk); err != nil {
|
||||
return err
|
||||
@@ -2342,7 +2459,6 @@ func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
opts.JetStreamTpm = tpm
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2384,7 +2500,7 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er
|
||||
switch strings.ToLower(mk) {
|
||||
case "strict":
|
||||
if v, ok := mv.(bool); ok {
|
||||
opts.JetStreamStrict = v
|
||||
opts.NoJetStreamStrict = !v
|
||||
} else {
|
||||
return &configErr{tk, fmt.Sprintf("Expected 'true' or 'false' for bool value, got '%s'", mv)}
|
||||
}
|
||||
@@ -2521,6 +2637,7 @@ func parseLeafNodes(v any, opts *Options, errors *[]error, warnings *[]error) er
|
||||
}
|
||||
opts.LeafNode.Username = auth.user
|
||||
opts.LeafNode.Password = auth.pass
|
||||
opts.LeafNode.ProxyRequired = auth.proxyRequired
|
||||
opts.LeafNode.AuthTimeout = auth.timeout
|
||||
opts.LeafNode.Account = auth.acc
|
||||
opts.LeafNode.Users = auth.users
|
||||
@@ -2575,6 +2692,8 @@ func parseLeafNodes(v any, opts *Options, errors *[]error, warnings *[]error) er
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
case "isolate_leafnode_interest", "isolate":
|
||||
opts.LeafNode.IsolateLeafnodeInterest = mv.(bool)
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
@@ -2647,6 +2766,8 @@ func parseLeafAuthorization(v any, errors, warnings *[]error) (*authorization, e
|
||||
auth.users = users
|
||||
case "account":
|
||||
auth.acc = mv.(string)
|
||||
case "proxy_required":
|
||||
auth.proxyRequired = mv.(bool)
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
@@ -2705,6 +2826,8 @@ func parseLeafUsers(mv any, errors *[]error) ([]*User, error) {
|
||||
// we need to create internal objects to store u/p and account
|
||||
// name and have a server structure to hold that.
|
||||
user.Account = NewAccount(v.(string))
|
||||
case "proxy_required":
|
||||
user.ProxyRequired = v.(bool)
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
@@ -2854,6 +2977,10 @@ func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteL
|
||||
default:
|
||||
*errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected boolean or map for jetstream_cluster_migrate, got %T", v)})
|
||||
}
|
||||
case "isolate_leafnode_interest", "isolate":
|
||||
remote.LocalIsolation = v.(bool)
|
||||
case "request_isolation":
|
||||
remote.RequestIsolation = v.(bool)
|
||||
case "compression":
|
||||
if err := parseCompression(&remote.Compression, CompressionS2Auto, tk, k, v); err != nil {
|
||||
*errors = append(*errors, err)
|
||||
@@ -2861,6 +2988,8 @@ func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteL
|
||||
}
|
||||
case "first_info_timeout":
|
||||
remote.FirstInfoTimeout = parseDuration(k, tk, v, errors, warnings)
|
||||
case "disabled":
|
||||
remote.Disabled = v.(bool)
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
@@ -4192,6 +4321,8 @@ func parseAuthorization(v any, errors, warnings *[]error) (*authorization, error
|
||||
continue
|
||||
}
|
||||
auth.callout = ac
|
||||
case "proxy_required":
|
||||
auth.proxyRequired = mv.(bool)
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
@@ -4264,6 +4395,9 @@ func parseUsers(mv any, errors *[]error) ([]*NkeyUser, []*User, error) {
|
||||
cts := parseAllowedConnectionTypes(tk, <, v, errors)
|
||||
nkey.AllowedConnectionTypes = cts
|
||||
user.AllowedConnectionTypes = cts
|
||||
case "proxy_required":
|
||||
nkey.ProxyRequired = v.(bool)
|
||||
user.ProxyRequired = v.(bool)
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
@@ -4639,12 +4773,11 @@ func PrintTLSHelpAndDie() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func parseCipher(cipherName string) (uint16, error) {
|
||||
func parseCipher(cipherName string) (*tls.CipherSuite, error) {
|
||||
cipher, exists := cipherMap[cipherName]
|
||||
if !exists {
|
||||
return 0, fmt.Errorf("unrecognized cipher %s", cipherName)
|
||||
return nil, fmt.Errorf("unrecognized cipher %s", cipherName)
|
||||
}
|
||||
|
||||
return cipher, nil
|
||||
}
|
||||
|
||||
@@ -4680,6 +4813,7 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
|
||||
tlsm map[string]any
|
||||
tc = TLSConfigOpts{}
|
||||
lt token
|
||||
ics []*tls.CipherSuite // Insecure ciphers found
|
||||
)
|
||||
defer convertPanicToError(<, &retErr)
|
||||
|
||||
@@ -4739,6 +4873,12 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
|
||||
tc.Verify = verify
|
||||
}
|
||||
tc.TLSCheckKnownURLs = verify
|
||||
case "allow_insecure_cipher_suites":
|
||||
allow, ok := mv.(bool)
|
||||
if !ok {
|
||||
return nil, &configErr{tk, "error parsing tls config, expected 'allow_insecure_cipher_suites' to be a boolean"}
|
||||
}
|
||||
tc.AllowInsecureCiphers = allow
|
||||
case "cipher_suites":
|
||||
ra := mv.([]any)
|
||||
if len(ra) == 0 {
|
||||
@@ -4751,7 +4891,10 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
|
||||
if err != nil {
|
||||
return nil, &configErr{tk, err.Error()}
|
||||
}
|
||||
tc.Ciphers = append(tc.Ciphers, cipher)
|
||||
tc.Ciphers = append(tc.Ciphers, cipher.ID)
|
||||
if cipher.Insecure {
|
||||
ics = append(ics, cipher)
|
||||
}
|
||||
}
|
||||
case "curve_preferences":
|
||||
ra := mv.([]any)
|
||||
@@ -4969,6 +5112,16 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
|
||||
tc.CurvePreferences = defaultCurvePreferences()
|
||||
}
|
||||
|
||||
// If we don't allow insecure ciphers, and yet some were configured, then we
|
||||
// should error.
|
||||
if !tc.AllowInsecureCiphers && len(ics) > 0 {
|
||||
names := make([]string, 0, len(ics))
|
||||
for _, ic := range ics {
|
||||
names = append(names, ic.Name)
|
||||
}
|
||||
return nil, &configErr{tk, fmt.Sprintf("insecure cipher suites configured without 'allow_insecure_cipher_suites' option set: %s", strings.Join(names, ", "))}
|
||||
}
|
||||
|
||||
return &tc, nil
|
||||
}
|
||||
|
||||
@@ -5258,6 +5411,95 @@ func parseMQTT(v any, o *Options, errors *[]error, warnings *[]error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseProxies(mv any, errors *[]error) (*ProxiesConfig, error) {
|
||||
var (
|
||||
tk token
|
||||
lt token
|
||||
proxies = &ProxiesConfig{}
|
||||
)
|
||||
defer convertPanicToErrorList(<, errors)
|
||||
|
||||
tk, mv = unwrapValue(mv, <)
|
||||
pm, ok := mv.(map[string]any)
|
||||
if !ok {
|
||||
return nil, &configErr{tk, fmt.Sprintf("expected proxies to be a map/struct, got %T", mv)}
|
||||
}
|
||||
for mk, mv := range pm {
|
||||
tk, _ = unwrapValue(mv, <)
|
||||
|
||||
switch strings.ToLower(mk) {
|
||||
case "trusted":
|
||||
trusted, err := parseProxiesTrusted(tk, errors)
|
||||
if err != nil {
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
proxies.Trusted = trusted
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
field: mk,
|
||||
configErr: configErr{
|
||||
token: tk,
|
||||
},
|
||||
}
|
||||
*errors = append(*errors, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
func parseProxiesTrusted(mv any, errors *[]error) ([]*ProxyConfig, error) {
|
||||
var (
|
||||
tk token
|
||||
lt token
|
||||
trusted []*ProxyConfig
|
||||
)
|
||||
defer convertPanicToErrorList(<, errors)
|
||||
|
||||
tk, mv = unwrapValue(mv, <)
|
||||
ta, ok := mv.([]any)
|
||||
if !ok {
|
||||
return nil, &configErr{tk, fmt.Sprintf("expected proxies' trusted field to be an array, got %T", mv)}
|
||||
}
|
||||
for _, t := range ta {
|
||||
tk, t = unwrapValue(t, <)
|
||||
// Check its a map/struct
|
||||
tm, ok := t.(map[string]any)
|
||||
if !ok {
|
||||
err := &configErr{tk, fmt.Sprintf("expected proxies' trusted entry to be a map/struct, got %T", t)}
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
proxy := &ProxyConfig{}
|
||||
for k, v := range tm {
|
||||
tk, v = unwrapValue(v, <)
|
||||
switch strings.ToLower(k) {
|
||||
case "key", "public_key":
|
||||
proxy.Key = v.(string)
|
||||
if !nkeys.IsValidPublicKey(proxy.Key) {
|
||||
*errors = append(*errors, &configErr{tk, fmt.Sprintf("invalid proxy key %q", proxy.Key)})
|
||||
continue
|
||||
}
|
||||
default:
|
||||
if !tk.IsUsedVariable() {
|
||||
err := &unknownConfigFieldErr{
|
||||
field: k,
|
||||
configErr: configErr{
|
||||
token: tk,
|
||||
},
|
||||
}
|
||||
*errors = append(*errors, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
trusted = append(trusted, proxy)
|
||||
}
|
||||
return trusted, nil
|
||||
}
|
||||
|
||||
// GenTLSConfig loads TLS related configuration parameters.
|
||||
func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
|
||||
// Create the tls.Config from our options before including the certs.
|
||||
|
||||
+202
-80
@@ -45,6 +45,7 @@ type RaftNode interface {
|
||||
SendSnapshot(snap []byte) error
|
||||
NeedSnapshot() bool
|
||||
Applied(index uint64) (entries uint64, bytes uint64)
|
||||
Processed(index uint64, applied uint64) (entries uint64, bytes uint64)
|
||||
State() RaftState
|
||||
Size() (entries, bytes uint64)
|
||||
Progress() (index, commit, applied uint64)
|
||||
@@ -98,7 +99,7 @@ type WAL interface {
|
||||
State() StreamState
|
||||
FastState(*StreamState)
|
||||
Stop() error
|
||||
Delete() error
|
||||
Delete(inline bool) error
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
@@ -168,12 +169,13 @@ type raft struct {
|
||||
llqrt time.Time // Last quorum lost time
|
||||
lsut time.Time // Last scale-up time
|
||||
|
||||
term uint64 // The current vote term
|
||||
pterm uint64 // Previous term from the last snapshot
|
||||
pindex uint64 // Previous index from the last snapshot
|
||||
commit uint64 // Index of the most recent commit
|
||||
applied uint64 // Index of the most recently applied commit
|
||||
papplied uint64 // First sequence of our log, matches when we last installed a snapshot.
|
||||
term uint64 // The current vote term
|
||||
pterm uint64 // Previous term from the last snapshot
|
||||
pindex uint64 // Previous index from the last snapshot
|
||||
commit uint64 // Index of the most recent commit
|
||||
processed uint64 // Index of the most recently processed commit
|
||||
applied uint64 // Index of the most recently applied commit
|
||||
papplied uint64 // First sequence of our log, matches when we last installed a snapshot.
|
||||
|
||||
aflr uint64 // Index when to signal initial messages have been applied after becoming leader. 0 means signaling is disabled.
|
||||
|
||||
@@ -220,12 +222,13 @@ type raft struct {
|
||||
leadc chan bool // Leader changes
|
||||
quit chan struct{} // Raft group shutdown
|
||||
|
||||
lxfer bool // Are we doing a leadership transfer?
|
||||
hcbehind bool // Were we falling behind at the last health check? (see: isCurrent)
|
||||
maybeLeader bool // The group had a preferred leader. And is maybe already acting as leader prior to scale up.
|
||||
paused bool // Whether or not applies are paused
|
||||
observer bool // The node is observing, i.e. not able to become leader
|
||||
pobserver bool // Were we previously an observer?
|
||||
lxfer bool // Are we doing a leadership transfer?
|
||||
hcbehind bool // Were we falling behind at the last health check? (see: isCurrent)
|
||||
maybeLeader bool // The group had a preferred leader. And is maybe already acting as leader prior to scale up.
|
||||
paused bool // Whether or not applies are paused
|
||||
observer bool // The node is observing, i.e. not able to become leader
|
||||
initializing bool // The node is new, and "empty log" checks can be temporarily relaxed.
|
||||
scaleUp bool // The node is part of a scale up, puts us in observer mode until the log contains data.
|
||||
}
|
||||
|
||||
type proposedEntry struct {
|
||||
@@ -281,6 +284,16 @@ type RaftConfig struct {
|
||||
Log WAL
|
||||
Track bool
|
||||
Observer bool
|
||||
|
||||
// Recovering must be set for a Raft group that's recovering after a restart, or if it's
|
||||
// first seen after a catchup from another server. If a server recovers with an empty log,
|
||||
// we know to protect against data loss.
|
||||
Recovering bool
|
||||
|
||||
// ScaleUp identifies the Raft peer set is being scaled up.
|
||||
// We need to protect against losing state due to the new peers starting with an empty log.
|
||||
// Therefore, these empty servers can't try to become leader until they at least have _some_ state.
|
||||
ScaleUp bool
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -539,6 +552,17 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel
|
||||
n.Lock()
|
||||
n.resetElectionTimeout()
|
||||
n.llqrt = time.Now()
|
||||
|
||||
// If our log is empty, and we're initializing, relax the "empty log" checks temporarily.
|
||||
if !cfg.Recovering && n.pindex == 0 {
|
||||
n.initializing = true
|
||||
// If we're scaling up and our log is empty, must put ourselves into observer
|
||||
// and wait for data from the leader.
|
||||
if !cfg.Observer && cfg.ScaleUp {
|
||||
n.scaleUp = true
|
||||
n.setObserverLocked(true, extUndetermined)
|
||||
}
|
||||
}
|
||||
n.Unlock()
|
||||
|
||||
// Register the Raft group.
|
||||
@@ -842,12 +866,13 @@ func (s *Server) transferRaftLeaders() bool {
|
||||
// Propose will propose a new entry to the group.
|
||||
// This should only be called on the leader.
|
||||
func (n *raft) Propose(data []byte) error {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
// Check state under lock, we might not be leader anymore.
|
||||
if state := n.State(); state != Leader {
|
||||
n.debug("Proposal ignored, not leader (state: %v)", state)
|
||||
return errNotLeader
|
||||
}
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
// Error if we had a previous write error.
|
||||
if werr := n.werr; werr != nil {
|
||||
@@ -857,15 +882,16 @@ func (n *raft) Propose(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProposeDirect will propose multiple entries at once.
|
||||
// ProposeMulti will propose multiple entries at once.
|
||||
// This should only be called on the leader.
|
||||
func (n *raft) ProposeMulti(entries []*Entry) error {
|
||||
if state := n.State(); state != Leader {
|
||||
n.debug("Direct proposal ignored, not leader (state: %v)", state)
|
||||
return errNotLeader
|
||||
}
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
// Check state under lock, we might not be leader anymore.
|
||||
if state := n.State(); state != Leader {
|
||||
n.debug("Multi proposal ignored, not leader (state: %v)", state)
|
||||
return errNotLeader
|
||||
}
|
||||
|
||||
// Error if we had a previous write error.
|
||||
if werr := n.werr; werr != nil {
|
||||
@@ -893,10 +919,12 @@ func (n *raft) ForwardProposal(entry []byte) error {
|
||||
|
||||
// ProposeAddPeer is called to add a peer to the group.
|
||||
func (n *raft) ProposeAddPeer(peer string) error {
|
||||
n.RLock()
|
||||
// Check state under lock, we might not be leader anymore.
|
||||
if n.State() != Leader {
|
||||
n.RUnlock()
|
||||
return errNotLeader
|
||||
}
|
||||
n.RLock()
|
||||
// Error if we had a previous write error.
|
||||
if werr := n.werr; werr != nil {
|
||||
n.RUnlock()
|
||||
@@ -984,10 +1012,13 @@ func (n *raft) AdjustBootClusterSize(csz int) error {
|
||||
// AdjustClusterSize will change the cluster set size.
|
||||
// Must be the leader.
|
||||
func (n *raft) AdjustClusterSize(csz int) error {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
// Check state under lock, we might not be leader anymore.
|
||||
if n.State() != Leader {
|
||||
return errNotLeader
|
||||
}
|
||||
n.Lock()
|
||||
// Same floor as bootstrap.
|
||||
if csz < 2 {
|
||||
csz = 2
|
||||
@@ -997,7 +1028,6 @@ func (n *raft) AdjustClusterSize(csz int) error {
|
||||
// a quorum.
|
||||
n.csz = csz
|
||||
n.qn = n.csz/2 + 1
|
||||
n.Unlock()
|
||||
|
||||
n.sendPeerState()
|
||||
return nil
|
||||
@@ -1023,7 +1053,6 @@ func (n *raft) PauseApply() error {
|
||||
n.paused = true
|
||||
n.hcommit = n.commit
|
||||
// Also prevent us from trying to become a leader while paused and catching up.
|
||||
n.pobserver, n.observer = n.observer, true
|
||||
n.resetElect(observerModeInterval)
|
||||
|
||||
return nil
|
||||
@@ -1066,8 +1095,7 @@ func (n *raft) ResumeApply() {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear our observer and paused state after we apply.
|
||||
n.observer, n.pobserver = n.pobserver, false
|
||||
// Clear our paused state after we apply.
|
||||
n.paused = false
|
||||
n.hcommit = 0
|
||||
|
||||
@@ -1084,6 +1112,16 @@ func (n *raft) ResumeApply() {
|
||||
// apply queue. It will return the number of entries and an estimation of the
|
||||
// byte size that could be removed with a snapshot/compact.
|
||||
func (n *raft) Applied(index uint64) (entries uint64, bytes uint64) {
|
||||
return n.Processed(index, index)
|
||||
}
|
||||
|
||||
// Processed is a callback that must be called by the upper layer when it
|
||||
// has processed the committed entries that it received from the apply queue,
|
||||
// but it (maybe) hasn't applied all the processed entries yet.
|
||||
// Used to indicate a commit was processed, even if it wasn't applied yet and
|
||||
// can't be compacted away by a snapshot just yet. Which allows us to try to
|
||||
// become leader if we've processed all commits, even if they're not all applied.
|
||||
func (n *raft) Processed(index uint64, applied uint64) (entries uint64, bytes uint64) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
@@ -1092,13 +1130,22 @@ func (n *raft) Applied(index uint64) (entries uint64, bytes uint64) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Ignore if already applied.
|
||||
if index > n.applied {
|
||||
n.applied = index
|
||||
// Ignore if already processed.
|
||||
if index > n.processed {
|
||||
n.processed = index
|
||||
}
|
||||
|
||||
// If it was set, and we reached the minimum applied index, reset and send signal to upper layer.
|
||||
if n.aflr > 0 && index >= n.aflr {
|
||||
// Ignore if already applied.
|
||||
if applied > index {
|
||||
applied = index
|
||||
}
|
||||
if applied > n.applied {
|
||||
n.applied = applied
|
||||
}
|
||||
|
||||
// If it was set, and we reached the minimum processed index, reset and send signal to upper layer.
|
||||
// We're not waiting for processed AND applied, because applying could take longer.
|
||||
if n.aflr > 0 && n.processed >= n.aflr {
|
||||
n.aflr = 0
|
||||
// Quick sanity-check to confirm we're still leader.
|
||||
// In which case we must signal, since switchToLeader would not have done so already.
|
||||
@@ -1165,7 +1212,10 @@ func (n *raft) encodeSnapshot(snap *snapshot) []byte {
|
||||
// Should only be used when the upper layers know this is most recent.
|
||||
// Used when restoring streams, moving a stream from R1 to R>1, etc.
|
||||
func (n *raft) SendSnapshot(data []byte) error {
|
||||
n.sendAppendEntry([]*Entry{{EntrySnapshot, data}})
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
// Don't check if we're leader before sending and storing, this is used on scaleup.
|
||||
n.sendAppendEntryLocked([]*Entry{{EntrySnapshot, data}}, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1592,11 +1642,12 @@ func (n *raft) selectNextLeader() string {
|
||||
|
||||
// StepDown will have a leader stepdown and optionally do a leader transfer.
|
||||
func (n *raft) StepDown(preferred ...string) error {
|
||||
n.Lock()
|
||||
// Check state under lock, we might not be leader anymore.
|
||||
if n.State() != Leader {
|
||||
n.Unlock()
|
||||
return errNotLeader
|
||||
}
|
||||
|
||||
n.Lock()
|
||||
if len(preferred) > 1 {
|
||||
n.Unlock()
|
||||
return errTooManyPrefs
|
||||
@@ -1686,6 +1737,7 @@ func (n *raft) CampaignImmediately() error {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
n.maybeLeader = true
|
||||
n.resetInitializing()
|
||||
return n.campaign(minCampaignTimeout / 2)
|
||||
}
|
||||
|
||||
@@ -1775,21 +1827,27 @@ func (n *raft) Peers() []*Peer {
|
||||
|
||||
// Update and propose our known set of peers.
|
||||
func (n *raft) ProposeKnownPeers(knownPeers []string) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
// If we are the leader update and send this update out.
|
||||
if n.State() != Leader {
|
||||
return
|
||||
}
|
||||
n.UpdateKnownPeers(knownPeers)
|
||||
n.updateKnownPeersLocked(knownPeers)
|
||||
n.sendPeerState()
|
||||
}
|
||||
|
||||
// Update our known set of peers.
|
||||
func (n *raft) UpdateKnownPeers(knownPeers []string) {
|
||||
n.Lock()
|
||||
n.updateKnownPeersLocked(knownPeers)
|
||||
n.Unlock()
|
||||
}
|
||||
|
||||
func (n *raft) updateKnownPeersLocked(knownPeers []string) {
|
||||
// Process like peer state update.
|
||||
ps := &peerState{knownPeers, len(knownPeers), n.extSt}
|
||||
n.processPeerState(ps)
|
||||
n.Unlock()
|
||||
}
|
||||
|
||||
// ApplyQ returns the apply queue that new commits will be sent to for the
|
||||
@@ -1826,7 +1884,7 @@ func (n *raft) Delete() {
|
||||
defer n.Unlock()
|
||||
|
||||
if wal := n.wal; wal != nil {
|
||||
wal.Delete()
|
||||
wal.Delete(false)
|
||||
}
|
||||
os.RemoveAll(n.sd)
|
||||
n.debug("Deleted")
|
||||
@@ -2082,15 +2140,10 @@ func (n *raft) SetObserver(isObserver bool) {
|
||||
func (n *raft) setObserver(isObserver bool, extSt extensionState) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
n.setObserverLocked(isObserver, extSt)
|
||||
}
|
||||
|
||||
if n.paused {
|
||||
// Applies are paused so we're already in observer state.
|
||||
// Resuming the applies will set the state back to whatever
|
||||
// is in "pobserver", so update that instead.
|
||||
n.pobserver = isObserver
|
||||
return
|
||||
}
|
||||
|
||||
func (n *raft) setObserverLocked(isObserver bool, extSt extensionState) {
|
||||
wasObserver := n.observer
|
||||
n.observer = isObserver
|
||||
n.extSt = extSt
|
||||
@@ -2387,7 +2440,7 @@ func (ae *appendEntry) encode(b []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// This can not be used post the wire level callback since we do not copy.
|
||||
func (n *raft) decodeAppendEntry(msg []byte, sub *subscription, reply string) (*appendEntry, error) {
|
||||
func decodeAppendEntry(msg []byte, sub *subscription, reply string) (*appendEntry, error) {
|
||||
if len(msg) < appendEntryBaseLen {
|
||||
return nil, errBadAppendEntry
|
||||
}
|
||||
@@ -2472,7 +2525,7 @@ func (ar *appendEntryResponse) encode(b []byte) []byte {
|
||||
// Track all peers we may have ever seen to use an string interns for appendEntryResponse decoding.
|
||||
var peers sync.Map
|
||||
|
||||
func (n *raft) decodeAppendEntryResponse(msg []byte) *appendEntryResponse {
|
||||
func decodeAppendEntryResponse(msg []byte) *appendEntryResponse {
|
||||
if len(msg) != appendEntryResponseLen {
|
||||
return nil
|
||||
}
|
||||
@@ -2496,16 +2549,18 @@ func (n *raft) decodeAppendEntryResponse(msg []byte) *appendEntryResponse {
|
||||
func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) {
|
||||
n.debug("Received forwarded remove peer proposal: %q", msg)
|
||||
|
||||
if n.State() != Leader {
|
||||
n.debug("Ignoring forwarded peer removal proposal, not leader")
|
||||
return
|
||||
}
|
||||
if len(msg) != idLen {
|
||||
n.warn("Received invalid peer name for remove proposal: %q", msg)
|
||||
return
|
||||
}
|
||||
|
||||
n.RLock()
|
||||
// Check state under lock, we might not be leader anymore.
|
||||
if n.State() != Leader {
|
||||
n.debug("Ignoring forwarded peer removal proposal, not leader")
|
||||
n.RUnlock()
|
||||
return
|
||||
}
|
||||
prop, werr := n.prop, n.werr
|
||||
n.RUnlock()
|
||||
|
||||
@@ -2521,14 +2576,16 @@ func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _
|
||||
|
||||
// Called when a peer has forwarded a proposal.
|
||||
func (n *raft) handleForwardedProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) {
|
||||
if n.State() != Leader {
|
||||
n.debug("Ignoring forwarded proposal, not leader")
|
||||
return
|
||||
}
|
||||
// Need to copy since this is underlying client/route buffer.
|
||||
msg = copyBytes(msg)
|
||||
|
||||
n.RLock()
|
||||
// Check state under lock, we might not be leader anymore.
|
||||
if n.State() != Leader {
|
||||
n.debug("Ignoring forwarded proposal, not leader")
|
||||
n.RUnlock()
|
||||
return
|
||||
}
|
||||
prop, werr := n.prop, n.werr
|
||||
n.RUnlock()
|
||||
|
||||
@@ -2564,7 +2621,6 @@ func (n *raft) runAsLeader() {
|
||||
n.Unlock()
|
||||
return
|
||||
}
|
||||
n.Unlock()
|
||||
|
||||
// Cleanup our subscription when we leave.
|
||||
defer func() {
|
||||
@@ -2576,6 +2632,7 @@ func (n *raft) runAsLeader() {
|
||||
|
||||
// To send out our initial peer state.
|
||||
n.sendPeerState()
|
||||
n.Unlock()
|
||||
|
||||
hb := time.NewTicker(hbInterval)
|
||||
defer hb.Stop()
|
||||
@@ -2728,6 +2785,7 @@ func (n *raft) runCatchup(ar *appendEntryResponse, indexUpdatesQ *ipQueue[uint64
|
||||
n.RLock()
|
||||
s, reply := n.s, n.areply
|
||||
peer, subj, term, pterm, last := ar.peer, ar.reply, n.term, n.pterm, n.pindex
|
||||
leader := n.State() == Leader // Grab while holding lock, to not race.
|
||||
n.RUnlock()
|
||||
|
||||
defer s.grWG.Done()
|
||||
@@ -2749,6 +2807,10 @@ func (n *raft) runCatchup(ar *appendEntryResponse, indexUpdatesQ *ipQueue[uint64
|
||||
indexUpdatesQ.unregister()
|
||||
}()
|
||||
|
||||
if !leader {
|
||||
n.debug("Canceling catchup for %q, not leader anymore", peer)
|
||||
return
|
||||
}
|
||||
n.debug("Running catchup for %q [%d:%d] to [%d:%d]", peer, ar.term, ar.index, pterm, last)
|
||||
|
||||
const maxOutstanding = 2 * 1024 * 1024 // 2MB for now.
|
||||
@@ -2935,7 +2997,7 @@ func (n *raft) loadEntry(index uint64) (*appendEntry, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n.decodeAppendEntry(sm.msg, nil, _EMPTY_)
|
||||
return decodeAppendEntry(sm.msg, nil, _EMPTY_)
|
||||
}
|
||||
|
||||
// applyCommit will update our commit index and apply the entry to the apply queue.
|
||||
@@ -3189,6 +3251,7 @@ func (n *raft) runAsCandidate() {
|
||||
votes := map[string]struct{}{
|
||||
n.ID(): {},
|
||||
}
|
||||
emptyVotes := map[string]struct{}{}
|
||||
|
||||
for n.State() == Candidate {
|
||||
elect := n.electTimer()
|
||||
@@ -3216,16 +3279,28 @@ func (n *raft) runAsCandidate() {
|
||||
}
|
||||
n.RLock()
|
||||
nterm := n.term
|
||||
csz := n.csz
|
||||
n.RUnlock()
|
||||
|
||||
if vresp.granted && nterm == vresp.term {
|
||||
// only track peers that would be our followers
|
||||
n.trackPeer(vresp.peer)
|
||||
votes[vresp.peer] = struct{}{}
|
||||
if !vresp.empty {
|
||||
votes[vresp.peer] = struct{}{}
|
||||
} else {
|
||||
emptyVotes[vresp.peer] = struct{}{}
|
||||
}
|
||||
if n.wonElection(len(votes)) {
|
||||
// Become LEADER if we have won and gotten a quorum with everyone we should hear from.
|
||||
n.switchToLeader()
|
||||
return
|
||||
} else if len(votes)+len(emptyVotes) == csz {
|
||||
// Become LEADER if we've got voted in by ALL servers.
|
||||
// We couldn't get quorum based on just our normal votes.
|
||||
// But, we have heard from the full cluster, and some servers came up empty.
|
||||
// We know for sure we have the most up-to-date log.
|
||||
n.switchToLeader()
|
||||
return
|
||||
}
|
||||
} else if vresp.term > nterm {
|
||||
// if we observe a bigger term, we should start over again or risk forming a quorum fully knowing
|
||||
@@ -3252,7 +3327,7 @@ func (n *raft) runAsCandidate() {
|
||||
// is an internal callback from the "asubj" append entry subscription.
|
||||
func (n *raft) handleAppendEntry(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) {
|
||||
msg = copyBytes(msg)
|
||||
if ae, err := n.decodeAppendEntry(msg, sub, reply); err == nil {
|
||||
if ae, err := decodeAppendEntry(msg, sub, reply); err == nil {
|
||||
// Push to the new entry channel. From here one of the worker
|
||||
// goroutines (runAsLeader, runAsFollower, runAsCandidate) will
|
||||
// pick it up.
|
||||
@@ -3327,10 +3402,13 @@ func (n *raft) truncateWAL(term, index uint64) {
|
||||
}
|
||||
if index < n.commit {
|
||||
assert.Unreachable("WAL truncate lost commits", map[string]any{
|
||||
"term": term,
|
||||
"index": index,
|
||||
"commit": n.commit,
|
||||
"applied": n.applied,
|
||||
"n.accName": n.accName,
|
||||
"n.group": n.group,
|
||||
"n.id": n.id,
|
||||
"term": term,
|
||||
"index": index,
|
||||
"commit": n.commit,
|
||||
"applied": n.applied,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3345,8 +3423,11 @@ func (n *raft) truncateWAL(term, index uint64) {
|
||||
if n.commit > n.pindex {
|
||||
n.commit = n.pindex
|
||||
}
|
||||
if n.applied > n.commit {
|
||||
n.applied = n.commit
|
||||
if n.processed > n.commit {
|
||||
n.processed = n.commit
|
||||
}
|
||||
if n.applied > n.processed {
|
||||
n.applied = n.processed
|
||||
}
|
||||
if n.papplied > n.applied {
|
||||
n.papplied = n.applied
|
||||
@@ -3432,11 +3513,13 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) {
|
||||
assert.Unreachable(
|
||||
"Two leaders using the same term",
|
||||
map[string]any{
|
||||
"Node id": n.id,
|
||||
"Node term": n.term,
|
||||
"AppendEntry id": ae.leader,
|
||||
"AppendEntry term": ae.term,
|
||||
"AppendEntry lterm": ae.lterm,
|
||||
"n.accName": n.accName,
|
||||
"n.group": n.group,
|
||||
"n.id": n.id,
|
||||
"n.term": n.term,
|
||||
"ae.leader": ae.leader,
|
||||
"ae.term": ae.term,
|
||||
"ae.lterm": ae.lterm,
|
||||
})
|
||||
}
|
||||
n.debug("Received append entry from another leader, stepping down to %q", ae.leader)
|
||||
@@ -3660,6 +3743,7 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) {
|
||||
n.Unlock()
|
||||
return
|
||||
}
|
||||
n.resetInitializing()
|
||||
|
||||
// Now send snapshot to upper levels. Only send the snapshot, not the peerstate entry.
|
||||
n.apply.push(newCommittedEntry(n.commit, ae.entries[:1]))
|
||||
@@ -3690,6 +3774,7 @@ CONTINUE:
|
||||
return
|
||||
}
|
||||
n.cachePendingEntry(ae)
|
||||
n.resetInitializing()
|
||||
} else {
|
||||
// This is a replay on startup so just take the appendEntry version.
|
||||
n.pterm = ae.term
|
||||
@@ -3710,7 +3795,7 @@ CONTINUE:
|
||||
if !n.observer && !n.paused {
|
||||
n.lxfer = true
|
||||
n.xferCampaign()
|
||||
} else if n.paused && !n.pobserver {
|
||||
} else if n.paused {
|
||||
// Here we can become a leader but need to wait for resume of the apply queue.
|
||||
n.lxfer = true
|
||||
}
|
||||
@@ -3768,6 +3853,17 @@ CONTINUE:
|
||||
}
|
||||
}
|
||||
|
||||
// resetInitializing resets the notion of initializing.
|
||||
// If we were scaling up, also leaves observer mode.
|
||||
// Lock should be held.
|
||||
func (n *raft) resetInitializing() {
|
||||
n.initializing = false
|
||||
if n.scaleUp {
|
||||
n.scaleUp = false
|
||||
n.setObserverLocked(false, extUndetermined)
|
||||
}
|
||||
}
|
||||
|
||||
// processPeerState is called when a peer state entry is received
|
||||
// over the wire or when we're updating known peers.
|
||||
// Lock should be held.
|
||||
@@ -3828,7 +3924,7 @@ func (n *raft) processAppendEntryResponse(ar *appendEntryResponse) {
|
||||
|
||||
// handleAppendEntryResponse processes responses to append entries.
|
||||
func (n *raft) handleAppendEntryResponse(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) {
|
||||
ar := n.decodeAppendEntryResponse(msg)
|
||||
ar := decodeAppendEntryResponse(msg)
|
||||
ar.reply = reply
|
||||
n.resp.push(ar)
|
||||
}
|
||||
@@ -3892,6 +3988,15 @@ const (
|
||||
func (n *raft) sendAppendEntry(entries []*Entry) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
n.sendAppendEntryLocked(entries, true)
|
||||
}
|
||||
func (n *raft) sendAppendEntryLocked(entries []*Entry, checkLeader bool) {
|
||||
// Safeguard against sending an append entry right after a stepdown from a different goroutine.
|
||||
// Specifically done while holding the lock to not race.
|
||||
if checkLeader && n.State() != Leader {
|
||||
n.debug("Not sending append entry, not leader")
|
||||
return
|
||||
}
|
||||
ae := n.buildAppendEntry(entries)
|
||||
|
||||
var err error
|
||||
@@ -3998,14 +4103,19 @@ func (n *raft) peerNames() []string {
|
||||
|
||||
func (n *raft) currentPeerState() *peerState {
|
||||
n.RLock()
|
||||
ps := &peerState{n.peerNames(), n.csz, n.extSt}
|
||||
ps := n.currentPeerStateLocked()
|
||||
n.RUnlock()
|
||||
return ps
|
||||
}
|
||||
|
||||
func (n *raft) currentPeerStateLocked() *peerState {
|
||||
return &peerState{n.peerNames(), n.csz, n.extSt}
|
||||
}
|
||||
|
||||
// sendPeerState will send our current peer state to the cluster.
|
||||
// Lock should be held.
|
||||
func (n *raft) sendPeerState() {
|
||||
n.sendAppendEntry([]*Entry{{EntryPeerState, encodePeerState(n.currentPeerState())}})
|
||||
n.sendAppendEntryLocked([]*Entry{{EntryPeerState, encodePeerState(n.currentPeerStateLocked())}}, true)
|
||||
}
|
||||
|
||||
// Send a heartbeat.
|
||||
@@ -4198,6 +4308,7 @@ type voteResponse struct {
|
||||
term uint64
|
||||
peer string
|
||||
granted bool
|
||||
empty bool // "Empty vote", whether this peer's log is empty.
|
||||
}
|
||||
|
||||
const voteResponseLen = 8 + 8 + 1
|
||||
@@ -4208,9 +4319,10 @@ func (vr *voteResponse) encode() []byte {
|
||||
le.PutUint64(buf[0:], vr.term)
|
||||
copy(buf[8:], vr.peer)
|
||||
if vr.granted {
|
||||
buf[16] = 1
|
||||
} else {
|
||||
buf[16] = 0
|
||||
buf[16] |= 1
|
||||
}
|
||||
if vr.empty {
|
||||
buf[16] |= 2
|
||||
}
|
||||
return buf[:voteResponseLen]
|
||||
}
|
||||
@@ -4221,7 +4333,8 @@ func decodeVoteResponse(msg []byte) *voteResponse {
|
||||
}
|
||||
var le = binary.LittleEndian
|
||||
vr := &voteResponse{term: le.Uint64(msg[0:]), peer: string(msg[8:16])}
|
||||
vr.granted = msg[16] == 1
|
||||
vr.granted = msg[16]&1 != 0
|
||||
vr.empty = msg[16]&2 != 0
|
||||
return vr
|
||||
}
|
||||
|
||||
@@ -4255,7 +4368,7 @@ func (n *raft) processVoteRequest(vr *voteRequest) error {
|
||||
|
||||
n.Lock()
|
||||
|
||||
vresp := &voteResponse{n.term, n.id, false}
|
||||
vresp := &voteResponse{n.term, n.id, false, n.pindex == 0}
|
||||
defer n.debug("Sending a voteResponse %+v -> %q", vresp, vr.reply)
|
||||
|
||||
// Ignore if we are newer. This is important so that we don't accidentally process
|
||||
@@ -4281,6 +4394,15 @@ func (n *raft) processVoteRequest(vr *voteRequest) error {
|
||||
|
||||
// Only way we get to yes is through here.
|
||||
voteOk := n.vote == noVote || n.vote == vr.candidate
|
||||
|
||||
// If we have an empty log, but are initializing.
|
||||
if voteOk && vresp.empty && n.initializing {
|
||||
// Reset notion of having an empty log if we're voting during initialization/scale up.
|
||||
// Ensures they only need quorum, and not need to hear from all servers.
|
||||
vresp.empty = false
|
||||
}
|
||||
|
||||
// Other server's log needs to be equal or more up-to-date than ours.
|
||||
if voteOk && (vr.lastTerm > n.pterm || vr.lastTerm == n.pterm && vr.lastIndex >= n.pindex) {
|
||||
vresp.granted = true
|
||||
n.term = vr.term
|
||||
@@ -4450,7 +4572,7 @@ func (n *raft) switchToCandidate() {
|
||||
|
||||
// If we are catching up or are in observer mode we can not switch.
|
||||
// Avoid petitioning to become leader if we're behind on applies.
|
||||
if n.observer || n.paused || n.applied < n.commit {
|
||||
if n.observer || n.paused || n.processed < n.commit {
|
||||
n.resetElect(minElectionTimeout / 4)
|
||||
return
|
||||
}
|
||||
|
||||
+123
-10
@@ -369,6 +369,19 @@ func (u *tagsOption) IsStatszChange() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// metadataOption implements the option interface for the `metadata` setting.
|
||||
type metadataOption struct {
|
||||
noopOption // Not authOption because this is a no-op; will be reloaded with options.
|
||||
}
|
||||
|
||||
func (u *metadataOption) Apply(server *Server) {
|
||||
server.Noticef("Reloaded: metadata")
|
||||
}
|
||||
|
||||
func (u *metadataOption) IsStatszChange() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// usersOption implements the option interface for the authorization `users`
|
||||
// setting.
|
||||
type usersOption struct {
|
||||
@@ -862,6 +875,7 @@ type leafNodeOption struct {
|
||||
noopOption
|
||||
tlsFirstChanged bool
|
||||
compressionChanged bool
|
||||
disabledChanged bool
|
||||
}
|
||||
|
||||
func (l *leafNodeOption) Apply(s *Server) {
|
||||
@@ -873,8 +887,9 @@ func (l *leafNodeOption) Apply(s *Server) {
|
||||
s.Noticef("Reloaded: LeafNode Remote to %v TLS HandshakeFirst value is: %v", r.URLs, r.TLSHandshakeFirst)
|
||||
}
|
||||
}
|
||||
if l.compressionChanged {
|
||||
if l.compressionChanged || l.disabledChanged {
|
||||
var leafs []*client
|
||||
var solicit []*leafNodeCfg
|
||||
acceptSideCompOpts := &opts.LeafNode.Compression
|
||||
|
||||
s.mu.RLock()
|
||||
@@ -887,10 +902,15 @@ func (l *leafNodeOption) Apply(s *Server) {
|
||||
if l := len(s.leafRemoteCfgs); l < max {
|
||||
max = l
|
||||
}
|
||||
for i := 0; i < max; i++ {
|
||||
for i := range max {
|
||||
lr := s.leafRemoteCfgs[i]
|
||||
or := opts.LeafNode.Remotes[i]
|
||||
lr.Lock()
|
||||
lr.Compression = opts.LeafNode.Remotes[i].Compression
|
||||
lr.Compression = or.Compression
|
||||
if lr.Disabled && !or.Disabled {
|
||||
solicit = append(solicit, lr)
|
||||
}
|
||||
lr.Disabled = or.Disabled
|
||||
lr.Unlock()
|
||||
}
|
||||
|
||||
@@ -899,6 +919,13 @@ func (l *leafNodeOption) Apply(s *Server) {
|
||||
|
||||
l.mu.Lock()
|
||||
if r := l.leaf.remote; r != nil {
|
||||
// If newly marked as disabled, collect and ignore the rest.
|
||||
if r.Disabled {
|
||||
l.flags.set(noReconnect)
|
||||
leafs = append(leafs, l)
|
||||
l.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
co = &r.Compression
|
||||
} else {
|
||||
co = acceptSideCompOpts
|
||||
@@ -929,11 +956,25 @@ func (l *leafNodeOption) Apply(s *Server) {
|
||||
l.mu.Unlock()
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
// Close the connections for which negotiation is required.
|
||||
// Close the connections for which negotiation is required, or that
|
||||
// have been disabled.
|
||||
for _, l := range leafs {
|
||||
l.closeConnection(ClientClosed)
|
||||
}
|
||||
s.Noticef("Reloaded: LeafNode compression settings")
|
||||
if l.compressionChanged {
|
||||
s.Noticef("Reloaded: LeafNode compression settings")
|
||||
}
|
||||
if l.disabledChanged {
|
||||
if len(leafs) > 0 {
|
||||
s.Noticef("Reloaded: LeafNode(s) disabled")
|
||||
}
|
||||
if len(solicit) > 0 {
|
||||
for _, remote := range solicit {
|
||||
s.startGoRoutine(func() { s.connectToRemoteLeafNode(remote, true) })
|
||||
}
|
||||
s.Noticef("Reloaded: LeafNode(s) enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1011,6 +1052,38 @@ func (s *Server) recheckPinnedCerts(curOpts *Options, newOpts *Options) {
|
||||
}
|
||||
}
|
||||
|
||||
type proxiesReload struct {
|
||||
noopOption
|
||||
add []string
|
||||
del []string
|
||||
}
|
||||
|
||||
func (p *proxiesReload) Apply(s *Server) {
|
||||
var clients []*client
|
||||
s.mu.Lock()
|
||||
for _, k := range p.del {
|
||||
cc := s.proxiedConns[k]
|
||||
delete(s.proxiedConns, k)
|
||||
if len(cc) > 0 {
|
||||
for _, c := range cc {
|
||||
clients = append(clients, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.processProxiesTrustedKeys()
|
||||
s.mu.Unlock()
|
||||
if len(p.del) > 0 {
|
||||
for _, c := range clients {
|
||||
c.setAuthError(ErrAuthProxyNotTrusted)
|
||||
c.authViolation()
|
||||
}
|
||||
s.Noticef("Reloaded: proxies trusted keys %q were removed", p.add)
|
||||
}
|
||||
if len(p.add) > 0 {
|
||||
s.Noticef("Reloaded: proxies trusted keys %q were added", p.add)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload reads the current configuration file and calls out to ReloadOptions
|
||||
// to apply the changes. This returns an error if the server was not started
|
||||
// with a config file or an option which doesn't support hot-swapping was changed.
|
||||
@@ -1186,7 +1259,7 @@ func imposeOrder(value any) error {
|
||||
slices.Sort(value.AllowedOrigins)
|
||||
case string, bool, uint8, uint16, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, PinnedCertSet,
|
||||
*URLAccResolver, *MemAccResolver, *DirAccResolver, *CacheDirAccResolver, Authentication, MQTTOpts, jwt.TagList,
|
||||
*OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig:
|
||||
*OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig, *ProxiesConfig:
|
||||
// explicitly skipped types
|
||||
case *AuthCallout:
|
||||
case JSTpmOpts:
|
||||
@@ -1283,6 +1356,8 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
|
||||
diffOpts = append(diffOpts, &passwordOption{})
|
||||
case "tags":
|
||||
diffOpts = append(diffOpts, &tagsOption{})
|
||||
case "metadata":
|
||||
diffOpts = append(diffOpts, &metadataOption{})
|
||||
case "authorization":
|
||||
diffOpts = append(diffOpts, &authorizationOption{})
|
||||
case "authtimeout":
|
||||
@@ -1300,7 +1375,7 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
|
||||
co := &clusterOption{
|
||||
newValue: newClusterOpts,
|
||||
permsChanged: !reflect.DeepEqual(newClusterOpts.Permissions, oldClusterOpts.Permissions),
|
||||
compressChanged: !reflect.DeepEqual(oldClusterOpts.Compression, newClusterOpts.Compression),
|
||||
compressChanged: !compressOptsEqual(&oldClusterOpts.Compression, &newClusterOpts.Compression),
|
||||
}
|
||||
co.diffPoolAndAccounts(&oldClusterOpts)
|
||||
// If there are added accounts, first make sure that we can look them up.
|
||||
@@ -1413,18 +1488,26 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
|
||||
}
|
||||
// We also support config reload for compression. Check if it changed before
|
||||
// blanking them out for the deep-equal check at the end.
|
||||
compressionChanged := !reflect.DeepEqual(tmpOld.Compression, tmpNew.Compression)
|
||||
compressionChanged := !compressOptsEqual(&tmpOld.Compression, &tmpNew.Compression)
|
||||
if compressionChanged {
|
||||
tmpOld.Compression, tmpNew.Compression = CompressionOpts{}, CompressionOpts{}
|
||||
} else if len(tmpOld.Remotes) == len(tmpNew.Remotes) {
|
||||
// Same that for tls first check, do the remotes now.
|
||||
for i := 0; i < len(tmpOld.Remotes); i++ {
|
||||
if !reflect.DeepEqual(tmpOld.Remotes[i].Compression, tmpNew.Remotes[i].Compression) {
|
||||
for i := range len(tmpOld.Remotes) {
|
||||
if !compressOptsEqual(&tmpOld.Remotes[i].Compression, &tmpNew.Remotes[i].Compression) {
|
||||
compressionChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if the "disabled" option of each remote has changed.
|
||||
var disabledChanged bool
|
||||
for i := range len(tmpOld.Remotes) {
|
||||
if tmpOld.Remotes[i].Disabled != tmpNew.Remotes[i].Disabled {
|
||||
disabledChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Need to do the same for remote leafnodes' TLS configs.
|
||||
// But we can't just set remotes' TLSConfig to nil otherwise this
|
||||
@@ -1517,6 +1600,7 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
|
||||
diffOpts = append(diffOpts, &leafNodeOption{
|
||||
tlsFirstChanged: handshakeFirstChanged,
|
||||
compressionChanged: compressionChanged,
|
||||
disabledChanged: disabledChanged,
|
||||
})
|
||||
case "jetstream":
|
||||
new := newValue.(bool)
|
||||
@@ -1670,6 +1754,12 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
|
||||
continue
|
||||
case "nofastproducerstall":
|
||||
diffOpts = append(diffOpts, &noFastProdStallReload{noStall: newValue.(bool)})
|
||||
case "proxies":
|
||||
new := newValue.(*ProxiesConfig)
|
||||
old := oldValue.(*ProxiesConfig)
|
||||
if add, del := diffProxiesTrustedKeys(old.Trusted, new.Trusted); len(add) > 0 || len(del) > 0 {
|
||||
diffOpts = append(diffOpts, &proxiesReload{add: add, del: del})
|
||||
}
|
||||
default:
|
||||
// TODO(ik): Implement String() on those options to have a nice print.
|
||||
// %v is difficult to figure what's what, %+v print private fields and
|
||||
@@ -1729,6 +1819,8 @@ func copyRemoteLNConfigForReloadCompare(current []*RemoteLeafOpts) []*RemoteLeaf
|
||||
cp.DenyImports, cp.DenyExports = nil, nil
|
||||
// Remove compression mode
|
||||
cp.Compression = CompressionOpts{}
|
||||
// Reset disabled status
|
||||
cp.Disabled = false
|
||||
rlns = append(rlns, &cp)
|
||||
}
|
||||
return rlns
|
||||
@@ -2535,3 +2627,24 @@ addLoop:
|
||||
|
||||
return add, remove
|
||||
}
|
||||
|
||||
func diffProxiesTrustedKeys(old, new []*ProxyConfig) ([]string, []string) {
|
||||
var add []string
|
||||
var del []string
|
||||
// Both "old" and "new" lists should be small...
|
||||
for _, op := range old {
|
||||
if !slices.ContainsFunc(new, func(pc *ProxyConfig) bool {
|
||||
return pc.Key == op.Key
|
||||
}) {
|
||||
del = append(del, op.Key)
|
||||
}
|
||||
}
|
||||
for _, np := range new {
|
||||
if !slices.ContainsFunc(old, func(pc *ProxyConfig) bool {
|
||||
return pc.Key == np.Key
|
||||
}) {
|
||||
add = append(add, np.Key)
|
||||
}
|
||||
}
|
||||
return add, del
|
||||
}
|
||||
|
||||
+11
-2
@@ -143,6 +143,7 @@ const (
|
||||
// Can be changed for tests
|
||||
var (
|
||||
routeConnectDelay = DEFAULT_ROUTE_CONNECT
|
||||
routeConnectMaxDelay = DEFAULT_ROUTE_CONNECT_MAX
|
||||
routeMaxPingInterval = defaultRouteMaxPingInterval
|
||||
)
|
||||
|
||||
@@ -2054,7 +2055,7 @@ func (s *Server) createRoute(conn net.Conn, rURL *url.URL, rtype RouteType, goss
|
||||
if tlsRequired {
|
||||
c.Debugf("TLS handshake complete")
|
||||
cs := c.nc.(*tls.Conn).ConnectionState()
|
||||
c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite))
|
||||
c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite))
|
||||
}
|
||||
|
||||
// Queue Connect proto if we solicited the connection.
|
||||
@@ -2893,6 +2894,7 @@ func (s *Server) connectToRoute(rURL *url.URL, rtype RouteType, firstConnect boo
|
||||
excludedAddresses := s.routesToSelf
|
||||
s.mu.RUnlock()
|
||||
|
||||
attemptDelay := routeConnectDelay
|
||||
for attempts := 0; s.isRunning(); {
|
||||
if tryForEver {
|
||||
if !s.routeStillValid(rURL) {
|
||||
@@ -2935,7 +2937,14 @@ func (s *Server) connectToRoute(rURL *url.URL, rtype RouteType, firstConnect boo
|
||||
select {
|
||||
case <-s.quitCh:
|
||||
return
|
||||
case <-time.After(routeConnectDelay):
|
||||
case <-time.After(attemptDelay):
|
||||
if opts.Cluster.ConnectBackoff {
|
||||
// Use exponential backoff for connection attempts.
|
||||
attemptDelay *= 2
|
||||
if attemptDelay > routeConnectMaxDelay {
|
||||
attemptDelay = routeConnectMaxDelay
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
+239
@@ -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
@@ -30,6 +30,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
@@ -129,6 +130,10 @@ type Info struct {
|
||||
WSConnectURLs []string `json:"ws_connect_urls,omitempty"` // Contains URLs a ws client can connect to.
|
||||
LameDuckMode bool `json:"ldm,omitempty"`
|
||||
Compression string `json:"compression,omitempty"`
|
||||
ConnectInfo bool `json:"connect_info,omitempty"` // When true this is the server INFO response to CONNECT
|
||||
RemoteAccount string `json:"remote_account,omitempty"` // Lets the client or leafnode side know the remote account that they bind to.
|
||||
IsSystemAccount bool `json:"acc_is_sys,omitempty"` // Indicates if the account is a system account.
|
||||
JSApiLevel int `json:"api_lvl,omitempty"`
|
||||
|
||||
// Route Specific
|
||||
Import *SubjectPermission `json:"import,omitempty"`
|
||||
@@ -136,7 +141,6 @@ type Info struct {
|
||||
LNOC bool `json:"lnoc,omitempty"`
|
||||
LNOCU bool `json:"lnocu,omitempty"`
|
||||
InfoOnConnect bool `json:"info_on_connect,omitempty"` // When true the server will respond to CONNECT with an INFO
|
||||
ConnectInfo bool `json:"connect_info,omitempty"` // When true this is the server INFO response to CONNECT
|
||||
RoutePoolSize int `json:"route_pool_size,omitempty"`
|
||||
RoutePoolIdx int `json:"route_pool_idx,omitempty"`
|
||||
RouteAccount string `json:"route_account,omitempty"`
|
||||
@@ -153,8 +157,7 @@ type Info struct {
|
||||
GatewayIOM bool `json:"gateway_iom,omitempty"` // Indicate that all accounts will be switched to InterestOnly mode "right away"
|
||||
|
||||
// LeafNode Specific
|
||||
LeafNodeURLs []string `json:"leafnode_urls,omitempty"` // LeafNode URLs that the server can reconnect to.
|
||||
RemoteAccount string `json:"remote_account,omitempty"` // Lets the other side know the remote account that they bind to.
|
||||
LeafNodeURLs []string `json:"leafnode_urls,omitempty"` // LeafNode URLs that the server can reconnect to.
|
||||
|
||||
XKey string `json:"xkey,omitempty"` // Public server's x25519 key.
|
||||
}
|
||||
@@ -167,6 +170,7 @@ type Server struct {
|
||||
pinnedAccFail uint64
|
||||
stats
|
||||
scStats
|
||||
staleStats
|
||||
mu sync.RWMutex
|
||||
reloadMu sync.RWMutex // Write-locked when a config reload is taking place ONLY
|
||||
kp nkeys.KeyPair
|
||||
@@ -259,8 +263,6 @@ type Server struct {
|
||||
// Used internally for quick look-ups.
|
||||
clientConnectURLsMap refCountedUrlSet
|
||||
|
||||
lastCURLsUpdate int64
|
||||
|
||||
// For Gateways
|
||||
gatewayListener net.Listener // Accept listener
|
||||
gatewayListenerErr error
|
||||
@@ -371,6 +373,11 @@ type Server struct {
|
||||
// Controls whether or not the account NRG capability is set in statsz.
|
||||
// Currently used by unit tests to simulate nodes not supporting account NRG.
|
||||
accountNRGAllowed atomic.Bool
|
||||
|
||||
// List of proxies trusted keys in `KeyPair` form so we can do signature
|
||||
// verification when processing incoming proxy connections.
|
||||
proxiesKeyPairs []nkeys.KeyPair
|
||||
proxiedConns map[string]map[uint64]*client
|
||||
}
|
||||
|
||||
// For tracking JS nodes.
|
||||
@@ -390,11 +397,13 @@ type nodeInfo struct {
|
||||
}
|
||||
|
||||
type stats struct {
|
||||
inMsgs int64
|
||||
outMsgs int64
|
||||
inBytes int64
|
||||
outBytes int64
|
||||
slowConsumers int64
|
||||
inMsgs int64
|
||||
outMsgs int64
|
||||
inBytes int64
|
||||
outBytes int64
|
||||
slowConsumers int64
|
||||
staleConnections int64
|
||||
stalls int64
|
||||
}
|
||||
|
||||
// scStats includes the total and per connection counters of Slow Consumers.
|
||||
@@ -405,6 +414,14 @@ type scStats struct {
|
||||
gateways atomic.Uint64
|
||||
}
|
||||
|
||||
// staleStats includes the total and per connection counters of Stale Connections.
|
||||
type staleStats struct {
|
||||
clients atomic.Uint64
|
||||
routes atomic.Uint64
|
||||
leafs atomic.Uint64
|
||||
gateways atomic.Uint64
|
||||
}
|
||||
|
||||
// This is used by tests so we can run all server tests with a default route
|
||||
// or leafnode compression mode. For instance:
|
||||
// go test -race -v ./server -cluster_compression=fast
|
||||
@@ -625,6 +642,32 @@ func selectS2AutoModeBasedOnRTT(rtt time.Duration, rttThresholds []time.Duration
|
||||
return CompressionS2Best
|
||||
}
|
||||
|
||||
func compressOptsEqual(c1, c2 *CompressionOpts) bool {
|
||||
if c1 == c2 {
|
||||
return true
|
||||
}
|
||||
if (c1 == nil && c2 != nil) || (c1 != nil && c2 == nil) {
|
||||
return false
|
||||
}
|
||||
if c1.Mode != c2.Mode {
|
||||
return false
|
||||
}
|
||||
// For s2_auto, if one has an empty RTTThresholds, it is equivalent
|
||||
// to the defaultCompressionS2AutoRTTThresholds array, so compare with that.
|
||||
if c1.Mode == CompressionS2Auto {
|
||||
if len(c1.RTTThresholds) == 0 && !reflect.DeepEqual(c2.RTTThresholds, defaultCompressionS2AutoRTTThresholds) {
|
||||
return false
|
||||
}
|
||||
if len(c2.RTTThresholds) == 0 && !reflect.DeepEqual(c1.RTTThresholds, defaultCompressionS2AutoRTTThresholds) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(c1.RTTThresholds, c2.RTTThresholds) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Returns an array of s2 WriterOption based on the route compression mode.
|
||||
// So far we return a single option, but this way we can call s2.NewWriter()
|
||||
// with a nil []s2.WriterOption, but not with a nil s2.WriterOption, so
|
||||
@@ -706,6 +749,7 @@ func NewServer(opts *Options) (*Server, error) {
|
||||
Headers: !opts.NoHeaderSupport,
|
||||
Cluster: opts.Cluster.Name,
|
||||
Domain: opts.JetStreamDomain,
|
||||
JSApiLevel: JSApiLevel,
|
||||
}
|
||||
|
||||
if tlsReq && !info.TLSRequired {
|
||||
@@ -774,6 +818,11 @@ func NewServer(opts *Options) (*Server, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// If there are proxies trusted public keys in the configuration
|
||||
// this will fill create the corresponding list of nkeys.KeyPair
|
||||
// that we can use for signature verification.
|
||||
s.processProxiesTrustedKeys()
|
||||
|
||||
// Place ourselves in the JetStream nodeInfo if needed.
|
||||
if opts.JetStream {
|
||||
ourNode := getHash(serverName)
|
||||
@@ -1099,6 +1148,10 @@ func validateOptions(o *Options) error {
|
||||
if err := validateAuth(o); err != nil {
|
||||
return err
|
||||
}
|
||||
// Check that proxies is properly configured.
|
||||
if err := validateProxies(o); err != nil {
|
||||
return err
|
||||
}
|
||||
// Check that gateway is properly configured. Returns no error
|
||||
// if there is no gateway defined.
|
||||
if err := validateGatewayOptions(o); err != nil {
|
||||
@@ -2352,7 +2405,7 @@ func (s *Server) Start() {
|
||||
StoreDir: opts.StoreDir,
|
||||
SyncInterval: opts.SyncInterval,
|
||||
SyncAlways: opts.SyncAlways,
|
||||
Strict: opts.JetStreamStrict,
|
||||
Strict: !opts.NoJetStreamStrict,
|
||||
MaxMemory: opts.JetStreamMaxMemory,
|
||||
MaxStore: opts.JetStreamMaxStore,
|
||||
Domain: opts.JetStreamDomain,
|
||||
@@ -3396,7 +3449,7 @@ func (s *Server) createClientEx(conn net.Conn, inProcess bool) *client {
|
||||
if tlsRequired {
|
||||
c.Debugf("TLS handshake complete")
|
||||
cs := c.nc.(*tls.Conn).ConnectionState()
|
||||
c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tlsCipher(cs.CipherSuite))
|
||||
c.Debugf("TLS version %s, cipher suite %s", tlsVersion(cs.Version), tls.CipherSuiteName(cs.CipherSuite))
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
@@ -3501,8 +3554,6 @@ func (s *Server) updateServerINFOAndSendINFOToClients(curls, wsurls []string, ad
|
||||
updateInfo(&s.info.WSConnectURLs, s.websocket.connectURLs, s.websocket.connectURLsMap)
|
||||
}
|
||||
if cliUpdated || wsUpdated {
|
||||
// Update the time of this update
|
||||
s.lastCURLsUpdate = time.Now().UnixNano()
|
||||
// Send to all registered clients that support async INFO protocols.
|
||||
s.sendAsyncInfoToClients(cliUpdated, wsUpdated)
|
||||
}
|
||||
@@ -3554,15 +3605,6 @@ func tlsVersionFromString(ver string) (uint16, error) {
|
||||
return 0, fmt.Errorf("unknown version: %v", ver)
|
||||
}
|
||||
|
||||
// We use hex here so we don't need multiple versions
|
||||
func tlsCipher(cs uint16) string {
|
||||
name, present := cipherMapByID[cs]
|
||||
if present {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("Unknown [0x%x]", cs)
|
||||
}
|
||||
|
||||
// Remove a client or route from our internal accounting.
|
||||
func (s *Server) removeClient(c *client) {
|
||||
// kind is immutable, so can check without lock
|
||||
@@ -3574,6 +3616,7 @@ func (s *Server) removeClient(c *client) {
|
||||
if c.kind == CLIENT && c.opts.Protocol >= ClientProtoInfo {
|
||||
updateProtoInfoCount = true
|
||||
}
|
||||
proxyKey := c.proxyKey
|
||||
c.mu.Unlock()
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -3581,6 +3624,9 @@ func (s *Server) removeClient(c *client) {
|
||||
if updateProtoInfoCount {
|
||||
s.cproto--
|
||||
}
|
||||
if proxyKey != _EMPTY_ {
|
||||
s.removeProxiedConn(proxyKey, cid)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
case ROUTER:
|
||||
s.removeRoute(c)
|
||||
@@ -3591,6 +3637,18 @@ func (s *Server) removeClient(c *client) {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the connection with id `cid` from the map of connections
|
||||
// under the public key `key` of the trusted proxies.
|
||||
//
|
||||
// Server lock must be held on entry.
|
||||
func (s *Server) removeProxiedConn(key string, cid uint64) {
|
||||
conns := s.proxiedConns[key]
|
||||
delete(conns, cid)
|
||||
if len(conns) == 0 {
|
||||
delete(s.proxiedConns, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) removeFromTempClients(cid uint64) {
|
||||
s.grMu.Lock()
|
||||
delete(s.grTmpClients, cid)
|
||||
@@ -3699,6 +3757,11 @@ func (s *Server) NumSlowConsumers() int64 {
|
||||
return atomic.LoadInt64(&s.slowConsumers)
|
||||
}
|
||||
|
||||
// NumStalledClients will report the total number of times clients have been stalled.
|
||||
func (s *Server) NumStalledClients() int64 {
|
||||
return atomic.LoadInt64(&s.stalls)
|
||||
}
|
||||
|
||||
// NumSlowConsumersClients will report the number of slow consumers clients.
|
||||
func (s *Server) NumSlowConsumersClients() uint64 {
|
||||
return s.scStats.clients.Load()
|
||||
@@ -3719,6 +3782,31 @@ func (s *Server) NumSlowConsumersLeafs() uint64 {
|
||||
return s.scStats.leafs.Load()
|
||||
}
|
||||
|
||||
// NumStaleConnections will report the number of stale connections.
|
||||
func (s *Server) NumStaleConnections() int64 {
|
||||
return atomic.LoadInt64(&s.staleConnections)
|
||||
}
|
||||
|
||||
// NumStaleConnectionsClients will report the number of stale client connections.
|
||||
func (s *Server) NumStaleConnectionsClients() uint64 {
|
||||
return s.staleStats.clients.Load()
|
||||
}
|
||||
|
||||
// NumStaleConnectionsRoutes will report the number of stale route connections.
|
||||
func (s *Server) NumStaleConnectionsRoutes() uint64 {
|
||||
return s.staleStats.routes.Load()
|
||||
}
|
||||
|
||||
// NumStaleConnectionsGateways will report the number of stale gateway connections.
|
||||
func (s *Server) NumStaleConnectionsGateways() uint64 {
|
||||
return s.staleStats.gateways.Load()
|
||||
}
|
||||
|
||||
// NumStaleConnectionsLeafs will report the number of stale leaf connections.
|
||||
func (s *Server) NumStaleConnectionsLeafs() uint64 {
|
||||
return s.staleStats.leafs.Load()
|
||||
}
|
||||
|
||||
// ConfigTime will report the last time the server configuration was loaded.
|
||||
func (s *Server) ConfigTime() time.Time {
|
||||
s.mu.RLock()
|
||||
|
||||
+9
-15
@@ -35,8 +35,6 @@ const (
|
||||
FileStorage = StorageType(22)
|
||||
// MemoryStorage specifies in memory only.
|
||||
MemoryStorage = StorageType(33)
|
||||
// Any is for internals.
|
||||
AnyStorage = StorageType(44)
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -86,14 +84,16 @@ type StorageUpdateHandler func(msgs, bytes int64, seq uint64, subj string)
|
||||
// Used to call back into the upper layers to remove a message.
|
||||
type StorageRemoveMsgHandler func(seq uint64)
|
||||
|
||||
// Used to call back into the upper layers to report on newly created subject delete markers.
|
||||
type SubjectDeleteMarkerUpdateHandler func(*inMsg)
|
||||
// Used to call back into the upper layers to process a JetStream message.
|
||||
// Will propose the message if the stream is replicated.
|
||||
type ProcessJetStreamMsgHandler func(*inMsg)
|
||||
|
||||
type StreamStore interface {
|
||||
StoreMsg(subject string, hdr, msg []byte, ttl int64) (uint64, int64, error)
|
||||
StoreRawMsg(subject string, hdr, msg []byte, seq uint64, ts int64, ttl int64) error
|
||||
SkipMsg() uint64
|
||||
SkipMsgs(seq uint64, num uint64) error
|
||||
FlushAllPending()
|
||||
LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error)
|
||||
LoadNextMsg(filter string, wc bool, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error)
|
||||
LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error)
|
||||
@@ -120,16 +120,17 @@ type StreamStore interface {
|
||||
SyncDeleted(dbs DeleteBlocks)
|
||||
Type() StorageType
|
||||
RegisterStorageUpdates(StorageUpdateHandler)
|
||||
RegisterStorageRemoveMsg(handler StorageRemoveMsgHandler)
|
||||
RegisterSubjectDeleteMarkerUpdates(SubjectDeleteMarkerUpdateHandler)
|
||||
RegisterStorageRemoveMsg(StorageRemoveMsgHandler)
|
||||
RegisterProcessJetStreamMsg(ProcessJetStreamMsgHandler)
|
||||
UpdateConfig(cfg *StreamConfig) error
|
||||
Delete() error
|
||||
Delete(inline bool) error
|
||||
Stop() error
|
||||
ConsumerStore(name string, cfg *ConsumerConfig) (ConsumerStore, error)
|
||||
AddConsumer(o ConsumerStore) error
|
||||
RemoveConsumer(o ConsumerStore) error
|
||||
Snapshot(deadline time.Duration, includeConsumers, checkMsgs bool) (*SnapshotResult, error)
|
||||
Utilization() (total, reported uint64, err error)
|
||||
ResetState()
|
||||
}
|
||||
|
||||
// RetentionPolicy determines how messages in a set are retained.
|
||||
@@ -461,6 +462,7 @@ type Pending struct {
|
||||
}
|
||||
|
||||
// TemplateStore stores templates.
|
||||
// Deprecated: stream templates are deprecated and will be removed in a future version.
|
||||
type TemplateStore interface {
|
||||
Store(*streamTemplate) error
|
||||
Delete(*streamTemplate) error
|
||||
@@ -555,13 +557,11 @@ func (dp *DiscardPolicy) UnmarshalJSON(data []byte) error {
|
||||
const (
|
||||
memoryStorageJSONString = `"memory"`
|
||||
fileStorageJSONString = `"file"`
|
||||
anyStorageJSONString = `"any"`
|
||||
)
|
||||
|
||||
var (
|
||||
memoryStorageJSONBytes = []byte(memoryStorageJSONString)
|
||||
fileStorageJSONBytes = []byte(fileStorageJSONString)
|
||||
anyStorageJSONBytes = []byte(anyStorageJSONString)
|
||||
)
|
||||
|
||||
func (st StorageType) String() string {
|
||||
@@ -570,8 +570,6 @@ func (st StorageType) String() string {
|
||||
return "Memory"
|
||||
case FileStorage:
|
||||
return "File"
|
||||
case AnyStorage:
|
||||
return "Any"
|
||||
default:
|
||||
return "Unknown Storage Type"
|
||||
}
|
||||
@@ -583,8 +581,6 @@ func (st StorageType) MarshalJSON() ([]byte, error) {
|
||||
return memoryStorageJSONBytes, nil
|
||||
case FileStorage:
|
||||
return fileStorageJSONBytes, nil
|
||||
case AnyStorage:
|
||||
return anyStorageJSONBytes, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("can not marshal %v", st)
|
||||
}
|
||||
@@ -596,8 +592,6 @@ func (st *StorageType) UnmarshalJSON(data []byte) error {
|
||||
*st = MemoryStorage
|
||||
case fileStorageJSONString:
|
||||
*st = FileStorage
|
||||
case anyStorageJSONString:
|
||||
*st = AnyStorage
|
||||
default:
|
||||
return fmt.Errorf("can not unmarshal %q", data)
|
||||
}
|
||||
|
||||
+1449
-293
File diff suppressed because it is too large
Load Diff
+67
-12
@@ -16,6 +16,8 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -33,6 +35,7 @@ var (
|
||||
splitMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit\s*\((.*)\)\s*}}`)
|
||||
leftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[lL]eft\s*\((.*)\)\s*}}`)
|
||||
rightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[rR]ight\s*\((.*)\)\s*}}`)
|
||||
randomMappingFunctionRegEx = regexp.MustCompile(`{{\s*[rR]andom\s*\((.*)\)\s*}}`)
|
||||
)
|
||||
|
||||
// Enum for the subject mapping subjectTransform function types
|
||||
@@ -48,6 +51,7 @@ const (
|
||||
Split
|
||||
Left
|
||||
Right
|
||||
Random
|
||||
)
|
||||
|
||||
// Transforms for arbitrarily mapping subjects from one to another for maps, tees and filters.
|
||||
@@ -123,17 +127,16 @@ func NewSubjectTransformWithStrict(src, dest string, strict bool) (*subjectTrans
|
||||
}
|
||||
}
|
||||
|
||||
if npwcs == 0 {
|
||||
if tranformType != NoTransform {
|
||||
return nil, &mappingDestinationErr{token, ErrMappingDestinationIndexOutOfRange}
|
||||
}
|
||||
}
|
||||
|
||||
if tranformType == NoTransform {
|
||||
dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform)
|
||||
dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1})
|
||||
dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1)
|
||||
dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_)
|
||||
} else if tranformType == Random {
|
||||
dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, Random)
|
||||
dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{})
|
||||
dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, transfomArgInt)
|
||||
dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_)
|
||||
} else {
|
||||
nphs += len(transformArgWildcardIndexes)
|
||||
// Now build up our runtime mapping from dest to source tokens.
|
||||
@@ -158,12 +161,22 @@ func NewSubjectTransformWithStrict(src, dest string, strict bool) (*subjectTrans
|
||||
} else {
|
||||
// no wildcards used in the source: check that no transform functions are used in the destination
|
||||
for _, token := range dtokens {
|
||||
tranformType, _, _, _, err := indexPlaceHolders(token)
|
||||
tranformType, _, transfomArgInt, _, err := indexPlaceHolders(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tranformType != NoTransform {
|
||||
if tranformType == NoTransform {
|
||||
dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform)
|
||||
dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1})
|
||||
dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1)
|
||||
dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_)
|
||||
} else if tranformType == Random || tranformType == Partition {
|
||||
dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, tranformType)
|
||||
dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{})
|
||||
dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, transfomArgInt)
|
||||
dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_)
|
||||
} else {
|
||||
return nil, &mappingDestinationErr{token, ErrMappingDestinationIndexOutOfRange}
|
||||
}
|
||||
}
|
||||
@@ -255,12 +268,19 @@ func indexPlaceHolders(token string) (int16, []int, int32, string, error) {
|
||||
// partition(number of partitions, token1, token2, ...)
|
||||
args = getMappingFunctionArgs(partitionMappingFunctionRegEx, token)
|
||||
if args != nil {
|
||||
if len(args) < 2 {
|
||||
if len(args) < 1 {
|
||||
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs}
|
||||
}
|
||||
if len(args) == 1 {
|
||||
mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " "))
|
||||
if err != nil || mappingFunctionIntArg > math.MaxInt32 {
|
||||
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
|
||||
}
|
||||
return Partition, []int{}, int32(mappingFunctionIntArg), _EMPTY_, nil
|
||||
}
|
||||
if len(args) >= 2 {
|
||||
mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " "))
|
||||
if err != nil {
|
||||
if err != nil || mappingFunctionIntArg > math.MaxInt32 {
|
||||
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
|
||||
}
|
||||
var numPositions = len(args[1:])
|
||||
@@ -333,6 +353,19 @@ func indexPlaceHolders(token string) (int16, []int, int32, string, error) {
|
||||
return Split, []int{i}, -1, args[1], nil
|
||||
}
|
||||
|
||||
// Random(max)
|
||||
args = getMappingFunctionArgs(randomMappingFunctionRegEx, token)
|
||||
if args != nil {
|
||||
if len(args) != 1 {
|
||||
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs}
|
||||
}
|
||||
mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " "))
|
||||
if err != nil || mappingFunctionIntArg > math.MaxInt32 {
|
||||
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
|
||||
}
|
||||
return Random, []int{}, int32(mappingFunctionIntArg), _EMPTY_, nil
|
||||
}
|
||||
|
||||
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction}
|
||||
}
|
||||
}
|
||||
@@ -423,7 +456,21 @@ func (tr *subjectTransform) TransformSubject(subject string) string {
|
||||
return tr.TransformTokenizedSubject(tokenizeSubject(subject))
|
||||
}
|
||||
|
||||
func (tr *subjectTransform) getRandomPartition(ceiling int) string {
|
||||
// Avoid an integer divide by zero panic below.
|
||||
if ceiling == 0 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
return strconv.Itoa(int(rand.Int31()) % ceiling)
|
||||
}
|
||||
|
||||
func (tr *subjectTransform) getHashPartition(key []byte, numBuckets int) string {
|
||||
// Avoid an integer divide by zero panic below.
|
||||
if numBuckets == 0 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write(key)
|
||||
|
||||
@@ -454,8 +501,14 @@ func (tr *subjectTransform) TransformTokenizedSubject(tokens []string) string {
|
||||
_buffer [64]byte
|
||||
keyForHashing = _buffer[:0]
|
||||
)
|
||||
for _, sourceToken := range tr.dtokmftokindexesargs[i] {
|
||||
keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...)
|
||||
if len(tr.dtokmftokindexesargs[i]) > 0 {
|
||||
// When token positions are specified.
|
||||
for _, sourceToken := range tr.dtokmftokindexesargs[i] {
|
||||
keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...)
|
||||
}
|
||||
} else {
|
||||
// When using the shorthand partition(n).
|
||||
keyForHashing = append(keyForHashing, strings.Join(tokens, ".")...)
|
||||
}
|
||||
b.WriteString(tr.getHashPartition(keyForHashing, int(tr.dtokmfintargs[i])))
|
||||
case Wildcard: // simple substitution
|
||||
@@ -558,6 +611,8 @@ func (tr *subjectTransform) TransformTokenizedSubject(tokens []string) string {
|
||||
} else { // too small to slice at the requested size: don't slice
|
||||
b.WriteString(sourceToken)
|
||||
}
|
||||
case Random:
|
||||
b.WriteString(tr.getRandomPartition(int(tr.dtokmfintargs[i])))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1292,7 +1292,8 @@ func ValidateMapping(src string, dest string) error {
|
||||
!splitFromRightMappingFunctionRegEx.MatchString(t) &&
|
||||
!sliceFromLeftMappingFunctionRegEx.MatchString(t) &&
|
||||
!sliceFromRightMappingFunctionRegEx.MatchString(t) &&
|
||||
!splitMappingFunctionRegEx.MatchString(t) {
|
||||
!splitMappingFunctionRegEx.MatchString(t) &&
|
||||
!randomMappingFunctionRegEx.MatchString(t) {
|
||||
return &mappingDestinationErr{t, ErrUnknownMappingDestinationFunction}
|
||||
} else {
|
||||
continue
|
||||
|
||||
+1
-1
@@ -211,7 +211,7 @@ func (hw *HashWheel) Count() uint64 {
|
||||
return hw.count
|
||||
}
|
||||
|
||||
// AppendEncode writes out the contents of the THW into a binary snapshot
|
||||
// Encode writes out the contents of the THW into a binary snapshot
|
||||
// and returns it. The high seq number is included in the snapshot and will
|
||||
// be returned on decode.
|
||||
func (hw *HashWheel) Encode(highSeq uint64) []byte {
|
||||
|
||||
+1
-1
@@ -312,7 +312,7 @@ func getURLsAsString(urls []*url.URL) []string {
|
||||
return a
|
||||
}
|
||||
|
||||
// copyBytes make a new slice of the same size than `src` and copy its content.
|
||||
// copyBytes make a new slice of the same size as `src` and copy its content.
|
||||
// If `src` is nil or its length is 0, then this returns `nil`
|
||||
func copyBytes(src []byte) []byte {
|
||||
if len(src) == 0 {
|
||||
|
||||
+6
-3
@@ -1147,21 +1147,24 @@ func (s *Server) startWebsocketServer() {
|
||||
// regardless of NoTLS. If we don't have a TLS config, it means that the
|
||||
// user has configured NoTLS because otherwise the server would have failed
|
||||
// to start due to options validation.
|
||||
var config *tls.Config
|
||||
if o.TLSConfig != nil {
|
||||
proto = wsSchemePrefixTLS
|
||||
config := o.TLSConfig.Clone()
|
||||
config = o.TLSConfig.Clone()
|
||||
config.GetConfigForClient = s.wsGetTLSConfig
|
||||
hl, err = tls.Listen("tcp", hp, config)
|
||||
} else {
|
||||
proto = wsSchemePrefix
|
||||
hl, err = net.Listen("tcp", hp)
|
||||
}
|
||||
hl, err = natsListen("tcp", hp)
|
||||
s.websocket.listenerErr = err
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
s.Fatalf("Unable to listen for websocket connections: %v", err)
|
||||
return
|
||||
}
|
||||
if config != nil {
|
||||
hl = tls.NewListener(hl, config)
|
||||
}
|
||||
if port == 0 {
|
||||
o.Port = hl.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
Vendored
+3
-2
@@ -1105,10 +1105,10 @@ github.com/mschoch/smat
|
||||
# github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
|
||||
## explicit
|
||||
github.com/munnerz/goautoneg
|
||||
# github.com/nats-io/jwt/v2 v2.7.4
|
||||
# github.com/nats-io/jwt/v2 v2.8.0
|
||||
## explicit; go 1.23.0
|
||||
github.com/nats-io/jwt/v2
|
||||
# github.com/nats-io/nats-server/v2 v2.11.9
|
||||
# github.com/nats-io/nats-server/v2 v2.12.0
|
||||
## explicit; go 1.24.0
|
||||
github.com/nats-io/nats-server/v2/conf
|
||||
github.com/nats-io/nats-server/v2/internal/fastrand
|
||||
@@ -1119,6 +1119,7 @@ github.com/nats-io/nats-server/v2/server/ats
|
||||
github.com/nats-io/nats-server/v2/server/avl
|
||||
github.com/nats-io/nats-server/v2/server/certidp
|
||||
github.com/nats-io/nats-server/v2/server/certstore
|
||||
github.com/nats-io/nats-server/v2/server/elastic
|
||||
github.com/nats-io/nats-server/v2/server/gsl
|
||||
github.com/nats-io/nats-server/v2/server/pse
|
||||
github.com/nats-io/nats-server/v2/server/stree
|
||||
|
||||
Reference in New Issue
Block a user