build(deps): bump github.com/nats-io/nats-server/v2

Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.11.9 to 2.12.0.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/.goreleaser.yml)
- [Commits](https://github.com/nats-io/nats-server/compare/v2.11.9...v2.12.0)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats-server/v2
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2025-09-29 11:04:16 +02:00
committed by Ralf Haferkamp
parent 4a0cc1004f
commit 703b8dd084
41 changed files with 6938 additions and 1700 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,60 @@
// Copyright 2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package elastic
import (
"weak"
)
func Make[T any](ptr *T) *Pointer[T] {
return &Pointer[T]{
weak: weak.Make(ptr),
}
}
type Pointer[T any] struct {
weak weak.Pointer[T]
strong *T
}
func (e *Pointer[T]) Set(ptr *T) {
e.weak = weak.Make(ptr)
if e.strong != nil {
e.strong = ptr
}
}
func (e *Pointer[T]) Strengthen() {
if e == nil || e.strong != nil {
return
}
e.strong = e.weak.Value()
}
func (e *Pointer[T]) Weaken() {
if e == nil || e.strong == nil {
return
}
e.strong = nil
}
func (e *Pointer[T]) Value() *T {
if e == nil {
return nil
}
if e.strong != nil {
return e.strong
}
return e.weak.Value()
}
+8
View File
@@ -31,6 +31,14 @@ var (
// ErrAuthExpired represents an expired authorization due to timeout.
ErrAuthExpired = 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
View File
@@ -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
View File
@@ -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,
},
File diff suppressed because it is too large Load Diff
+16 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,653 @@
// Copyright 2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"path/filepath"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
)
var (
// Tracks the total inflight batches, across all streams and accounts that enable batching.
globalInflightBatches atomic.Int32
)
type batching struct {
mu sync.Mutex
group map[string]*batchGroup
}
type batchGroup struct {
lseq uint64
store StreamStore
timer *time.Timer
}
// Lock should be held.
func (batches *batching) newBatchGroup(mset *stream, batchId string) (*batchGroup, error) {
store, err := newBatchStore(mset, batchId)
if err != nil {
return nil, err
}
b := &batchGroup{store: store}
// Create a timer to clean up after timeout.
timeout := streamMaxBatchTimeout
if maxBatchTimeout := mset.srv.getOpts().JetStreamLimits.MaxBatchTimeout; maxBatchTimeout > 0 {
timeout = maxBatchTimeout
}
b.timer = time.AfterFunc(timeout, func() {
b.cleanup(batchId, batches)
mset.sendStreamBatchAbandonedAdvisory(batchId, BatchTimeout)
})
return b, nil
}
func getBatchStoreDir(mset *stream, batchId string) (string, string) {
mset.mu.RLock()
jsa, name := mset.jsa, mset.cfg.Name
mset.mu.RUnlock()
jsa.mu.RLock()
sd := jsa.storeDir
jsa.mu.RUnlock()
bname := getHash(batchId)
return bname, filepath.Join(sd, streamsDir, name, batchesDir, bname)
}
func newBatchStore(mset *stream, batchId string) (StreamStore, error) {
mset.mu.RLock()
replicas, storage := mset.cfg.Replicas, mset.cfg.Storage
mset.mu.RUnlock()
if replicas == 1 && storage == FileStorage {
bname, storeDir := getBatchStoreDir(mset, batchId)
fcfg := FileStoreConfig{AsyncFlush: true, BlockSize: defaultLargeBlockSize, StoreDir: storeDir}
s := mset.srv
prf := s.jsKeyGen(s.getOpts().JetStreamKey, mset.acc.Name)
if prf != nil {
// We are encrypted here, fill in correct cipher selection.
fcfg.Cipher = s.getOpts().JetStreamCipher
}
oldprf := s.jsKeyGen(s.getOpts().JetStreamOldKey, mset.acc.Name)
cfg := StreamConfig{Name: bname, Storage: FileStorage}
return newFileStoreWithCreated(fcfg, cfg, time.Time{}, prf, oldprf)
}
return newMemStore(&StreamConfig{Name: _EMPTY_, Storage: MemoryStorage})
}
// readyForCommit indicates the batch is ready to be committed.
// If the timer has already cleaned up the batch, we can't commit.
// Otherwise, we ensure the timer does not clean up the batch in the meantime.
// Lock should be held.
func (b *batchGroup) readyForCommit() bool {
if !b.timer.Stop() {
return false
}
b.store.FlushAllPending()
return true
}
// cleanup deletes underlying resources associated with the batch and unregisters it from the stream's batches.
func (b *batchGroup) cleanup(batchId string, batches *batching) {
batches.mu.Lock()
defer batches.mu.Unlock()
b.cleanupLocked(batchId, batches)
}
// Lock should be held.
func (b *batchGroup) cleanupLocked(batchId string, batches *batching) {
globalInflightBatches.Add(-1)
b.timer.Stop()
b.store.Delete(true)
delete(batches.group, batchId)
}
// Lock should be held.
func (b *batchGroup) stopLocked() {
globalInflightBatches.Add(-1)
b.timer.Stop()
b.store.Stop()
}
// batchStagedDiff stages all changes for consistency checks until commit.
type batchStagedDiff struct {
msgIds map[string]struct{}
counter map[string]*msgCounterRunningTotal
inflight map[string]*inflightSubjectRunningTotal
expectedPerSubject map[string]*batchExpectedPerSubject
}
type batchExpectedPerSubject struct {
sseq uint64 // Stream sequence.
clseq uint64 // Clustered proposal sequence.
}
func (diff *batchStagedDiff) commit(mset *stream) {
if len(diff.msgIds) > 0 {
ts := time.Now().UnixNano()
mset.ddMu.Lock()
for msgId := range diff.msgIds {
// We stage with zero, and will update in processJetStreamMsg once we know the sequence.
mset.storeMsgIdLocked(&ddentry{msgId, 0, ts})
}
mset.ddMu.Unlock()
}
// Store running totals for counters, we could have multiple counter increments proposed, but not applied yet.
if len(diff.counter) > 0 {
if mset.clusteredCounterTotal == nil {
mset.clusteredCounterTotal = make(map[string]*msgCounterRunningTotal, len(diff.counter))
}
for k, c := range diff.counter {
mset.clusteredCounterTotal[k] = c
}
}
// Track inflight.
if len(diff.inflight) > 0 {
if mset.inflight == nil {
mset.inflight = make(map[string]*inflightSubjectRunningTotal, len(diff.inflight))
}
for subj, i := range diff.inflight {
if c, ok := mset.inflight[subj]; ok {
c.bytes += i.bytes
c.ops += i.ops
} else {
mset.inflight[subj] = i
}
}
}
// Track sequence and subject.
if len(diff.expectedPerSubject) > 0 {
if mset.expectedPerSubjectSequence == nil {
mset.expectedPerSubjectSequence = make(map[uint64]string, len(diff.expectedPerSubject))
}
if mset.expectedPerSubjectInProcess == nil {
mset.expectedPerSubjectInProcess = make(map[string]struct{}, len(diff.expectedPerSubject))
}
for subj, e := range diff.expectedPerSubject {
mset.expectedPerSubjectSequence[e.clseq] = subj
mset.expectedPerSubjectInProcess[subj] = struct{}{}
}
}
}
type batchApply struct {
mu sync.Mutex
id string // ID of the current batch.
count uint64 // Number of entries in the batch, for consistency checks.
entries []*CommittedEntry // Previous entries that are part of this batch.
entryStart int // The index into an entry indicating the first message of the batch.
maxApplied uint64 // Applied value before the entry containing the first message of the batch.
}
// clearBatchStateLocked clears in-memory apply-batch-related state.
// batch.mu lock should be held.
func (batch *batchApply) clearBatchStateLocked() {
batch.id = _EMPTY_
batch.count = 0
batch.entries = nil
batch.entryStart = 0
batch.maxApplied = 0
}
// rejectBatchStateLocked rejects the batch and clears in-memory apply-batch-related state.
// Corrects mset.clfs to take the failed batch into account.
// batch.mu lock should be held.
func (batch *batchApply) rejectBatchStateLocked(mset *stream) {
mset.clMu.Lock()
mset.clfs += batch.count
mset.clMu.Unlock()
// We're rejecting the batch, so all entries need to be returned to the pool.
for _, bce := range batch.entries {
bce.ReturnToPool()
}
batch.clearBatchStateLocked()
}
func (batch *batchApply) rejectBatchState(mset *stream) {
batch.mu.Lock()
defer batch.mu.Unlock()
batch.rejectBatchStateLocked(mset)
}
// checkMsgHeadersPreClusteredProposal checks the message for expected/consistency headers.
// mset.mu lock must NOT be held or used.
// mset.clMu lock must be held.
func checkMsgHeadersPreClusteredProposal(
diff *batchStagedDiff, mset *stream, subject string, hdr []byte, msg []byte, sourced bool, name string,
jsa *jsAccount, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules bool,
discard DiscardPolicy, discardNewPer bool, maxMsgSize int, maxMsgs int64, maxMsgsPer int64, maxBytes int64,
) ([]byte, []byte, uint64, *ApiError, error) {
var incr *big.Int
// Some header checks must be checked pre proposal.
if len(hdr) > 0 {
// Since we encode header len as u16 make sure we do not exceed.
// Again this works if it goes through but better to be pre-emptive.
if len(hdr) > math.MaxUint16 {
err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name)
return hdr, msg, 0, NewJSStreamHeaderExceedsMaximumError(), err
}
// Counter increments.
// Only supported on counter streams, and payload must be empty (if not coming from a source).
var ok bool
if incr, ok = getMessageIncr(hdr); !ok {
apiErr := NewJSMessageIncrInvalidError()
return hdr, msg, 0, apiErr, apiErr
} else if incr != nil && !sourced {
// Only do checks if the message isn't sourced. Otherwise, we need to store verbatim.
if !allowMsgCounter {
apiErr := NewJSMessageIncrDisabledError()
return hdr, msg, 0, apiErr, apiErr
} else if len(msg) > 0 {
apiErr := NewJSMessageIncrPayloadError()
return hdr, msg, 0, apiErr, apiErr
} else {
// Check for incompatible headers.
var doErr bool
if getRollup(hdr) != _EMPTY_ ||
getExpectedStream(hdr) != _EMPTY_ ||
getExpectedLastMsgId(hdr) != _EMPTY_ ||
getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ {
doErr = true
} else if _, ok = getExpectedLastSeq(hdr); ok {
doErr = true
} else if _, ok = getExpectedLastSeqPerSubject(hdr); ok {
doErr = true
}
if doErr {
apiErr := NewJSMessageIncrInvalidError()
return hdr, msg, 0, apiErr, apiErr
}
}
}
// Expected stream name can also be pre-checked.
if sname := getExpectedStream(hdr); sname != _EMPTY_ && sname != name {
return hdr, msg, 0, NewJSStreamNotMatchError(), errStreamMismatch
}
// TTL'd messages are rejected entirely if TTLs are not enabled on the stream, or if the TTL is invalid.
if ttl, err := getMessageTTL(hdr); !sourced && (ttl != 0 || err != nil) {
if !allowTTL {
return hdr, msg, 0, NewJSMessageTTLDisabledError(), errMsgTTLDisabled
} else if err != nil {
return hdr, msg, 0, NewJSMessageTTLInvalidError(), err
}
}
// Check for MsgIds here at the cluster level to avoid excessive CLFS accounting.
// Will help during restarts.
if msgId := getMsgId(hdr); msgId != _EMPTY_ {
// Dedupe if staged.
if _, ok = diff.msgIds[msgId]; ok {
return hdr, msg, 0, nil, errMsgIdDuplicate
}
mset.ddMu.Lock()
if dde := mset.checkMsgId(msgId); dde != nil {
seq := dde.seq
mset.ddMu.Unlock()
// Should not return an invalid sequence, in that case error.
if seq > 0 {
return hdr, msg, seq, nil, errMsgIdDuplicate
} else {
return hdr, msg, 0, NewJSStreamDuplicateMessageConflictError(), errMsgIdDuplicate
}
}
if diff.msgIds == nil {
diff.msgIds = map[string]struct{}{msgId: {}}
} else {
diff.msgIds[msgId] = struct{}{}
}
mset.ddMu.Unlock()
}
}
// Apply increment for counter.
// But only if it's allowed for this stream. This can happen when we store verbatim for a sourced stream.
if incr == nil && allowMsgCounter {
apiErr := NewJSMessageIncrMissingError()
return hdr, msg, 0, apiErr, apiErr
}
if incr != nil && allowMsgCounter {
var initial big.Int
var sources CounterSources
// If we've got a running total, update that, since we have inflight proposals updating the same counter.
var ok bool
var counter *msgCounterRunningTotal
if counter, ok = diff.counter[subject]; ok {
initial = *counter.total
sources = counter.sources
} else if counter, ok = mset.clusteredCounterTotal[subject]; ok {
initial = *counter.total
sources = counter.sources
// Make an explicit copy to separate the staged data from what's committed.
// Don't need to initialize all values, they'll be overwritten later.
counter = &msgCounterRunningTotal{ops: counter.ops}
} else {
// Load last message, and store as inflight running total.
var smv StoreMsg
sm, err := mset.store.LoadLastMsg(subject, &smv)
if err == nil && sm != nil {
var val CounterValue
// Return an error if the counter is broken somehow.
if json.Unmarshal(sm.msg, &val) != nil {
apiErr := NewJSMessageCounterBrokenError()
return hdr, msg, 0, apiErr, apiErr
}
if ncs := sliceHeader(JSMessageCounterSources, sm.hdr); len(ncs) > 0 {
if err := json.Unmarshal(ncs, &sources); err != nil {
apiErr := NewJSMessageCounterBrokenError()
return hdr, msg, 0, apiErr, apiErr
}
}
initial.SetString(val.Value, 10)
}
}
srchdr := sliceHeader(JSStreamSource, hdr)
if len(srchdr) > 0 {
// This is a sourced message, so we can't apply Nats-Incr but
// instead should just update the source count header.
fields := strings.Split(string(srchdr), " ")
origStream := fields[0]
origSubj := subject
if len(fields) >= 5 {
origSubj = fields[4]
}
var val CounterValue
if json.Unmarshal(msg, &val) != nil {
apiErr := NewJSMessageCounterBrokenError()
return hdr, msg, 0, apiErr, apiErr
}
var sourced big.Int
sourced.SetString(val.Value, 10)
if sources == nil {
sources = map[string]map[string]string{}
}
if _, ok = sources[origStream]; !ok {
sources[origStream] = map[string]string{}
}
prevVal := sources[origStream][origSubj]
sources[origStream][origSubj] = sourced.String()
// We will also replace the Nats-Incr header with the diff
// between our last value from this source and this one, so
// that the arithmetic is always correct.
var previous big.Int
previous.SetString(prevVal, 10)
incr.Sub(&sourced, &previous)
hdr = setHeader(JSMessageIncr, incr.String(), hdr)
}
// Now make the change.
initial.Add(&initial, incr)
// Generate the new payload.
var _msg [128]byte
msg = fmt.Appendf(_msg[:0], "{%q:%q}", "val", initial.String())
// Write the updated source count headers.
if len(sources) > 0 {
nhdr, err := json.Marshal(sources)
if err != nil {
return hdr, msg, 0, NewJSMessageCounterBrokenError(), err
}
hdr = setHeader(JSMessageCounterSources, string(nhdr), hdr)
}
// Check to see if we are over the max msg size.
maxSize := int64(mset.srv.getOpts().MaxPayload)
if maxMsgSize >= 0 && int64(maxMsgSize) < maxSize {
maxSize = int64(maxMsgSize)
}
hdrLen, msgLen := int64(len(hdr)), int64(len(msg))
// Subtract to prevent against overflows.
if hdrLen > maxSize || msgLen > maxSize-hdrLen {
return hdr, msg, 0, NewJSStreamMessageExceedsMaximumError(), ErrMaxPayload
}
// Keep the in-memory counters up-to-date.
if counter == nil {
counter = &msgCounterRunningTotal{}
}
counter.total = &initial
counter.sources = sources
counter.ops++
if diff.counter == nil {
diff.counter = map[string]*msgCounterRunningTotal{subject: counter}
} else {
diff.counter[subject] = counter
}
}
if len(hdr) > 0 {
// Expected last sequence.
if seq, exists := getExpectedLastSeq(hdr); exists && seq != mset.clseq-mset.clfs {
mlseq := mset.clseq - mset.clfs
err := fmt.Errorf("last sequence mismatch: %d vs %d", seq, mlseq)
return hdr, msg, 0, NewJSStreamWrongLastSequenceError(mlseq), err
} else if exists && len(diff.inflight) > 0 {
// Only the first message in a batch can contain an expected last sequence.
err := fmt.Errorf("last sequence mismatch")
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
}
// Expected last sequence per subject.
if seq, exists := getExpectedLastSeqPerSubject(hdr); exists {
// Allow override of the subject used for the check.
seqSubj := subject
if optSubj := getExpectedLastSeqPerSubjectForSubject(hdr); optSubj != _EMPTY_ {
seqSubj = optSubj
}
// The subject is already written to in this batch, we can't allow
// expected checks since they would be incorrect.
if _, ok := diff.inflight[seqSubj]; ok {
err := errors.New("last sequence by subject mismatch")
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
}
// If the subject is already in process, block as otherwise we could have
// multiple messages inflight with the same subject.
if _, found := mset.expectedPerSubjectInProcess[seqSubj]; found {
err := errors.New("last sequence by subject mismatch")
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
}
// If the subject is already in process but without expected headers, block as we would have
// multiple messages inflight with the same subject.
if _, ok := mset.inflight[seqSubj]; ok {
err := errors.New("last sequence by subject mismatch")
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
}
// If we've already done an expected-check on this subject, use the cached result.
if e, ok := diff.expectedPerSubject[seqSubj]; ok {
if e.sseq != seq {
err := fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, e.sseq)
return hdr, msg, 0, NewJSStreamWrongLastSequenceError(e.sseq), err
}
e.clseq = mset.clseq
} else {
var smv StoreMsg
var fseq uint64
sm, err := mset.store.LoadLastMsg(seqSubj, &smv)
if sm != nil {
fseq = sm.seq
}
if err == ErrStoreMsgNotFound && seq == 0 {
fseq, err = 0, nil
}
if err != nil || fseq != seq {
err = fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, fseq)
return hdr, msg, 0, NewJSStreamWrongLastSequenceError(fseq), err
}
e = &batchExpectedPerSubject{sseq: fseq, clseq: mset.clseq}
if diff.expectedPerSubject == nil {
diff.expectedPerSubject = map[string]*batchExpectedPerSubject{seqSubj: e}
} else {
diff.expectedPerSubject[seqSubj] = e
}
}
} else if getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ {
apiErr := NewJSStreamExpectedLastSeqPerSubjectInvalidError()
return hdr, msg, 0, apiErr, apiErr
}
// Message scheduling.
if schedule, ok := getMessageSchedule(hdr); !ok {
apiErr := NewJSMessageSchedulesPatternInvalidError()
if !allowMsgSchedules {
apiErr = NewJSMessageSchedulesDisabledError()
}
return hdr, msg, 0, apiErr, apiErr
} else if !schedule.IsZero() {
if !allowMsgSchedules {
apiErr := NewJSMessageSchedulesDisabledError()
return hdr, msg, 0, apiErr, apiErr
} else if scheduleTtl, ok := getMessageScheduleTTL(hdr); !ok {
apiErr := NewJSMessageSchedulesTTLInvalidError()
return hdr, msg, 0, apiErr, apiErr
} else if scheduleTtl != _EMPTY_ && !allowTTL {
return hdr, msg, 0, NewJSMessageTTLDisabledError(), errMsgTTLDisabled
} else if scheduleTarget := getMessageScheduleTarget(hdr); scheduleTarget == _EMPTY_ ||
!IsValidPublishSubject(scheduleTarget) || SubjectsCollide(scheduleTarget, subject) {
apiErr := NewJSMessageSchedulesTargetInvalidError()
return hdr, msg, 0, apiErr, apiErr
} else {
mset.cfgMu.RLock()
match := slices.ContainsFunc(mset.cfg.Subjects, func(subj string) bool {
return SubjectsCollide(subj, scheduleTarget)
})
mset.cfgMu.RUnlock()
if !match {
apiErr := NewJSMessageSchedulesTargetInvalidError()
return hdr, msg, 0, apiErr, apiErr
}
// Add a rollup sub header if it doesn't already exist.
// Otherwise, it must exist already as a rollup on the subject.
if rollup := getRollup(hdr); rollup == _EMPTY_ {
hdr = genHeader(hdr, JSMsgRollup, JSMsgRollupSubject)
} else if rollup != JSMsgRollupSubject {
apiErr := NewJSMessageSchedulesRollupInvalidError()
return hdr, msg, 0, apiErr, apiErr
}
}
}
// Check for any rollups.
if rollup := getRollup(hdr); rollup != _EMPTY_ {
if !allowRollup || denyPurge {
err := errors.New("rollup not permitted")
return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
}
switch rollup {
case JSMsgRollupSubject:
// Rolling up the subject is only allowed if the first occurrence of this subject in the batch.
if _, ok := diff.inflight[subject]; ok {
err := errors.New("batch rollup sub invalid")
return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
}
case JSMsgRollupAll:
// Rolling up the whole stream is only allowed if this is the first message of the batch.
if len(diff.inflight) > 0 {
err := errors.New("batch rollup all invalid")
return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
}
default:
err := fmt.Errorf("rollup value invalid: %q", rollup)
return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
}
}
}
// Track inflight.
// Store the subject to ensure other messages in this batch using
// an expected check or rollup on the same subject fail.
if diff.inflight == nil {
diff.inflight = make(map[string]*inflightSubjectRunningTotal, 1)
}
var sz uint64
if mset.store.Type() == FileStorage {
sz = fileStoreMsgSizeRaw(len(subject), len(hdr), len(msg))
} else {
sz = memStoreMsgSizeRaw(len(subject), len(hdr), len(msg))
}
var (
i *inflightSubjectRunningTotal
ok bool
err error
)
if i, ok = diff.inflight[subject]; ok {
i.bytes += sz
i.ops++
} else {
i = &inflightSubjectRunningTotal{bytes: sz, ops: 1}
diff.inflight[subject] = i
}
// Check if we have discard new with max msgs or bytes.
// We need to deny here otherwise we'd need to bump CLFS, and it could succeed on some
// peers and not others depending on consumer ack state (if interest policy).
// So we deny here, if we allow that means we know it would succeed on every peer.
if discard == DiscardNew && (maxMsgs > 0 || maxBytes > 0) {
// Error if over DiscardNew per subject threshold.
if discardNewPer {
totalMsgsForSubject := i.ops
if i, ok = mset.inflight[subject]; ok {
totalMsgsForSubject += i.ops
}
if maxMsgsPer > 0 && totalMsgsForSubject > uint64(maxMsgsPer) {
err = ErrMaxMsgsPerSubject
return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err
}
}
// Track usual max msgs/bytes thresholds for DiscardNew.
var state StreamState
mset.store.FastState(&state)
totalMsgs := state.Msgs
totalBytes := state.Bytes
for _, i = range mset.inflight {
totalMsgs += i.ops
totalBytes += i.bytes
}
for _, i = range diff.inflight {
totalMsgs += i.ops
totalBytes += i.bytes
}
if maxMsgs > 0 && totalMsgs > uint64(maxMsgs) {
err = ErrMaxMsgs
} else if maxBytes > 0 && totalBytes > uint64(maxBytes) {
err = ErrMaxBytes
}
if err != nil {
return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err
}
}
return hdr, msg, 0, nil, nil
}
File diff suppressed because it is too large Load Diff
+592 -174
View File
@@ -8,6 +8,24 @@ const (
// JSAccountResourcesExceededErr resource limits exceeded for account
JSAccountResourcesExceededErr 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, &lt)
if o.Metadata == nil {
o.Metadata = make(map[string]string)
}
o.Metadata[mk] = mv.(string)
}
default:
err = &configErr{tk, fmt.Sprintf("error parsing metadata: unsupported type %T", v)}
}
if err != nil {
*errors = append(*errors, err)
return
}
case "default_js_domain":
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, &lt)
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, &lt)
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, &lt)
vv, ok := v.(map[string]any)
if !ok {
return &configErr{tk, fmt.Sprintf("Expected a map to define batch limits, got %T", v)}
}
for mk, mv := range vv {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "max_inflight_per_stream":
opts.JetStreamLimits.MaxBatchInflightPerStream = int(mv.(int64))
case "max_inflight_total":
opts.JetStreamLimits.MaxBatchInflightTotal = int(mv.(int64))
case "max_msgs":
opts.JetStreamLimits.MaxBatchSize = int(mv.(int64))
case "timeout":
var err error
opts.JetStreamLimits.MaxBatchTimeout, err = time.ParseDuration(mv.(string))
if err != nil {
*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, &lt)
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, &lt)
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, &lt, 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(&lt, &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(&lt, errors)
tk, mv = unwrapValue(mv, &lt)
pm, ok := mv.(map[string]any)
if !ok {
return nil, &configErr{tk, fmt.Sprintf("expected proxies to be a map/struct, got %T", mv)}
}
for mk, mv := range pm {
tk, _ = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "trusted":
trusted, err := parseProxiesTrusted(tk, errors)
if err != nil {
*errors = append(*errors, err)
continue
}
proxies.Trusted = trusted
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
field: mk,
configErr: configErr{
token: tk,
},
}
*errors = append(*errors, err)
}
}
}
return proxies, nil
}
func parseProxiesTrusted(mv any, errors *[]error) ([]*ProxyConfig, error) {
var (
tk token
lt token
trusted []*ProxyConfig
)
defer convertPanicToErrorList(&lt, errors)
tk, mv = unwrapValue(mv, &lt)
ta, ok := mv.([]any)
if !ok {
return nil, &configErr{tk, fmt.Sprintf("expected proxies' trusted field to be an array, got %T", mv)}
}
for _, t := range ta {
tk, t = unwrapValue(t, &lt)
// Check its a map/struct
tm, ok := t.(map[string]any)
if !ok {
err := &configErr{tk, fmt.Sprintf("expected proxies' trusted entry to be a map/struct, got %T", t)}
*errors = append(*errors, err)
continue
}
proxy := &ProxyConfig{}
for k, v := range tm {
tk, v = unwrapValue(v, &lt)
switch strings.ToLower(k) {
case "key", "public_key":
proxy.Key = v.(string)
if !nkeys.IsValidPublicKey(proxy.Key) {
*errors = append(*errors, &configErr{tk, fmt.Sprintf("invalid proxy key %q", proxy.Key)})
continue
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
field: k,
configErr: configErr{
token: tk,
},
}
*errors = append(*errors, err)
continue
}
}
}
trusted = append(trusted, proxy)
}
return trusted, nil
}
// GenTLSConfig loads TLS related configuration parameters.
func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
// Create the tls.Config from our options before including the certs.
+202 -80
View File
@@ -45,6 +45,7 @@ type RaftNode interface {
SendSnapshot(snap []byte) error
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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,239 @@
// Copyright 2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"encoding/binary"
"errors"
"io"
"math"
"slices"
"time"
"github.com/nats-io/nats-server/v2/server/thw"
)
// Error for when we try to decode a binary-encoded message schedule with an unknown version number.
var ErrMsgScheduleInvalidVersion = errors.New("msg scheduling: encoded version not known")
const (
headerLen = 17 // 1 byte magic + 2x uint64s
)
type MsgScheduling struct {
run func()
ttls *thw.HashWheel
timer *time.Timer
schedules map[string]*MsgSchedule
seqToSubj map[uint64]string
inflight map[string]struct{}
}
type MsgSchedule struct {
seq uint64
ts int64
}
func newMsgScheduling(run func()) *MsgScheduling {
return &MsgScheduling{
run: run,
ttls: thw.NewHashWheel(),
schedules: make(map[string]*MsgSchedule),
seqToSubj: make(map[uint64]string),
inflight: make(map[string]struct{}),
}
}
func (ms *MsgScheduling) add(seq uint64, subj string, ts int64) {
ms.init(seq, subj, ts)
ms.resetTimer()
}
func (ms *MsgScheduling) init(seq uint64, subj string, ts int64) {
if sched, ok := ms.schedules[subj]; ok {
delete(ms.seqToSubj, sched.seq)
// Remove and add separately, since they'll have different sequences.
ms.ttls.Remove(sched.seq, sched.ts)
ms.ttls.Add(seq, ts)
sched.ts, sched.seq = ts, seq
} else {
ms.ttls.Add(seq, ts)
ms.schedules[subj] = &MsgSchedule{seq: seq, ts: ts}
}
ms.seqToSubj[seq] = subj
delete(ms.inflight, subj)
}
func (ms *MsgScheduling) markInflight(subj string) {
if _, ok := ms.schedules[subj]; ok {
ms.inflight[subj] = struct{}{}
}
}
func (ms *MsgScheduling) isInflight(subj string) bool {
_, ok := ms.inflight[subj]
return ok
}
func (ms *MsgScheduling) remove(seq uint64) {
if subj, ok := ms.seqToSubj[seq]; ok {
delete(ms.seqToSubj, seq)
delete(ms.schedules, subj)
}
}
func (ms *MsgScheduling) clearInflight() {
ms.inflight = make(map[string]struct{})
}
func (ms *MsgScheduling) resetTimer() {
next := ms.ttls.GetNextExpiration(math.MaxInt64)
if next == math.MaxInt64 {
clearTimer(&ms.timer)
return
}
fireIn := time.Until(time.Unix(0, next))
// Make sure we aren't firing too often either way, otherwise we can
// negatively impact stream ingest performance.
if fireIn < 250*time.Millisecond {
fireIn = 250 * time.Millisecond
}
if ms.timer != nil {
ms.timer.Reset(fireIn)
} else {
ms.timer = time.AfterFunc(fireIn, ms.run)
}
}
func (ms *MsgScheduling) getScheduledMessages(loadMsg func(seq uint64, smv *StoreMsg) *StoreMsg) []*inMsg {
var (
smv StoreMsg
sm *StoreMsg
msgs []*inMsg
)
ms.ttls.ExpireTasks(func(seq uint64, ts int64) bool {
// Need to grab the message for the specified sequence, and check
// if it hasn't been removed in the meantime.
sm = loadMsg(seq, &smv)
if sm != nil {
// If already inflight, don't duplicate a scheduled message. The stream could
// be replicated and the scheduled message could take some time to propagate.
if ms.isInflight(sm.subj) {
return false
}
// Validate the contents are correct if not, we just remove it from THW.
ttl, ok := getMessageScheduleTTL(sm.hdr)
if !ok {
ms.remove(seq)
return true
}
target := getMessageScheduleTarget(sm.hdr)
if target == _EMPTY_ {
ms.remove(seq)
return true
}
// Copy, as this is retrieved directly from storage, and we'll need to keep hold of this for some time.
// And in the case of headers, we'll copy all of them, but make changes.
hdr, msg := copyBytes(sm.hdr), copyBytes(sm.msg)
// Strip headers specific to the schedule.
hdr = removeHeaderIfPresent(hdr, JSSchedulePattern)
hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Schedule-")
hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Expected-")
hdr = removeHeaderIfPresent(hdr, JSMsgId)
hdr = removeHeaderIfPresent(hdr, JSMessageTTL)
hdr = removeHeaderIfPresent(hdr, JSMsgRollup)
// Add headers for the scheduled message.
hdr = genHeader(hdr, JSScheduler, sm.subj)
hdr = genHeader(hdr, JSScheduleNext, JSScheduleNextPurge) // Purge the schedule message itself.
if ttl != _EMPTY_ {
hdr = genHeader(hdr, JSMessageTTL, ttl)
}
msgs = append(msgs, &inMsg{seq: seq, subj: target, hdr: hdr, msg: msg})
ms.markInflight(sm.subj)
return false
}
ms.remove(seq)
return true
})
// THW is unordered, so must sort by sequence.
slices.SortFunc(msgs, func(a, b *inMsg) int {
if a.seq == b.seq {
return 0
} else if a.seq < b.seq {
return -1
} else {
return 1
}
})
return msgs
}
// encode writes out the contents of the schedule into a binary snapshot
// and returns it. The high seq number is included in the snapshot and will
// be returned on decode.
func (ms *MsgScheduling) encode(highSeq uint64) []byte {
count := uint64(len(ms.schedules))
b := make([]byte, 0, headerLen+(count*(2*binary.MaxVarintLen64)))
b = append(b, 1) // Magic version
b = binary.LittleEndian.AppendUint64(b, count) // Entry count
b = binary.LittleEndian.AppendUint64(b, highSeq) // Stamp
for subj, sched := range ms.schedules {
slen := min(uint64(len(subj)), math.MaxUint16)
b = binary.LittleEndian.AppendUint16(b, uint16(slen))
b = append(b, subj[:slen]...)
b = binary.AppendVarint(b, sched.ts)
b = binary.AppendUvarint(b, sched.seq)
}
return b
}
// decode snapshots a binary-encoded schedule and replaces the contents of this
// schedule with them. Returns the high seq number from the snapshot.
func (ms *MsgScheduling) decode(b []byte) (uint64, error) {
if len(b) < headerLen {
return 0, io.ErrShortBuffer
}
if b[0] != 1 {
return 0, ErrMsgScheduleInvalidVersion
}
count := binary.LittleEndian.Uint64(b[1:])
stamp := binary.LittleEndian.Uint64(b[9:])
b = b[headerLen:]
for i := uint64(0); i < count; i++ {
sl := int(binary.LittleEndian.Uint16(b))
b = b[2:]
if len(b) < sl {
return 0, io.ErrUnexpectedEOF
}
subj := string(b[:sl])
b = b[sl:]
ts, tn := binary.Varint(b)
if tn < 0 {
return 0, io.ErrUnexpectedEOF
}
seq, vn := binary.Uvarint(b[tn:])
if vn < 0 {
return 0, io.ErrUnexpectedEOF
}
ms.init(seq, subj, ts)
b = b[tn+vn:]
}
return stamp, nil
}
+111 -23
View File
@@ -30,6 +30,7 @@ import (
"os"
"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
View File
@@ -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)
}
File diff suppressed because it is too large Load Diff
+67 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1147,21 +1147,24 @@ func (s *Server) startWebsocketServer() {
// regardless of NoTLS. If we don't have a TLS config, it means that the
// 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
}