Bump github.com/nats-io/nats-server/v2 from 2.9.22 to 2.10.1

Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.9.22 to 2.10.1.
- [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.9.22...v2.10.1)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats-server/v2
  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]
2023-09-26 06:09:29 +00:00
committed by Ralf Haferkamp
parent a1b7dc34cd
commit 502ec695f1
84 changed files with 15208 additions and 4368 deletions
+29
View File
@@ -7,6 +7,7 @@ package flate
import (
"encoding/binary"
"errors"
"fmt"
"io"
"math"
@@ -833,6 +834,12 @@ func (d *compressor) init(w io.Writer, level int) (err error) {
d.initDeflate()
d.fill = (*compressor).fillDeflate
d.step = (*compressor).deflateLazy
case -level >= MinCustomWindowSize && -level <= MaxCustomWindowSize:
d.w.logNewTablePenalty = 7
d.fast = &fastEncL5Window{maxOffset: int32(-level), cur: maxStoreBlockSize}
d.window = make([]byte, maxStoreBlockSize)
d.fill = (*compressor).fillBlock
d.step = (*compressor).storeFast
default:
return fmt.Errorf("flate: invalid compression level %d: want value in range [-2, 9]", level)
}
@@ -929,6 +936,28 @@ func NewWriterDict(w io.Writer, level int, dict []byte) (*Writer, error) {
return zw, err
}
// MinCustomWindowSize is the minimum window size that can be sent to NewWriterWindow.
const MinCustomWindowSize = 32
// MaxCustomWindowSize is the maximum custom window that can be sent to NewWriterWindow.
const MaxCustomWindowSize = windowSize
// NewWriterWindow returns a new Writer compressing data with a custom window size.
// windowSize must be from MinCustomWindowSize to MaxCustomWindowSize.
func NewWriterWindow(w io.Writer, windowSize int) (*Writer, error) {
if windowSize < MinCustomWindowSize {
return nil, errors.New("flate: requested window size less than MinWindowSize")
}
if windowSize > MaxCustomWindowSize {
return nil, errors.New("flate: requested window size bigger than MaxCustomWindowSize")
}
var dw Writer
if err := dw.d.init(w, -windowSize); err != nil {
return nil, err
}
return &dw, nil
}
// A Writer takes data written to it and writes the compressed
// form of that data to an underlying writer (see NewWriter).
type Writer struct {
-23
View File
@@ -8,7 +8,6 @@ package flate
import (
"encoding/binary"
"fmt"
"math/bits"
)
type fastEnc interface {
@@ -192,25 +191,3 @@ func (e *fastGen) Reset() {
}
e.hist = e.hist[:0]
}
// matchLen returns the maximum length.
// 'a' must be the shortest of the two.
func matchLen(a, b []byte) int {
var checked int
for len(a) >= 8 {
if diff := binary.LittleEndian.Uint64(a) ^ binary.LittleEndian.Uint64(b); diff != 0 {
return checked + (bits.TrailingZeros64(diff) >> 3)
}
checked += 8
a = a[8:]
b = b[8:]
}
b = b[:len(a)]
for i := range a {
if a[i] != b[i] {
return i + checked
}
}
return len(a) + checked
}
+398
View File
@@ -308,3 +308,401 @@ emitRemainder:
emitLiteral(dst, src[nextEmit:])
}
}
// fastEncL5Window is a level 5 encoder,
// but with a custom window size.
type fastEncL5Window struct {
hist []byte
cur int32
maxOffset int32
table [tableSize]tableEntry
bTable [tableSize]tableEntryPrev
}
func (e *fastEncL5Window) Encode(dst *tokens, src []byte) {
const (
inputMargin = 12 - 1
minNonLiteralBlockSize = 1 + 1 + inputMargin
hashShortBytes = 4
)
maxMatchOffset := e.maxOffset
if debugDeflate && e.cur < 0 {
panic(fmt.Sprint("e.cur < 0: ", e.cur))
}
// Protect against e.cur wraparound.
for e.cur >= bufferReset {
if len(e.hist) == 0 {
for i := range e.table[:] {
e.table[i] = tableEntry{}
}
for i := range e.bTable[:] {
e.bTable[i] = tableEntryPrev{}
}
e.cur = maxMatchOffset
break
}
// Shift down everything in the table that isn't already too far away.
minOff := e.cur + int32(len(e.hist)) - maxMatchOffset
for i := range e.table[:] {
v := e.table[i].offset
if v <= minOff {
v = 0
} else {
v = v - e.cur + maxMatchOffset
}
e.table[i].offset = v
}
for i := range e.bTable[:] {
v := e.bTable[i]
if v.Cur.offset <= minOff {
v.Cur.offset = 0
v.Prev.offset = 0
} else {
v.Cur.offset = v.Cur.offset - e.cur + maxMatchOffset
if v.Prev.offset <= minOff {
v.Prev.offset = 0
} else {
v.Prev.offset = v.Prev.offset - e.cur + maxMatchOffset
}
}
e.bTable[i] = v
}
e.cur = maxMatchOffset
}
s := e.addBlock(src)
// This check isn't in the Snappy implementation, but there, the caller
// instead of the callee handles this case.
if len(src) < minNonLiteralBlockSize {
// We do not fill the token table.
// This will be picked up by caller.
dst.n = uint16(len(src))
return
}
// Override src
src = e.hist
nextEmit := s
// sLimit is when to stop looking for offset/length copies. The inputMargin
// lets us use a fast path for emitLiteral in the main loop, while we are
// looking for copies.
sLimit := int32(len(src) - inputMargin)
// nextEmit is where in src the next emitLiteral should start from.
cv := load6432(src, s)
for {
const skipLog = 6
const doEvery = 1
nextS := s
var l int32
var t int32
for {
nextHashS := hashLen(cv, tableBits, hashShortBytes)
nextHashL := hash7(cv, tableBits)
s = nextS
nextS = s + doEvery + (s-nextEmit)>>skipLog
if nextS > sLimit {
goto emitRemainder
}
// Fetch a short+long candidate
sCandidate := e.table[nextHashS]
lCandidate := e.bTable[nextHashL]
next := load6432(src, nextS)
entry := tableEntry{offset: s + e.cur}
e.table[nextHashS] = entry
eLong := &e.bTable[nextHashL]
eLong.Cur, eLong.Prev = entry, eLong.Cur
nextHashS = hashLen(next, tableBits, hashShortBytes)
nextHashL = hash7(next, tableBits)
t = lCandidate.Cur.offset - e.cur
if s-t < maxMatchOffset {
if uint32(cv) == load3232(src, lCandidate.Cur.offset-e.cur) {
// Store the next match
e.table[nextHashS] = tableEntry{offset: nextS + e.cur}
eLong := &e.bTable[nextHashL]
eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur
t2 := lCandidate.Prev.offset - e.cur
if s-t2 < maxMatchOffset && uint32(cv) == load3232(src, lCandidate.Prev.offset-e.cur) {
l = e.matchlen(s+4, t+4, src) + 4
ml1 := e.matchlen(s+4, t2+4, src) + 4
if ml1 > l {
t = t2
l = ml1
break
}
}
break
}
t = lCandidate.Prev.offset - e.cur
if s-t < maxMatchOffset && uint32(cv) == load3232(src, lCandidate.Prev.offset-e.cur) {
// Store the next match
e.table[nextHashS] = tableEntry{offset: nextS + e.cur}
eLong := &e.bTable[nextHashL]
eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur
break
}
}
t = sCandidate.offset - e.cur
if s-t < maxMatchOffset && uint32(cv) == load3232(src, sCandidate.offset-e.cur) {
// Found a 4 match...
l = e.matchlen(s+4, t+4, src) + 4
lCandidate = e.bTable[nextHashL]
// Store the next match
e.table[nextHashS] = tableEntry{offset: nextS + e.cur}
eLong := &e.bTable[nextHashL]
eLong.Cur, eLong.Prev = tableEntry{offset: nextS + e.cur}, eLong.Cur
// If the next long is a candidate, use that...
t2 := lCandidate.Cur.offset - e.cur
if nextS-t2 < maxMatchOffset {
if load3232(src, lCandidate.Cur.offset-e.cur) == uint32(next) {
ml := e.matchlen(nextS+4, t2+4, src) + 4
if ml > l {
t = t2
s = nextS
l = ml
break
}
}
// If the previous long is a candidate, use that...
t2 = lCandidate.Prev.offset - e.cur
if nextS-t2 < maxMatchOffset && load3232(src, lCandidate.Prev.offset-e.cur) == uint32(next) {
ml := e.matchlen(nextS+4, t2+4, src) + 4
if ml > l {
t = t2
s = nextS
l = ml
break
}
}
}
break
}
cv = next
}
// A 4-byte match has been found. We'll later see if more than 4 bytes
// match. But, prior to the match, src[nextEmit:s] are unmatched. Emit
// them as literal bytes.
if l == 0 {
// Extend the 4-byte match as long as possible.
l = e.matchlenLong(s+4, t+4, src) + 4
} else if l == maxMatchLength {
l += e.matchlenLong(s+l, t+l, src)
}
// Try to locate a better match by checking the end of best match...
if sAt := s + l; l < 30 && sAt < sLimit {
// Allow some bytes at the beginning to mismatch.
// Sweet spot is 2/3 bytes depending on input.
// 3 is only a little better when it is but sometimes a lot worse.
// The skipped bytes are tested in Extend backwards,
// and still picked up as part of the match if they do.
const skipBeginning = 2
eLong := e.bTable[hash7(load6432(src, sAt), tableBits)].Cur.offset
t2 := eLong - e.cur - l + skipBeginning
s2 := s + skipBeginning
off := s2 - t2
if t2 >= 0 && off < maxMatchOffset && off > 0 {
if l2 := e.matchlenLong(s2, t2, src); l2 > l {
t = t2
l = l2
s = s2
}
}
}
// Extend backwards
for t > 0 && s > nextEmit && src[t-1] == src[s-1] {
s--
t--
l++
}
if nextEmit < s {
if false {
emitLiteral(dst, src[nextEmit:s])
} else {
for _, v := range src[nextEmit:s] {
dst.tokens[dst.n] = token(v)
dst.litHist[v]++
dst.n++
}
}
}
if debugDeflate {
if t >= s {
panic(fmt.Sprintln("s-t", s, t))
}
if (s - t) > maxMatchOffset {
panic(fmt.Sprintln("mmo", s-t))
}
if l < baseMatchLength {
panic("bml")
}
}
dst.AddMatchLong(l, uint32(s-t-baseMatchOffset))
s += l
nextEmit = s
if nextS >= s {
s = nextS + 1
}
if s >= sLimit {
goto emitRemainder
}
// Store every 3rd hash in-between.
if true {
const hashEvery = 3
i := s - l + 1
if i < s-1 {
cv := load6432(src, i)
t := tableEntry{offset: i + e.cur}
e.table[hashLen(cv, tableBits, hashShortBytes)] = t
eLong := &e.bTable[hash7(cv, tableBits)]
eLong.Cur, eLong.Prev = t, eLong.Cur
// Do an long at i+1
cv >>= 8
t = tableEntry{offset: t.offset + 1}
eLong = &e.bTable[hash7(cv, tableBits)]
eLong.Cur, eLong.Prev = t, eLong.Cur
// We only have enough bits for a short entry at i+2
cv >>= 8
t = tableEntry{offset: t.offset + 1}
e.table[hashLen(cv, tableBits, hashShortBytes)] = t
// Skip one - otherwise we risk hitting 's'
i += 4
for ; i < s-1; i += hashEvery {
cv := load6432(src, i)
t := tableEntry{offset: i + e.cur}
t2 := tableEntry{offset: t.offset + 1}
eLong := &e.bTable[hash7(cv, tableBits)]
eLong.Cur, eLong.Prev = t, eLong.Cur
e.table[hashLen(cv>>8, tableBits, hashShortBytes)] = t2
}
}
}
// We could immediately start working at s now, but to improve
// compression we first update the hash table at s-1 and at s.
x := load6432(src, s-1)
o := e.cur + s - 1
prevHashS := hashLen(x, tableBits, hashShortBytes)
prevHashL := hash7(x, tableBits)
e.table[prevHashS] = tableEntry{offset: o}
eLong := &e.bTable[prevHashL]
eLong.Cur, eLong.Prev = tableEntry{offset: o}, eLong.Cur
cv = x >> 8
}
emitRemainder:
if int(nextEmit) < len(src) {
// If nothing was added, don't encode literals.
if dst.n == 0 {
return
}
emitLiteral(dst, src[nextEmit:])
}
}
// Reset the encoding table.
func (e *fastEncL5Window) Reset() {
// We keep the same allocs, since we are compressing the same block sizes.
if cap(e.hist) < allocHistory {
e.hist = make([]byte, 0, allocHistory)
}
// We offset current position so everything will be out of reach.
// If we are above the buffer reset it will be cleared anyway since len(hist) == 0.
if e.cur <= int32(bufferReset) {
e.cur += e.maxOffset + int32(len(e.hist))
}
e.hist = e.hist[:0]
}
func (e *fastEncL5Window) addBlock(src []byte) int32 {
// check if we have space already
maxMatchOffset := e.maxOffset
if len(e.hist)+len(src) > cap(e.hist) {
if cap(e.hist) == 0 {
e.hist = make([]byte, 0, allocHistory)
} else {
if cap(e.hist) < int(maxMatchOffset*2) {
panic("unexpected buffer size")
}
// Move down
offset := int32(len(e.hist)) - maxMatchOffset
copy(e.hist[0:maxMatchOffset], e.hist[offset:])
e.cur += offset
e.hist = e.hist[:maxMatchOffset]
}
}
s := int32(len(e.hist))
e.hist = append(e.hist, src...)
return s
}
// matchlen will return the match length between offsets and t in src.
// The maximum length returned is maxMatchLength - 4.
// It is assumed that s > t, that t >=0 and s < len(src).
func (e *fastEncL5Window) matchlen(s, t int32, src []byte) int32 {
if debugDecode {
if t >= s {
panic(fmt.Sprint("t >=s:", t, s))
}
if int(s) >= len(src) {
panic(fmt.Sprint("s >= len(src):", s, len(src)))
}
if t < 0 {
panic(fmt.Sprint("t < 0:", t))
}
if s-t > e.maxOffset {
panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")"))
}
}
s1 := int(s) + maxMatchLength - 4
if s1 > len(src) {
s1 = len(src)
}
// Extend the match to be as long as possible.
return int32(matchLen(src[s:s1], src[t:]))
}
// matchlenLong will return the match length between offsets and t in src.
// It is assumed that s > t, that t >=0 and s < len(src).
func (e *fastEncL5Window) matchlenLong(s, t int32, src []byte) int32 {
if debugDeflate {
if t >= s {
panic(fmt.Sprint("t >=s:", t, s))
}
if int(s) >= len(src) {
panic(fmt.Sprint("s >= len(src):", s, len(src)))
}
if t < 0 {
panic(fmt.Sprint("t < 0:", t))
}
if s-t > e.maxOffset {
panic(fmt.Sprint(s, "-", t, "(", s-t, ") > maxMatchLength (", maxMatchOffset, ")"))
}
}
// Extend the match to be as long as possible.
return int32(matchLen(src[s:], src[t:]))
}
+16
View File
@@ -0,0 +1,16 @@
//go:build amd64 && !appengine && !noasm && gc
// +build amd64,!appengine,!noasm,gc
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
package flate
// matchLen returns how many bytes match in a and b
//
// It assumes that:
//
// len(a) <= len(b) and len(a) > 0
//
//go:noescape
func matchLen(a []byte, b []byte) int
+68
View File
@@ -0,0 +1,68 @@
// Copied from S2 implementation.
//go:build !appengine && !noasm && gc && !noasm
#include "textflag.h"
// func matchLen(a []byte, b []byte) int
// Requires: BMI
TEXT ·matchLen(SB), NOSPLIT, $0-56
MOVQ a_base+0(FP), AX
MOVQ b_base+24(FP), CX
MOVQ a_len+8(FP), DX
// matchLen
XORL SI, SI
CMPL DX, $0x08
JB matchlen_match4_standalone
matchlen_loopback_standalone:
MOVQ (AX)(SI*1), BX
XORQ (CX)(SI*1), BX
TESTQ BX, BX
JZ matchlen_loop_standalone
#ifdef GOAMD64_v3
TZCNTQ BX, BX
#else
BSFQ BX, BX
#endif
SARQ $0x03, BX
LEAL (SI)(BX*1), SI
JMP gen_match_len_end
matchlen_loop_standalone:
LEAL -8(DX), DX
LEAL 8(SI), SI
CMPL DX, $0x08
JAE matchlen_loopback_standalone
matchlen_match4_standalone:
CMPL DX, $0x04
JB matchlen_match2_standalone
MOVL (AX)(SI*1), BX
CMPL (CX)(SI*1), BX
JNE matchlen_match2_standalone
LEAL -4(DX), DX
LEAL 4(SI), SI
matchlen_match2_standalone:
CMPL DX, $0x02
JB matchlen_match1_standalone
MOVW (AX)(SI*1), BX
CMPW (CX)(SI*1), BX
JNE matchlen_match1_standalone
LEAL -2(DX), DX
LEAL 2(SI), SI
matchlen_match1_standalone:
CMPL DX, $0x01
JB gen_match_len_end
MOVB (AX)(SI*1), BL
CMPB (CX)(SI*1), BL
JNE gen_match_len_end
INCL SI
gen_match_len_end:
MOVQ SI, ret+48(FP)
RET
+33
View File
@@ -0,0 +1,33 @@
//go:build !amd64 || appengine || !gc || noasm
// +build !amd64 appengine !gc noasm
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
package flate
import (
"encoding/binary"
"math/bits"
)
// matchLen returns the maximum common prefix length of a and b.
// a must be the shortest of the two.
func matchLen(a, b []byte) (n int) {
for ; len(a) >= 8 && len(b) >= 8; a, b = a[8:], b[8:] {
diff := binary.LittleEndian.Uint64(a) ^ binary.LittleEndian.Uint64(b)
if diff != 0 {
return n + bits.TrailingZeros64(diff)>>3
}
n += 8
}
for i := range a {
if a[i] != b[i] {
break
}
n++
}
return n
}
+19
View File
@@ -106,6 +106,25 @@ func MakeDict(data []byte, searchStart []byte) *Dict {
return &d
}
// MakeDictManual will create a dictionary.
// 'data' must be at least MinDictSize and less than or equal to MaxDictSize.
// A manual first repeat index into data must be provided.
// It must be less than len(data)-8.
func MakeDictManual(data []byte, firstIdx uint16) *Dict {
if len(data) < MinDictSize || int(firstIdx) >= len(data)-8 || len(data) > MaxDictSize {
return nil
}
var d Dict
dict := data
d.dict = dict
if cap(d.dict) < len(d.dict)+16 {
d.dict = append(make([]byte, 0, len(d.dict)+16), d.dict...)
}
d.repeat = int(firstIdx)
return &d
}
// Encode returns the encoded form of src. The returned slice may be a sub-
// slice of dst if dst was large enough to hold the entire encoded block.
// Otherwise, a newly allocated slice will be returned.
File diff suppressed because it is too large Load Diff
+9 -11
View File
@@ -511,24 +511,22 @@ func IndexStream(r io.Reader) ([]byte, error) {
// JSON returns the index as JSON text.
func (i *Index) JSON() []byte {
type offset struct {
CompressedOffset int64 `json:"compressed"`
UncompressedOffset int64 `json:"uncompressed"`
}
x := struct {
TotalUncompressed int64 `json:"total_uncompressed"` // Total Uncompressed size if known. Will be -1 if unknown.
TotalCompressed int64 `json:"total_compressed"` // Total Compressed size if known. Will be -1 if unknown.
Offsets []struct {
CompressedOffset int64 `json:"compressed"`
UncompressedOffset int64 `json:"uncompressed"`
} `json:"offsets"`
EstBlockUncomp int64 `json:"est_block_uncompressed"`
TotalUncompressed int64 `json:"total_uncompressed"` // Total Uncompressed size if known. Will be -1 if unknown.
TotalCompressed int64 `json:"total_compressed"` // Total Compressed size if known. Will be -1 if unknown.
Offsets []offset `json:"offsets"`
EstBlockUncomp int64 `json:"est_block_uncompressed"`
}{
TotalUncompressed: i.TotalUncompressed,
TotalCompressed: i.TotalCompressed,
EstBlockUncomp: i.estBlockUncomp,
}
for _, v := range i.info {
x.Offsets = append(x.Offsets, struct {
CompressedOffset int64 `json:"compressed"`
UncompressedOffset int64 `json:"uncompressed"`
}{CompressedOffset: v.compressedOffset, UncompressedOffset: v.uncompressedOffset})
x.Offsets = append(x.Offsets, offset{CompressedOffset: v.compressedOffset, UncompressedOffset: v.uncompressedOffset})
}
b, _ := json.MarshalIndent(x, "", " ")
return b
+3
View File
@@ -357,6 +357,9 @@ func (a *AccountClaims) ExpectedPrefixes() []nkeys.PrefixByte {
func (a *AccountClaims) Claims() *ClaimsData {
return &a.ClaimsData
}
func (a *AccountClaims) GetTags() TagList {
return a.Account.Tags
}
// DidSign checks the claims against the account's public key and its signing keys
func (a *AccountClaims) DidSign(c Claims) bool {
+4
View File
@@ -243,3 +243,7 @@ func (oc *OperatorClaims) Claims() *ClaimsData {
func (oc *OperatorClaims) updateVersion() {
oc.GenericFields.Version = libVersion
}
func (oc *OperatorClaims) GetTags() TagList {
return oc.Operator.Tags
}
+8 -3
View File
@@ -19,6 +19,7 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"github.com/nats-io/nkeys"
)
@@ -124,10 +125,14 @@ func (sk *SigningKeys) MarshalJSON() ([]byte, error) {
if sk == nil {
return nil, nil
}
keys := sk.Keys()
sort.Strings(keys)
var a []interface{}
for k, v := range *sk {
if v != nil {
a = append(a, v)
for _, k := range keys {
if (*sk)[k] != nil {
a = append(a, (*sk)[k])
} else {
a = append(a, k)
}
+4
View File
@@ -151,3 +151,7 @@ func (u *UserClaims) updateVersion() {
func (u *UserClaims) IsBearerToken() bool {
return u.BearerToken
}
func (u *UserClaims) GetTags() TagList {
return u.User.Tags
}
+67 -7
View File
@@ -18,6 +18,8 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
@@ -128,13 +130,14 @@ type fileLogger struct {
out int64
canRotate int32
sync.Mutex
l *Logger
f writerAndCloser
limit int64
olimit int64
pid string
time bool
closed bool
l *Logger
f writerAndCloser
limit int64
olimit int64
pid string
time bool
closed bool
maxNumFiles int
}
func newFileLogger(filename, pidPrefix string, time bool) (*fileLogger, error) {
@@ -169,6 +172,12 @@ func (l *fileLogger) setLimit(limit int64) {
}
}
func (l *fileLogger) setMaxNumFiles(max int) {
l.Lock()
l.maxNumFiles = max
l.Unlock()
}
func (l *fileLogger) logDirect(label, format string, v ...interface{}) int {
var entrya = [256]byte{}
var entry = entrya[:0]
@@ -190,6 +199,41 @@ func (l *fileLogger) logDirect(label, format string, v ...interface{}) int {
return len(entry)
}
func (l *fileLogger) logPurge(fname string) {
var backups []string
lDir := filepath.Dir(fname)
lBase := filepath.Base(fname)
entries, err := os.ReadDir(lDir)
if err != nil {
l.logDirect(l.l.errorLabel, "Unable to read directory %q for log purge (%v), will attempt next rotation", lDir, err)
return
}
for _, entry := range entries {
if entry.IsDir() || entry.Name() == lBase || !strings.HasPrefix(entry.Name(), lBase) {
continue
}
if stamp, found := strings.CutPrefix(entry.Name(), fmt.Sprintf("%s%s", lBase, ".")); found {
_, err := time.Parse("2006:01:02:15:04:05.999999999", strings.Replace(stamp, ".", ":", 5))
if err == nil {
backups = append(backups, entry.Name())
}
}
}
currBackups := len(backups)
maxBackups := l.maxNumFiles - 1
if currBackups > maxBackups {
// backups sorted oldest to latest based on timestamped lexical filename (ReadDir)
for i := 0; i < currBackups-maxBackups; i++ {
if err := os.Remove(filepath.Join(lDir, string(os.PathSeparator), backups[i])); err != nil {
l.logDirect(l.l.errorLabel, "Unable to remove backup log file %q (%v), will attempt next rotation", backups[i], err)
// Bail fast, we'll try again next rotation
return
}
l.logDirect(l.l.infoLabel, "Purged log file %q", backups[i])
}
}
}
func (l *fileLogger) Write(b []byte) (int, error) {
if atomic.LoadInt32(&l.canRotate) == 0 {
n, err := l.f.Write(b)
@@ -225,6 +269,9 @@ func (l *fileLogger) Write(b []byte) (int, error) {
n := l.logDirect(l.l.infoLabel, "Rotated log, backup saved as %q", bak)
l.out = int64(n)
l.limit = l.olimit
if l.maxNumFiles > 0 {
l.logPurge(fname)
}
}
}
l.Unlock()
@@ -257,6 +304,19 @@ func (l *Logger) SetSizeLimit(limit int64) error {
return nil
}
// SetMaxNumFiles sets the number of archived log files that will be retained
func (l *Logger) SetMaxNumFiles(max int) error {
l.Lock()
if l.fl == nil {
l.Unlock()
return fmt.Errorf("can set log max number of files only for file logger")
}
fl := l.fl
l.Unlock()
fl.setMaxNumFiles(max)
return nil
}
// NewTestLogger creates a logger with output directed to Stderr with a prefix.
// Useful for tracing in tests when multiple servers are in the same pid
func NewTestLogger(prefix string, time bool) *Logger {
+595
View File
@@ -0,0 +1,595 @@
**MQTT Implementation Overview**
Revision 1.1
Authors: Ivan Kozlovic, Lev Brouk
NATS Server currently supports most of MQTT 3.1.1. This document describes how
it is implementated.
It is strongly recommended to review the [MQTT v3.1.1
specifications](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
and get a detailed understanding before proceeding with this document.
# Contents
1. [Concepts](#1-concepts)
- [Server, client](#server-client)
- [Connection, client ID, session](#connection-client-id-session)
- [Packets, messages, and subscriptions](#packets-messages-and-subscriptions)
- [Quality of Service (QoS), publish identifier (PI)](#quality-of-service-qos-publish-identifier-pi)
- [Retained message](#retained-message)
- [Will message](#will-message)
2. [Use of JetStream](#2-use-of-jetstream)
- [JetStream API](#jetstream-api)
- [Streams](#streams)
- [Consumers and Internal NATS Subscriptions](#consumers-and-internal-nats-subscriptions)
3. [Lifecycles](#3-lifecycles)
- [Connection, Session](#connection-session)
- [Subscription](#subscription)
- [Message](#message)
- [Retained messages](#retained-messages)
4. [Implementation Notes](#4-implementation-notes)
- [Hooking into NATS I/O](#hooking-into-nats-io)
- [Session Management](#session-management)
- [Processing QoS acks: PUBACK, PUBREC, PUBCOMP](#processing-qos-acks-puback-pubrec-pubcomp)
- [Subject Wildcards](#subject-wildcards)
5. [Known issues](#5-known-issues)
# 1. Concepts
## Server, client
In the MQTT specification there are concepts of **Client** and **Server**, used
somewhat interchangeably with those of **Sender** and **Receiver**. A **Server**
acts as a **Receiver** when it gets `PUBLISH` messages from a **Sender**
**Client**, and acts as a **Sender** when it delivers them to subscribed
**Clients**.
In the NATS server implementation there are also concepts (types) `server` and
`client`. `client` is an internal representation of a (connected) client and
runs its own read and write loops. Both of these have an `mqtt` field that if
set makes them behave as MQTT-compliant.
The code and comments may sometimes be confusing as they refer to `server` and
`client` sometimes ambiguously between MQTT and NATS.
## Connection, client ID, session
When an MQTT client connects to a server, it must send a `CONNECT` packet to
create an **MQTT Connection**. The packet must include a **Client Identifier**.
The server will then create or load a previously saved **Session** for the (hash
of) the client ID.
## Packets, messages, and subscriptions
The low level unit of transmission in MQTT is a **Packet**. Examples of packets
are: `CONNECT`, `SUBSCRIBE`, `SUBACK`, `PUBLISH`, `PUBCOMP`, etc.
An **MQTT Message** starts with a `PUBLISH` packet that a client sends to the
server. It is then matched against the current **MQTT Subscriptions** and is
delivered to them as appropriate. During the message delivery the server acts as
an MQTT client, and the receiver acts as an MQTT server.
Internally we use **NATS Messages** and **NATS Subscriptions** to facilitate
message delivery. This may be somewhat confusing as the code refers to `msg` and
`sub`. What may be even more confusing is that some MQTT packets (specifically,
`PUBREL`) are represented as NATS messages, and that the original MQTT packet
"metadata" may be encoded as NATS message headers.
## Quality of Service (QoS), publish identifier (PI)
MQTT specifies 3 levels of quality of service (**QoS**):
- `0` for at most once. A single delivery attempt.
- `1` for at least once. Will try to redeliver until acknowledged by the
receiver.
- `2` for exactly once. See the [SPEC REF] for the acknowledgement flow.
QoS 1 and 2 messages need to be identified with publish identifiers (**PI**s). A
PI is a 16-bit integer that must uniquely identify a message for the duration of
the required exchange of acknowledgment packets.
Note that the QoS applies separately to the transmission of a message from a
sender client to the server, and from the server to the receiving client. There
is no protocol-level acknowledgements between the receiver and the original
sender. The sender passes the ownership of messages to the server, and the
server then delivers them at maximum possible QoS to the receivers
(subscribers). The PIs for in-flight outgoing messages are issued and stored per
session.
## Retained message
A **Retained Message** is not part of any MQTT session and is not removed when the
session that produced it goes away. Instead, the server needs to persist a
_single_ retained message per topic. When a subscription is started, the server
needs to send the “matching” retained messages, that is, messages that would
have been delivered to the new subscription should that subscription had been
running prior to the publication of this message.
Retained messages are removed when the server receives a retained message with
an empty body. Still, this retained message that serves as a “delete” of a
retained message will be processed as a normal published message.
Retained messages can have QoS.
## Will message
The `CONNECT` packet can contain information about a **Will Message** that needs to
be sent to any client subscribing on the Will topic/subject in the event that
the client is disconnected implicitly, that is, not as a result as the client
sending the `DISCONNECT` packet.
Will messages can have the retain flag and QoS.
# 2. Use of JetStream
The MQTT implementation relies heavily on JetStream. We use it to:
- Persist (and restore) the [Session](#connection-client-id-session) state.
- Store and retrieve [Retained messages](#retained-message).
- Persist incoming [QoS 1 and
2](#quality-of-service-qos-publish-identifier-pi) messages, and
re-deliver if needed.
- Store and de-duplicate incoming [QoS
2](#quality-of-service-qos-publish-identifier-pi) messages.
- Persist and re-deliver outgoing [QoS
2](#quality-of-service-qos-publish-identifier-pi) `PUBREL` packets.
Here is the overview of how we set up and use JetStream **streams**,
**consumers**, and **internal NATS subscriptions**.
## JetStream API
All interactions with JetStream are performed via `mqttJSA` that sends NATS
requests to JetStream. Most are processed syncronously and await a response,
some (e.g. `jsa.sendAck()`) are sent asynchronously. JetStream API is usually
referred to as `jsa` in the code. No special locking is required to use `jsa`,
however the asynchronous use of JetStream may create race conditions with
delivery callbacks.
## Streams
We create the following streams unless they already exist. Failing to ensure the
streams would prevent the client from connecting.
Each stream is created with a replica value that is determined by the size of
the cluster but limited to 3. It can also be overwritten by the stream_replicas
option in the MQTT configuration block.
The streams are created the first time an Account Session Manager is initialized
and are used by all sessions in it. Note that to avoid race conditions, some
subscriptions are created first. The streams are never deleted. See
`mqttCreateAccountSessionManager()` for details.
1. `$MQTT_sess` stores persisted **Session** records. It filters on
`"$MQTT.sess.>` subject and has a “limits” policy with `MaxMsgsPer` setting
of 1.
2. `$MQTT_msgs` is used for **QoS 1 and 2 message delivery**.
It filters on `$MQTT.msgs.>` subject and has an “interest” policy.
3. `$MQTT_rmsgs` stores **Retained Messages**. They are all
stored (and filtered) on a single subject `$MQTT.rmsg`. This stream has a
limits policy.
4. `$MQTT_qos2in` stores and deduplicates **Incoming QoS 2 Messages**. It
filters on `$MQTT.qos2.in.>` and has a "limits" policy with `MaxMsgsPer` of
1.
5. `$MQTT_out` stores **Outgoing QoS 2** `PUBREL` packets. It filters on
`$MQTT.out.>` and has a "interest" retention policy.
## Consumers and Internal NATS Subscriptions
### Account Scope
- A durable consumer for [Retained Messages](#retained-message) -
`$MQTT_rmsgs_<server name hash>`
- A subscription to handle all [jsa](#jetstream-api) replies for the account.
- A subscription to replies to "session persist" requests, so that we can detect
the use of a session with the same client ID anywhere in the cluster.
- 2 subscriptions to support [retained messages](#retained-message):
`$MQTT.sub.<nuid>` for the messages themselves, and one to receive replies to
"delete retained message" JS API (on the JS reply subject var).
### Session Scope
When a new QoS 2 MQTT subscription is detected in a session, we ensure that
there is a durable consumer for [QoS
2](#quality-of-service-qos-publish-identifier-pi) `PUBREL`s out for delivery -
`$MQTT_PUBREL_<session id hash>`
### Subscription Scope
For all MQTT subscriptions, regardless of their QoS, we create internal NATS subscriptions to
- `subject` (directly encoded from `topic`). This subscription is used to
deliver QoS 0 messages, and messages originating from NATS.
- if needed, `subject fwc` complements `subject` for topics like `topic.#` to
include `topic` itself, see [top-level wildcards](#subject-wildcards)
For QoS 1 or 2 MQTT subscriptions we ensure:
- A durable consumer for messages out for delivery - `<session ID hash>_<nuid>`
- An internal subscription to `$MQTT.sub.<nuid>` to deliver the messages to the
receiving client.
### (Old) Notes
As indicated before, for a QoS1 or QoS2 subscription, the server will create a
JetStream consumer with the appropriate subject filter. If the subscription
already existed, then only the NATS subscription is created for the JetStream
consumers delivery subject.
Note that JS consumers can be created with an “Replicas” override, which from
recent discussion is problematic with “Interest” policy streams, which
“$MQTT_msgs” is.
We do handle situations where a subscription on the same subject filter is sent
with a different QoS as per MQTT specifications. If the existing was on QoS 1 or
2, and the “new” is for QoS 0, then we delete the existing JS consumer.
Subscriptions that are QoS 0 have a NATS subscription with the callback function
being `mqttDeliverMsgCbQos0()`; while QoS 1 and 2 have a NATS subscription with
callback `mqttDeliverMsgCbQos12()`. Both those functions have comments that
describe the reason for their existence and what they are doing. For instance
the `mqttDeliverMsgCbQos0()` callback will reject any producing client that is
of type JETSTREAM, so that it handles only non JetStream (QoS 1 and 2) messages.
Both these functions end-up calling mqttDeliver() which will first enqueue the
possible retained messages buffer before delivering any new message. The message
itself being delivered is serialized in MQTT format and enqueued to the clients
outbound buffer and call to addToPCD is made so that it is flushed out of the
readloop.
# 3. Lifecycles
## Connection, Session
An MQTT connection is created when a listening MQTT server receives a `CONNECT`
packet. See `mqttProcessConnect()`. A connection is associated with a session.
Steps:
1. Ensure that we have an `AccountSessionManager` so we can have an
`mqttSession`. Lazily initialize JetStream streams, and internal consumers
and subscriptions. See `getOrCreateMQTTAccountSessionManager()`.
2. Find and disconnect any previous session/client for the same ID. See
`mqttProcessConnect()`.
3. Ensure we have an `mqttSession` - create a new or load a previously persisted
one. If the clean flag is set in `CONNECT`, clean the session. see
`mqttSession.clear()`
4. Initialize session's subscriptions, if any.
5. Always send back a `CONNACK` packet. If there were errors in previous steps,
include the error.
An MQTT connection can be closed for a number of reasons, including receiving a
`DISCONNECT` from the client, explicit internal errors processing MQTT packets,
or the server receiving another `CONNECT` packet with the same client ID. See
`mqttHandleClosedClient()` and `mqttHandleWill()`. Steps:
1. Send out the Will Message if applicable (if not caused by a `DISCONNECT` packet)
2. Delete the the JetStream consumers for to QoS 1 and 2 packet delivery through
JS API calls (if "clean" session flag is set)
3. Delete the session record from the “$MQTT_sess” stream, based on recorded
stream sequence. (if "clean" session flag is set)
4. Close the client connection.
On an explicit disconnect, that is, the client sends the DISCONNECT packet, the
server will NOT send the Will, as per specifications.
For sessions that had the “clean” flag, the JS consumers corresponding to QoS 1
subscriptions are deleted through JS API calls, the session record is then
deleted (based on recorded stream sequence) from the “$MQTT_sess” stream.
Finally, the client connection is closed
Sessions are persisted on disconnect, and on subscriptions changes.
## Subscription
Receiving an MQTT `SUBSCRIBE` packet creates new subscriptions, or updates
existing subscriptions in a session. Each `SUBSCRIBE` packet may contain several
specific subscriptions (`topic` + QoS in each). We always respond with a
`SUBACK`, which may indicate which subscriptions errored out.
For each subscription in the packet, we:
1. Ignore it if `topic` starts with `$MQTT.sub.`.
2. Set up QoS 0 message delivery - an internal NATS subscription on `topic`.
3. Replay any retained messages for `topic`, once as QoS 0.
4. If we already have a subscription on `topic`, update its QoS
5. If this is a QoS 2 subscription in the session, ensure we have the [PUBREL
consumer](#session-scope) for the session.
6. If this is a QoS 1 or 2 subscription, ensure we have the [Message
consumer](#subscription-scope) for this subscription (or delete one if it
exists and this is now a QoS 0 sub).
7. Add an extra subscription for the [top-level wildcard](#subject-wildcards) case.
8. Update the session, persist it if changed.
When a session is restored (no clean flag), we go through the same steps to
re-subscribe to its stored subscription, except step #8 which would have been
redundant.
When we get an `UNSUBSCRIBE` packet, it can contain multiple subscriptions to
unsubscribe. The parsing will generate a slice of mqttFilter objects that
contain the “filter” (the topic with possibly wildcard of the subscription) and
the QoS value. The server goes through the list and deletes the JS consumer (if
QoS 1 or 2) and unsubscribes the NATS subscription for the delivery subject (if
it was a QoS 1 or 2) or on the actual topic/subject. In case of the “#”
wildcard, the server will handle the “level up” subscriptions that NATS had to
create.
Again, we update the session and persist it as needed in the `$MQTT_sess`
stream.
## Message
1. Detect an incoming PUBLISH packet, parse and check the message QoS. Fill out
the session's `mqttPublish` struct that contains information about the
published message. (see `mqttParse()`, `mqttParsePub()`)
2. Process the message according to its QoS (see `mqttProcessPub()`)
- QoS 0:
- Initiate message delivery
- QoS 1:
- Initiate message delivery
- Send back a `PUBACK`
- QoS 2:
- Store the message in `$MQTT_qos2in` stream, using a PI-specific subject.
Since `MaxMsgsPer` is set to 1, we will ignore duplicates on the PI.
- Send back a `PUBREC`
- "Wait" for a `PUBREL`, then initiate message delivery
- Remove the previously stored QoS2 message
- Send back a `PUBCOMP`
3. Initiate message delivery (see `mqttInitiateMsgDelivery()`)
- Convert the MQTT `topic` into a NATS `subject` using
`mqttTopicToNATSPubSubject()` function. If there is a known subject
mapping, then we select the new subject using `selectMappedSubject()`
function and then convert back this subject into an MQTT topic using
`natsSubjectToMQTTTopic()` function.
- Re-serialize the `PUBLISH` packet received as a NATS message. Use NATS
headers for the metadata, and the deliverable MQTT `PUBLISH` packet as the
contents.
- Publish the messages as `subject` (and `subject fwc` if applicable, see
[subject wildcards](#subject-wildcards)). Use the "standard" NATS
`c.processInboundClientMsg()` to do that. `processInboundClientMsg()` will
distribute the message to any NATS subscriptions (including routes,
gateways, leafnodes) and the relevant MQTT subscriptions.
- Check for retained messages, process as needed. See
`c.processInboundClientMsg()` calling `c.mqttHandlePubRetain()` For MQTT
clients.
- If the message QoS is 1 or 2, store it in `$MQTT_msgs` stream as
`$MQTT.msgs.<subject>` for "at least once" delivery with retries.
4. Let NATS and JetStream deliver to the internal subscriptions, and to the
receiving clients. See `mqttDeliverMsgCb...()`
- The NATS message posted to `subject` (and `subject fwc`) will be delivered
to each relevant internal subscription by calling `mqttDeliverMsgCbQoS0()`.
The function has access to both the publishing and the receiving clients.
- Ignore all irrelevant invocations. Specifically, do nothing if the
message needs to be delivered with a higher QoS - that will be handled by
the other, `...QoS12` callback. Note that if the original message was
publuished with a QoS 1 or 2, but the subscription has its maximum QoS
set to 0, the message will be delivered by this callback.
- Ignore "reserved" subscriptions, as per MQTT spec.
- Decode delivery `topic` from the NATS `subject`.
- Write (enqueue) outgoing `PUBLISH` packet.
- **DONE for QoS 0**
- The NATS message posted to JetStream as `$MQTT.msgs.subject` will be
consumed by subscription-specific consumers. Note that MQTT subscriptions
with max QoS 0 do not have JetStream consumers. They are handled by the
QoS0 callback.
The consumers will deliver it to the `$MQTT.sub.<nuid>`
subject for their respective NATS subscriptions by calling
`mqttDeliverMsgCbQoS12()`. This callback too has access to both the
publishing and the receiving clients.
- Ignore "reserved" subscriptions, as per MQTT spec.
- See if this is a re-delivery from JetStream by checking `sess.cpending`
for the JS reply subject. If so, use the existing PI and treat this as a
duplicate redelivery.
- Otherwise, assign the message a new PI (see `trackPublish()` and
`bumpPI()`) and store it in `sess.cpending` and `sess.pendingPublish`,
along with the JS reply subject that can be used to remove this pending
message from the consumer once it's delivered to the receipient.
- Decode delivery `topic` from the NATS `subject`.
- Write (enqueue) outgoing `PUBLISH` packet.
5. QoS 1: "Wait" for a `PUBACK`. See `mqttProcessPubAck()`.
- When received, remove the PI from the tracking maps, send an ACK to
consumer to remove the message.
- **DONE for QoS 1**
6. QoS 2: "Wait" for a `PUBREC`. When received, we need to do all the same
things as in the QoS 1 `PUBACK` case, but we need to send out a `PUBREL`, and
continue using the same PI until the delivery flow is complete and we get
back a `PUBCOMP`. For that, we add the PI to `sess.pendingPubRel`, and to
`sess.cpending` with the PubRel consumer durable name.
We also compose and store a headers-only NATS message signifying a `PUBREL`
out for delivery, and store it in the `$MQTT_qos2out` stream, as
`$MQTT.qos2.out.<session-id>`.
7. QoS 2: Deliver `PUBREL`. The PubRel session-specific consumer will publish to
internal subscription on `$MQTT.qos2.delivery`, calling
`mqttDeliverPubRelCb()`. We store the ACK reply subject in `cpending` to
remove the JS message on `PUBCOMP`, compose and send out a `PUBREL` packet.
8. QoS 2: "Wait" for a `PUBCOMP`. See `mqttProcessPubComp()`.
- When received, remove the PI from the tracking maps, send an ACK to
consumer to remove the `PUBREL` message.
- **DONE for QoS 2**
## Retained messages
When we process an inbound `PUBLISH` and submit it to
`processInboundClientMsg()` function, for MQTT clients it will invoke
`mqttHandlePubRetain()` which checks if the published message is “retained” or
not.
If it is, then we construct a record representing the retained message and store
it in the `$MQTT_rmsg` stream, under the single `$MQTT.rmsg` subject. The stored
record (in JSON) contains information about the subject, topic, MQTT flags, user
that produced this message and the message content itself. It is stored and the
stream sequence is remembered in the memory structure that contains retained
messages.
Note that when creating an account session manager, the retained messages stream
is read from scratch to load all the messages through the use of a JS consumer.
The associated subscription will process the recovered retained messages or any
new that comes from the network.
A retained message is added to a map and a subscription is created and inserted
into a sublist that will be used to perform a ReverseMatch() when a subscription
is started and we want to find all retained messages that the subscription would
have received if it had been running prior to the message being published.
If a retained message on topic “foo” already exists, then the server has to
delete the old message at the stream sequence we saved when storing it.
This could have been done with having retained messages stored under
`$MQTT.rmsg.<subject>` as opposed to all under a single subject, and make use of
the `MaxMsgsPer` field set to 1. The `MaxMsgsPer` option was introduced well into
the availability of MQTT and changes to the sessions was made in [PR
#2501](https://github.com/nats-io/nats-server/pull/2501), with a conversion of
existing streams such as `$MQTT*sess*<sess ID>` into a single stream with unique
subjects, but the changes were not made to the retained messages stream.
There are also subscriptions for the handling of retained messages which are
messages that are asked by the publisher to be retained by the MQTT server to be
delivered to matching subscriptions when they start. There is a single message
per topic. Retained messages are deleted when the user sends a retained message
(there is a flag in the PUBLISH protocol) on a given topic with an empty body.
The difficulty with retained messages is to handle them in a cluster since all
servers need to be aware of their presence so that they can deliver them to
subscriptions that those servers may become the leader for.
- `$MQTT_rmsgs` which has a “limits” policy and holds retained messages, all
under `$MQTT.rmsg` single subject. Not sure why I did not use MaxMsgsPer for
this stream and not filter `$MQTT.rmsg.>`.
The first step when processing a new subscription is to gather the retained
messages that would be a match for this subscription. To do so, the server will
serialize into a buffer all messages for the account session managers sublists
ReverseMatch result. We use the returned subscriptions subject to find from a
map appropriate retained message (see `serializeRetainedMsgsForSub()` for
details).
# 4. Implementation Notes
## Hooking into NATS I/O
### Starting the accept loop
The MQTT accept loop is started when the server detects that an MQTT port has
been defined in the configuration file. It works similarly to all other accept
loops. Note that for MQTT over websocket, the websocket port has to be defined
and MQTT clients will connect to that port instead of the MQTT port and need to
provide `/mqtt` as part of the URL to redirect the creation of the client to an
MQTT client (with websocket support) instead of a regular NATS with websocket.
See the branching done in `startWebsocketServer()`. See `startMQTT()`.
### Starting the read/write loops
When a TCP connection is accepted, the internal go routine will invoke
`createMQTTClient()`. This function will set a `c.mqtt` object that will make it
become an MQTT client (through the `isMqtt()` helper function). The `readLoop()`
and `writeLoop()` are started similarly to other clients. However, the read loop
will branch out to `mqttParse()` instead when detecting that this is an MQTT
client.
## Session Management
### Account Session Manager
`mqttAccountSessionManager` is an object that holds the state of all sessions in
an account. It also manages the lifecycle of JetStream streams and internal
subscriptions for processing JS API replies, session updates, etc. See
`mqttCreateAccountSessionManager()`. It is lazily initialized upon the first
MQTT `CONNECT` packet received. Account session manager is referred to as `asm`
in the code.
Note that creating the account session manager (and attempting to create the
streams) is done only once per account on a given server, since once created the
account session manager for a given account would be found in the sessions map
of the mqttSessionManager object.
### Find and disconnect previous session/client
Once all that is done, we now go to the creation of the session object itself.
For that, we first need to make sure that it does not already exist, meaning
that it is registered on the server - or anywhere in the cluster. Note that MQTT
dictates that if a session with the same ID connects, the OLD session needs to
be closed, not the new one being created. NATS Server complies with this
requirement.
Once a session is detected to already exists, the old one (as described above)
is closed and the new one accepted, however, the session ID is maintained in a
flappers map so that we detect situations where sessions with the same ID are
started multiple times causing the previous one to be closed. When that
detection occurs, the newly created session is put in “jail” for a second to
avoid a very rapid succession of connect/disconnect. This has already been seen
by users since there was some issue there where we would schedule the connection
closed instead of waiting in place which was causing a panic.
We also protect from multiple clients on a given server trying to connect with
the same ID at the “same time” while the processing of a CONNECT of a session is
not yet finished. This is done with the use of a sessLocked map, keyed by the
session ID.
### Create or restore the session
If everything is good up to that point, the server will either create or restore
a session from the stream. This is done in the `createOrRestoreSession()`
function. The client/session ID is hashed and added to the sessions stream
subject along with the JS domain to prevent clients connecting from different
domains to “pollute” the session stream of a given domain.
Since each session constitutes a subject and the stream has a maximum of 1
message per subject, we attempt to load the last message on the formed subject.
If we dont find it, then the session object is created “empty”, while if we
find a record, we create the session object based on the record persisted on the
stream.
If the session was restored from the JS stream, we keep track of the stream
sequence where the record was located. When we save the session (even if it
already exists) we will use this sequence number to set the
`JSExpectedLastSubjSeq` header so that we handle possibly different servers in a
(super)cluster to detect the race of clients trying to use the same session ID,
since only one of the write should succeed. On success, the sessions new
sequence is remembered by the server that did the write.
When created or restored, the CONNACK can now be sent back to the client, and if
there were any recovered subscriptions, they are now processed.
## Processing QoS acks: PUBACK, PUBREC, PUBCOMP
When the server delivers a message with QoS 1 or 2 (also a `PUBREL` for QoS 2) to a subscribed client, the client will send back an acknowledgement. See `mqttProcessPubAck()`, `mqttProcessPubRec()`, and `mqttProcessPubComp()`
While the specific logic for each packet differs, these handlers all update the
session's PI mappings (`cpending`, `pendingPublish`, `pendingPubRel`), and if
needed send an ACK to JetStream to remove the message from its consumer and stop
the re-delivery attempts.
## Subject Wildcards
Note that MQTT subscriptions have wildcards too, the `“+”` wildcard is equivalent
to NATSs `“*”` wildcard, however, MQTTs wildcard `“#”` is similar to `“>”`, except
that it also includes the level above. That is, a subscription on `“foo/#”` would
receive messages on `“foo/bar/baz”`, but also on `“foo”`.
So, for MQTT subscriptions enging with a `'#'` we are forced to create 2
internal NATS subscriptions, one on `“foo”` and one on `“foo.>”`.
# 5. Known issues
- "active" redelivery for QoS from JetStream (compliant, just a note)
- JetStream QoS redelivery happens out of (original) order
- finish delivery of in-flight messages after UNSUB
- finish delivery of in-flight messages after a reconnect
- consider replacing `$MQTT_msgs` with `$MQTT_out`.
- consider using unique `$MQTT.rmsg.>` and `MaxMsgsPer` for retained messages.
- add a cli command to list/clean old sessions
+113 -522
View File
@@ -18,7 +18,6 @@ import (
"encoding/hex"
"errors"
"fmt"
"hash/fnv"
"hash/maphash"
"io"
"io/fs"
@@ -27,7 +26,6 @@ import (
"net/http"
"net/textproto"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
@@ -85,6 +83,7 @@ type Account struct {
expired bool
incomplete bool
signingKeys map[string]jwt.Scope
extAuth *jwt.ExternalAuthorization
srv *Server // server this account is registered with (possibly nil)
lds string // loop detection subject for leaf nodes
siReply []byte // service reply prefix, will form wildcard subscription.
@@ -95,8 +94,14 @@ type Account struct {
tags jwt.TagList
nameTag string
lastLimErr int64
routePoolIdx int
}
const (
accDedicatedRoute = -1
accTransitioningToDedicatedRoute = -2
)
// Account based limits.
type limits struct {
mpay int32
@@ -117,8 +122,8 @@ type streamImport struct {
acc *Account
from string
to string
tr *transform
rtr *transform
tr *subjectTransform
rtr *subjectTransform
claim *jwt.Import
usePub bool
invalid bool
@@ -134,7 +139,7 @@ type serviceImport struct {
sid []byte
from string
to string
tr *transform
tr *subjectTransform
ts int64
rt ServiceRespType
latency *serviceLatency
@@ -168,31 +173,6 @@ const (
Chunked
)
// Subject mapping and transform setups.
var (
commaSeparatorRegEx = regexp.MustCompile(`,\s*`)
partitionMappingFunctionRegEx = regexp.MustCompile(`{{\s*[pP]artition\s*\((.*)\)\s*}}`)
wildcardMappingFunctionRegEx = regexp.MustCompile(`{{\s*[wW]ildcard\s*\((.*)\)\s*}}`)
splitFromLeftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit[fF]rom[lL]eft\s*\((.*)\)\s*}}`)
splitFromRightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit[fF]rom[rR]ight\s*\((.*)\)\s*}}`)
sliceFromLeftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]lice[fF]rom[lL]eft\s*\((.*)\)\s*}}`)
sliceFromRightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]lice[fF]rom[rR]ight\s*\((.*)\)\s*}}`)
splitMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit\s*\((.*)\)\s*}}`)
)
// Enum for the subject mapping transform function types
const (
NoTransform int16 = iota
BadTransform
Partition
Wildcard
SplitFromLeft
SplitFromRight
SliceFromLeft
SliceFromRight
Split
)
// String helper.
func (rt ServiceRespType) String() string {
switch rt {
@@ -598,7 +578,7 @@ func NewMapDest(subject string, weight uint8) *MapDest {
// destination is for internal representation for a weighted mapped destination.
type destination struct {
tr *transform
tr *subjectTransform
weight uint8
}
@@ -617,7 +597,6 @@ func (a *Account) AddMapping(src, dest string) error {
}
// AddWeightedMappings will add in a weighted mappings for the destinations.
// TODO(dlc) - Allow cluster filtering
func (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error {
a.mu.Lock()
defer a.mu.Unlock()
@@ -634,7 +613,7 @@ func (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error {
m := &mapping{src: src, wc: subjectHasWildcard(src), dests: make([]*destination, 0, len(dests)+1)}
seen := make(map[string]struct{})
var tw uint8
var tw = make(map[string]uint8)
for _, d := range dests {
if _, ok := seen[d.Subject]; ok {
return fmt.Errorf("duplicate entry for %q", d.Subject)
@@ -643,15 +622,15 @@ func (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error {
if d.Weight > 100 {
return fmt.Errorf("individual weights need to be <= 100")
}
tw += d.Weight
if tw > 100 {
tw[d.Cluster] += d.Weight
if tw[d.Cluster] > 100 {
return fmt.Errorf("total weight needs to be <= 100")
}
err := ValidateMappingDestination(d.Subject)
if err != nil {
return err
}
tr, err := newTransform(src, d.Subject)
tr, err := NewSubjectTransform(src, d.Subject)
if err != nil {
return err
}
@@ -682,7 +661,7 @@ func (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error {
// We need to make the appropriate markers for the wildcards etc.
dest = transformTokenize(dest)
}
tr, err := newTransform(src, dest)
tr, err := NewSubjectTransform(src, dest)
if err != nil {
return nil, err
}
@@ -740,38 +719,6 @@ func (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error {
return nil
}
// Helper function to tokenize subjects with partial wildcards into formal transform destinations.
// e.g. foo.*.* -> foo.$1.$2
func transformTokenize(subject string) string {
// We need to make the appropriate markers for the wildcards etc.
i := 1
var nda []string
for _, token := range strings.Split(subject, tsep) {
if token == "*" {
nda = append(nda, fmt.Sprintf("$%d", i))
i++
} else {
nda = append(nda, token)
}
}
return strings.Join(nda, tsep)
}
func transformUntokenize(subject string) (string, []string) {
var phs []string
var nda []string
for _, token := range strings.Split(subject, tsep) {
if len(token) > 1 && token[0] == '$' && token[1] >= '1' && token[1] <= '9' {
phs = append(phs, token)
nda = append(nda, "*")
} else {
nda = append(nda, token)
}
}
return strings.Join(nda, tsep), phs
}
// RemoveMapping will remove an existing mapping.
func (a *Account) RemoveMapping(src string) bool {
a.mu.Lock()
@@ -879,8 +826,8 @@ func (a *Account) selectMappedSubject(dest string) (string, bool) {
if d != nil {
if len(d.tr.dtokmftokindexesargs) == 0 {
ndest = d.tr.dest
} else if nsubj, err := d.tr.transform(tts); err == nil {
ndest = nsubj
} else {
ndest = d.tr.TransformTokenizedSubject(tts)
}
}
@@ -1014,13 +961,9 @@ func (a *Account) removeClient(c *client) int {
}
if c != nil && c.srv != nil && removed {
c.srv.mu.Lock()
doRemove := a != c.srv.gacc
c.srv.mu.Unlock()
if doRemove {
c.srv.accConnsUpdate(a)
}
c.srv.accConnsUpdate(a)
}
return n
}
@@ -1624,9 +1567,14 @@ func (a *Account) checkStreamImportsForCycles(to string, visited map[string]bool
// SetServiceImportSharing will allow sharing of information about requests with the export account.
// Used for service latency tracking at the moment.
func (a *Account) SetServiceImportSharing(destination *Account, to string, allow bool) error {
return a.setServiceImportSharing(destination, to, true, allow)
}
// setServiceImportSharing will allow sharing of information about requests with the export account.
func (a *Account) setServiceImportSharing(destination *Account, to string, check, allow bool) error {
a.mu.Lock()
defer a.mu.Unlock()
if a.isClaimAccount() {
if check && a.isClaimAccount() {
return fmt.Errorf("claim based accounts can not be updated directly")
}
for _, si := range a.imports.services {
@@ -1945,7 +1893,7 @@ func (a *Account) addServiceImport(dest *Account, from, to string, claim *jwt.Im
// Check to see if we have a wildcard
var (
usePub bool
tr *transform
tr *subjectTransform
err error
)
if subjectHasWildcard(to) {
@@ -1955,9 +1903,9 @@ func (a *Account) addServiceImport(dest *Account, from, to string, claim *jwt.Im
} else {
to, _ = transformUntokenize(to)
// Create a transform. Do so in reverse such that $ symbols only exist in to
if tr, err = newTransform(to, transformTokenize(from)); err != nil {
if tr, err = NewSubjectTransformStrict(to, transformTokenize(from)); err != nil {
a.mu.Unlock()
return nil, fmt.Errorf("failed to create mapping transform for service import subject %q to %q: %v",
return nil, fmt.Errorf("failed to create mapping transform for service import subject from %q to %q: %v",
from, to, err)
} else {
// un-tokenize and reverse transform so we get the transform needed
@@ -1996,6 +1944,13 @@ func (a *Account) subscribeInternal(subject string, cb msgHandler) (*subscriptio
return a.subscribeInternalEx(subject, cb, false)
}
// Unsubscribe from an internal account subscription.
func (a *Account) unsubscribeInternal(sub *subscription) {
if ic := a.internalClient(); ic != nil {
ic.processUnsub(sub.sid)
}
}
// Creates internal subscription for service import responses.
func (a *Account) subscribeServiceImportResponse(subject string) (*subscription, error) {
return a.subscribeInternalEx(subject, a.processServiceImportResponse, true)
@@ -2496,7 +2451,7 @@ func (a *Account) AddMappedStreamImportWithClaim(account *Account, from, to stri
var (
usePub bool
tr *transform
tr *subjectTransform
err error
)
if subjectHasWildcard(from) {
@@ -2504,8 +2459,8 @@ func (a *Account) AddMappedStreamImportWithClaim(account *Account, from, to stri
usePub = true
} else {
// Create a transform
if tr, err = newTransform(from, transformTokenize(to)); err != nil {
return fmt.Errorf("failed to create mapping transform for stream import subject %q to %q: %v",
if tr, err = NewSubjectTransformStrict(from, transformTokenize(to)); err != nil {
return fmt.Errorf("failed to create mapping transform for stream import subject from %q to %q: %v",
from, to, err)
}
to, _ = transformUntokenize(to)
@@ -3129,6 +3084,72 @@ func (a *Account) traceLabel() string {
return a.Name
}
// Check if an account has external auth set.
// Operator/Account Resolver only.
func (a *Account) hasExternalAuth() bool {
if a == nil {
return false
}
a.mu.RLock()
defer a.mu.RUnlock()
return a.extAuth != nil
}
// Deterimine if this is an external auth user.
func (a *Account) isExternalAuthUser(userID string) bool {
if a == nil {
return false
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.extAuth != nil {
for _, u := range a.extAuth.AuthUsers {
if userID == u {
return true
}
}
}
return false
}
// Return the external authorization xkey if external authorization is enabled and the xkey is set.
// Operator/Account Resolver only.
func (a *Account) externalAuthXKey() string {
if a == nil {
return _EMPTY_
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.extAuth != nil && a.extAuth.XKey != _EMPTY_ {
return a.extAuth.XKey
}
return _EMPTY_
}
// Check if an account switch for external authorization is allowed.
func (a *Account) isAllowedAcount(acc string) bool {
if a == nil {
return false
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.extAuth != nil {
// if we have a single allowed account, and we have a wildcard
// we accept it
if len(a.extAuth.AllowedAccounts) == 1 &&
a.extAuth.AllowedAccounts[0] == jwt.AnyAccount {
return true
}
// otherwise must match exactly
for _, a := range a.extAuth.AllowedAccounts {
if a == acc {
return true
}
}
}
return false
}
// updateAccountClaimsWithRefresh will update an existing account with new claims.
// If refreshImportingAccounts is true it will also update incomplete dependent accounts
// This will replace any exports or imports previously defined.
@@ -3148,6 +3169,14 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
a.nameTag = ac.Name
a.tags = ac.Tags
// Check for external authorization.
if ac.HasExternalAuthorization() {
a.extAuth = &jwt.ExternalAuthorization{}
a.extAuth.AuthUsers.Add(ac.Authorization.AuthUsers...)
a.extAuth.AllowedAccounts.Add(ac.Authorization.AllowedAccounts...)
a.extAuth.XKey = ac.Authorization.XKey
}
// Reset exports and imports here.
// Exports is creating a whole new map.
@@ -4400,441 +4429,3 @@ func (dr *CacheDirAccResolver) Start(s *Server) error {
func (dr *CacheDirAccResolver) Reload() error {
return dr.DirAccResolver.Reload()
}
// Transforms for arbitrarily mapping subjects from one to another for maps, tees and filters.
// These can also be used for proper mapping on wildcard exports/imports.
// These will be grouped and caching and locking are assumed to be in the upper layers.
type transform struct {
src, dest string
dtoks []string // destination tokens
stoks []string // source tokens
dtokmftypes []int16 // destination token mapping function types
dtokmftokindexesargs [][]int // destination token mapping function array of source token index arguments
dtokmfintargs []int32 // destination token mapping function int32 arguments
dtokmfstringargs []string // destination token mapping function string arguments
}
func getMappingFunctionArgs(functionRegEx *regexp.Regexp, token string) []string {
commandStrings := functionRegEx.FindStringSubmatch(token)
if len(commandStrings) > 1 {
return commaSeparatorRegEx.Split(commandStrings[1], -1)
}
return nil
}
// Helper for mapping functions that take a wildcard index and an integer as arguments
func transformIndexIntArgsHelper(token string, args []string, transformType int16) (int16, []int, int32, string, error) {
if len(args) < 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments}
}
if len(args) > 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments}
}
i, err := strconv.Atoi(strings.Trim(args[0], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument}
}
mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[1], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument}
}
return transformType, []int{i}, int32(mappingFunctionIntArg), _EMPTY_, nil
}
// Helper to ingest and index the transform destination token (e.g. $x or {{}}) in the token
// returns a transformation type, and three function arguments: an array of source subject token indexes, and a single number (e.g. number of partitions, or a slice size), and a string (e.g.a split delimiter)
func indexPlaceHolders(token string) (int16, []int, int32, string, error) {
length := len(token)
if length > 1 {
// old $1, $2, etc... mapping format still supported to maintain backwards compatibility
if token[0] == '$' { // simple non-partition mapping
tp, err := strconv.Atoi(token[1:])
if err != nil {
// other things rely on tokens starting with $ so not an error just leave it as is
return NoTransform, []int{-1}, -1, _EMPTY_, nil
}
return Wildcard, []int{tp}, -1, _EMPTY_, nil
}
// New 'mustache' style mapping
if length > 4 && token[0] == '{' && token[1] == '{' && token[length-2] == '}' && token[length-1] == '}' {
// wildcard(wildcard token index) (equivalent to $)
args := getMappingFunctionArgs(wildcardMappingFunctionRegEx, token)
if args != nil {
if len(args) == 1 && args[0] == _EMPTY_ {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments}
}
if len(args) == 1 {
tokenIndex, err := strconv.Atoi(strings.Trim(args[0], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument}
}
return Wildcard, []int{tokenIndex}, -1, _EMPTY_, nil
} else {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments}
}
}
// partition(number of partitions, token1, token2, ...)
args = getMappingFunctionArgs(partitionMappingFunctionRegEx, token)
if args != nil {
if len(args) < 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments}
}
if len(args) >= 2 {
mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument}
}
var numPositions = len(args[1:])
tokenIndexes := make([]int, numPositions)
for ti, t := range args[1:] {
i, err := strconv.Atoi(strings.Trim(t, " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument}
}
tokenIndexes[ti] = i
}
return Partition, tokenIndexes, int32(mappingFunctionIntArg), _EMPTY_, nil
}
}
// SplitFromLeft(token, position)
args = getMappingFunctionArgs(splitFromLeftMappingFunctionRegEx, token)
if args != nil {
return transformIndexIntArgsHelper(token, args, SplitFromLeft)
}
// SplitFromRight(token, position)
args = getMappingFunctionArgs(splitFromRightMappingFunctionRegEx, token)
if args != nil {
return transformIndexIntArgsHelper(token, args, SplitFromRight)
}
// SliceFromLeft(token, position)
args = getMappingFunctionArgs(sliceFromLeftMappingFunctionRegEx, token)
if args != nil {
return transformIndexIntArgsHelper(token, args, SliceFromLeft)
}
// SliceFromRight(token, position)
args = getMappingFunctionArgs(sliceFromRightMappingFunctionRegEx, token)
if args != nil {
return transformIndexIntArgsHelper(token, args, SliceFromRight)
}
// split(token, deliminator)
args = getMappingFunctionArgs(splitMappingFunctionRegEx, token)
if args != nil {
if len(args) < 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments}
}
if len(args) > 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments}
}
i, err := strconv.Atoi(strings.Trim(args[0], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument}
}
if strings.Contains(args[1], " ") || strings.Contains(args[1], tsep) {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token: token, err: ErrorMappingDestinationFunctionInvalidArgument}
}
return Split, []int{i}, -1, args[1], nil
}
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction}
}
}
return NoTransform, []int{-1}, -1, _EMPTY_, nil
}
// SubjectTransformer transforms subjects using mappings
//
// This API is not part of the public API and not subject to SemVer protections
type SubjectTransformer interface {
Match(string) (string, error)
}
// NewSubjectTransformer creates a new SubjectTransformer
//
// This API is not part of the public API and not subject to SemVer protections
func NewSubjectTransformer(src, dest string) (SubjectTransformer, error) {
return newTransform(src, dest)
}
// newTransform will create a new transform checking the src and dest subjects for accuracy.
func newTransform(src, dest string) (*transform, error) {
// Both entries need to be valid subjects.
sv, stokens, npwcs, hasFwc := subjectInfo(src)
dv, dtokens, dnpwcs, dHasFwc := subjectInfo(dest)
// Make sure both are valid, match fwc if present and there are no pwcs in the dest subject.
if !sv || !dv || dnpwcs > 0 || hasFwc != dHasFwc {
return nil, ErrBadSubject
}
var dtokMappingFunctionTypes []int16
var dtokMappingFunctionTokenIndexes [][]int
var dtokMappingFunctionIntArgs []int32
var dtokMappingFunctionStringArgs []string
// If the src has partial wildcards then the dest needs to have the token place markers.
if npwcs > 0 || hasFwc {
// We need to count to make sure that the dest has token holders for the pwcs.
sti := make(map[int]int)
for i, token := range stokens {
if len(token) == 1 && token[0] == pwc {
sti[len(sti)+1] = i
}
}
nphs := 0
for _, token := range dtokens {
tranformType, transformArgWildcardIndexes, transfomArgInt, transformArgString, err := indexPlaceHolders(token)
if err != nil {
return nil, err
}
if tranformType == NoTransform {
dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform)
dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1})
dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1)
dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, _EMPTY_)
} else {
// We might combine multiple tokens into one, for example with a partition
nphs += len(transformArgWildcardIndexes)
// Now build up our runtime mapping from dest to source tokens.
var stis []int
for _, wildcardIndex := range transformArgWildcardIndexes {
if wildcardIndex > npwcs {
return nil, &mappingDestinationErr{fmt.Sprintf("%s: [%d]", token, wildcardIndex), ErrorMappingDestinationFunctionWildcardIndexOutOfRange}
}
stis = append(stis, sti[wildcardIndex])
}
dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, tranformType)
dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, stis)
dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, transfomArgInt)
dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, transformArgString)
}
}
if nphs < npwcs {
// not all wildcards are being used in the destination
return nil, &mappingDestinationErr{dest, ErrMappingDestinationNotUsingAllWildcards}
}
}
return &transform{src: src, dest: dest, dtoks: dtokens, stoks: stokens, dtokmftypes: dtokMappingFunctionTypes, dtokmftokindexesargs: dtokMappingFunctionTokenIndexes, dtokmfintargs: dtokMappingFunctionIntArgs, dtokmfstringargs: dtokMappingFunctionStringArgs}, nil
}
// Match will take a literal published subject that is associated with a client and will match and transform
// the subject if possible.
//
// This API is not part of the public API and not subject to SemVer protections
func (tr *transform) Match(subject string) (string, error) {
// TODO(dlc) - We could add in client here to allow for things like foo -> foo.$ACCOUNT
// Special case: matches any and no no-op transform. May not be legal config for some features
// but specific validations made at transform create time
if (tr.src == fwcs || tr.src == _EMPTY_) && (tr.dest == fwcs || tr.dest == _EMPTY_) {
return subject, nil
}
// Tokenize the subject. This should always be a literal subject.
tsa := [32]string{}
tts := tsa[:0]
start := 0
for i := 0; i < len(subject); i++ {
if subject[i] == btsep {
tts = append(tts, subject[start:i])
start = i + 1
}
}
tts = append(tts, subject[start:])
if !isValidLiteralSubject(tts) {
return _EMPTY_, ErrBadSubject
}
if (tr.src == _EMPTY_ || tr.src == fwcs) || isSubsetMatch(tts, tr.src) {
return tr.transform(tts)
}
return _EMPTY_, ErrNoTransforms
}
// transformSubject do not need to match, just transform.
func (tr *transform) transformSubject(subject string) (string, error) {
// Tokenize the subject.
tsa := [32]string{}
tts := tsa[:0]
start := 0
for i := 0; i < len(subject); i++ {
if subject[i] == btsep {
tts = append(tts, subject[start:i])
start = i + 1
}
}
tts = append(tts, subject[start:])
return tr.transform(tts)
}
func (tr *transform) getHashPartition(key []byte, numBuckets int) string {
h := fnv.New32a()
h.Write(key)
return strconv.Itoa(int(h.Sum32() % uint32(numBuckets)))
}
// Do a transform on the subject to the dest subject.
func (tr *transform) transform(tokens []string) (string, error) {
if len(tr.dtokmftypes) == 0 {
return tr.dest, nil
}
var b strings.Builder
// We need to walk destination tokens and create the mapped subject pulling tokens or mapping functions
// This is slow and that is ok, transforms should have caching layer in front for mapping transforms
// and export/import semantics with streams and services.
li := len(tr.dtokmftypes) - 1
for i, mfType := range tr.dtokmftypes {
if mfType == NoTransform {
// Break if fwc
if len(tr.dtoks[i]) == 1 && tr.dtoks[i][0] == fwc {
break
}
b.WriteString(tr.dtoks[i])
} else {
switch mfType {
case Partition:
var (
_buffer [64]byte
keyForHashing = _buffer[:0]
)
for _, sourceToken := range tr.dtokmftokindexesargs[i] {
keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...)
}
b.WriteString(tr.getHashPartition(keyForHashing, int(tr.dtokmfintargs[i])))
case Wildcard: // simple substitution
b.WriteString(tokens[tr.dtokmftokindexesargs[i][0]])
case SplitFromLeft:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
position := int(tr.dtokmfintargs[i])
if position > 0 && position < sourceTokenLen {
b.WriteString(sourceToken[:position])
b.WriteString(tsep)
b.WriteString(sourceToken[position:])
} else { // too small to split at the requested position: don't split
b.WriteString(sourceToken)
}
case SplitFromRight:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
position := int(tr.dtokmfintargs[i])
if position > 0 && position < sourceTokenLen {
b.WriteString(sourceToken[:sourceTokenLen-position])
b.WriteString(tsep)
b.WriteString(sourceToken[sourceTokenLen-position:])
} else { // too small to split at the requested position: don't split
b.WriteString(sourceToken)
}
case SliceFromLeft:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
sliceSize := int(tr.dtokmfintargs[i])
if sliceSize > 0 && sliceSize < sourceTokenLen {
for i := 0; i+sliceSize <= sourceTokenLen; i += sliceSize {
if i != 0 {
b.WriteString(tsep)
}
b.WriteString(sourceToken[i : i+sliceSize])
if i+sliceSize != sourceTokenLen && i+sliceSize+sliceSize > sourceTokenLen {
b.WriteString(tsep)
b.WriteString(sourceToken[i+sliceSize:])
break
}
}
} else { // too small to slice at the requested size: don't slice
b.WriteString(sourceToken)
}
case SliceFromRight:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
sliceSize := int(tr.dtokmfintargs[i])
if sliceSize > 0 && sliceSize < sourceTokenLen {
remainder := sourceTokenLen % sliceSize
if remainder > 0 {
b.WriteString(sourceToken[:remainder])
b.WriteString(tsep)
}
for i := remainder; i+sliceSize <= sourceTokenLen; i += sliceSize {
b.WriteString(sourceToken[i : i+sliceSize])
if i+sliceSize < sourceTokenLen {
b.WriteString(tsep)
}
}
} else { // too small to slice at the requested size: don't slice
b.WriteString(sourceToken)
}
case Split:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
splits := strings.Split(sourceToken, tr.dtokmfstringargs[i])
for j, split := range splits {
if split != _EMPTY_ {
b.WriteString(split)
}
if j < len(splits)-1 && splits[j+1] != _EMPTY_ && !(j == 0 && split == _EMPTY_) {
b.WriteString(tsep)
}
}
}
}
if i < li {
b.WriteByte(btsep)
}
}
// We may have more source tokens available. This happens with ">".
if tr.dtoks[len(tr.dtoks)-1] == ">" {
for sli, i := len(tokens)-1, len(tr.stoks)-1; i < len(tokens); i++ {
b.WriteString(tokens[i])
if i < sli {
b.WriteByte(btsep)
}
}
}
return b.String(), nil
}
// Reverse a transform.
func (tr *transform) reverse() *transform {
if len(tr.dtokmftokindexesargs) == 0 {
rtr, _ := newTransform(tr.dest, tr.src)
return rtr
}
// If we are here we need to dynamically get the correct reverse
// of this transform.
nsrc, phs := transformUntokenize(tr.dest)
var nda []string
for _, token := range tr.stoks {
if token == "*" {
if len(phs) == 0 {
// TODO(dlc) - Should not happen
return nil
}
nda = append(nda, phs[0])
phs = phs[1:]
} else {
nda = append(nda, token)
}
}
ndest := strings.Join(nda, tsep)
rtr, _ := newTransform(nsrc, ndest)
return rtr
}
+116 -19
View File
@@ -1,4 +1,4 @@
// Copyright 2012-2022 The NATS Authors
// Copyright 2012-2023 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
@@ -71,6 +71,7 @@ type User struct {
Password string `json:"password"`
Permissions *Permissions `json:"permissions,omitempty"`
Account *Account `json:"account,omitempty"`
ConnectionDeadline time.Time `json:"connection_deadline,omitempty"`
AllowedConnectionTypes map[string]struct{} `json:"connection_types,omitempty"`
}
@@ -83,6 +84,14 @@ func (u *User) clone() *User {
clone := &User{}
*clone = *u
clone.Permissions = u.Permissions.clone()
if len(u.AllowedConnectionTypes) > 0 {
clone.AllowedConnectionTypes = make(map[string]struct{})
for k, v := range u.AllowedConnectionTypes {
clone.AllowedConnectionTypes[k] = v
}
}
return clone
}
@@ -254,7 +263,7 @@ func (s *Server) configureAuthorization() {
} else if opts.Nkeys != nil || opts.Users != nil {
s.nkeys, s.users = s.buildNkeysAndUsersFromOptions(opts.Nkeys, opts.Users)
s.info.AuthRequired = true
} else if opts.Username != "" || opts.Authorization != "" {
} else if opts.Username != _EMPTY_ || opts.Authorization != _EMPTY_ {
s.info.AuthRequired = true
} else {
s.users = nil
@@ -266,6 +275,30 @@ func (s *Server) configureAuthorization() {
s.wsConfigAuth(&opts.Websocket)
// And for mqtt config
s.mqttConfigAuth(&opts.MQTT)
// Check for server configured auth callouts.
if opts.AuthCallout != nil {
s.mu.Unlock()
// Give operator log entries if not valid account and auth_users.
_, err := s.lookupAccount(opts.AuthCallout.Account)
s.mu.Lock()
if err != nil {
s.Errorf("Authorization callout account %q not valid", opts.AuthCallout.Account)
}
for _, u := range opts.AuthCallout.AuthUsers {
// Check for user in users and nkeys since this is server config.
var found bool
if len(s.users) > 0 {
_, found = s.users[u]
}
if !found && len(s.nkeys) > 0 {
_, found = s.nkeys[u]
}
if !found {
s.Errorf("Authorization callout user %q not valid: %v", u, err)
}
}
}
}
// Takes the given slices of NkeyUser and User options and build
@@ -539,7 +572,7 @@ func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.User
return lim, nil
}
func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) bool {
func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (authorized bool) {
var (
nkey *NkeyUser
juc *jwt.UserClaims
@@ -549,6 +582,70 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo
err error
ao bool // auth override
)
// Check if we have auth callouts enabled at the server level or in the bound account.
defer func() {
// Default reason
reason := AuthenticationViolation.String()
// No-op
if juc == nil && opts.AuthCallout == nil {
if !authorized {
s.sendAccountAuthErrorEvent(c, c.acc, reason)
}
return
}
// We have a juc defined here, check account.
if juc != nil && !acc.hasExternalAuth() {
if !authorized {
s.sendAccountAuthErrorEvent(c, c.acc, reason)
}
return
}
// We have auth callout set here.
var skip bool
// Check if we are on the list of auth_users.
userID := c.getRawAuthUser()
if juc != nil {
skip = acc.isExternalAuthUser(userID)
} else {
for _, u := range opts.AuthCallout.AuthUsers {
if userID == u {
skip = true
break
}
}
}
// 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)
}
// 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 {
var authAccountName string
if juc == nil && opts.AuthCallout != nil {
authAccountName = opts.AuthCallout.Account
} else if juc != nil {
authAccountName = acc.Name
}
c.mu.Lock()
if c.acc != nil && c.acc.Name == authAccountName {
c.mergeDenyPermissions(pub, []string{AuthCalloutSubject})
}
c.mu.Unlock()
} else {
// If we are here we failed external authorization.
// Send an account scoped event. Server config mode acc will be nil,
// so lookup the auth callout assigned account, that is where this will be sent.
if acc == nil {
acc, _ = s.lookupAccount(opts.AuthCallout.Account)
}
s.sendAccountAuthErrorEvent(c, acc, reason)
}
}()
s.mu.Lock()
authRequired := s.info.AuthRequired
if !authRequired {
@@ -803,7 +900,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo
return false
}
if juc.BearerToken && acc.failBearer() {
c.Debugf("Account does not allow bearer token")
c.Debugf("Account does not allow bearer tokens")
return false
}
// skip validation of nonce when presented with a bearer token
@@ -1028,7 +1125,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool {
// https://github.com/golang/go/issues/12342
dn, err := ldap.FromRawCertSubject(cert.RawSubject)
if err == nil {
if match, ok := fn("", dn, false); ok {
if match, ok := fn(_EMPTY_, dn, false); ok {
c.Debugf("Using DistinguishedNameMatch for auth [%q]", match)
return true
}
@@ -1105,28 +1202,28 @@ func (s *Server) isRouterAuthorized(c *client) bool {
// Check custom auth first, then TLS map if enabled
// then single user/pass.
if s.opts.CustomRouterAuthentication != nil {
return s.opts.CustomRouterAuthentication.Check(c)
if opts.CustomRouterAuthentication != nil {
return opts.CustomRouterAuthentication.Check(c)
}
if opts.Cluster.TLSMap || opts.Cluster.TLSCheckKnownURLs {
return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) {
if user == "" {
return "", false
if user == _EMPTY_ {
return _EMPTY_, false
}
if opts.Cluster.TLSCheckKnownURLs && isDNSAltName {
if dnsAltNameMatches(dnsAltNameLabels(user), opts.Routes) {
return "", true
return _EMPTY_, true
}
}
if opts.Cluster.TLSMap && opts.Cluster.Username == user {
return "", true
return _EMPTY_, true
}
return "", false
return _EMPTY_, false
})
}
if opts.Cluster.Username == "" {
if opts.Cluster.Username == _EMPTY_ {
return true
}
@@ -1147,25 +1244,25 @@ func (s *Server) isGatewayAuthorized(c *client) bool {
// Check whether TLS map is enabled, otherwise use single user/pass.
if opts.Gateway.TLSMap || opts.Gateway.TLSCheckKnownURLs {
return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) {
if user == "" {
return "", false
if user == _EMPTY_ {
return _EMPTY_, false
}
if opts.Gateway.TLSCheckKnownURLs && isDNSAltName {
labels := dnsAltNameLabels(user)
for _, gw := range opts.Gateway.Gateways {
if gw != nil && dnsAltNameMatches(labels, gw.URLs) {
return "", true
return _EMPTY_, true
}
}
}
if opts.Gateway.TLSMap && opts.Gateway.Username == user {
return "", true
return _EMPTY_, true
}
return "", false
return _EMPTY_, false
})
}
if opts.Gateway.Username == "" {
if opts.Gateway.Username == _EMPTY_ {
return true
}
+467
View File
@@ -0,0 +1,467 @@
// Copyright 2022-2023 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 (
"bytes"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
"time"
"unicode"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
)
const (
AuthCalloutSubject = "$SYS.REQ.USER.AUTH"
AuthRequestSubject = "nats-authorization-request"
AuthRequestXKeyHeader = "Nats-Server-Xkey"
)
// Process a callout on this client's behalf.
func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorized bool, errStr string) {
isOperatorMode := len(opts.TrustedKeys) > 0
// this is the account the user connected in, or the one running the callout
var acc *Account
if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.Account != _EMPTY_ {
aname := opts.AuthCallout.Account
var err error
acc, err = s.LookupAccount(aname)
if err != nil {
errStr = fmt.Sprintf("No valid account %q for auth callout request: %v", aname, err)
s.Warnf(errStr)
return false, errStr
}
} else {
acc = c.acc
}
// Check if we have been requested to encrypt.
var xkp nkeys.KeyPair
var xkey string
var pubAccXKey string
if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.XKey != _EMPTY_ {
pubAccXKey = opts.AuthCallout.XKey
} else if isOperatorMode {
pubAccXKey = acc.externalAuthXKey()
}
// If set grab server's xkey keypair and public key.
if pubAccXKey != _EMPTY_ {
// These are only set on creation, so lock not needed.
xkp, xkey = s.xkp, s.info.XKey
}
// FIXME: so things like the server ID that get assigned, are used as a sort of nonce - but
// reality is that the keypair here, is generated, so the response generated a JWT has to be
// this user - no replay possible
// Create a keypair for the user. We will expect this public user to be in the signed response.
// This prevents replay attacks.
ukp, _ := nkeys.CreateUser()
pub, _ := ukp.PublicKey()
reply := s.newRespInbox()
respCh := make(chan string, 1)
decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
account := acc.Name
_, msg := rc.msgParts(rmsg)
// This signals not authorized.
// Since this is an account subscription will always have "\r\n".
if len(msg) <= LEN_CR_LF {
return nil, fmt.Errorf("auth callout violation: %q on account %q", "no reason supplied", account)
}
// Strip trailing CRLF.
msg = msg[:len(msg)-LEN_CR_LF]
encrypted := false
// If we sent an encrypted request the response could be encrypted as well.
// we are expecting the input to be `eyJ` if it is a JWT
if xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
var err error
msg, err = xkp.Open(msg, pubAccXKey)
if err != nil {
return nil, fmt.Errorf("error decrypting auth callout response on account %q: %v", account, err)
}
encrypted = true
}
cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
if err != nil {
return nil, err
}
vr := jwt.CreateValidationResults()
cr.Validate(vr)
if len(vr.Issues) > 0 {
return nil, fmt.Errorf("authorization response had validation errors: %v", vr.Issues[0])
}
// the subject is the user id
if cr.Subject != pub {
return nil, errors.New("auth callout violation: auth callout response is not for expected user")
}
// check the audience to be the server ID
if cr.Audience != s.info.ID {
return nil, errors.New("auth callout violation: auth callout response is not for server")
}
// check if had an error message from the auth account
if cr.Error != _EMPTY_ {
return nil, fmt.Errorf("auth callout service returned an error: %v", cr.Error)
}
// if response is encrypted none of this is needed
if isOperatorMode && !encrypted {
pkStr := cr.Issuer
if cr.IssuerAccount != _EMPTY_ {
pkStr = cr.IssuerAccount
}
if pkStr != account {
if _, ok := acc.signingKeys[pkStr]; !ok {
return nil, errors.New("auth callout signing key is unknown")
}
}
}
return jwt.DecodeUserClaims(cr.Jwt)
}
// getIssuerAccount returns the issuer (as per JWT) - it also asserts that
// only in operator mode we expect to receive `issuer_account`.
getIssuerAccount := func(arc *jwt.UserClaims, account string) (string, error) {
// Make sure correct issuer.
var issuer string
if opts.AuthCallout != nil {
issuer = opts.AuthCallout.Issuer
} else {
// Operator mode is who we send the request on unless switching accounts.
issuer = acc.Name
}
// the jwt issuer can be a signing key
jwtIssuer := arc.Issuer
if arc.IssuerAccount != _EMPTY_ {
if !isOperatorMode {
// this should be invalid - effectively it would allow the auth callout
// to issue on another account which may be allowed given the configuration
// where the auth callout account can handle multiple different ones..
return _EMPTY_, fmt.Errorf("error non operator mode account %q: attempted to use issuer_account", account)
}
jwtIssuer = arc.IssuerAccount
}
if jwtIssuer != issuer {
if !isOperatorMode {
return _EMPTY_, fmt.Errorf("wrong issuer for auth callout response on account %q, expected %q got %q", account, issuer, jwtIssuer)
} else if !acc.isAllowedAcount(jwtIssuer) {
return _EMPTY_, fmt.Errorf("account %q not permitted as valid account option for auth callout for account %q",
arc.Issuer, account)
}
}
return jwtIssuer, nil
}
getExpirationAndAllowedConnections := func(arc *jwt.UserClaims, account string) (time.Duration, map[string]struct{}, error) {
allowNow, expiration := validateTimes(arc)
if !allowNow {
c.Errorf("Outside connect times")
return 0, nil, fmt.Errorf("authorized user on account %q outside of valid connect times", account)
}
allowedConnTypes, err := convertAllowedConnectionTypes(arc.User.AllowedConnectionTypes)
if err != nil {
c.Debugf("%v", err)
if len(allowedConnTypes) == 0 {
return 0, nil, fmt.Errorf("authorized user on account %q using invalid connection type", account)
}
}
return expiration, allowedConnTypes, nil
}
assignAccountAndPermissions := func(arc *jwt.UserClaims, account string) (*Account, error) {
// Apply to this client.
var err error
issuerAccount, err := getIssuerAccount(arc, account)
if err != nil {
return nil, err
}
// if we are not in operator mode, they can specify placement as a tag
var placement string
if !isOperatorMode {
// only allow placement if we are not in operator mode
placement = arc.Audience
} else {
placement = issuerAccount
}
targetAcc, err := s.LookupAccount(placement)
if err != nil {
return nil, fmt.Errorf("no valid account %q for auth callout response on account %q: %v", placement, account, err)
}
if isOperatorMode {
// this will validate the signing key that emitted the user, and if it is a signing
// key it assigns the permissions from the target account
if scope, ok := targetAcc.hasIssuer(arc.Issuer); !ok {
return nil, fmt.Errorf("user JWT issuer %q is not known", arc.Issuer)
} else if scope != nil {
// this possibly has to be different because it could just be a plain issued by a non-scoped signing key
if err := scope.ValidateScopedSigner(arc); err != nil {
return nil, fmt.Errorf("user JWT is not valid: %v", err)
} else if uSc, ok := scope.(*jwt.UserScope); !ok {
return nil, fmt.Errorf("user JWT is not a valid scoped user")
} else if arc.User.UserPermissionLimits, err = processUserPermissionsTemplate(uSc.Template, arc, targetAcc); err != nil {
return nil, fmt.Errorf("user JWT generated invalid permissions: %v", err)
}
}
}
return targetAcc, nil
}
processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
titleCase := func(m string) string {
r := []rune(m)
return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...))
}
arc, err := decodeResponse(rc, rmsg, racc)
if err != nil {
respCh <- titleCase(err.Error())
return
}
vr := jwt.CreateValidationResults()
arc.Validate(vr)
if len(vr.Issues) > 0 {
respCh <- fmt.Sprintf("Error validating user JWT: %v", vr.Issues[0])
return
}
// Make sure that the user is what we requested.
if arc.Subject != pub {
respCh <- fmt.Sprintf("Expected authorized user of %q but got %q on account %q", pub, arc.Subject, racc.Name)
return
}
expiration, allowedConnTypes, err := getExpirationAndAllowedConnections(arc, racc.Name)
if err != nil {
respCh <- titleCase(err.Error())
return
}
targetAcc, err := assignAccountAndPermissions(arc, racc.Name)
if err != nil {
respCh <- titleCase(err.Error())
return
}
// Build internal user and bind to the targeted account.
nkuser := buildInternalNkeyUser(arc, allowedConnTypes, targetAcc)
if err := c.RegisterNkeyUser(nkuser); err != nil {
respCh <- fmt.Sprintf("Could not register auth callout user: %v", err)
return
}
// See if the response wants to override the username.
if arc.Name != _EMPTY_ {
c.mu.Lock()
c.opts.Username = arc.Name
// Clear any others.
c.opts.Nkey = _EMPTY_
c.pubKey = _EMPTY_
c.opts.Token = _EMPTY_
c.mu.Unlock()
}
// Check if we need to set an auth timer if the user jwt expires.
c.setExpiration(arc.Claims(), expiration)
respCh <- _EMPTY_
}
// create a subscription to receive a response from the authcallout
sub, err := acc.subscribeInternal(reply, processReply)
if err != nil {
errStr = fmt.Sprintf("Error setting up reply subscription for auth request: %v", err)
s.Warnf(errStr)
return false, errStr
}
defer acc.unsubscribeInternal(sub)
// Build our request claims - jwt subject should be nkey
jwtSub := acc.Name
if opts.AuthCallout != nil {
jwtSub = opts.AuthCallout.Issuer
}
// The public key of the server, if set is available on Varz.Key
// This means that when a service connects, it can now peer
// authenticate if it wants to - but that also means that it needs to be
// listening to cluster changes
claim := jwt.NewAuthorizationRequestClaims(jwtSub)
claim.Audience = AuthRequestSubject
// Set expected public user nkey.
claim.UserNkey = pub
s.mu.RLock()
claim.Server = jwt.ServerID{
Name: s.info.Name,
Host: s.info.Host,
ID: s.info.ID,
Version: s.info.Version,
Cluster: s.info.Cluster,
}
s.mu.RUnlock()
// Tags
claim.Server.Tags = s.getOpts().Tags
// Check if we have been requested to encrypt.
// FIXME: possibly this public key also needs to be on the
// Varz, because then it can be peer verified?
if xkp != nil {
claim.Server.XKey = xkey
}
authTimeout := secondsToDuration(s.getOpts().AuthTimeout)
claim.Expires = time.Now().Add(time.Duration(authTimeout)).UTC().Unix()
// Grab client info for the request.
c.mu.Lock()
c.fillClientInfo(&claim.ClientInformation)
c.fillConnectOpts(&claim.ConnectOptions)
// If we have a sig in the client opts, fill in nonce.
if claim.ConnectOptions.SignedNonce != _EMPTY_ {
claim.ClientInformation.Nonce = string(c.nonce)
}
// TLS
if c.flags.isSet(handshakeComplete) && c.nc != nil {
var ct jwt.ClientTLS
conn := c.nc.(*tls.Conn)
cs := conn.ConnectionState()
ct.Version = tlsVersion(cs.Version)
ct.Cipher = tlsCipher(cs.CipherSuite)
// Check verified chains.
for _, vs := range cs.VerifiedChains {
var certs []string
for _, c := range vs {
blk := &pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}
certs = append(certs, string(pem.EncodeToMemory(blk)))
}
ct.VerifiedChains = append(ct.VerifiedChains, certs)
}
// If we do not have verified chains put in peer certs.
if len(ct.VerifiedChains) == 0 {
for _, c := range cs.PeerCertificates {
blk := &pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}
ct.Certs = append(ct.Certs, string(pem.EncodeToMemory(blk)))
}
}
claim.TLS = &ct
}
c.mu.Unlock()
b, err := claim.Encode(s.kp)
if err != nil {
errStr = fmt.Sprintf("Error encoding auth request claim on account %q: %v", acc.Name, err)
s.Warnf(errStr)
return false, errStr
}
req := []byte(b)
var hdr map[string]string
// Check if we have been asked to encrypt.
if xkp != nil {
req, err = xkp.Seal([]byte(req), pubAccXKey)
if err != nil {
errStr = fmt.Sprintf("Error encrypting auth request claim on account %q: %v", acc.Name, err)
s.Warnf(errStr)
return false, errStr
}
hdr = map[string]string{AuthRequestXKeyHeader: xkey}
}
// Send out our request.
if err := s.sendInternalAccountMsgWithReply(acc, AuthCalloutSubject, reply, hdr, req, false); err != nil {
errStr = fmt.Sprintf("Error sending authorization request: %v", err)
s.Debugf(errStr)
return false, errStr
}
select {
case errStr = <-respCh:
if authorized = errStr == _EMPTY_; !authorized {
s.Warnf(errStr)
}
case <-time.After(authTimeout):
s.Debugf(fmt.Sprintf("Authorization callout response not received in time on account %q", acc.Name))
}
return authorized, errStr
}
// Fill in client information for the request.
// Lock should be held.
func (c *client) fillClientInfo(ci *jwt.ClientInformation) {
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
return
}
// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
*ci = jwt.ClientInformation{
Host: c.host,
ID: c.cid,
User: c.getRawAuthUser(),
Name: c.opts.Name,
Tags: c.tags,
NameTag: c.nameTag,
Kind: c.kindString(),
Type: c.clientTypeString(),
MQTT: c.getMQTTClientID(),
}
}
// Fill in client options.
// Lock should be held.
func (c *client) fillConnectOpts(opts *jwt.ConnectOptions) {
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
return
}
o := c.opts
// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
*opts = jwt.ConnectOptions{
JWT: o.JWT,
Nkey: o.Nkey,
SignedNonce: o.Sig,
Token: o.Token,
Username: o.Username,
Password: o.Password,
Name: o.Name,
Lang: o.Lang,
Version: o.Version,
Protocol: o.Protocol,
}
}
+677
View File
@@ -0,0 +1,677 @@
// Copyright 2023 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 avl
import (
"encoding/binary"
"errors"
"math/bits"
"sort"
)
// SequenceSet is a memory and encoding optimized set for storing unsigned ints.
//
// SequenceSet is ~80-100 times more efficient memory wise than a map[uint64]struct{}.
// SequenceSet is ~1.75 times slower at inserts than the same map.
// SequenceSet is not thread safe.
//
// We use an AVL tree with nodes that hold bitmasks for set membership.
//
// Encoding will convert to a space optimized encoding using bitmasks.
type SequenceSet struct {
root *node // root node
size int // number of items
nodes int // number of nodes
// Having this here vs on the stack in Insert/Delete
// makes a difference in memory usage.
changed bool
}
// Insert will insert the sequence into the set.
// The tree will be balanced inline.
func (ss *SequenceSet) Insert(seq uint64) {
if ss.root = ss.root.insert(seq, &ss.changed, &ss.nodes); ss.changed {
ss.changed = false
ss.size++
}
}
// Exists will return true iff the sequence is a member of this set.
func (ss *SequenceSet) Exists(seq uint64) bool {
for n := ss.root; n != nil; {
if seq < n.base {
n = n.l
continue
} else if seq >= n.base+numEntries {
n = n.r
continue
}
return n.exists(seq)
}
return false
}
// SetInitialMin should be used to set the initial minimum sequence when known.
// This will more effectively utilize space versus self selecting.
// The set should be empty.
func (ss *SequenceSet) SetInitialMin(min uint64) error {
if !ss.IsEmpty() {
return ErrSetNotEmpty
}
ss.root, ss.nodes = &node{base: min, h: 1}, 1
return nil
}
// Delete will remove the sequence from the set.
// Will optionally remove nodes and rebalance.
// Returns where the sequence was set.
func (ss *SequenceSet) Delete(seq uint64) bool {
if ss == nil || ss.root == nil {
return false
}
ss.root = ss.root.delete(seq, &ss.changed, &ss.nodes)
if ss.changed {
ss.changed = false
ss.size--
if ss.size == 0 {
ss.Empty()
}
return true
}
return false
}
// Size returns the number of items in the set.
func (ss *SequenceSet) Size() int {
return ss.size
}
// Nodes returns the number of nodes in the tree.
func (ss *SequenceSet) Nodes() int {
return ss.nodes
}
// Empty will clear all items from a set.
func (ss *SequenceSet) Empty() {
ss.root = nil
ss.size = 0
ss.nodes = 0
}
// IsEmpty is a fast check of the set being empty.
func (ss *SequenceSet) IsEmpty() bool {
if ss == nil || ss.root == nil {
return true
}
return false
}
// Range will invoke the given function for each item in the set.
// They will range over the set in ascending order.
// If the callback returns false we terminate the iteration.
func (ss *SequenceSet) Range(f func(uint64) bool) {
ss.root.iter(f)
}
// Heights returns the left and right heights of the tree.
func (ss *SequenceSet) Heights() (l, r int) {
if ss.root == nil {
return 0, 0
}
if ss.root.l != nil {
l = ss.root.l.h
}
if ss.root.r != nil {
r = ss.root.r.h
}
return l, r
}
// Returns min, max and number of set items.
func (ss *SequenceSet) State() (min, max, num uint64) {
if ss == nil || ss.root == nil {
return 0, 0, 0
}
min, max = ss.MinMax()
return min, max, uint64(ss.Size())
}
// MinMax will return the minunum and maximum values in the set.
func (ss *SequenceSet) MinMax() (min, max uint64) {
if ss.root == nil {
return 0, 0
}
for l := ss.root; l != nil; l = l.l {
if l.l == nil {
min = l.min()
}
}
for r := ss.root; r != nil; r = r.r {
if r.r == nil {
max = r.max()
}
}
return min, max
}
func clone(src *node, target **node) {
if src == nil {
return
}
n := &node{base: src.base, bits: src.bits, h: src.h}
*target = n
clone(src.l, &n.l)
clone(src.r, &n.r)
}
// Clone will return a clone of the given SequenceSet.
func (ss *SequenceSet) Clone() *SequenceSet {
if ss == nil {
return nil
}
css := &SequenceSet{nodes: ss.nodes, size: ss.size}
clone(ss.root, &css.root)
return css
}
// Union will union this SequenceSet with ssa.
func (ss *SequenceSet) Union(ssa ...*SequenceSet) {
for _, sa := range ssa {
sa.root.nodeIter(func(n *node) {
for nb, b := range n.bits {
for pos := uint64(0); b != 0; pos++ {
if b&1 == 1 {
seq := n.base + (uint64(nb) * uint64(bitsPerBucket)) + pos
ss.Insert(seq)
}
b >>= 1
}
}
})
}
}
// Union will return a union of all sets.
func Union(ssa ...*SequenceSet) *SequenceSet {
if len(ssa) == 0 {
return nil
}
// Sort so we can clone largest.
sort.Slice(ssa, func(i, j int) bool { return ssa[i].Size() > ssa[j].Size() })
ss := ssa[0].Clone()
// Insert the rest through range call.
for i := 1; i < len(ssa); i++ {
ssa[i].Range(func(n uint64) bool {
ss.Insert(n)
return true
})
}
return ss
}
const (
// Magic is used to identify the encode binary state..
magic = uint8(22)
// Version
version = uint8(2)
// hdrLen
hdrLen = 2
// minimum length of an encoded SequenceSet.
minLen = 2 + 8 // magic + version + num nodes + num entries.
)
// EncodeLen returns the bytes needed for encoding.
func (ss SequenceSet) EncodeLen() int {
return minLen + (ss.Nodes() * ((numBuckets+1)*8 + 2))
}
func (ss SequenceSet) Encode(buf []byte) ([]byte, error) {
nn, encLen := ss.Nodes(), ss.EncodeLen()
if cap(buf) < encLen {
buf = make([]byte, encLen)
} else {
buf = buf[:encLen]
}
// TODO(dlc) - Go 1.19 introduced Append to not have to keep track.
// Once 1.20 is out we could change this over.
// Also binary.Write() is way slower, do not use.
var le = binary.LittleEndian
buf[0], buf[1] = magic, version
i := hdrLen
le.PutUint32(buf[i:], uint32(nn))
le.PutUint32(buf[i+4:], uint32(ss.size))
i += 8
ss.root.nodeIter(func(n *node) {
le.PutUint64(buf[i:], n.base)
i += 8
for _, b := range n.bits {
le.PutUint64(buf[i:], b)
i += 8
}
le.PutUint16(buf[i:], uint16(n.h))
i += 2
})
return buf[:i], nil
}
// ErrBadEncoding is returned when we can not decode properly.
var (
ErrBadEncoding = errors.New("ss: bad encoding")
ErrBadVersion = errors.New("ss: bad version")
ErrSetNotEmpty = errors.New("ss: set not empty")
)
// Decode returns the sequence set and number of bytes read from the buffer on success.
func Decode(buf []byte) (*SequenceSet, int, error) {
if len(buf) < minLen || buf[0] != magic {
return nil, -1, ErrBadEncoding
}
switch v := buf[1]; v {
case 1:
return decodev1(buf)
case 2:
return decodev2(buf)
default:
return nil, -1, ErrBadVersion
}
}
// Helper to decode v2.
func decodev2(buf []byte) (*SequenceSet, int, error) {
var le = binary.LittleEndian
index := 2
nn := int(le.Uint32(buf[index:]))
sz := int(le.Uint32(buf[index+4:]))
index += 8
expectedLen := minLen + (nn * ((numBuckets+1)*8 + 2))
if len(buf) < expectedLen {
return nil, -1, ErrBadEncoding
}
ss, nodes := SequenceSet{size: sz}, make([]node, nn)
for i := 0; i < nn; i++ {
n := &nodes[i]
n.base = le.Uint64(buf[index:])
index += 8
for bi := range n.bits {
n.bits[bi] = le.Uint64(buf[index:])
index += 8
}
n.h = int(le.Uint16(buf[index:]))
index += 2
ss.insertNode(n)
}
return &ss, index, nil
}
// Helper to decode v1 into v2 which has fixed buckets of 32 vs 64 originally.
func decodev1(buf []byte) (*SequenceSet, int, error) {
var le = binary.LittleEndian
index := 2
nn := int(le.Uint32(buf[index:]))
sz := int(le.Uint32(buf[index+4:]))
index += 8
const v1NumBuckets = 64
expectedLen := minLen + (nn * ((v1NumBuckets+1)*8 + 2))
if len(buf) < expectedLen {
return nil, -1, ErrBadEncoding
}
var ss SequenceSet
for i := 0; i < nn; i++ {
base := le.Uint64(buf[index:])
index += 8
for nb := uint64(0); nb < v1NumBuckets; nb++ {
n := le.Uint64(buf[index:])
// Walk all set bits and insert sequences manually for this decode from v1.
for pos := uint64(0); n != 0; pos++ {
if n&1 == 1 {
seq := base + (nb * uint64(bitsPerBucket)) + pos
ss.Insert(seq)
}
n >>= 1
}
index += 8
}
// Skip over encoded height.
index += 2
}
// Sanity check.
if ss.Size() != sz {
return nil, -1, ErrBadEncoding
}
return &ss, index, nil
}
// insertNode places a decoded node into the tree.
// These should be done in tree order as defined by Encode()
// This allows us to not have to calculate height or do rebalancing.
// So much better performance this way.
func (ss *SequenceSet) insertNode(n *node) {
ss.nodes++
if ss.root == nil {
ss.root = n
return
}
// Walk our way to the insertion point.
for p := ss.root; p != nil; {
if n.base < p.base {
if p.l == nil {
p.l = n
return
}
p = p.l
} else {
if p.r == nil {
p.r = n
return
}
p = p.r
}
}
}
const (
bitsPerBucket = 64 // bits in uint64
numBuckets = 32
numEntries = numBuckets * bitsPerBucket
)
type node struct {
//v dvalue
base uint64
bits [numBuckets]uint64
l *node
r *node
h int
}
// Set the proper bit.
// seq should have already been qualified and inserted should be non nil.
func (n *node) set(seq uint64, inserted *bool) {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
if (n.bits[i] & mask) == 0 {
n.bits[i] |= mask
*inserted = true
}
}
func (n *node) insert(seq uint64, inserted *bool, nodes *int) *node {
if n == nil {
base := (seq / numEntries) * numEntries
n := &node{base: base, h: 1}
n.set(seq, inserted)
*nodes++
return n
}
if seq < n.base {
n.l = n.l.insert(seq, inserted, nodes)
} else if seq >= n.base+numEntries {
n.r = n.r.insert(seq, inserted, nodes)
} else {
n.set(seq, inserted)
}
n.h = maxH(n) + 1
// Don't make a function, impacts performance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// Right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
func (n *node) rotateL() *node {
r := n.r
if r != nil {
n.r = r.l
r.l = n
n.h = maxH(n) + 1
r.h = maxH(r) + 1
} else {
n.r = nil
n.h = maxH(n) + 1
}
return r
}
func (n *node) rotateR() *node {
l := n.l
if l != nil {
n.l = l.r
l.r = n
n.h = maxH(n) + 1
l.h = maxH(l) + 1
} else {
n.l = nil
n.h = maxH(n) + 1
}
return l
}
func balanceF(n *node) int {
if n == nil {
return 0
}
var lh, rh int
if n.l != nil {
lh = n.l.h
}
if n.r != nil {
rh = n.r.h
}
return lh - rh
}
func maxH(n *node) int {
if n == nil {
return 0
}
var lh, rh int
if n.l != nil {
lh = n.l.h
}
if n.r != nil {
rh = n.r.h
}
if lh > rh {
return lh
}
return rh
}
// Clear the proper bit.
// seq should have already been qualified and deleted should be non nil.
// Will return true if this node is now empty.
func (n *node) clear(seq uint64, deleted *bool) bool {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
if (n.bits[i] & mask) != 0 {
n.bits[i] &^= mask
*deleted = true
}
for _, b := range n.bits {
if b != 0 {
return false
}
}
return true
}
func (n *node) delete(seq uint64, deleted *bool, nodes *int) *node {
if n == nil {
return nil
}
if seq < n.base {
n.l = n.l.delete(seq, deleted, nodes)
} else if seq >= n.base+numEntries {
n.r = n.r.delete(seq, deleted, nodes)
} else if empty := n.clear(seq, deleted); empty {
*nodes--
if n.l == nil {
n = n.r
} else if n.r == nil {
n = n.l
} else {
// We have both children.
n.r = n.r.insertNodePrev(n.l)
n = n.r
}
}
if n != nil {
n.h = maxH(n) + 1
}
// Check balance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
// Will insert nn into the node assuming it is less than all other nodes in n.
// Will re-calculate height and balance.
func (n *node) insertNodePrev(nn *node) *node {
if n.l == nil {
n.l = nn
} else {
n.l = n.l.insertNodePrev(nn)
}
n.h = maxH(n) + 1
// Check balance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
func (n *node) exists(seq uint64) bool {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
return n.bits[i]&mask != 0
}
// Return minimum sequence in the set.
// This node can not be empty.
func (n *node) min() uint64 {
for i, b := range n.bits {
if b != 0 {
return n.base +
uint64(i*bitsPerBucket) +
uint64(bits.TrailingZeros64(b))
}
}
return 0
}
// Return maximum sequence in the set.
// This node can not be empty.
func (n *node) max() uint64 {
for i := numBuckets - 1; i >= 0; i-- {
if b := n.bits[i]; b != 0 {
return n.base +
uint64(i*bitsPerBucket) +
uint64(bitsPerBucket-bits.LeadingZeros64(b>>1))
}
}
return 0
}
// This is done in tree order.
func (n *node) nodeIter(f func(n *node)) {
if n == nil {
return
}
f(n)
n.l.nodeIter(f)
n.r.nodeIter(f)
}
// iter will iterate through the set's items in this node.
// If the supplied function returns false we terminate the iteration.
func (n *node) iter(f func(uint64) bool) bool {
if n == nil {
return true
}
if ok := n.l.iter(f); !ok {
return false
}
for num := n.base; num < n.base+numEntries; num++ {
if n.exists(num) {
if ok := f(num); !ok {
return false
}
}
}
if ok := n.r.iter(f); !ok {
return false
}
return true
}
+286 -86
View File
@@ -33,6 +33,7 @@ import (
"sync/atomic"
"time"
"github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2"
)
@@ -84,9 +85,11 @@ const (
okProto = "+OK" + _CRLF_
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// TLS Hanshake client types
const (
tlsHandshakeLeaf = "leafnode"
tlsHandshakeMQTT = "mqtt"
)
const (
// Scratch buffer size for the processMsg() calls.
@@ -137,6 +140,7 @@ const (
skipFlushOnClose // Marks that flushOutbound() should not be called on connection close.
expectConnect // Marks if this connection is expected to send a CONNECT
connectProcessFinished // Marks if this connection has finished the connect process.
compressionNegotiated // Marks if this connection has negotiated compression level with remote.
)
// set the flag (would be equivalent to set the boolean to true)
@@ -205,6 +209,7 @@ const (
DuplicateServerName
MinimumVersionRequired
ClusterNamesIdentical
Kicked
)
// Some flags passed to processMsgResults
@@ -248,6 +253,7 @@ type client struct {
darray []string
pcd map[*client]struct{}
atmr *time.Timer
expires time.Time
ping pinfo
msgb [msgScratchSize]byte
last time.Time
@@ -301,6 +307,7 @@ type outbound struct {
mp int64 // Snapshot of max pending for client.
lft time.Duration // Last flush time for Write.
stc chan struct{} // Stall chan we create to slow down producers on overrun, e.g. fan-in.
cw *s2.Writer
}
const nbPoolSizeSmall = 512 // Underlying array size of small buffer
@@ -408,10 +415,12 @@ const (
type readCacheFlag uint16
const (
hasMappings readCacheFlag = 1 << iota // For account subject mappings.
sysGroup = "_sys_"
hasMappings readCacheFlag = 1 << iota // For account subject mappings.
switchToCompression readCacheFlag = 1 << 1
)
const sysGroup = "_sys_"
// Used in readloop to cache hot subject lookups and group statistics.
type readCache struct {
// These are for clients who are bound to a single account.
@@ -613,6 +622,9 @@ type ClientOpts struct {
// Routes and Leafnodes only
Import *SubjectPermission `json:"import,omitempty"`
Export *SubjectPermission `json:"export,omitempty"`
// Leafnodes
RemoteAccount string `json:"remote_account,omitempty"`
}
var defaultOpts = ClientOpts{Verbose: true, Pedantic: true, Echo: true}
@@ -752,7 +764,13 @@ func (c *client) Kind() int {
// registerWithAccount will register the given user with a specific
// account. This will change the subject namespace.
func (c *client) registerWithAccount(acc *Account) error {
if acc == nil || acc.sl == nil {
if acc == nil {
return ErrBadAccount
}
acc.mu.RLock()
bad := acc.sl == nil
acc.mu.RUnlock()
if bad {
return ErrBadAccount
}
// If we were previously registered, usually to $G, do accounting here to remove.
@@ -888,6 +906,11 @@ func (c *client) RegisterUser(user *User) {
c.opts.Username = user.Username
}
// if a deadline time stamp is set we start a timer to disconnect the user at that time
if !user.ConnectionDeadline.IsZero() {
c.setExpirationTimerUnlocked(time.Until(user.ConnectionDeadline))
}
c.mu.Unlock()
}
@@ -1003,6 +1026,61 @@ func (c *client) setPermissions(perms *Permissions) {
}
}
// Build public permissions from internal ones.
// Used for user info requests.
func (c *client) publicPermissions() *Permissions {
c.mu.Lock()
defer c.mu.Unlock()
if c.perms == nil {
return nil
}
perms := &Permissions{
Publish: &SubjectPermission{},
Subscribe: &SubjectPermission{},
}
_subs := [32]*subscription{}
// Publish
if c.perms.pub.allow != nil {
subs := _subs[:0]
c.perms.pub.allow.All(&subs)
for _, sub := range subs {
perms.Publish.Allow = append(perms.Publish.Allow, string(sub.subject))
}
}
if c.perms.pub.deny != nil {
subs := _subs[:0]
c.perms.pub.deny.All(&subs)
for _, sub := range subs {
perms.Publish.Deny = append(perms.Publish.Deny, string(sub.subject))
}
}
// Subsribe
if c.perms.sub.allow != nil {
subs := _subs[:0]
c.perms.sub.allow.All(&subs)
for _, sub := range subs {
perms.Subscribe.Allow = append(perms.Subscribe.Allow, string(sub.subject))
}
}
if c.perms.sub.deny != nil {
subs := _subs[:0]
c.perms.sub.deny.All(&subs)
for _, sub := range subs {
perms.Subscribe.Deny = append(perms.Subscribe.Deny, string(sub.subject))
}
}
// Responses.
if c.perms.resp != nil {
rp := *c.perms.resp
perms.Response = &rp
}
return perms
}
type denyType int
const (
@@ -1205,6 +1283,7 @@ func (c *client) readLoop(pre []byte) {
if ws {
masking = c.ws.maskread
}
checkCompress := c.kind == ROUTER || c.kind == LEAF
c.mu.Unlock()
defer func() {
@@ -1232,6 +1311,10 @@ func (c *client) readLoop(pre []byte) {
wsr.init()
}
var decompress bool
var reader io.Reader
reader = nc
for {
var n int
var err error
@@ -1242,7 +1325,7 @@ func (c *client) readLoop(pre []byte) {
n = len(pre)
pre = nil
} else {
n, err = nc.Read(b)
n, err = reader.Read(b)
// If we have any data we will try to parse and exit at the end.
if n == 0 && err != nil {
c.closeConnection(closedStateForErr(err))
@@ -1250,7 +1333,7 @@ func (c *client) readLoop(pre []byte) {
}
}
if ws {
bufs, err = c.wsRead(wsr, nc, b[:n])
bufs, err = c.wsRead(wsr, reader, b[:n])
if bufs == nil && err != nil {
if err != io.EOF {
c.Errorf("read error: %v", err)
@@ -1309,6 +1392,15 @@ func (c *client) readLoop(pre []byte) {
}
}
// If we are a ROUTER/LEAF and have processed an INFO, it is possible that
// we are asked to switch to compression now.
if checkCompress && c.in.flags.isSet(switchToCompression) {
c.in.flags.clear(switchToCompression)
// For now we support only s2 compression...
reader = s2.NewReader(nc)
decompress = true
}
// Updates stats for client and server that were collected
// from parsing through the buffer.
if c.in.msgs > 0 {
@@ -1354,7 +1446,15 @@ func (c *client) readLoop(pre []byte) {
// re-snapshot the account since it can change during reload, etc.
acc = c.acc
// Refresh nc because in some cases, we have upgraded c.nc to TLS.
nc = c.nc
if nc != c.nc {
nc = c.nc
if decompress && nc != nil {
// For now we support only s2 compression...
reader.(*s2.Reader).Reset(nc)
} else if !decompress {
reader = nc
}
}
c.mu.Unlock()
// Connection was closed
@@ -1441,17 +1541,9 @@ func (c *client) flushOutbound() bool {
// previous write, and in the case of WebSockets, that data may already
// be framed, so we are careful not to re-frame "wnb" here. Instead we
// will just frame up "nb" and append it onto whatever is left on "wnb".
// "nb" will be reset back to its starting position so it can be modified
// safely by queueOutbound calls.
c.out.wnb = append(c.out.wnb, collapsed...)
var _orig [1024][]byte
orig := append(_orig[:0], c.out.wnb...)
c.out.nb = c.out.nb[:0]
// Since WriteTo is lopping things off the beginning, we need to remember
// the start position of the underlying array so that we can get back to it.
// Otherwise we'll always "slide forward" and that will result in reallocs.
startOfWnb := c.out.wnb[0:]
// "nb" will be set to nil so that we can manipulate "collapsed" outside
// of the client's lock, which is interesting in case of compression.
c.out.nb = nil
// In case it goes away after releasing the lock.
nc := c.nc
@@ -1459,9 +1551,54 @@ func (c *client) flushOutbound() bool {
// Capture this (we change the value in some tests)
wdl := c.out.wdl
// Check for compression
cw := c.out.cw
if cw != nil {
// We will have to adjust once we have compressed, so remove for now.
c.out.pb -= attempted
if c.isWebsocket() {
c.ws.fs -= attempted
}
}
// Do NOT hold lock during actual IO.
c.mu.Unlock()
// Compress outside of the lock
if cw != nil {
var err error
bb := bytes.Buffer{}
cw.Reset(&bb)
for _, buf := range collapsed {
if _, err = cw.Write(buf); err != nil {
break
}
}
if err == nil {
err = cw.Close()
}
if err != nil {
c.Errorf("Error compressing data: %v", err)
c.markConnAsClosed(WriteError)
return false
}
collapsed = append(net.Buffers(nil), bb.Bytes())
attempted = int64(len(collapsed[0]))
}
// This is safe to do outside of the lock since "collapsed" is no longer
// referenced in c.out.nb (which can be modified in queueOutboud() while
// the lock is released).
c.out.wnb = append(c.out.wnb, collapsed...)
var _orig [1024][]byte
orig := append(_orig[:0], c.out.wnb...)
// Since WriteTo is lopping things off the beginning, we need to remember
// the start position of the underlying array so that we can get back to it.
// Otherwise we'll always "slide forward" and that will result in reallocs.
startOfWnb := c.out.wnb[0:]
// flush here
start := time.Now()
@@ -1478,6 +1615,14 @@ func (c *client) flushOutbound() bool {
// Re-acquire client lock.
c.mu.Lock()
// Adjust if we were compressing.
if cw != nil {
c.out.pb += attempted
if c.isWebsocket() {
c.ws.fs += attempted
}
}
// At this point, "wnb" has been mutated by WriteTo and any consumed
// buffers have been lopped off the beginning, so in order to return
// them to the pool, we need to look at the difference between "orig"
@@ -1575,8 +1720,18 @@ func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) boo
return true
}
// Slow consumer here..
// Aggregate slow consumers.
atomic.AddInt64(&c.srv.slowConsumers, 1)
switch c.kind {
case CLIENT:
c.srv.scStats.clients.Add(1)
case ROUTER:
c.srv.scStats.routes.Add(1)
case GATEWAY:
c.srv.scStats.gateways.Add(1)
case LEAF:
c.srv.scStats.leafs.Add(1)
}
if c.acc != nil {
atomic.AddInt64(&c.acc.slowConsumers, 1)
}
@@ -1624,7 +1779,11 @@ func (c *client) markConnAsClosed(reason ClosedState) {
// we use Noticef on create, so use that too for delete.
if c.srv != nil {
if c.kind == LEAF {
c.Noticef("%s connection closed: %s account: %s", c.kindString(), reason, c.acc.traceLabel())
if c.acc != nil {
c.Noticef("%s connection closed: %s - Account: %s", c.kindString(), reason, c.acc.traceLabel())
} 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.
@@ -1972,14 +2131,14 @@ func (c *client) authViolation() {
var s *Server
var hasTrustedNkeys, hasNkeys, hasUsers bool
if s = c.srv; s != nil {
s.mu.Lock()
s.mu.RLock()
hasTrustedNkeys = s.trustedKeys != nil
hasNkeys = s.nkeys != nil
hasUsers = s.users != nil
s.mu.Unlock()
s.mu.RUnlock()
defer s.sendAuthErrorEvent(c)
}
if hasTrustedNkeys {
c.Errorf("%v", ErrAuthentication)
} else if hasNkeys {
@@ -2072,7 +2231,10 @@ func (c *client) queueOutbound(data []byte) {
// Perf wise, it looks like it is faster to optimistically add than
// checking current pb+len(data) and then add to pb.
c.out.pb -= int64(len(data))
// Increment the total and client's slow consumer counters.
atomic.AddInt64(&c.srv.slowConsumers, 1)
c.srv.scStats.clients.Add(1)
if c.acc != nil {
atomic.AddInt64(&c.acc.slowConsumers, 1)
}
@@ -2190,10 +2352,7 @@ func (c *client) generateClientInfoJSON(info Info) []byte {
}
}
info.WSConnectURLs = nil
// Generate the info json
b, _ := json.Marshal(info)
pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
return bytes.Join(pcs, []byte(" "))
return generateInfoJSON(&info)
}
func (c *client) sendErr(err string) {
@@ -2281,12 +2440,40 @@ func (c *client) processPong() {
c.rtt = computeRTT(c.rttStart)
srv := c.srv
reorderGWs := c.kind == GATEWAY && c.gw.outbound
// If compression is currently active for a route/leaf connection, if the
// compression configuration is s2_auto, check if we should change
// the compression level.
if c.kind == ROUTER && needsCompression(c.route.compression) {
c.updateS2AutoCompressionLevel(&srv.getOpts().Cluster.Compression, &c.route.compression)
} else if c.kind == LEAF && needsCompression(c.leaf.compression) {
var co *CompressionOpts
if r := c.leaf.remote; r != nil {
co = &r.Compression
} else {
co = &srv.getOpts().LeafNode.Compression
}
c.updateS2AutoCompressionLevel(co, &c.leaf.compression)
}
c.mu.Unlock()
if reorderGWs {
srv.gateway.orderOutboundConnections()
}
}
// Select the s2 compression level based on the client's current RTT and the configured
// RTT thresholds slice. If current level is different than selected one, save the
// new compression level string and create a new s2 writer.
// Lock held on entry.
func (c *client) updateS2AutoCompressionLevel(co *CompressionOpts, compression *string) {
if co.Mode != CompressionS2Auto {
return
}
if cm := selectS2AutoModeBasedOnRTT(c.rtt, co.RTTThresholds); cm != *compression {
*compression = cm
c.out.cw = s2.NewWriter(nil, s2WriterOptions(cm)...)
}
}
// Will return the parts from the raw wire msg.
func (c *client) msgParts(data []byte) (hdr []byte, msg []byte) {
if c != nil && c.pa.hdr > 0 {
@@ -2725,10 +2912,8 @@ func (c *client) addShadowSub(sub *subscription, ime *ime) (*subscription, error
if ime.overlapSubj != _EMPTY_ {
s = ime.overlapSubj
}
subj, err := im.rtr.transformSubject(s)
if err != nil {
return nil, err
}
subj := im.rtr.TransformSubject(s)
nsub.subject = []byte(subj)
} else if !im.usePub || (im.usePub && ime.overlapSubj != _EMPTY_) || !ime.dyn {
if ime.overlapSubj != _EMPTY_ {
@@ -2842,10 +3027,6 @@ func (c *client) unsubscribe(acc *Account, sub *subscription, force, remove bool
c.traceOp("<-> %s", "DELSUB", sub.sid)
}
if c.kind != CLIENT && c.kind != SYSTEM {
c.removeReplySubTimeout(sub)
}
// Remove accounting if requested. This will be false when we close a connection
// with open subscriptions.
if remove {
@@ -2998,15 +3179,17 @@ func (c *client) msgHeaderForRouteOrLeaf(subj, reply []byte, rt *routeTarget, ac
// Router (and Gateway) nodes are RMSG. Set here since leafnodes may rewrite.
mh[0] = 'R'
}
mh = append(mh, acc.Name...)
mh = append(mh, ' ')
if len(subclient.route.accName) == 0 {
mh = append(mh, acc.Name...)
mh = append(mh, ' ')
}
} else {
// Leaf nodes are LMSG
mh[0] = 'L'
// Remap subject if its a shadow subscription, treat like a normal client.
if rt.sub.im != nil {
if rt.sub.im.tr != nil {
to, _ := rt.sub.im.tr.transformSubject(string(subj))
to := rt.sub.im.tr.TransformSubject(string(subj))
subj = []byte(to)
} else if !rt.sub.im.usePub {
subj = []byte(rt.sub.im.to)
@@ -3967,7 +4150,7 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
if si.tr != nil {
// FIXME(dlc) - This could be slow, may want to look at adding cache to bare transforms?
to, _ = si.tr.transformSubject(subject)
to = si.tr.TransformSubject(subject)
} else if si.usePub {
to = subject
}
@@ -4036,7 +4219,7 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
c.pa.reply = nrr
if changed && c.isMqtt() && c.pa.hdr > 0 {
c.srv.mqttStoreQoS1MsgForAccountOnNewSubject(c.pa.hdr, msg, siAcc.GetName(), to)
c.srv.mqttStoreQoSMsgForAccountOnNewSubject(c.pa.hdr, msg, siAcc.GetName(), to)
}
// FIXME(dlc) - Do L1 cache trick like normal client?
@@ -4256,7 +4439,7 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
continue
}
if sub.im.tr != nil {
to, _ := sub.im.tr.transformSubject(string(subject))
to := sub.im.tr.TransformSubject(string(subject))
dsubj = append(_dsubj[:0], to...)
} else if sub.im.usePub {
dsubj = append(_dsubj[:0], subj...)
@@ -4403,7 +4586,7 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
continue
}
if sub.im.tr != nil {
to, _ := sub.im.tr.transformSubject(string(subject))
to := sub.im.tr.TransformSubject(string(subject))
dsubj = append(_dsubj[:0], to...)
} else if sub.im.usePub {
dsubj = append(_dsubj[:0], subj...)
@@ -4595,9 +4778,7 @@ func (c *client) processPingTimer() {
var sendPing bool
pingInterval := c.srv.getOpts().PingInterval
if c.kind == GATEWAY {
pingInterval = adjustPingIntervalForGateway(pingInterval)
}
pingInterval = adjustPingInterval(c.kind, pingInterval)
now := time.Now()
needRTT := c.rtt == 0 || now.Sub(c.rttStart) > DEFAULT_RTT_MEASUREMENT_INTERVAL
@@ -4632,11 +4813,18 @@ func (c *client) processPingTimer() {
c.mu.Unlock()
}
// Returns the smallest value between the given `d` and `gatewayMaxPingInterval` durations.
// Invoked for connections known to be of GATEWAY type.
func adjustPingIntervalForGateway(d time.Duration) time.Duration {
if d > gatewayMaxPingInterval {
return gatewayMaxPingInterval
// Returns the smallest value between the given `d` and some max value
// based on the connection kind.
func adjustPingInterval(kind int, d time.Duration) time.Duration {
switch kind {
case ROUTER:
if d > routeMaxPingInterval {
return routeMaxPingInterval
}
case GATEWAY:
if d > gatewayMaxPingInterval {
return gatewayMaxPingInterval
}
}
return d
}
@@ -4647,9 +4835,7 @@ func (c *client) setPingTimer() {
return
}
d := c.srv.getOpts().PingInterval
if c.kind == GATEWAY {
d = adjustPingIntervalForGateway(d)
}
d = adjustPingInterval(c.kind, d)
c.ping.tmr = time.AfterFunc(d, c.processPingTimer)
}
@@ -4696,10 +4882,29 @@ func (c *client) awaitingAuth() bool {
// We will lock on entry.
func (c *client) setExpirationTimer(d time.Duration) {
c.mu.Lock()
c.atmr = time.AfterFunc(d, c.authExpired)
c.setExpirationTimerUnlocked(d)
c.mu.Unlock()
}
// This will set the atmr for the JWT expiration time. client lock should be held before call
func (c *client) setExpirationTimerUnlocked(d time.Duration) {
c.atmr = time.AfterFunc(d, c.authExpired)
// This is an JWT expiration.
if c.flags.isSet(connectReceived) {
c.expires = time.Now().Add(d).Truncate(time.Second)
}
}
// Return when this client expires via a claim, or 0 if not set.
func (c *client) claimExpiration() time.Duration {
c.mu.Lock()
defer c.mu.Unlock()
if c.expires.IsZero() {
return 0
}
return time.Until(c.expires).Truncate(time.Second)
}
// Possibly flush the connection and then close the low level connection.
// The boolean `minimalFlush` indicates if the flush operation should have a
// minimal write deadline.
@@ -4867,13 +5072,11 @@ func (c *client) closeConnection(reason ClosedState) {
}
var (
connectURLs []string
wsConnectURLs []string
kind = c.kind
srv = c.srv
noReconnect = c.flags.isSet(noReconnect)
acc = c.acc
spoke bool
kind = c.kind
srv = c.srv
noReconnect = c.flags.isSet(noReconnect)
acc = c.acc
spoke bool
)
// Snapshot for use if we are a client connection.
@@ -4892,11 +5095,6 @@ func (c *client) closeConnection(reason ClosedState) {
spoke = c.isSpokeLeafNode()
}
if c.route != nil {
connectURLs = c.route.connectURLs
wsConnectURLs = c.route.wsConnURLs
}
// If we have remote latency tracking running shut that down.
if c.rrTracking != nil {
c.rrTracking.ptmr.Stop()
@@ -4909,17 +5107,10 @@ func (c *client) closeConnection(reason ClosedState) {
if acc != nil && (kind == CLIENT || kind == LEAF || kind == JETSTREAM) {
acc.sl.RemoveBatch(subs)
} else if kind == ROUTER {
go c.removeRemoteSubs()
c.removeRemoteSubs()
}
if srv != nil {
// If this is a route that disconnected, possibly send an INFO with
// the updated list of connect URLs to clients that know how to
// handle async INFOs.
if (len(connectURLs) > 0 || len(wsConnectURLs) > 0) && !srv.getOpts().Cluster.NoAdvertise {
srv.removeConnectURLsAndSendINFOToClients(connectURLs, wsConnectURLs)
}
// Unregister
srv.removeClient(c)
@@ -5014,33 +5205,35 @@ func (c *client) reconnect() {
// Check for a solicited route. If it was, start up a reconnect unless
// we are already connected to the other end.
if c.isSolicitedRoute() || retryImplicit {
srv.mu.Lock()
defer srv.mu.Unlock()
// Capture these under lock
c.mu.Lock()
rid := c.route.remoteID
rtype := c.route.routeType
rurl := c.route.url
accName := string(c.route.accName)
checkRID := accName == _EMPTY_ && srv.routesPoolSize <= 1 && rid != _EMPTY_
c.mu.Unlock()
srv.mu.Lock()
defer srv.mu.Unlock()
// It is possible that the server is being shutdown.
// If so, don't try to reconnect
if !srv.running {
return
}
if rid != "" && srv.remotes[rid] != nil {
srv.Debugf("Not attempting reconnect for solicited route, already connected to \"%s\"", rid)
if checkRID && srv.routes[rid] != nil {
srv.Debugf("Not attempting reconnect for solicited route, already connected to %q", rid)
return
} else if rid == srv.info.ID {
srv.Debugf("Detected route to self, ignoring %q", rurl.Redacted())
return
} else if rtype != Implicit || retryImplicit {
srv.Debugf("Attempting reconnect for solicited route \"%s\"", rurl.Redacted())
srv.Debugf("Attempting reconnect for solicited route %q", rurl.Redacted())
// Keep track of this go-routine so we can wait for it on
// server shutdown.
srv.startGoRoutine(func() { srv.reConnectToRoute(rurl, rtype) })
srv.startGoRoutine(func() { srv.reConnectToRoute(rurl, rtype, accName) })
}
} else if srv != nil && kind == GATEWAY && gwIsOutbound {
if gwCfg != nil {
@@ -5387,6 +5580,8 @@ func (c *client) getAuthUser() string {
return fmt.Sprintf("User %q", c.opts.Username)
case c.opts.JWT != _EMPTY_:
return fmt.Sprintf("JWT User %q", c.pubKey)
case c.opts.Token != _EMPTY_:
return fmt.Sprintf("Token %q", c.opts.Token)
default:
return `User "N/A"`
}
@@ -5517,9 +5712,7 @@ func (c *client) setFirstPingTimer() {
if d > firstPingInterval {
d = firstPingInterval
}
if c.kind == GATEWAY {
d = adjustPingIntervalForGateway(d)
}
d = adjustPingInterval(c.kind, d)
} else if d > firstClientPingInterval {
d = firstClientPingInterval
}
@@ -5527,5 +5720,12 @@ func (c *client) setFirstPingTimer() {
// We randomize the first one by an offset up to 20%, e.g. 2m ~= max 24s.
addDelay := rand.Int63n(int64(d / 5))
d += time.Duration(addDelay)
// In the case of ROUTER/LEAF and when compression is configured, it is possible
// that this timer was already set, but just to detect a stale connection
// since we have to delay the first PING after compression negotiation
// occurred.
if c.ping.tmr != nil {
c.ping.tmr.Stop()
}
c.ping.tmr = time.AfterFunc(d, c.processPingTimer)
}
+5 -2
View File
@@ -41,7 +41,7 @@ var (
const (
// VERSION is the current version for the server.
VERSION = "2.9.22"
VERSION = "2.10.1"
// PROTO is the currently supported protocol.
// 0 was the original
@@ -85,7 +85,7 @@ const (
// AUTH_TIMEOUT is the authorization wait time.
AUTH_TIMEOUT = 2 * time.Second
// DEFAULT_PING_INTERVAL is how often pings are sent to clients and routes.
// DEFAULT_PING_INTERVAL is how often pings are sent to clients, etc...
DEFAULT_PING_INTERVAL = 2 * time.Minute
// DEFAULT_PING_MAX_OUT is maximum allowed pings outstanding before disconnect.
@@ -121,6 +121,9 @@ const (
// DEFAULT_ROUTE_DIAL Route dial timeout.
DEFAULT_ROUTE_DIAL = 1 * time.Second
// DEFAULT_ROUTE_POOL_SIZE Route default pool size
DEFAULT_ROUTE_POOL_SIZE = 3
// DEFAULT_LEAF_NODE_RECONNECT LeafNode reconnect interval.
DEFAULT_LEAF_NODE_RECONNECT = time.Second
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The NATS Authors
// Copyright 2020-2022 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
@@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows && !openbsd && !wasm
// +build !windows,!openbsd,!wasm
//go:build !windows && !openbsd && !netbsd && !wasm
// +build !windows,!openbsd,!netbsd,!wasm
package server
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2022 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.
//go:build netbsd
// +build netbsd
package server
// TODO - See if there is a version of this for NetBSD.
func diskAvailable(storeDir string) int64 {
return JetStreamMaxStoreDefault
}
+11 -8
View File
@@ -208,17 +208,20 @@ var (
// ErrUnknownMappingDestinationFunction is returned when a subject mapping destination contains an unknown mustache-escaped mapping function.
ErrUnknownMappingDestinationFunction = fmt.Errorf("%w: unknown function", ErrInvalidMappingDestination)
// ErrorMappingDestinationFunctionWildcardIndexOutOfRange is returned when the mapping destination function is passed an out of range wildcard index value for one of it's arguments
ErrorMappingDestinationFunctionWildcardIndexOutOfRange = fmt.Errorf("%w: wildcard index out of range", ErrInvalidMappingDestination)
// ErrMappingDestinationIndexOutOfRange is returned when the mapping destination function is passed an out of range wildcard index value for one of it's arguments
ErrMappingDestinationIndexOutOfRange = fmt.Errorf("%w: wildcard index out of range", ErrInvalidMappingDestination)
// ErrorMappingDestinationFunctionNotEnoughArguments is returned when the mapping destination function is not passed enough arguments
ErrorMappingDestinationFunctionNotEnoughArguments = fmt.Errorf("%w: not enough arguments passed to the function", ErrInvalidMappingDestination)
// ErrMappingDestinationNotEnoughArgs is returned when the mapping destination function is not passed enough arguments
ErrMappingDestinationNotEnoughArgs = fmt.Errorf("%w: not enough arguments passed to the function", ErrInvalidMappingDestination)
// ErrorMappingDestinationFunctionInvalidArgument is returned when the mapping destination function is passed and invalid argument
ErrorMappingDestinationFunctionInvalidArgument = fmt.Errorf("%w: function argument is invalid or in the wrong format", ErrInvalidMappingDestination)
// ErrMappingDestinationInvalidArg is returned when the mapping destination function is passed and invalid argument
ErrMappingDestinationInvalidArg = fmt.Errorf("%w: function argument is invalid or in the wrong format", ErrInvalidMappingDestination)
// ErrorMappingDestinationFunctionTooManyArguments is returned when the mapping destination function is passed too many arguments
ErrorMappingDestinationFunctionTooManyArguments = fmt.Errorf("%w: too many arguments passed to the function", ErrInvalidMappingDestination)
// ErrMappingDestinationTooManyArgs is returned when the mapping destination function is passed too many arguments
ErrMappingDestinationTooManyArgs = fmt.Errorf("%w: too many arguments passed to the function", ErrInvalidMappingDestination)
// ErrMappingDestinationNotSupportedForImport is returned when you try to use a mapping function other than wildcard in a transform that needs to be reversible (i.e. an import)
ErrMappingDestinationNotSupportedForImport = fmt.Errorf("%w: the only mapping function allowed for import transforms is {{Wildcard()}}", ErrInvalidMappingDestination)
)
// mappingDestinationErr is a type of subject mapping destination error
+191 -1
View File
@@ -299,6 +299,16 @@
"url": "",
"deprecates": ""
},
{
"constant": "JSMirrorWithFirstSeqErr",
"code": 400,
"error_code": 10143,
"description": "stream mirrors can not have first sequence configured",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSNotEnabledErr",
"code": 503,
@@ -1328,5 +1338,185 @@
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerMetadataLengthErrF",
"code": 400,
"error_code": 10135,
"description": "consumer metadata exceeds maximum size of {limit}",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerDuplicateFilterSubjects",
"code": 400,
"error_code": 10136,
"description": "consumer cannot have both FilterSubject and FilterSubjects specified",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerMultipleFiltersNotAllowed",
"code": 400,
"error_code": 10137,
"description": "consumer with multiple subject filters cannot use subject based API",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerOverlappingSubjectFilters",
"code": 400,
"error_code": 10138,
"description": "consumer subject filters cannot overlap",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerEmptyFilter",
"code": 400,
"error_code": 10139,
"description": "consumer filter in FilterSubjects cannot be empty",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSSourceDuplicateDetected",
"code": 400,
"error_code": 10140,
"description": "duplicate source configuration detected",
"comment": "source stream, filter and transform (plus external if present) must form a unique combination",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSSourceInvalidStreamName",
"code": 400,
"error_code": 10141,
"description": "sourced stream name is invalid",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSMirrorInvalidStreamName",
"code": 400,
"error_code": 10142,
"description": "mirrored stream name is invalid",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSSourceMultipleFiltersNotAllowed",
"code": 400,
"error_code": 10144,
"description": "source with multiple subject transforms cannot also have a single subject filter",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSSourceInvalidSubjectFilter",
"code": 400,
"error_code": 10145,
"description": "source subject filter is invalid",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSSourceInvalidTransformDestination",
"code": 400,
"error_code": 10146,
"description": "source transform destination is invalid",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSSourceOverlappingSubjectFilters",
"code": 400,
"error_code": 10147,
"description": "source filters can not overlap",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerAlreadyExists",
"code": 400,
"error_code": 10148,
"description": "consumer already exists",
"comment": "action CREATE is used for a existing consumer with a different config",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerDoesNotExist",
"code": 400,
"error_code": 10149,
"description": "consumer does not exist",
"comment": "action UPDATE is used for a nonexisting consumer",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSMirrorMultipleFiltersNotAllowed",
"code": 400,
"error_code": 10150,
"description": "mirror with multiple subject transforms cannot also have a single subject filter",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSMirrorInvalidSubjectFilter",
"code": 400,
"error_code": 10151,
"description": "mirror subject filter is invalid",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSMirrorOverlappingSubjectFilters",
"code": 400,
"error_code": 10152,
"description": "mirror subject filters can not overlap",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerInactiveThresholdExcess",
"code": 400,
"error_code": 10153,
"description": "consumer inactive threshold exceeds system limit of {limit}",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
}
]
]
+360 -51
View File
@@ -51,21 +51,29 @@ const (
accPingReqSubj = "$SYS.REQ.ACCOUNT.PING.%s" // atm. only used for STATZ and CONNZ import from system account
// kept for backward compatibility when using http resolver
// this overlaps with the names for events but you'd have to have the operator private key in order to succeed.
accUpdateEventSubjOld = "$SYS.ACCOUNT.%s.CLAIMS.UPDATE"
accUpdateEventSubjNew = "$SYS.REQ.ACCOUNT.%s.CLAIMS.UPDATE"
connsRespSubj = "$SYS._INBOX_.%s"
accConnsEventSubjNew = "$SYS.ACCOUNT.%s.SERVER.CONNS"
accConnsEventSubjOld = "$SYS.SERVER.ACCOUNT.%s.CONNS" // kept for backward compatibility
lameDuckEventSubj = "$SYS.SERVER.%s.LAMEDUCK"
shutdownEventSubj = "$SYS.SERVER.%s.SHUTDOWN"
authErrorEventSubj = "$SYS.SERVER.%s.CLIENT.AUTH.ERR"
serverStatsSubj = "$SYS.SERVER.%s.STATSZ"
serverDirectReqSubj = "$SYS.REQ.SERVER.%s.%s"
serverPingReqSubj = "$SYS.REQ.SERVER.PING.%s"
serverStatsPingReqSubj = "$SYS.REQ.SERVER.PING" // use $SYS.REQ.SERVER.PING.STATSZ instead
leafNodeConnectEventSubj = "$SYS.ACCOUNT.%s.LEAFNODE.CONNECT" // for internal use only
remoteLatencyEventSubj = "$SYS.LATENCY.M2.%s"
inboxRespSubj = "$SYS._INBOX.%s.%s"
accUpdateEventSubjOld = "$SYS.ACCOUNT.%s.CLAIMS.UPDATE"
accUpdateEventSubjNew = "$SYS.REQ.ACCOUNT.%s.CLAIMS.UPDATE"
connsRespSubj = "$SYS._INBOX_.%s"
accConnsEventSubjNew = "$SYS.ACCOUNT.%s.SERVER.CONNS"
accConnsEventSubjOld = "$SYS.SERVER.ACCOUNT.%s.CONNS" // kept for backward compatibility
lameDuckEventSubj = "$SYS.SERVER.%s.LAMEDUCK"
shutdownEventSubj = "$SYS.SERVER.%s.SHUTDOWN"
clientKickReqSubj = "$SYS.REQ.SERVER.%s.KICK"
clientLDMReqSubj = "$SYS.REQ.SERVER.%s.LDM"
authErrorEventSubj = "$SYS.SERVER.%s.CLIENT.AUTH.ERR"
authErrorAccountEventSubj = "$SYS.ACCOUNT.CLIENT.AUTH.ERR"
serverStatsSubj = "$SYS.SERVER.%s.STATSZ"
serverDirectReqSubj = "$SYS.REQ.SERVER.%s.%s"
serverPingReqSubj = "$SYS.REQ.SERVER.PING.%s"
serverStatsPingReqSubj = "$SYS.REQ.SERVER.PING" // use $SYS.REQ.SERVER.PING.STATSZ instead
serverReloadReqSubj = "$SYS.REQ.SERVER.%s.RELOAD" // with server ID
leafNodeConnectEventSubj = "$SYS.ACCOUNT.%s.LEAFNODE.CONNECT" // for internal use only
remoteLatencyEventSubj = "$SYS.LATENCY.M2.%s"
inboxRespSubj = "$SYS._INBOX.%s.%s"
// Used to return information to a user on bound account and user permissions.
userDirectInfoSubj = "$SYS.REQ.USER.INFO"
userDirectReqSubj = "$SYS.REQ.USER.%s.INFO"
// FIXME(dlc) - Should account scope, even with wc for now, but later on
// we can then shard as needed.
@@ -201,6 +209,7 @@ type AccountStat struct {
Conns int `json:"conns"`
LeafNodes int `json:"leafnodes"`
TotalConns int `json:"total_conns"`
NumSubs uint32 `json:"num_subscriptions"`
Sent DataStats `json:"sent"`
Received DataStats `json:"received"`
SlowConsumers int64 `json:"slow_consumers"`
@@ -215,18 +224,60 @@ type accNumConnsReq struct {
Account string `json:"acc"`
}
// ServerID is basic static info for a server.
type ServerID struct {
Name string `json:"name"`
Host string `json:"host"`
ID string `json:"id"`
}
// Type for our server capabilities.
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"`
Seq uint64 `json:"seq"`
JetStream bool `json:"jetstream"`
Time time.Time `json:"time"`
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"`
// Whether JetStream is enabled (deprecated in favor of the `ServerCapability`).
JetStream bool `json:"jetstream"`
// Generic capability flags
Flags ServerCapability `json:"flags"`
// Sequence and Time from the remote server for this message.
Seq uint64 `json:"seq"`
Time time.Time `json:"time"`
}
const (
JetStreamEnabled ServerCapability = 1 << iota // Server had JetStream enabled.
BinaryStreamSnapshot // New stream snapshot capability.
)
// Set JetStream capability.
func (si *ServerInfo) SetJetStreamEnabled() {
si.Flags |= JetStreamEnabled
// Still set old version.
si.JetStream = true
}
// JetStreamEnabled indicates whether or not we have JetStream enabled.
func (si *ServerInfo) JetStreamEnabled() bool {
// Take into account old version.
return si.Flags&JetStreamEnabled != 0 || si.JetStream
}
// Set binary stream snapshot capability.
func (si *ServerInfo) SetBinaryStreamSnapshot() {
si.Flags |= BinaryStreamSnapshot
}
// JetStreamEnabled indicates whether or not we have binary stream snapshot capbilities.
func (si *ServerInfo) BinaryStreamSnapshot() bool {
return si.Flags&BinaryStreamSnapshot != 0
}
// ClientInfo is detailed information about the client forming a connection.
@@ -234,7 +285,7 @@ type ClientInfo struct {
Start *time.Time `json:"start,omitempty"`
Host string `json:"host,omitempty"`
ID uint64 `json:"id,omitempty"`
Account string `json:"acc"`
Account string `json:"acc,omitempty"`
Service string `json:"svc,omitempty"`
User string `json:"user,omitempty"`
Name string `json:"name,omitempty"`
@@ -252,6 +303,7 @@ type ClientInfo struct {
Kind string `json:"kind,omitempty"`
ClientType string `json:"client_type,omitempty"`
MQTTClient string `json:"client_id,omitempty"` // This is the MQTT client ID
Nonce string `json:"nonce,omitempty"`
}
// ServerStats hold various statistics that we will periodically send out.
@@ -412,17 +464,21 @@ RESET:
case <-sendq.ch:
msgs := sendq.pop()
for _, pm := range msgs {
if pm.si != nil {
pm.si.Name = servername
pm.si.Domain = domain
pm.si.Host = host
pm.si.Cluster = cluster
pm.si.ID = id
pm.si.Seq = atomic.AddUint64(seqp, 1)
pm.si.Version = VERSION
pm.si.Time = time.Now().UTC()
pm.si.JetStream = js
pm.si.Tags = tags
if si := pm.si; si != nil {
si.Name = servername
si.Domain = domain
si.Host = host
si.Cluster = cluster
si.ID = id
si.Seq = atomic.AddUint64(seqp, 1)
si.Version = VERSION
si.Time = time.Now().UTC()
si.Tags = tags
if js {
// New capability based flags.
si.SetJetStreamEnabled()
si.SetBinaryStreamSnapshot()
}
}
var b []byte
if pm.msg != nil {
@@ -590,6 +646,23 @@ func (s *Server) sendInternalAccountMsgWithReply(a *Account, subject, reply stri
return nil
}
// Send system style message to an account scope.
func (s *Server) sendInternalAccountSysMsg(a *Account, subj string, si *ServerInfo, msg interface{}) {
s.mu.RLock()
if s.sys == nil || s.sys.sendq == nil || a == nil {
s.mu.RUnlock()
return
}
sendq := s.sys.sendq
s.mu.RUnlock()
a.mu.Lock()
c := a.internalClient()
a.mu.Unlock()
sendq.push(newPubMsg(c, subj, _EMPTY_, si, nil, msg, noCompression, false, false))
}
// This will queue up a message to be sent.
// Lock should not be held.
func (s *Server) sendInternalMsgLocked(subj, rply string, si *ServerInfo, msg interface{}) {
@@ -702,11 +775,13 @@ func routeStat(r *client) *RouteStat {
return nil
}
r.mu.Lock()
// Note: *client.out[Msgs|Bytes] are not set using atomics,
// unlike in[Msgs|Bytes].
rs := &RouteStat{
ID: r.cid,
Sent: DataStats{
Msgs: atomic.LoadInt64(&r.outMsgs),
Bytes: atomic.LoadInt64(&r.outBytes),
Msgs: r.outMsgs,
Bytes: r.outBytes,
},
Received: DataStats{
Msgs: atomic.LoadInt64(&r.inMsgs),
@@ -763,9 +838,9 @@ func (s *Server) sendStatsz(subj string) {
m.Stats.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
m.Stats.NumSubs = s.numSubscriptions()
// Routes
for _, r := range s.routes {
s.forEachRoute(func(r *client) {
m.Stats.Routes = append(m.Stats.Routes, routeStat(r))
}
})
// Gateways
if s.gateway.enabled {
gw := s.gateway
@@ -774,9 +849,11 @@ func (s *Server) sendStatsz(subj string) {
gs := &GatewayStat{Name: name}
c.mu.Lock()
gs.ID = c.cid
// Note that *client.out[Msgs|Bytes] are not set using atomic,
// unlike the in[Msgs|bytes].
gs.Sent = DataStats{
Msgs: atomic.LoadInt64(&c.outMsgs),
Bytes: atomic.LoadInt64(&c.outBytes),
Msgs: c.outMsgs,
Bytes: c.outBytes,
}
c.mu.Unlock()
// Gather matching inbound connections
@@ -983,6 +1060,7 @@ func (s *Server) initEventTracking() {
s.Errorf("Error setting up internal tracking: %v", err)
}
monSrvc := map[string]sysMsgHandler{
"IDZ": s.idzReq,
"STATSZ": s.statszReq,
"VARZ": func(sub *subscription, c *client, _ *Account, subject, reply string, hdr, msg []byte) {
optz := &VarzEventOptions{}
@@ -1020,6 +1098,10 @@ func (s *Server) initEventTracking() {
optz := &HealthzEventOptions{}
s.zReq(c, reply, hdr, msg, &optz.EventFilterOptions, optz, func() (interface{}, error) { return s.healthz(&optz.HealthzOptions), nil })
},
"PROFILEZ": func(sub *subscription, c *client, _ *Account, subject, reply string, hdr, msg []byte) {
optz := &ProfilezEventOptions{}
s.zReq(c, reply, hdr, msg, &optz.EventFilterOptions, optz, func() (interface{}, error) { return s.profilez(&optz.ProfilezOptions), nil })
},
}
for name, req := range monSrvc {
subject = fmt.Sprintf(serverDirectReqSubj, s.info.ID, name)
@@ -1124,6 +1206,13 @@ func (s *Server) initEventTracking() {
}
}
// User info.
// TODO(dlc) - Can be internal and not forwarded since bound server for the client connection
// is only one that will answer. This breaks tests since we still forward on remote server connect.
if _, err := s.sysSubscribe(fmt.Sprintf(userDirectReqSubj, "*"), s.userInfoReq); err != nil {
s.Errorf("Error setting up internal tracking: %v", err)
}
// For now only the STATZ subject has an account specific ping equivalent.
if _, err := s.sysSubscribe(fmt.Sprintf(accPingReqSubj, "STATZ"),
s.noInlineCallback(func(sub *subscription, c *client, _ *Account, subject, reply string, hdr, msg []byte) {
@@ -1156,6 +1245,57 @@ func (s *Server) initEventTracking() {
if _, err := s.sysSubscribeInternal(accSubsSubj, s.noInlineCallback(s.debugSubscribers)); err != nil {
s.Errorf("Error setting up internal debug service for subscribers: %v", err)
}
// Listen for requests to reload the server configuration.
subject = fmt.Sprintf(serverReloadReqSubj, s.info.ID)
if _, err := s.sysSubscribe(subject, s.noInlineCallback(s.reloadConfig)); err != nil {
s.Errorf("Error setting up server reload handler: %v", err)
}
// Client connection kick
subject = fmt.Sprintf(clientKickReqSubj, s.info.ID)
if _, err := s.sysSubscribe(subject, s.noInlineCallback(s.kickClient)); err != nil {
s.Errorf("Error setting up client kick service: %v", err)
}
// Client connection LDM
subject = fmt.Sprintf(clientLDMReqSubj, s.info.ID)
if _, err := s.sysSubscribe(subject, s.noInlineCallback(s.ldmClient)); err != nil {
s.Errorf("Error setting up client LDM service: %v", err)
}
}
// UserInfo returns basic information to a user about bound account and user permissions.
// For account information they will need to ping that separately, and this allows security
// controls on each subsystem if desired, e.g. account info, jetstream account info, etc.
type UserInfo struct {
UserID string `json:"user"`
Account string `json:"account"`
Permissions *Permissions `json:"permissions,omitempty"`
Expires time.Duration `json:"expires,omitempty"`
}
// Process a user info request.
func (s *Server) userInfoReq(sub *subscription, c *client, _ *Account, subject, reply string, msg []byte) {
if !s.EventsEnabled() || reply == _EMPTY_ {
return
}
response := &ServerAPIResponse{Server: &ServerInfo{}}
ci, _, _, _, err := s.getRequestInfo(c, msg)
if err != nil {
response.Error = &ApiError{Code: http.StatusBadRequest}
s.sendInternalResponse(reply, response)
return
}
response.Data = &UserInfo{
UserID: ci.User,
Account: ci.Account,
Permissions: c.publicPermissions(),
Expires: c.claimExpiration(),
}
s.sendInternalResponse(reply, response)
}
// register existing accounts with any system exports.
@@ -1211,6 +1351,20 @@ func (s *Server) addSystemAccountExports(sacc *Account) {
}
}
// User info export.
userInfoSubj := fmt.Sprintf(userDirectReqSubj, "*")
if !sacc.hasServiceExportMatching(userInfoSubj) {
if err := sacc.AddServiceExport(userInfoSubj, nil); err != nil {
s.Errorf("Error adding system service export for %q: %v", userInfoSubj, err)
}
mappedSubj := fmt.Sprintf(userDirectReqSubj, sacc.GetName())
if err := sacc.AddServiceImport(sacc, userDirectInfoSubj, mappedSubj); err != nil {
s.Errorf("Error setting up system service import %s: %v", mappedSubj, err)
}
// Make sure to share details.
sacc.setServiceImportSharing(sacc, mappedSubj, false, true)
}
// Register any accounts that existed prior.
s.registerSystemImportsForExisting()
@@ -1358,7 +1512,9 @@ func (s *Server) remoteServerUpdate(sub *subscription, c *client, _ *Account, su
si.Tags,
cfg,
stats,
false, si.JetStream,
false,
si.JetStreamEnabled(),
si.BinaryStreamSnapshot(),
})
}
@@ -1394,7 +1550,19 @@ func (s *Server) processNewServer(si *ServerInfo) {
node := getHash(si.Name)
// Only update if non-existent
if _, ok := s.nodeToInfo.Load(node); !ok {
s.nodeToInfo.Store(node, nodeInfo{si.Name, si.Version, si.Cluster, si.Domain, si.ID, si.Tags, nil, nil, false, si.JetStream})
s.nodeToInfo.Store(node, nodeInfo{
si.Name,
si.Version,
si.Cluster,
si.Domain,
si.ID,
si.Tags,
nil,
nil,
false,
si.JetStreamEnabled(),
si.BinaryStreamSnapshot(),
})
}
}
// Announce ourselves..
@@ -1592,6 +1760,12 @@ type HealthzEventOptions struct {
EventFilterOptions
}
// In the context of system events, ProfilezEventOptions are options passed to Profilez
type ProfilezEventOptions struct {
ProfilezOptions
EventFilterOptions
}
// returns true if the request does NOT apply to this server and can be ignored.
// DO NOT hold the server lock when
func (s *Server) filterRequest(fOpts *EventFilterOptions) bool {
@@ -1676,6 +1850,19 @@ func (s *Server) statszReq(sub *subscription, c *client, _ *Account, subject, re
s.sendStatsz(reply)
}
// idzReq is for a request for basic static server info.
// Try to not hold the write lock or dynamically create data.
func (s *Server) idzReq(sub *subscription, c *client, _ *Account, subject, reply string, hdr, msg []byte) {
s.mu.RLock()
defer s.mu.RUnlock()
id := &ServerID{
Name: s.info.Name,
Host: s.info.Host,
ID: s.info.ID,
}
s.sendInternalMsg(reply, _EMPTY_, nil, &id)
}
var errSkipZreq = errors.New("filtered response")
const (
@@ -1782,7 +1969,7 @@ func (s *Server) registerSystemImports(a *Account) {
return
}
sacc := s.SystemAccount()
if sacc == nil {
if sacc == nil || sacc == a {
return
}
// FIXME(dlc) - make a shared list between sys exports etc.
@@ -1801,6 +1988,12 @@ func (s *Server) registerSystemImports(a *Account) {
importSrvc(fmt.Sprintf(accPingReqSubj, "CONNZ"), mappedConnzSubj)
importSrvc(fmt.Sprintf(serverPingReqSubj, "CONNZ"), mappedConnzSubj)
importSrvc(fmt.Sprintf(accPingReqSubj, "STATZ"), fmt.Sprintf(accDirectReqSubj, a.Name, "STATZ"))
// This is for user's looking up their own info.
mappedSubject := fmt.Sprintf(userDirectReqSubj, a.Name)
importSrvc(userDirectInfoSubj, mappedSubject)
// Make sure to share details.
a.setServiceImportSharing(sacc, mappedSubject, false, true)
}
// Setup tracking for this account. This allows us to track global account activity.
@@ -1884,7 +2077,7 @@ func (s *Server) sendAccConnsUpdate(a *Account, subj ...string) {
a.mu.Unlock()
}
// Lock shoulc be held on entry
// Lock should be held on entry.
func (a *Account) statz() *AccountStat {
localConns := a.numLocalConnections()
leafConns := a.numLocalLeafNodes()
@@ -1893,22 +2086,26 @@ func (a *Account) statz() *AccountStat {
Conns: localConns,
LeafNodes: leafConns,
TotalConns: localConns + leafConns,
NumSubs: a.sl.Count(),
Received: DataStats{
Msgs: atomic.LoadInt64(&a.inMsgs),
Bytes: atomic.LoadInt64(&a.inBytes)},
Bytes: atomic.LoadInt64(&a.inBytes),
},
Sent: DataStats{
Msgs: atomic.LoadInt64(&a.outMsgs),
Bytes: atomic.LoadInt64(&a.outBytes)},
Bytes: atomic.LoadInt64(&a.outBytes),
},
SlowConsumers: atomic.LoadInt64(&a.slowConsumers),
}
}
// accConnsUpdate is called whenever there is a change to the account's
// number of active connections, or during a heartbeat.
// We will not send for $G.
func (s *Server) accConnsUpdate(a *Account) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.eventsEnabled() || a == nil {
if !s.eventsEnabled() || a == nil || a == s.gacc {
return
}
s.sendAccConnsUpdate(a, fmt.Sprintf(accConnsEventSubjOld, a.Name), fmt.Sprintf(accConnsEventSubjNew, a.Name))
@@ -1962,9 +2159,9 @@ func (s *Server) accountConnectEvent(c *client) {
MQTTClient: c.getMQTTClientID(),
},
}
subj := fmt.Sprintf(connectEventSubj, c.acc.Name)
c.mu.Unlock()
subj := fmt.Sprintf(connectEventSubj, c.acc.Name)
s.sendInternalMsgLocked(subj, _EMPTY_, &m.Server, &m)
}
@@ -2030,6 +2227,7 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string)
s.sendInternalMsgLocked(subj, _EMPTY_, &m.Server, &m)
}
// This is the system level event sent to the system account for operators.
func (s *Server) sendAuthErrorEvent(c *client) {
s.mu.Lock()
if !s.eventsEnabled() {
@@ -2084,6 +2282,61 @@ func (s *Server) sendAuthErrorEvent(c *client) {
s.mu.Unlock()
}
// This is the account level event sent to the origin account for account owners.
func (s *Server) sendAccountAuthErrorEvent(c *client, acc *Account, reason string) {
if acc == nil {
return
}
s.mu.Lock()
if !s.eventsEnabled() {
s.mu.Unlock()
return
}
eid := s.nextEventID()
s.mu.Unlock()
now := time.Now().UTC()
c.mu.Lock()
m := DisconnectEventMsg{
TypedEvent: TypedEvent{
Type: DisconnectEventMsgType,
ID: eid,
Time: now,
},
Client: ClientInfo{
Start: &c.start,
Stop: &now,
Host: c.host,
ID: c.cid,
Account: acc.Name,
User: c.getRawAuthUser(),
Name: c.opts.Name,
Lang: c.opts.Lang,
Version: c.opts.Version,
RTT: c.getRTT(),
Jwt: c.opts.JWT,
IssuerKey: issuerForClient(c),
Tags: c.tags,
NameTag: c.nameTag,
Kind: c.kindString(),
ClientType: c.clientTypeString(),
MQTTClient: c.getMQTTClientID(),
},
Sent: DataStats{
Msgs: c.inMsgs,
Bytes: c.inBytes,
},
Received: DataStats{
Msgs: c.outMsgs,
Bytes: c.outBytes,
},
Reason: reason,
}
c.mu.Unlock()
s.sendInternalAccountSysMsg(acc, authErrorAccountEventSubj, &m.Server, &m)
}
// Internal message callback.
// If the msg is needed past the callback it is required to be copied.
// rmsg contains header and the message. use client.msgParts(rmsg) to split them apart
@@ -2222,6 +2475,7 @@ func (s *Server) remoteLatencyUpdate(sub *subscription, _ *client, _ *Account, s
si.m1 = &m2
}
si.acc.mu.Unlock()
if m1 == nil {
return
}
@@ -2482,6 +2736,61 @@ func (s *Server) nsubsRequest(sub *subscription, c *client, _ *Account, subject,
s.sendInternalMsgLocked(reply, _EMPTY_, nil, nsubs)
}
func (s *Server) reloadConfig(sub *subscription, c *client, _ *Account, subject, reply string, hdr, msg []byte) {
if !s.eventsRunning() {
return
}
optz := &EventFilterOptions{}
s.zReq(c, reply, hdr, msg, optz, optz, func() (interface{}, error) {
// Reload the server config, as requested.
return nil, s.Reload()
})
}
type KickClientReq struct {
CID uint64 `json:"cid"`
}
type LDMClientReq struct {
CID uint64 `json:"cid"`
}
func (s *Server) kickClient(_ *subscription, c *client, _ *Account, subject, reply string, hdr, msg []byte) {
if !s.eventsRunning() {
return
}
var req KickClientReq
if err := json.Unmarshal(msg, &req); err != nil {
s.sys.client.Errorf("Error unmarshalling kick client request: %v", err)
return
}
optz := &EventFilterOptions{}
s.zReq(c, reply, hdr, msg, optz, optz, func() (interface{}, error) {
return nil, s.DisconnectClientByID(req.CID)
})
}
func (s *Server) ldmClient(_ *subscription, c *client, _ *Account, subject, reply string, hdr, msg []byte) {
if !s.eventsRunning() {
return
}
var req LDMClientReq
if err := json.Unmarshal(msg, &req); err != nil {
s.sys.client.Errorf("Error unmarshalling kick client request: %v", err)
return
}
optz := &EventFilterOptions{}
s.zReq(c, reply, hdr, msg, optz, optz, func() (interface{}, error) {
return nil, s.LDMClientByID(req.CID)
})
}
// Helper to grab account name for a client.
func accForClient(c *client) string {
if c.acc != nil {
File diff suppressed because it is too large Load Diff
+42 -15
View File
@@ -1213,11 +1213,11 @@ func (s *Server) forwardNewGatewayToLocalCluster(oinfo *Info) {
b, _ := json.Marshal(info)
infoJSON := []byte(fmt.Sprintf(InfoProto, b))
for _, r := range s.routes {
s.forEachRemote(func(r *client) {
r.mu.Lock()
r.enqueueProto(infoJSON)
r.mu.Unlock()
}
})
}
// Sends queue subscriptions interest to remote gateway.
@@ -2742,8 +2742,7 @@ func (g *srvGateway) getClusterHash() []byte {
// Store this route in map with the key being the remote server's name hash
// and the remote server's ID hash used by gateway replies mapping routing.
func (s *Server) storeRouteByHash(srvNameHash, srvIDHash string, c *client) {
s.routesByHash.Store(srvNameHash, c)
func (s *Server) storeRouteByHash(srvIDHash string, c *client) {
if !s.gateway.enabled {
return
}
@@ -2751,8 +2750,7 @@ func (s *Server) storeRouteByHash(srvNameHash, srvIDHash string, c *client) {
}
// Remove the route with the given keys from the map.
func (s *Server) removeRouteByHash(srvNameHash, srvIDHash string) {
s.routesByHash.Delete(srvNameHash)
func (s *Server) removeRouteByHash(srvIDHash string) {
if !s.gateway.enabled {
return
}
@@ -2761,11 +2759,33 @@ func (s *Server) removeRouteByHash(srvNameHash, srvIDHash string) {
// Returns the route with given hash or nil if not found.
// This is for gateways only.
func (g *srvGateway) getRouteByHash(hash []byte) *client {
if v, ok := g.routesIDByHash.Load(string(hash)); ok {
return v.(*client)
func (s *Server) getRouteByHash(hash, accName []byte) (*client, bool) {
id := string(hash)
var perAccount bool
if v, ok := s.accRouteByHash.Load(string(accName)); ok {
if v == nil {
id += string(accName)
perAccount = true
} else {
id += strconv.Itoa(v.(int))
}
}
return nil
if v, ok := s.gateway.routesIDByHash.Load(id); ok {
return v.(*client), perAccount
} else if !perAccount {
// Check if we have a "no pool" connection at index 0.
if v, ok := s.gateway.routesIDByHash.Load(string(hash) + "0"); ok {
if r := v.(*client); r != nil {
r.mu.Lock()
noPool := r.route.noPool
r.mu.Unlock()
if noPool {
return r, false
}
}
}
}
return nil, perAccount
}
// Returns the subject from the routed reply
@@ -2821,10 +2841,11 @@ func (c *client) handleGatewayReply(msg []byte) (processed bool) {
}
var route *client
var perAccount bool
// If the origin is not this server, get the route this should be sent to.
if c.kind == GATEWAY && srvHash != nil && !bytes.Equal(srvHash, c.srv.gateway.sIDHash) {
route = c.srv.gateway.getRouteByHash(srvHash)
route, perAccount = c.srv.getRouteByHash(srvHash, c.pa.account)
// This will be possibly nil, and in this case we will try to process
// the interest from this server.
}
@@ -2836,8 +2857,12 @@ func (c *client) handleGatewayReply(msg []byte) (processed bool) {
// getAccAndResultFromCache()
var _pacache [256]byte
pacache := _pacache[:0]
pacache = append(pacache, c.pa.account...)
pacache = append(pacache, ' ')
// For routes that are dedicated to an account, do not put the account
// name in the pacache.
if c.kind == GATEWAY || (c.kind == ROUTER && c.route != nil && len(c.route.accName) == 0) {
pacache = append(pacache, c.pa.account...)
pacache = append(pacache, ' ')
}
pacache = append(pacache, c.pa.subject...)
c.pa.pacache = pacache
@@ -2882,8 +2907,10 @@ func (c *client) handleGatewayReply(msg []byte) (processed bool) {
var bufa [256]byte
var buf = bufa[:0]
buf = append(buf, msgHeadProto...)
buf = append(buf, acc.Name...)
buf = append(buf, ' ')
if !perAccount {
buf = append(buf, acc.Name...)
buf = append(buf, ' ')
}
buf = append(buf, orgSubject...)
buf = append(buf, ' ')
if len(c.pa.reply) > 0 {
+116 -83
View File
@@ -38,11 +38,14 @@ import (
// JetStreamConfig determines this server's configuration.
// MaxMemory and MaxStore are in bytes.
type JetStreamConfig struct {
MaxMemory int64 `json:"max_memory"`
MaxStore int64 `json:"max_storage"`
StoreDir string `json:"store_dir,omitempty"`
Domain string `json:"domain,omitempty"`
CompressOK bool `json:"compress_ok,omitempty"`
MaxMemory int64 `json:"max_memory"`
MaxStore int64 `json:"max_storage"`
StoreDir string `json:"store_dir,omitempty"`
SyncInterval time.Duration `json:"sync_interval,omitempty"`
SyncAlways bool `json:"sync_always,omitempty"`
Domain string `json:"domain,omitempty"`
CompressOK bool `json:"compress_ok,omitempty"`
UniqueTag string `json:"unique_tag,omitempty"`
}
// Statistics about JetStream for this server.
@@ -181,10 +184,10 @@ func (s *Server) EnableJetStream(config *JetStreamConfig) error {
s.Noticef("Starting JetStream")
if config == nil || config.MaxMemory <= 0 || config.MaxStore <= 0 {
var storeDir, domain string
var storeDir, domain, uniqueTag string
var maxStore, maxMem int64
if config != nil {
storeDir, domain = config.StoreDir, config.Domain
storeDir, domain, uniqueTag = config.StoreDir, config.Domain, config.UniqueTag
maxStore, maxMem = config.MaxStore, config.MaxMemory
}
config = s.dynJetStreamConfig(storeDir, maxStore, maxMem)
@@ -194,6 +197,9 @@ func (s *Server) EnableJetStream(config *JetStreamConfig) error {
if domain != _EMPTY_ {
config.Domain = domain
}
if uniqueTag != _EMPTY_ {
config.UniqueTag = uniqueTag
}
s.Debugf("JetStream creating dynamic configuration - %s memory, %s disk", friendlyBytes(config.MaxMemory), friendlyBytes(config.MaxStore))
} else if config.StoreDir != _EMPTY_ {
config.StoreDir = filepath.Join(config.StoreDir, JetStreamStoreDir)
@@ -218,8 +224,8 @@ type keyGen func(context []byte) ([]byte, error)
// Return a key generation function or nil if encryption not enabled.
// keyGen defined in filestore.go - keyGen func(iv, context []byte) []byte
func (s *Server) jsKeyGen(info string) keyGen {
if ek := s.getOpts().JetStreamKey; ek != _EMPTY_ {
func (s *Server) jsKeyGen(jsKey, info string) keyGen {
if ek := jsKey; ek != _EMPTY_ {
return func(context []byte) ([]byte, error) {
h := hmac.New(sha256.New, []byte(ek))
if _, err := h.Write([]byte(info)); err != nil {
@@ -235,37 +241,65 @@ func (s *Server) jsKeyGen(info string) keyGen {
}
// Decode the encrypted metafile.
func (s *Server) decryptMeta(sc StoreCipher, ekey, buf []byte, acc, context string) ([]byte, error) {
func (s *Server) decryptMeta(sc StoreCipher, ekey, buf []byte, acc, context string) ([]byte, bool, error) {
if len(ekey) < minMetaKeySize {
return nil, errBadKeySize
return nil, false, errBadKeySize
}
prf := s.jsKeyGen(acc)
if prf == nil {
return nil, errNoEncryption
var osc StoreCipher
switch sc {
case AES:
osc = ChaCha
case ChaCha:
osc = AES
}
rb, err := prf([]byte(context))
if err != nil {
return nil, err
type prfWithCipher struct {
keyGen
StoreCipher
}
var prfs []prfWithCipher
if prf := s.jsKeyGen(s.getOpts().JetStreamKey, acc); prf == nil {
return nil, false, errNoEncryption
} else {
// First of all, try our current encryption keys with both
// store cipher algorithms.
prfs = append(prfs, prfWithCipher{prf, sc})
prfs = append(prfs, prfWithCipher{prf, osc})
}
if prf := s.jsKeyGen(s.getOpts().JetStreamOldKey, acc); prf != nil {
// Then, if we have an old encryption key, try with also with
// both store cipher algorithms.
prfs = append(prfs, prfWithCipher{prf, sc})
prfs = append(prfs, prfWithCipher{prf, osc})
}
kek, err := genEncryptionKey(sc, rb)
if err != nil {
return nil, err
for i, prf := range prfs {
rb, err := prf.keyGen([]byte(context))
if err != nil {
continue
}
kek, err := genEncryptionKey(prf.StoreCipher, rb)
if err != nil {
continue
}
ns := kek.NonceSize()
seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil)
if err != nil {
continue
}
aek, err := genEncryptionKey(prf.StoreCipher, seed)
if err != nil {
continue
}
if aek.NonceSize() != kek.NonceSize() {
continue
}
plain, err := aek.Open(nil, buf[:ns], buf[ns:], nil)
if err != nil {
continue
}
return plain, i > 0, nil
}
ns := kek.NonceSize()
seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil)
if err != nil {
return nil, err
}
aek, err := genEncryptionKey(sc, seed)
if err != nil {
return nil, err
}
plain, err := aek.Open(nil, buf[:ns], buf[ns:], nil)
if err != nil {
return nil, err
}
return plain, nil
return nil, false, fmt.Errorf("unable to recover keys")
}
// Check to make sure directory has the jetstream directory.
@@ -366,13 +400,16 @@ func (s *Server) enableJetStream(cfg JetStreamConfig) error {
s.SetDefaultSystemAccount()
}
s.Noticef(" _ ___ _____ ___ _____ ___ ___ _ __ __")
s.Noticef(" _ | | __|_ _/ __|_ _| _ \\ __| /_\\ | \\/ |")
s.Noticef("| || | _| | | \\__ \\ | | | / _| / _ \\| |\\/| |")
s.Noticef(" \\__/|___| |_| |___/ |_| |_|_\\___/_/ \\_\\_| |_|")
s.Noticef("")
s.Noticef(" https://docs.nats.io/jetstream")
s.Noticef("")
opts := s.getOpts()
if !opts.DisableJetStreamBanner {
s.Noticef(" _ ___ _____ ___ _____ ___ ___ _ __ __")
s.Noticef(" _ | | __|_ _/ __|_ _| _ \\ __| /_\\ | \\/ |")
s.Noticef("| || | _| | | \\__ \\ | | | / _| / _ \\| |\\/| |")
s.Noticef(" \\__/|___| |_| |___/ |_| |_|_\\___/_/ \\_\\_| |_|")
s.Noticef("")
s.Noticef(" https://docs.nats.io/jetstream")
s.Noticef("")
}
s.Noticef("---------------- JETSTREAM ----------------")
s.Noticef(" Max Memory: %s", friendlyBytes(cfg.MaxMemory))
s.Noticef(" Max Storage: %s", friendlyBytes(cfg.MaxStore))
@@ -380,7 +417,7 @@ func (s *Server) enableJetStream(cfg JetStreamConfig) error {
if cfg.Domain != _EMPTY_ {
s.Noticef(" Domain: %s", cfg.Domain)
}
opts := s.getOpts()
if ek := opts.JetStreamKey; ek != _EMPTY_ {
s.Noticef(" Encryption: %s", opts.JetStreamCipher)
}
@@ -455,10 +492,12 @@ func (s *Server) updateJetStreamInfoStatus(enabled bool) {
func (s *Server) restartJetStream() error {
opts := s.getOpts()
cfg := JetStreamConfig{
StoreDir: opts.StoreDir,
MaxMemory: opts.JetStreamMaxMemory,
MaxStore: opts.JetStreamMaxStore,
Domain: opts.JetStreamDomain,
StoreDir: opts.StoreDir,
SyncInterval: opts.SyncInterval,
SyncAlways: opts.SyncAlways,
MaxMemory: opts.JetStreamMaxMemory,
MaxStore: opts.JetStreamMaxStore,
Domain: opts.JetStreamDomain,
}
s.Noticef("Restarting JetStream")
err := s.EnableJetStream(&cfg)
@@ -964,6 +1003,7 @@ func (s *Server) JetStreamConfig() *JetStreamConfig {
return c
}
// StoreDir returns the current JetStream directory.
func (s *Server) StoreDir() string {
s.mu.Lock()
defer s.mu.Unlock()
@@ -1041,9 +1081,12 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
}
js.mu.Lock()
if _, ok := js.accounts[a.Name]; ok && a.JetStreamEnabled() {
if jsa, ok := js.accounts[a.Name]; ok {
a.mu.Lock()
a.js = jsa
a.mu.Unlock()
js.mu.Unlock()
return fmt.Errorf("jetstream already enabled for account")
return a.enableAllJetStreamServiceImportsAndMappings()
}
// Check the limits against existing reservations.
@@ -1068,12 +1111,11 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
}
js.accounts[a.Name] = jsa
js.mu.Unlock()
// Stamp inside account as well.
// Stamp inside account as well. Needs to be done under js's lock.
a.mu.Lock()
a.js = jsa
a.mu.Unlock()
js.mu.Unlock()
// Create the proper imports here.
if err := a.enableAllJetStreamServiceImportsAndMappings(); err != nil {
@@ -1210,7 +1252,6 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
}
// Track if we are converting ciphers.
var osc StoreCipher
var convertingCiphers bool
// Check if we are encrypted.
@@ -1223,21 +1264,11 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
continue
}
// Decode the buffer before proceeding.
nbuf, err := s.decryptMeta(sc, keyBuf, buf, a.Name, fi.Name())
var nbuf []byte
nbuf, convertingCiphers, err = s.decryptMeta(sc, keyBuf, buf, a.Name, fi.Name())
if err != nil {
// See if we are changing ciphers.
switch sc {
case ChaCha:
nbuf, err = s.decryptMeta(AES, keyBuf, buf, a.Name, fi.Name())
osc, convertingCiphers = AES, true
case AES:
nbuf, err = s.decryptMeta(ChaCha, keyBuf, buf, a.Name, fi.Name())
osc, convertingCiphers = ChaCha, true
}
if err != nil {
s.Warnf(" Error decrypting our stream metafile: %v", err)
continue
}
s.Warnf(" Error decrypting our stream metafile: %v", err)
continue
}
buf = nbuf
plaintext = false
@@ -1285,13 +1316,14 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
}
s.Noticef(" Starting restore for stream '%s > %s'", a.Name, cfg.StreamConfig.Name)
rt := time.Now()
// Log if we are converting from plaintext to encrypted.
if encrypted {
if plaintext {
s.Noticef(" Encrypting stream '%s > %s'", a.Name, cfg.StreamConfig.Name)
} else if convertingCiphers {
s.Noticef(" Converting from %s to %s for stream '%s > %s'", osc, sc, a.Name, cfg.StreamConfig.Name)
s.Noticef(" Converting to %s for stream '%s > %s'", sc, a.Name, cfg.StreamConfig.Name)
// Remove the key file to have system regenerate with the new cipher.
os.Remove(keyFile)
}
@@ -1315,7 +1347,8 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
}
state := mset.state()
s.Noticef(" Restored %s messages for stream '%s > %s'", comma(int64(state.Msgs)), mset.accName(), mset.name())
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))
// Collect to check for dangling messages.
// TODO(dlc) - Can be removed eventually.
@@ -1355,19 +1388,10 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
s.Debugf(" Consumer metafile is encrypted, reading encrypted keyfile")
// Decode the buffer before proceeding.
ctxName := e.mset.name() + tsep + ofi.Name()
nbuf, err := s.decryptMeta(sc, key, buf, a.Name, ctxName)
nbuf, _, err := s.decryptMeta(sc, key, buf, a.Name, ctxName)
if err != nil {
// See if we are changing ciphers.
switch sc {
case ChaCha:
nbuf, err = s.decryptMeta(AES, key, buf, a.Name, ctxName)
case AES:
nbuf, err = s.decryptMeta(ChaCha, key, buf, a.Name, ctxName)
}
if err != nil {
s.Warnf(" Error decrypting our consumer metafile: %v", err)
continue
}
s.Warnf(" Error decrypting our consumer metafile: %v", err)
continue
}
buf = nbuf
}
@@ -1383,7 +1407,7 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
// the consumer can reconnect. We will create it as a durable and switch it.
cfg.ConsumerConfig.Durable = ofi.Name()
}
obs, err := e.mset.addConsumerWithAssignment(&cfg.ConsumerConfig, _EMPTY_, nil, true)
obs, err := e.mset.addConsumerWithAssignment(&cfg.ConsumerConfig, _EMPTY_, nil, true, ActionCreateOrUpdate)
if err != nil {
s.Warnf(" Error adding consumer %q: %v", cfg.Name, err)
continue
@@ -2023,7 +2047,11 @@ func (js *jetStream) limitsExceeded(storeType StorageType) bool {
func tierName(cfg *StreamConfig) string {
// TODO (mh) this is where we could select based off a placement tag as well "qos:tier"
return fmt.Sprintf("R%d", cfg.Replicas)
replicas := cfg.Replicas
if replicas == 0 {
replicas = 1
}
return fmt.Sprintf("R%d", replicas)
}
func isSameTier(cfgA, cfgB *StreamConfig) bool {
@@ -2376,6 +2404,10 @@ func (s *Server) dynJetStreamConfig(storeDir string, maxStore, maxMem int64) *Je
opts := s.getOpts()
// Sync options.
jsc.SyncInterval = opts.SyncInterval
jsc.SyncAlways = opts.SyncAlways
if opts.maxStoreSet && maxStore >= 0 {
jsc.MaxStore = maxStore
} else {
@@ -2392,6 +2424,7 @@ func (s *Server) dynJetStreamConfig(storeDir string, maxStore, maxMem int64) *Je
jsc.MaxMemory = JetStreamMaxMemDefault
}
}
return jsc
}
@@ -2733,7 +2766,7 @@ func friendlyBytes[T Number](bytes T) string {
}
func isValidName(name string) bool {
if name == "" {
if name == _EMPTY_ {
return false
}
return !strings.ContainsAny(name, " \t\r\n\f.*>")
+52 -25
View File
@@ -330,6 +330,10 @@ func generateJSMappingTable(domain string) map[string]string {
// JSMaxDescription is the maximum description length for streams and consumers.
const JSMaxDescriptionLen = 4 * 1024
// JSMaxMetadataLen is the maximum length for streams an consumers metadata map.
// It's calculated by summing length of all keys an values.
const JSMaxMetadataLen = 128 * 1024
// JSMaxNameLen is the maximum name lengths for streams, consumers and templates.
// Picked 255 as it seems to be a widely used file name limit
const JSMaxNameLen = 255
@@ -1369,9 +1373,12 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, _ *Account,
return
}
resp.StreamInfo = &StreamInfo{
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
TimeStamp: time.Now().UTC(),
Mirror: mset.mirrorInfo(),
Sources: mset.sourcesInfo(),
}
resp.DidCreate = true
s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp))
@@ -1457,12 +1464,13 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account,
}
resp.StreamInfo = &StreamInfo{
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Domain: s.getOpts().JetStreamDomain,
Mirror: mset.mirrorInfo(),
Sources: mset.sourcesInfo(),
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Domain: s.getOpts().JetStreamDomain,
Mirror: mset.mirrorInfo(),
Sources: mset.sourcesInfo(),
TimeStamp: time.Now().UTC(),
}
s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp))
}
@@ -1682,12 +1690,13 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s
for _, mset := range msets[offset:] {
config := mset.config()
resp.Streams = append(resp.Streams, &StreamInfo{
Created: mset.createdTime(),
State: mset.state(),
Config: config,
Domain: s.getOpts().JetStreamDomain,
Mirror: mset.mirrorInfo(),
Sources: mset.sourcesInfo(),
Created: mset.createdTime(),
State: mset.state(),
Config: config,
Domain: s.getOpts().JetStreamDomain,
Mirror: mset.mirrorInfo(),
Sources: mset.sourcesInfo(),
TimeStamp: time.Now().UTC(),
})
if len(resp.Streams) >= JSApiListLimit {
break
@@ -1842,6 +1851,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, a *Account, s
Mirror: mset.mirrorInfo(),
Sources: mset.sourcesInfo(),
Alternates: js.streamAlternates(ci, config.Name),
TimeStamp: time.Now().UTC(),
}
if clusterWideConsCount > 0 {
resp.StreamInfo.State.Consumers = clusterWideConsCount
@@ -3451,9 +3461,14 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC
s.Warnf("Restore failed for %s for stream '%s > %s' in %v",
friendlyBytes(int64(total)), streamName, acc.Name, end.Sub(start))
} else {
resp.StreamInfo = &StreamInfo{Created: mset.createdTime(), State: mset.state(), Config: mset.config()}
resp.StreamInfo = &StreamInfo{
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
TimeStamp: time.Now().UTC(),
}
s.Noticef("Completed restore of %s for stream '%s > %s' in %v",
friendlyBytes(int64(total)), streamName, acc.Name, end.Sub(start))
friendlyBytes(int64(total)), streamName, acc.Name, end.Sub(start).Round(time.Millisecond))
}
// On the last EOF, send back the stream info or error status.
@@ -3835,6 +3850,13 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun
return
}
// in case of multiple filters provided, error if new API is used.
if filteredSubject != _EMPTY_ && len(req.Config.FilterSubjects) != 0 {
resp.Error = NewJSConsumerMultipleFiltersNotAllowedError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
// Check for a filter subject.
if filteredSubject != _EMPTY_ && req.Config.FilterSubject != filteredSubject {
resp.Error = NewJSConsumerCreateFilterSubjectMismatchError()
@@ -3847,9 +3869,9 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun
// 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)
go s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action)
} else {
s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config)
s.jsClusteredConsumerRequest(ci, acc, subject, reply, rmsg, req.Stream, &req.Config, req.Action)
}
return
}
@@ -3868,7 +3890,7 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun
return
}
o, err := stream.addConsumer(&req.Config)
o, err := stream.addConsumerWithAction(&req.Config, req.Action)
if err != nil {
if IsNatsErr(err, JSConsumerStoreFailedErrF) {
@@ -4209,13 +4231,18 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, _ *Account,
// We have been assigned but have not created a node yet. If we are a member return
// our config and defaults for state and no cluster info.
if isMember {
// Since we access consumerAssignment, need js lock.
js.mu.RLock()
resp.ConsumerInfo = &ConsumerInfo{
Stream: ca.Stream,
Name: ca.Name,
Created: ca.Created,
Config: ca.Config,
Stream: ca.Stream,
Name: ca.Name,
Created: ca.Created,
Config: ca.Config,
TimeStamp: time.Now().UTC(),
}
s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp))
b := s.jsonResponse(resp)
js.mu.RUnlock()
s.sendAPIResponse(ci, acc, subject, reply, string(msg), b)
}
return
}
+290 -78
View File
@@ -15,6 +15,7 @@ package server
import (
"bytes"
crand "crypto/rand"
"encoding/binary"
"encoding/json"
"errors"
@@ -753,7 +754,8 @@ func (js *jetStream) setupMetaGroup() error {
FileStoreConfig{StoreDir: storeDir, BlockSize: defaultMetaFSBlkSize, AsyncFlush: false},
StreamConfig{Name: defaultMetaGroupName, Storage: FileStorage},
time.Now().UTC(),
s.jsKeyGen(defaultMetaGroupName),
s.jsKeyGen(s.getOpts().JetStreamKey, defaultMetaGroupName),
s.jsKeyGen(s.getOpts().JetStreamOldKey, defaultMetaGroupName),
)
if err != nil {
s.Errorf("Error creating filestore: %v", err)
@@ -808,7 +810,10 @@ func (js *jetStream) setupMetaGroup() error {
}
// Start up our meta node.
n, err := s.startRaftNode(sysAcc.GetName(), cfg)
n, err := s.startRaftNode(sysAcc.GetName(), cfg, pprofLabels{
"type": "metaleader",
"account": sysAcc.Name,
})
if err != nil {
s.Warnf("Could not start metadata controller: %v", err)
return err
@@ -834,7 +839,13 @@ func (js *jetStream) setupMetaGroup() error {
atomic.StoreInt32(&js.clustered, 1)
c.registerWithAccount(sacc)
js.srv.startGoRoutine(js.monitorCluster)
js.srv.startGoRoutine(
js.monitorCluster,
pprofLabels{
"type": "metaleader",
"account": sacc.Name,
},
)
return nil
}
@@ -1159,6 +1170,65 @@ func (js *jetStream) checkForOrphans() {
}
}
// Check and delete any orphans we may come across.
func (s *Server) checkForNRGOrphans() {
js, cc := s.getJetStreamCluster()
if js == nil || cc == nil || js.isMetaRecovering() {
// No cluster means no NRGs. Also return if still recovering.
return
}
// Track which assets R>1 should be on this server.
nrgMap := make(map[string]struct{})
trackGroup := func(rg *raftGroup) {
// If R>1 track this as a legit NRG.
if rg.node != nil {
nrgMap[rg.Name] = struct{}{}
}
}
// Register our meta.
js.mu.RLock()
meta := cc.meta
if meta == nil {
js.mu.RUnlock()
// Bail with no meta node.
return
}
ourID := meta.ID()
nrgMap[meta.Group()] = struct{}{}
// Collect all valid groups from our assignments.
for _, asa := range cc.streams {
for _, sa := range asa {
if sa.Group.isMember(ourID) && sa.Restore == nil {
trackGroup(sa.Group)
for _, ca := range sa.consumers {
if ca.Group.isMember(ourID) {
trackGroup(ca.Group)
}
}
}
}
}
js.mu.RUnlock()
// Check NRGs that are running.
var needDelete []RaftNode
s.rnMu.RLock()
for name, n := range s.raftNodes {
if _, ok := nrgMap[name]; !ok {
needDelete = append(needDelete, n)
}
}
s.rnMu.RUnlock()
for _, n := range needDelete {
s.Warnf("Detected orphaned NRG %q, will cleanup", n.Group())
n.Delete()
}
}
func (js *jetStream) monitorCluster() {
s, n := js.server(), js.getMetaGroup()
qch, rqch, lch, aq := js.clusterQuitC(), n.QuitC(), n.LeadChangeC(), n.ApplyQ()
@@ -1189,6 +1259,8 @@ func (js *jetStream) monitorCluster() {
if hs := s.healthz(nil); hs.Error != _EMPTY_ {
s.Warnf("%v", hs.Error)
}
// Also check for orphaned NRGs.
s.checkForNRGOrphans()
}
var (
@@ -1200,7 +1272,7 @@ func (js *jetStream) monitorCluster() {
// Highwayhash key for generating hashes.
key := make([]byte, 32)
rand.Read(key)
crand.Read(key)
// Set to true to start.
js.setMetaRecovering()
@@ -1269,7 +1341,6 @@ func (js *jetStream) monitorCluster() {
go checkHealth()
continue
}
// FIXME(dlc) - Deal with errors.
if didSnap, didStreamRemoval, didConsumerRemoval, err := js.applyMetaEntries(ce.Entries, ru); err == nil {
_, nb := n.Applied(ce.Index)
if js.hasPeerEntries(ce.Entries) || didStreamRemoval || (didSnap && !isLeader) {
@@ -1280,6 +1351,8 @@ func (js *jetStream) monitorCluster() {
doSnapshot()
}
ce.ReturnToPool()
} else {
s.Warnf("Error applying JetStream cluster entries: %v", err)
}
}
aq.recycle(&ces)
@@ -1909,7 +1982,7 @@ func (rg *raftGroup) setPreferred() {
}
// createRaftGroup is called to spin up this raft group if needed.
func (js *jetStream) createRaftGroup(accName string, rg *raftGroup, storage StorageType) error {
func (js *jetStream) createRaftGroup(accName string, rg *raftGroup, storage StorageType, labels pprofLabels) error {
js.mu.Lock()
s, cc := js.srv, js.cluster
if cc == nil || cc.meta == nil {
@@ -1958,7 +2031,8 @@ func (js *jetStream) createRaftGroup(accName string, rg *raftGroup, storage Stor
FileStoreConfig{StoreDir: storeDir, BlockSize: defaultMediumBlockSize, AsyncFlush: false, SyncInterval: 5 * time.Minute},
StreamConfig{Name: rg.Name, Storage: FileStorage},
time.Now().UTC(),
s.jsKeyGen(rg.Name),
s.jsKeyGen(s.getOpts().JetStreamKey, rg.Name),
s.jsKeyGen(s.getOpts().JetStreamOldKey, rg.Name),
)
if err != nil {
s.Errorf("Error creating filestore WAL: %v", err)
@@ -1982,7 +2056,7 @@ func (js *jetStream) createRaftGroup(accName string, rg *raftGroup, storage Stor
s.bootstrapRaftNode(cfg, rg.Peers, true)
}
n, err := s.startRaftNode(accName, cfg)
n, err := s.startRaftNode(accName, cfg, labels)
if err != nil || n == nil {
s.Debugf("Error creating raft group: %v", err)
return err
@@ -2028,6 +2102,15 @@ func (mset *stream) removeNode() {
}
}
func (mset *stream) clearRaftNode() {
if mset == nil {
return
}
mset.mu.Lock()
defer mset.mu.Unlock()
mset.node = nil
}
// Helper function to generate peer info.
// lists and sets for old and new.
func genPeerInfo(peers []string, split int) (newPeers, oldPeers []string, newPeerSet, oldPeerSet map[string]bool) {
@@ -2586,7 +2669,7 @@ func (mset *stream) isMigrating() bool {
return true
}
// resetClusteredState is called when a clustered stream had a sequence mismatch and needs to be reset.
// resetClusteredState is called when a clustered stream had an error (e.g sequence mismatch, bad snapshot) and needs to be reset.
func (mset *stream) resetClusteredState(err error) bool {
mset.mu.RLock()
s, js, jsa, sa, acc, node := mset.srv, mset.js, mset.jsa, mset.sa, mset.acc, mset.node
@@ -2835,26 +2918,55 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco
panic(fmt.Sprintf("JetStream Cluster Unknown group entry op type: %v", op))
}
} else if e.Type == EntrySnapshot {
if !isRecovering && mset != nil {
var snap streamSnapshot
if err := json.Unmarshal(e.Data, &snap); err != nil {
return err
}
if !mset.IsLeader() {
if err := mset.processSnapshot(&snap); err != nil {
return err
}
}
} else if isRecovering && mset != nil {
// On recovery, reset CLFS/FAILED.
var snap streamSnapshot
if err := json.Unmarshal(e.Data, &snap); err != nil {
return err
}
if mset == nil {
return nil
}
mset.mu.Lock()
mset.clfs = snap.Failed
mset.mu.Unlock()
// Everything operates on new replicated state. Will convert legacy snapshots to this for processing.
var ss *StreamReplicatedState
onBadState := func(err error) {
// If we are the leader or recovering, meaning we own the snapshot,
// we should stepdown and clear our raft state since our snapshot is bad.
if isRecovering || mset.IsLeader() {
mset.resetClusteredState(err)
}
}
// Check if we are the new binary encoding.
if IsEncodedStreamState(e.Data) {
var err error
ss, err = DecodeStreamState(e.Data)
if err != nil {
onBadState(err)
return err
}
} else {
var snap streamSnapshot
if err := json.Unmarshal(e.Data, &snap); err != nil {
onBadState(err)
return err
}
// Convert over to StreamReplicatedState
ss = &StreamReplicatedState{
Msgs: snap.Msgs,
Bytes: snap.Bytes,
FirstSeq: snap.FirstSeq,
LastSeq: snap.LastSeq,
Failed: snap.Failed,
}
if len(snap.Deleted) > 0 {
ss.Deleted = append(ss.Deleted, DeleteSlice(snap.Deleted))
}
}
if !isRecovering && !mset.IsLeader() {
if err := mset.processSnapshot(ss); err != nil {
return err
}
} else if isRecovering {
// On recovery, reset CLFS/FAILED.
mset.setCLFS(ss.Failed)
}
} else if e.Type == EntryRemovePeer {
js.mu.RLock()
@@ -2965,12 +3077,13 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp))
} else {
resp.StreamInfo = &StreamInfo{
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Cluster: js.clusterInfo(mset.raftGroup()),
Sources: mset.sourcesInfo(),
Mirror: mset.mirrorInfo(),
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Cluster: js.clusterInfo(mset.raftGroup()),
Sources: mset.sourcesInfo(),
Mirror: mset.mirrorInfo(),
TimeStamp: time.Now().UTC(),
}
resp.DidCreate = true
s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp))
@@ -3304,11 +3417,22 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
if !alreadyRunning && numReplicas > 1 {
if needsNode {
mset.setLeader(false)
js.createRaftGroup(acc.GetName(), rg, storage)
js.createRaftGroup(acc.GetName(), rg, storage, pprofLabels{
"type": "stream",
"account": mset.accName(),
"stream": mset.name(),
})
}
mset.monitorWg.Add(1)
// Start monitoring..
s.startGoRoutine(func() { js.monitorStream(mset, sa, needsNode) })
s.startGoRoutine(
func() { js.monitorStream(mset, sa, needsNode) },
pprofLabels{
"type": "stream",
"account": mset.accName(),
"stream": mset.name(),
},
)
} else if numReplicas == 1 && alreadyRunning {
// We downgraded to R1. Make sure we cleanup the raft node and the stream monitor.
mset.removeNode()
@@ -3378,12 +3502,13 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
// Send our response.
var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}}
resp.StreamInfo = &StreamInfo{
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Cluster: js.clusterInfo(mset.raftGroup()),
Mirror: mset.mirrorInfo(),
Sources: mset.sourcesInfo(),
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Cluster: js.clusterInfo(mset.raftGroup()),
Mirror: mset.mirrorInfo(),
Sources: mset.sourcesInfo(),
TimeStamp: time.Now().UTC(),
}
s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp))
@@ -3404,7 +3529,11 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
js.mu.RUnlock()
// Process the raft group and make sure it's running if needed.
err := js.createRaftGroup(acc.GetName(), rg, storage)
err := js.createRaftGroup(acc.GetName(), rg, storage, pprofLabels{
"type": "stream",
"account": acc.Name,
"stream": sa.Config.Name,
})
// If we are restoring, create the stream if we are R>1 and not the preferred who handles the
// receipt of the snapshot itself.
@@ -3440,12 +3569,13 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
if !recovering {
var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}
resp.StreamInfo = &StreamInfo{
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Cluster: js.clusterInfo(mset.raftGroup()),
Sources: mset.sourcesInfo(),
Mirror: mset.mirrorInfo(),
Created: mset.createdTime(),
State: mset.state(),
Config: mset.config(),
Cluster: js.clusterInfo(mset.raftGroup()),
Sources: mset.sourcesInfo(),
Mirror: mset.mirrorInfo(),
TimeStamp: time.Now().UTC(),
}
s.sendAPIResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp))
}
@@ -3468,7 +3598,11 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
s.Warnf("JetStream cluster error updating stream %q for account %q: %v", sa.Config.Name, acc.Name, err)
if osa != nil {
// Process the raft group and make sure it's running if needed.
js.createRaftGroup(acc.GetName(), osa.Group, storage)
js.createRaftGroup(acc.GetName(), osa.Group, storage, pprofLabels{
"type": "stream",
"account": mset.accName(),
"stream": mset.name(),
})
mset.setStreamAssignment(osa)
}
if rg.node != nil {
@@ -3523,13 +3657,25 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
return
}
// Re-capture node.
js.mu.RLock()
node := rg.node
js.mu.RUnlock()
// Start our monitoring routine.
if rg.node != nil {
if node != nil {
if !alreadyRunning {
if mset != nil {
mset.monitorWg.Add(1)
}
s.startGoRoutine(func() { js.monitorStream(mset, sa, false) })
s.startGoRoutine(
func() { js.monitorStream(mset, sa, false) },
pprofLabels{
"type": "stream",
"account": mset.accName(),
"stream": mset.name(),
},
)
}
} else {
// Single replica stream, process manually here.
@@ -3974,7 +4120,12 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state
storage = MemoryStorage
}
// No-op if R1.
js.createRaftGroup(accName, rg, storage)
js.createRaftGroup(accName, rg, storage, pprofLabels{
"type": "consumer",
"account": mset.accName(),
"stream": ca.Stream,
"consumer": ca.Name,
})
} else {
// If we are clustered update the known peers.
js.mu.RLock()
@@ -3988,7 +4139,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state
var didCreate, isConfigUpdate, needsLocalResponse bool
if o == nil {
// Add in the consumer if needed.
if o, err = mset.addConsumerWithAssignment(ca.Config, ca.Name, ca, wasExisting); err == nil {
if o, err = mset.addConsumerWithAssignment(ca.Config, ca.Name, ca, wasExisting, ActionCreateOrUpdate); err == nil {
didCreate = true
}
} else {
@@ -4049,7 +4200,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state
// Set CA for our consumer.
o.setConsumerAssignment(cca)
s.Debugf("JetStream cluster, consumer was already running")
s.Debugf("JetStream cluster, consumer '%s > %s > %s' was already running", ca.Client.serviceAccount(), ca.Stream, ca.Name)
}
// If we have an initial state set apply that now.
@@ -4141,7 +4292,15 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state
// Start our monitoring routine if needed.
if !alreadyRunning && !o.isMonitorRunning() {
o.monitorWg.Add(1)
s.startGoRoutine(func() { js.monitorConsumer(o, ca) })
s.startGoRoutine(
func() { js.monitorConsumer(o, ca) },
pprofLabels{
"type": "consumer",
"account": mset.accName(),
"stream": mset.name(),
"consumer": ca.Name,
},
)
}
// For existing consumer, only send response if not recovering.
if wasExisting && !js.isMetaRecovering() {
@@ -4376,7 +4535,7 @@ func (js *jetStream) monitorConsumer(o *consumer, ca *consumerAssignment) {
// Highwayhash key for generating hashes.
key := make([]byte, 32)
rand.Read(key)
crand.Read(key)
// Hash of the last snapshot (fixed size in memory).
var lastSnap []byte
@@ -6332,7 +6491,12 @@ func (s *Server) jsClusteredStreamListRequest(acc *Account, ci *ClientInfo, filt
for _, sa := range streams {
if s.allPeersOffline(sa.Group) {
// Place offline onto our results by hand here.
si := &StreamInfo{Config: *sa.Config, Created: sa.Created, Cluster: js.offlineClusterInfo(sa.Group)}
si := &StreamInfo{
Config: *sa.Config,
Created: sa.Created,
Cluster: js.offlineClusterInfo(sa.Group),
TimeStamp: time.Now().UTC(),
}
resp.Streams = append(resp.Streams, si)
missingNames = append(missingNames, sa.Config.Name)
} else {
@@ -6478,7 +6642,12 @@ func (s *Server) jsClusteredConsumerListRequest(acc *Account, ci *ClientInfo, of
for _, ca := range consumers {
if s.allPeersOffline(ca.Group) {
// Place offline onto our results by hand here.
ci := &ConsumerInfo{Config: ca.Config, Created: ca.Created, Cluster: js.offlineClusterInfo(ca.Group)}
ci := &ConsumerInfo{
Config: ca.Config,
Created: ca.Created,
Cluster: js.offlineClusterInfo(ca.Group),
TimeStamp: time.Now().UTC(),
}
resp.Consumers = append(resp.Consumers, ci)
missingNames = append(missingNames, ca.Name)
} else {
@@ -6710,7 +6879,7 @@ func (cc *jetStreamCluster) createGroupForConsumer(cfg *ConsumerConfig, sa *stre
}
// jsClusteredConsumerRequest is first point of entry to create a consumer with R > 1.
func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subject, reply string, rmsg []byte, stream string, cfg *ConsumerConfig) {
func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subject, reply string, rmsg []byte, stream string, cfg *ConsumerConfig, action ConsumerAction) {
js, cc := s.getJetStreamCluster()
if js == nil || cc == nil {
return
@@ -6732,7 +6901,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
}
srvLim := &s.getOpts().JetStreamLimits
// Make sure we have sane defaults
setConsumerConfigDefaults(cfg, srvLim, selectedLimits)
setConsumerConfigDefaults(cfg, &streamCfg, srvLim, selectedLimits)
if err := checkConsumerCfg(cfg, srvLim, &streamCfg, acc, selectedLimits, false); err != nil {
resp.Error = err
@@ -6811,6 +6980,11 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
oname = cfg.Durable
}
if ca = sa.consumers[oname]; ca != nil && !ca.deleted {
if action == ActionCreate && !reflect.DeepEqual(cfg, ca.Config) {
resp.Error = NewJSConsumerAlreadyExistsError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
return
}
// Do quick sanity check on new cfg to prevent here if possible.
if err := acc.checkNewConsumerConfig(ca.Config, cfg); err != nil {
resp.Error = NewJSConsumerCreateError(err, Unless(err))
@@ -6822,6 +6996,11 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
// If this is new consumer.
if ca == nil {
if action == ActionUpdate {
resp.Error = NewJSConsumerDoesNotExistError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
return
}
rg := cc.createGroupForConsumer(cfg, sa)
if rg == nil {
resp.Error = NewJSInsufficientResourcesError()
@@ -6887,7 +7066,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
} else {
nca := ca.copyGroup()
rBefore := ca.Config.replicas(sa.Config)
rBefore := nca.Config.replicas(sa.Config)
rAfter := cfg.replicas(sa.Config)
var curLeader string
@@ -7132,7 +7311,36 @@ func encodeStreamMsgAllowCompress(subject, reply string, hdr, msg []byte, lseq u
return buf[:wi]
}
// Determine if all peers in our set support the binary snapshot.
func (mset *stream) supportsBinarySnapshot() bool {
mset.mu.RLock()
defer mset.mu.RUnlock()
return mset.supportsBinarySnapshotLocked()
}
// Determine if all peers in our set support the binary snapshot.
// Lock should be held.
func (mset *stream) supportsBinarySnapshotLocked() bool {
s, n := mset.srv, mset.node
if s == nil || n == nil {
return false
}
// Grab our peers and walk them to make sure we can all support binary stream snapshots.
id, peers := n.ID(), n.Peers()
for _, p := range peers {
if p.ID == id {
// We know we support ourselves.
continue
}
if sir, ok := s.nodeToInfo.Load(p.ID); !ok || sir == nil || !sir.(nodeInfo).binarySnapshots {
return false
}
}
return true
}
// StreamSnapshot is used for snapshotting and out of band catch up in clustered mode.
// Legacy, replace with binary stream snapshots.
type streamSnapshot struct {
Msgs uint64 `json:"messages"`
Bytes uint64 `json:"bytes"`
@@ -7152,6 +7360,13 @@ func (mset *stream) stateSnapshot() []byte {
// Grab a snapshot of a stream for clustered mode.
// Lock should be held.
func (mset *stream) stateSnapshotLocked() []byte {
// Decide if we can support the new style of stream snapshots.
if mset.supportsBinarySnapshotLocked() {
snap, _ := mset.store.EncodedStreamState(mset.getCLFS())
return snap
}
// Older v1 version with deleted as a sorted []uint64.
state := mset.store.State()
snap := &streamSnapshot{
Msgs: state.Msgs,
@@ -7349,10 +7564,9 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [
mset.clMu.Lock()
if mset.clseq == 0 || mset.clseq < lseq {
// Re-capture
lseq, clfs = mset.lastSeqAndCLFS()
lseq, clfs = mset.lseq, mset.clfs
mset.clseq = lseq + clfs
}
esm := encodeStreamMsgAllowCompress(subject, reply, hdr, msg, mset.clseq, time.Now().UnixNano(), mset.compressOK)
mset.clseq++
@@ -7396,7 +7610,7 @@ type streamSyncRequest struct {
}
// Given a stream state that represents a snapshot, calculate the sync request based on our current state.
func (mset *stream) calculateSyncRequest(state *StreamState, snap *streamSnapshot) *streamSyncRequest {
func (mset *stream) calculateSyncRequest(state *StreamState, snap *StreamReplicatedState) *streamSyncRequest {
// Quick check if we are already caught up.
if state.LastSeq >= snap.LastSeq {
return nil
@@ -7406,7 +7620,7 @@ func (mset *stream) calculateSyncRequest(state *StreamState, snap *streamSnapsho
// processSnapshotDeletes will update our current store based on the snapshot
// but only processing deletes and new FirstSeq / purges.
func (mset *stream) processSnapshotDeletes(snap *streamSnapshot) {
func (mset *stream) processSnapshotDeletes(snap *StreamReplicatedState) {
mset.mu.Lock()
var state StreamState
mset.store.FastState(&state)
@@ -7419,11 +7633,8 @@ func (mset *stream) processSnapshotDeletes(snap *streamSnapshot) {
}
mset.mu.Unlock()
// Range the deleted and delete if applicable.
for _, dseq := range snap.Deleted {
if dseq > state.FirstSeq && dseq <= state.LastSeq {
mset.store.RemoveMsg(dseq)
}
if len(snap.Deleted) > 0 {
mset.store.SyncDeleted(snap.Deleted)
}
}
@@ -7520,7 +7731,7 @@ var (
)
// Process a stream snapshot.
func (mset *stream) processSnapshot(snap *streamSnapshot) (e error) {
func (mset *stream) processSnapshot(snap *StreamReplicatedState) (e error) {
// Update any deletes, etc.
mset.processSnapshotDeletes(snap)
@@ -8072,12 +8283,13 @@ func (mset *stream) processClusterStreamInfoRequest(reply string) {
}
si := &StreamInfo{
Created: mset.createdTime(),
State: mset.state(),
Config: config,
Cluster: js.clusterInfo(mset.raftGroup()),
Sources: mset.sourcesInfo(),
Mirror: mset.mirrorInfo(),
Created: mset.createdTime(),
State: mset.state(),
Config: config,
Cluster: js.clusterInfo(mset.raftGroup()),
Sources: mset.sourcesInfo(),
Mirror: mset.mirrorInfo(),
TimeStamp: time.Now().UTC(),
}
// Check for out of band catchups.
@@ -44,6 +44,9 @@ const (
// JSClusterUnSupportFeatureErr not currently supported in clustered mode
JSClusterUnSupportFeatureErr ErrorIdentifier = 10036
// JSConsumerAlreadyExists action CREATE is used for a existing consumer with a different config (consumer already exists)
JSConsumerAlreadyExists ErrorIdentifier = 10148
// JSConsumerBadDurableNameErr durable name can not contain '.', '*', '>'
JSConsumerBadDurableNameErr ErrorIdentifier = 10103
@@ -74,6 +77,12 @@ const (
// JSConsumerDirectRequiresPushErr consumer direct requires a push based consumer
JSConsumerDirectRequiresPushErr ErrorIdentifier = 10090
// JSConsumerDoesNotExist action UPDATE is used for a nonexisting consumer (consumer does not exist)
JSConsumerDoesNotExist ErrorIdentifier = 10149
// JSConsumerDuplicateFilterSubjects consumer cannot have both FilterSubject and FilterSubjects specified
JSConsumerDuplicateFilterSubjects ErrorIdentifier = 10136
// JSConsumerDurableNameNotInSubjectErr consumer expected to be durable but no durable name set in subject
JSConsumerDurableNameNotInSubjectErr ErrorIdentifier = 10016
@@ -83,6 +92,9 @@ const (
// JSConsumerDurableNameNotSetErr consumer expected to be durable but a durable name was not set
JSConsumerDurableNameNotSetErr ErrorIdentifier = 10018
// JSConsumerEmptyFilter consumer filter in FilterSubjects cannot be empty
JSConsumerEmptyFilter ErrorIdentifier = 10139
// JSConsumerEphemeralWithDurableInSubjectErr consumer expected to be ephemeral but detected a durable name set in subject
JSConsumerEphemeralWithDurableInSubjectErr ErrorIdentifier = 10019
@@ -101,6 +113,9 @@ const (
// JSConsumerHBRequiresPushErr consumer idle heartbeat requires a push based consumer
JSConsumerHBRequiresPushErr ErrorIdentifier = 10088
// JSConsumerInactiveThresholdExcess consumer inactive threshold exceeds system limit of {limit}
JSConsumerInactiveThresholdExcess ErrorIdentifier = 10153
// JSConsumerInvalidDeliverSubject invalid push consumer deliver subject
JSConsumerInvalidDeliverSubject ErrorIdentifier = 10112
@@ -131,6 +146,12 @@ const (
// JSConsumerMaxWaitingNegativeErr consumer max waiting needs to be positive
JSConsumerMaxWaitingNegativeErr ErrorIdentifier = 10087
// JSConsumerMetadataLengthErrF consumer metadata exceeds maximum size of {limit}
JSConsumerMetadataLengthErrF ErrorIdentifier = 10135
// JSConsumerMultipleFiltersNotAllowed consumer with multiple subject filters cannot use subject based API
JSConsumerMultipleFiltersNotAllowed ErrorIdentifier = 10137
// JSConsumerNameContainsPathSeparatorsErr Consumer name can not contain path separators
JSConsumerNameContainsPathSeparatorsErr ErrorIdentifier = 10127
@@ -149,6 +170,9 @@ const (
// JSConsumerOnMappedErr consumer direct on a mapped consumer
JSConsumerOnMappedErr ErrorIdentifier = 10092
// JSConsumerOverlappingSubjectFilters consumer subject filters cannot overlap
JSConsumerOverlappingSubjectFilters ErrorIdentifier = 10138
// JSConsumerPullNotDurableErr consumer in pull mode requires a durable name
JSConsumerPullNotDurableErr ErrorIdentifier = 10085
@@ -209,9 +233,24 @@ const (
// JSMirrorConsumerSetupFailedErrF generic mirror consumer setup failure string ({err})
JSMirrorConsumerSetupFailedErrF ErrorIdentifier = 10029
// JSMirrorInvalidStreamName mirrored stream name is invalid
JSMirrorInvalidStreamName ErrorIdentifier = 10142
// JSMirrorInvalidSubjectFilter mirror subject filter is invalid
JSMirrorInvalidSubjectFilter ErrorIdentifier = 10151
// JSMirrorMaxMessageSizeTooBigErr stream mirror must have max message size >= source
JSMirrorMaxMessageSizeTooBigErr ErrorIdentifier = 10030
// JSMirrorMultipleFiltersNotAllowed mirror with multiple subject transforms cannot also have a single subject filter
JSMirrorMultipleFiltersNotAllowed ErrorIdentifier = 10150
// JSMirrorOverlappingSubjectFilters mirror subject filters can not overlap
JSMirrorOverlappingSubjectFilters ErrorIdentifier = 10152
// JSMirrorWithFirstSeqErr stream mirrors can not have first sequence configured
JSMirrorWithFirstSeqErr ErrorIdentifier = 10143
// JSMirrorWithSourcesErr stream mirrors can not also contain other sources
JSMirrorWithSourcesErr ErrorIdentifier = 10031
@@ -263,9 +302,27 @@ const (
// JSSourceConsumerSetupFailedErrF General source consumer setup failure string ({err})
JSSourceConsumerSetupFailedErrF ErrorIdentifier = 10045
// JSSourceDuplicateDetected source stream, filter and transform (plus external if present) must form a unique combination (duplicate source configuration detected)
JSSourceDuplicateDetected ErrorIdentifier = 10140
// JSSourceInvalidStreamName sourced stream name is invalid
JSSourceInvalidStreamName ErrorIdentifier = 10141
// JSSourceInvalidSubjectFilter source subject filter is invalid
JSSourceInvalidSubjectFilter ErrorIdentifier = 10145
// JSSourceInvalidTransformDestination source transform destination is invalid
JSSourceInvalidTransformDestination ErrorIdentifier = 10146
// JSSourceMaxMessageSizeTooBigErr stream source must have max message size >= target
JSSourceMaxMessageSizeTooBigErr ErrorIdentifier = 10046
// JSSourceMultipleFiltersNotAllowed source with multiple subject transforms cannot also have a single subject filter
JSSourceMultipleFiltersNotAllowed ErrorIdentifier = 10144
// JSSourceOverlappingSubjectFilters source filters can not overlap
JSSourceOverlappingSubjectFilters ErrorIdentifier = 10147
// JSStorageResourcesExceededErr insufficient storage resources available
JSStorageResourcesExceededErr ErrorIdentifier = 10047
@@ -420,6 +477,7 @@ var (
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"},
@@ -430,15 +488,19 @@ var (
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"},
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"},
JSConsumerInvalidPolicyErrF: {Code: 400, ErrCode: 10094, Description: "{err}"},
JSConsumerInvalidSamplingErrF: {Code: 400, ErrCode: 10095, Description: "failed to parse consumer sampling configuration: {err}"},
@@ -449,12 +511,15 @@ var (
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"},
JSConsumerOnMappedErr: {Code: 400, ErrCode: 10092, Description: "consumer direct on a mapped consumer"},
JSConsumerOverlappingSubjectFilters: {Code: 400, ErrCode: 10138, Description: "consumer subject filters cannot overlap"},
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 ack policy"},
JSConsumerPullWithRateLimitErr: {Code: 400, ErrCode: 10086, Description: "consumer in pull mode can not have rate limit set"},
@@ -475,7 +540,12 @@ var (
JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"},
JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"},
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 subject filter is invalid"},
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"},
@@ -493,7 +563,13 @@ var (
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 subject filter is invalid"},
JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform destination is invalid"},
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}"},
@@ -701,6 +777,16 @@ func NewJSClusterUnSupportFeatureError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSClusterUnSupportFeatureErr]
}
// NewJSConsumerAlreadyExistsError creates a new JSConsumerAlreadyExists error: "consumer already exists"
func NewJSConsumerAlreadyExistsError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerAlreadyExists]
}
// NewJSConsumerBadDurableNameError creates a new JSConsumerBadDurableNameErr error: "durable name can not contain '.', '*', '>'"
func NewJSConsumerBadDurableNameError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -813,6 +899,26 @@ func NewJSConsumerDirectRequiresPushError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerDirectRequiresPushErr]
}
// NewJSConsumerDoesNotExistError creates a new JSConsumerDoesNotExist error: "consumer does not exist"
func NewJSConsumerDoesNotExistError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerDoesNotExist]
}
// NewJSConsumerDuplicateFilterSubjectsError creates a new JSConsumerDuplicateFilterSubjects error: "consumer cannot have both FilterSubject and FilterSubjects specified"
func NewJSConsumerDuplicateFilterSubjectsError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerDuplicateFilterSubjects]
}
// NewJSConsumerDurableNameNotInSubjectError creates a new JSConsumerDurableNameNotInSubjectErr error: "consumer expected to be durable but no durable name set in subject"
func NewJSConsumerDurableNameNotInSubjectError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -843,6 +949,16 @@ func NewJSConsumerDurableNameNotSetError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerDurableNameNotSetErr]
}
// NewJSConsumerEmptyFilterError creates a new JSConsumerEmptyFilter error: "consumer filter in FilterSubjects cannot be empty"
func NewJSConsumerEmptyFilterError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerEmptyFilter]
}
// NewJSConsumerEphemeralWithDurableInSubjectError creates a new JSConsumerEphemeralWithDurableInSubjectErr error: "consumer expected to be ephemeral but detected a durable name set in subject"
func NewJSConsumerEphemeralWithDurableInSubjectError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -903,6 +1019,22 @@ func NewJSConsumerHBRequiresPushError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerHBRequiresPushErr]
}
// NewJSConsumerInactiveThresholdExcessError creates a new JSConsumerInactiveThresholdExcess error: "consumer inactive threshold exceeds system limit of {limit}"
func NewJSConsumerInactiveThresholdExcessError(limit interface{}, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
e := ApiErrors[JSConsumerInactiveThresholdExcess]
args := e.toReplacerArgs([]interface{}{"{limit}", limit})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSConsumerInvalidDeliverSubjectError creates a new JSConsumerInvalidDeliverSubject error: "invalid push consumer deliver subject"
func NewJSConsumerInvalidDeliverSubjectError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1027,6 +1159,32 @@ func NewJSConsumerMaxWaitingNegativeError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerMaxWaitingNegativeErr]
}
// NewJSConsumerMetadataLengthError creates a new JSConsumerMetadataLengthErrF error: "consumer metadata exceeds maximum size of {limit}"
func NewJSConsumerMetadataLengthError(limit interface{}, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
e := ApiErrors[JSConsumerMetadataLengthErrF]
args := e.toReplacerArgs([]interface{}{"{limit}", limit})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSConsumerMultipleFiltersNotAllowedError creates a new JSConsumerMultipleFiltersNotAllowed error: "consumer with multiple subject filters cannot use subject based API"
func NewJSConsumerMultipleFiltersNotAllowedError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerMultipleFiltersNotAllowed]
}
// NewJSConsumerNameContainsPathSeparatorsError creates a new JSConsumerNameContainsPathSeparatorsErr error: "Consumer name can not contain path separators"
func NewJSConsumerNameContainsPathSeparatorsError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1093,6 +1251,16 @@ func NewJSConsumerOnMappedError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerOnMappedErr]
}
// NewJSConsumerOverlappingSubjectFiltersError creates a new JSConsumerOverlappingSubjectFilters error: "consumer subject filters cannot overlap"
func NewJSConsumerOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerOverlappingSubjectFilters]
}
// NewJSConsumerPullNotDurableError creates a new JSConsumerPullNotDurableErr error: "consumer in pull mode requires a durable name"
func NewJSConsumerPullNotDurableError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1305,6 +1473,26 @@ func NewJSMirrorConsumerSetupFailedError(err error, opts ...ErrorOption) *ApiErr
}
}
// NewJSMirrorInvalidStreamNameError creates a new JSMirrorInvalidStreamName error: "mirrored stream name is invalid"
func NewJSMirrorInvalidStreamNameError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSMirrorInvalidStreamName]
}
// NewJSMirrorInvalidSubjectFilterError creates a new JSMirrorInvalidSubjectFilter error: "mirror subject filter is invalid"
func NewJSMirrorInvalidSubjectFilterError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSMirrorInvalidSubjectFilter]
}
// NewJSMirrorMaxMessageSizeTooBigError creates a new JSMirrorMaxMessageSizeTooBigErr error: "stream mirror must have max message size >= source"
func NewJSMirrorMaxMessageSizeTooBigError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1315,6 +1503,36 @@ func NewJSMirrorMaxMessageSizeTooBigError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSMirrorMaxMessageSizeTooBigErr]
}
// NewJSMirrorMultipleFiltersNotAllowedError creates a new JSMirrorMultipleFiltersNotAllowed error: "mirror with multiple subject transforms cannot also have a single subject filter"
func NewJSMirrorMultipleFiltersNotAllowedError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSMirrorMultipleFiltersNotAllowed]
}
// NewJSMirrorOverlappingSubjectFiltersError creates a new JSMirrorOverlappingSubjectFilters error: "mirror subject filters can not overlap"
func NewJSMirrorOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSMirrorOverlappingSubjectFilters]
}
// NewJSMirrorWithFirstSeqError creates a new JSMirrorWithFirstSeqErr error: "stream mirrors can not have first sequence configured"
func NewJSMirrorWithFirstSeqError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSMirrorWithFirstSeqErr]
}
// NewJSMirrorWithSourcesError creates a new JSMirrorWithSourcesErr error: "stream mirrors can not also contain other sources"
func NewJSMirrorWithSourcesError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1509,6 +1727,46 @@ func NewJSSourceConsumerSetupFailedError(err error, opts ...ErrorOption) *ApiErr
}
}
// NewJSSourceDuplicateDetectedError creates a new JSSourceDuplicateDetected error: "duplicate source configuration detected"
func NewJSSourceDuplicateDetectedError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSSourceDuplicateDetected]
}
// NewJSSourceInvalidStreamNameError creates a new JSSourceInvalidStreamName error: "sourced stream name is invalid"
func NewJSSourceInvalidStreamNameError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSSourceInvalidStreamName]
}
// NewJSSourceInvalidSubjectFilterError creates a new JSSourceInvalidSubjectFilter error: "source subject filter is invalid"
func NewJSSourceInvalidSubjectFilterError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSSourceInvalidSubjectFilter]
}
// NewJSSourceInvalidTransformDestinationError creates a new JSSourceInvalidTransformDestination error: "source transform destination is invalid"
func NewJSSourceInvalidTransformDestinationError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSSourceInvalidTransformDestination]
}
// NewJSSourceMaxMessageSizeTooBigError creates a new JSSourceMaxMessageSizeTooBigErr error: "stream source must have max message size >= target"
func NewJSSourceMaxMessageSizeTooBigError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1519,6 +1777,26 @@ func NewJSSourceMaxMessageSizeTooBigError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSSourceMaxMessageSizeTooBigErr]
}
// NewJSSourceMultipleFiltersNotAllowedError creates a new JSSourceMultipleFiltersNotAllowed error: "source with multiple subject transforms cannot also have a single subject filter"
func NewJSSourceMultipleFiltersNotAllowedError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSSourceMultipleFiltersNotAllowed]
}
// NewJSSourceOverlappingSubjectFiltersError creates a new JSSourceOverlappingSubjectFilters error: "source filters can not overlap"
func NewJSSourceOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSSourceOverlappingSubjectFilters]
}
// NewJSStorageResourcesExceededError creates a new JSStorageResourcesExceededErr error: "insufficient storage resources available"
func NewJSStorageResourcesExceededError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
+7
View File
@@ -14,6 +14,7 @@
package server
import (
"errors"
"fmt"
"net"
"os"
@@ -152,6 +153,12 @@ func validateTrustedOperators(o *Options) error {
o.resolverPinnedAccounts[o.SystemAccount] = struct{}{}
}
}
// If we have an auth callout defined make sure we are not in operator mode.
if o.AuthCallout != nil {
return errors.New("operators do not allow authorization callouts to be configured directly")
}
return nil
}
+325 -125
View File
@@ -34,6 +34,7 @@ import (
"sync/atomic"
"time"
"github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
"github.com/nats-io/nuid"
@@ -77,6 +78,8 @@ type leaf struct {
remoteServer string
// domain name of remote server
remoteDomain string
// account name of remote server
remoteAccName string
// 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.
@@ -90,6 +93,8 @@ type leaf struct {
// we would add it a second time in the smap causing later unsub to suppress the LS-.
tsub map[*subscription]struct{}
tsubt *time.Timer
// Selected compression mode, which may be different from the server configured mode.
compression string
}
// Used for remote (solicited) leafnodes.
@@ -188,6 +193,13 @@ func validateLeafNode(o *Options) error {
return err
}
// Users can bind to any local account, if its empty we will assume the $G account.
for _, r := range o.LeafNode.Remotes {
if r.LocalAccount == _EMPTY_ {
r.LocalAccount = globalAccountName
}
}
// In local config mode, check that leafnode configuration refers to accounts that exist.
if len(o.TrustedOperators) == 0 {
accNames := map[string]struct{}{}
@@ -241,6 +253,13 @@ func validateLeafNode(o *Options) error {
}
}
// Validate compression settings
if o.LeafNode.Compression.Mode != _EMPTY_ {
if err := validateAndNormalizeCompressionOption(&o.LeafNode.Compression, CompressionS2Auto); err != nil {
return err
}
}
// If a remote has a websocket scheme, all need to have it.
for _, rcfg := range o.LeafNode.Remotes {
if len(rcfg.URLs) >= 2 {
@@ -256,6 +275,12 @@ func validateLeafNode(o *Options) error {
return fmt.Errorf("remote leaf node configuration cannot have a mix of websocket and non-websocket urls: %q", redactURLList(rcfg.URLs))
}
}
// Validate compression settings
if rcfg.Compression.Mode != _EMPTY_ {
if err := validateAndNormalizeCompressionOption(&rcfg.Compression, CompressionS2Auto); err != nil {
return err
}
}
}
if o.LeafNode.Port == 0 {
@@ -325,8 +350,8 @@ func (s *Server) updateRemoteLeafNodesTLSConfig(opts *Options) {
return
}
s.mu.Lock()
defer s.mu.Unlock()
s.mu.RLock()
defer s.mu.RUnlock()
// Changes in the list of remote leaf nodes is not supported.
// However, make sure that we don't go over the arrays.
@@ -339,6 +364,7 @@ func (s *Server) updateRemoteLeafNodesTLSConfig(opts *Options) {
if ro.TLSConfig != nil {
cfg.Lock()
cfg.TLSConfig = ro.TLSConfig.Clone()
cfg.TLSHandshakeFirst = ro.TLSHandshakeFirst
cfg.Unlock()
}
}
@@ -507,6 +533,8 @@ func (s *Server) connectToRemoteLeafNode(remote *leafNodeCfg, firstConnect bool)
}
}
if err != nil {
jitter := time.Duration(rand.Int63n(int64(reconnectDelay)))
delay := reconnectDelay + jitter
attempts++
if s.shouldReportConnectErr(firstConnect, attempts) {
s.Errorf(connErrFmt, rURL.Host, attempts, err)
@@ -516,7 +544,7 @@ func (s *Server) connectToRemoteLeafNode(remote *leafNodeCfg, firstConnect bool)
select {
case <-s.quitCh:
return
case <-time.After(reconnectDelay):
case <-time.After(delay):
// Check if we should migrate any JetStream assets while this remote is down.
s.checkJetStreamMigrate(remote)
continue
@@ -672,6 +700,8 @@ func (s *Server) startLeafNodeAcceptLoop() {
tlsRequired := opts.LeafNode.TLSConfig != nil
tlsVerify := tlsRequired && opts.LeafNode.TLSConfig.ClientAuth == tls.RequireAndVerifyClientCert
// Do not set compression in this Info object, it would possibly cause
// issues when sending asynchronous INFO to the remote.
info := Info{
ID: s.info.ID,
Name: s.info.Name,
@@ -735,19 +765,20 @@ var credsRe = regexp.MustCompile(`\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-
// clusterName is provided as argument to avoid lock ordering issues with the locked client c
// Lock should be held entering here.
func (c *client) sendLeafConnect(clusterName string, tlsRequired, headers bool) error {
func (c *client) sendLeafConnect(clusterName string, headers bool) error {
// We support basic user/pass and operator based user JWT with signatures.
cinfo := leafConnectInfo{
Version: VERSION,
TLS: tlsRequired,
ID: c.srv.info.ID,
Domain: c.srv.info.Domain,
Name: c.srv.info.Name,
Hub: c.leaf.remote.Hub,
Cluster: clusterName,
Headers: headers,
JetStream: c.acc.jetStreamConfigured(),
DenyPub: c.leaf.remote.DenyImports,
Version: VERSION,
ID: c.srv.info.ID,
Domain: c.srv.info.Domain,
Name: c.srv.info.Name,
Hub: c.leaf.remote.Hub,
Cluster: clusterName,
Headers: headers,
JetStream: c.acc.jetStreamConfigured(),
DenyPub: c.leaf.remote.DenyImports,
Compression: c.leaf.compression,
RemoteAccount: c.acc.GetName(),
}
// If a signature callback is specified, this takes precedence over anything else.
@@ -862,9 +893,7 @@ func (s *Server) generateLeafNodeInfoJSON() {
s.leafNodeInfo.Cluster = s.cachedClusterName()
s.leafNodeInfo.LeafNodeURLs = s.leafURLsMap.getAsStringSlice()
s.leafNodeInfo.WSConnectURLs = s.websocket.connectURLsMap.getAsStringSlice()
b, _ := json.Marshal(s.leafNodeInfo)
pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
s.leafNodeInfoJSON = bytes.Join(pcs, []byte(" "))
s.leafNodeInfoJSON = generateInfoJSON(&s.leafNodeInfo)
}
// Sends an async INFO protocol so that the connected servers can update
@@ -913,15 +942,7 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
if remote != nil {
// For now, if lookup fails, we will constantly try
// to recreate this LN connection.
remote.Lock()
// Users can bind to any local account, if its empty
// we will assume the $G account.
if remote.LocalAccount == _EMPTY_ {
remote.LocalAccount = globalAccountName
}
lacc := remote.LocalAccount
remote.Unlock()
var err error
acc, err = s.LookupAccount(lacc)
if err != nil {
@@ -940,6 +961,7 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
c.initClient()
c.Noticef("Leafnode connection created%s %s", remoteSuffix, c.opts.Name)
var tlsFirst bool
if remote != nil {
solicited = true
remote.Lock()
@@ -948,6 +970,7 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
if !c.leaf.remote.Hub {
c.leaf.isSpoke = true
}
tlsFirst = remote.TLSHandshakeFirst
remote.Unlock()
c.acc = acc
} else {
@@ -966,6 +989,11 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
// Grab server variables
s.mu.Lock()
info = s.copyLeafNodeInfo()
// For tests that want to simulate old servers, do not set the compression
// on the INFO protocol if configured with CompressionNotSupported.
if cm := opts.LeafNode.Compression.Mode; cm != CompressionNotSupported {
info.Compression = cm
}
s.generateNonce(nonce[:])
s.mu.Unlock()
}
@@ -992,6 +1020,13 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
return nil
}
} else {
// If configured to do TLS handshake first
if tlsFirst {
if _, err := c.leafClientHandshakeIfNeeded(remote, opts); err != nil {
c.mu.Unlock()
return nil
}
}
// We need to wait for the info, but not for too long.
c.nc.SetReadDeadline(time.Now().Add(DEFAULT_LEAFNODE_INFO_WAIT))
}
@@ -1005,34 +1040,58 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
copy(c.nonce, nonce[:])
info.Nonce = string(c.nonce)
info.CID = c.cid
b, _ := json.Marshal(info)
proto := generateInfoJSON(info)
if !opts.LeafNode.TLSHandshakeFirst {
// We have to send from this go routine because we may
// have to block for TLS handshake before we start our
// writeLoop go routine. The other side needs to receive
// this before it can initiate the TLS handshake..
c.sendProtoNow(proto)
pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
// We have to send from this go routine because we may
// have to block for TLS handshake before we start our
// writeLoop go routine. The other side needs to receive
// this before it can initiate the TLS handshake..
c.sendProtoNow(bytes.Join(pcs, []byte(" ")))
// The above call could have marked the connection as closed (due to TCP error).
if c.isClosed() {
c.mu.Unlock()
c.closeConnection(WriteError)
return nil
// The above call could have marked the connection as closed (due to TCP error).
if c.isClosed() {
c.mu.Unlock()
c.closeConnection(WriteError)
return nil
}
}
// Check to see if we need to spin up TLS.
if !c.isWebsocket() && info.TLSRequired {
// Perform server-side TLS handshake.
if err := c.doTLSServerHandshake("leafnode", opts.LeafNode.TLSConfig, opts.LeafNode.TLSTimeout, opts.LeafNode.TLSPinnedCerts); err != nil {
if err := c.doTLSServerHandshake(tlsHandshakeLeaf, opts.LeafNode.TLSConfig, opts.LeafNode.TLSTimeout, opts.LeafNode.TLSPinnedCerts); err != nil {
c.mu.Unlock()
return nil
}
}
// If the user wants the TLS handshake to occur first, now that it is
// done, send the INFO protocol.
if opts.LeafNode.TLSHandshakeFirst {
c.sendProtoNow(proto)
if c.isClosed() {
c.mu.Unlock()
c.closeConnection(WriteError)
return nil
}
}
// Leaf nodes will always require a CONNECT to let us know
// when we are properly bound to an account.
c.setAuthTimer(secondsToDuration(opts.LeafNode.AuthTimeout))
//
// If compression is configured, we can't set the authTimer here because
// it would cause the parser to fail any incoming protocol that is not a
// CONNECT (and we need to exchange INFO protocols for compression
// negotiation). So instead, use the ping timer until we are done with
// negotiation and can set the auth timer.
timeout := secondsToDuration(opts.LeafNode.AuthTimeout)
if needsCompression(opts.LeafNode.Compression.Mode) {
c.ping.tmr = time.AfterFunc(timeout, func() {
c.authTimeout()
})
} else {
c.setAuthTimer(timeout)
}
}
// Keep track in case server is shutdown before we can successfully register.
@@ -1046,7 +1105,7 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
// Spin up the read loop.
s.startGoRoutine(func() { c.readLoop(preBuf) })
// We will sping the write loop for solicited connections only
// We will spin the write loop for solicited connections only
// when processing the INFO and after switching to TLS if needed.
if !solicited {
s.startGoRoutine(func() { c.writeLoop() })
@@ -1057,22 +1116,125 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
return c
}
func (c *client) processLeafnodeInfo(info *Info) {
s := c.srv
// Will perform the client-side TLS handshake if needed. Assumes that this
// is called by the solicit side (remote will be non nil). Returns `true`
// if TLS is required, `false` otherwise.
// Lock held on entry.
func (c *client) leafClientHandshakeIfNeeded(remote *leafNodeCfg, opts *Options) (bool, error) {
// Check if TLS is required and gather TLS config variables.
tlsRequired, tlsConfig, tlsName, tlsTimeout := c.leafNodeGetTLSConfigForSolicit(remote)
if !tlsRequired {
return false, nil
}
// If TLS required, peform handshake.
// Get the URL that was used to connect to the remote server.
rURL := remote.getCurrentURL()
// Perform the client-side TLS handshake.
if resetTLSName, err := c.doTLSClientHandshake(tlsHandshakeLeaf, rURL, tlsConfig, tlsName, tlsTimeout, opts.LeafNode.TLSPinnedCerts); err != nil {
// Check if we need to reset the remote's TLS name.
if resetTLSName {
remote.Lock()
remote.tlsName = _EMPTY_
remote.Unlock()
}
return false, err
}
return true, nil
}
func (c *client) processLeafnodeInfo(info *Info) {
c.mu.Lock()
if c.leaf == nil || c.isClosed() {
c.mu.Unlock()
return
}
s := c.srv
opts := s.getOpts()
remote := c.leaf.remote
didSolicit := remote != nil
firstINFO := !c.flags.isSet(infoReceived)
var firstINFO bool
// In case of websocket, the TLS handshake has been already done.
// So check only for non websocket connections and for configurations
// where the TLS Handshake was not done first.
if didSolicit && !c.flags.isSet(handshakeComplete) && !c.isWebsocket() && !remote.TLSHandshakeFirst {
// If the server requires TLS, we need to set this in the remote
// otherwise if there is no TLS configuration block for the remote,
// the solicit side will not attempt to perform the TLS handshake.
if firstINFO && info.TLSRequired {
remote.TLS = true
}
if _, err := c.leafClientHandshakeIfNeeded(remote, opts); err != nil {
c.mu.Unlock()
return
}
}
// Check for compression, unless already done.
if firstINFO && !c.flags.isSet(compressionNegotiated) {
// Prevent from getting back here.
c.flags.set(compressionNegotiated)
var co *CompressionOpts
if !didSolicit {
co = &opts.LeafNode.Compression
} else {
co = &remote.Compression
}
if needsCompression(co.Mode) {
// Release client lock since following function will need server lock.
c.mu.Unlock()
compress, err := s.negotiateLeafCompression(c, didSolicit, info.Compression, co)
if err != nil {
c.sendErrAndErr(err.Error())
c.closeConnection(ProtocolViolation)
return
}
if compress {
// Done for now, will get back another INFO protocol...
return
}
// No compression because one side does not want/can't, so proceed.
c.mu.Lock()
// Check that the connection did not close if the lock was released.
if c.isClosed() {
c.mu.Unlock()
return
}
} else {
// Coming from an old server, the Compression field would be the empty
// string. For servers that are configured with CompressionNotSupported,
// this makes them behave as old servers.
if info.Compression == _EMPTY_ || co.Mode == CompressionNotSupported {
c.leaf.compression = CompressionNotSupported
} else {
c.leaf.compression = CompressionOff
}
}
// Accepting side does not normally process an INFO protocol during
// initial connection handshake. So we keep it consistent by returning
// if we are not soliciting.
if !didSolicit {
// If we had created the ping timer instead of the auth timer, we will
// clear the ping timer and set the auth timer now that the compression
// negotiation is done.
if info.Compression != _EMPTY_ && c.ping.tmr != nil {
clearTimer(&c.ping.tmr)
c.setAuthTimer(secondsToDuration(opts.LeafNode.AuthTimeout))
}
c.mu.Unlock()
return
}
// Fall through and process the INFO protocol as usual.
}
// Mark that the INFO protocol has been received.
// Note: For now, only the initial INFO has a nonce. We
// will probably do auto key rotation at some point.
if c.flags.setIfNotSet(infoReceived) {
firstINFO = true
if firstINFO {
// Mark that the INFO protocol has been received.
c.flags.set(infoReceived)
// Prevent connecting to non leafnode port. Need to do this only for
// the first INFO, not for async INFO updates...
//
@@ -1092,7 +1254,7 @@ func (c *client) processLeafnodeInfo(info *Info) {
// As seen from above, a solicited LeafNode connection should receive
// from the remote server an INFO with CID and LeafNodeURLs. Anything
// else should be considered an attempt to connect to a wrong port.
if c.leaf.remote != nil && (info.CID == 0 || info.LeafNodeURLs == nil) {
if didSolicit && (info.CID == 0 || info.LeafNodeURLs == nil) {
c.mu.Unlock()
c.Errorf(ErrConnectedToWrongPort.Error())
c.closeConnection(WrongPort)
@@ -1100,8 +1262,8 @@ func (c *client) processLeafnodeInfo(info *Info) {
}
// Capture a nonce here.
c.nonce = []byte(info.Nonce)
if info.TLSRequired && c.leaf.remote != nil {
c.leaf.remote.TLS = true
if info.TLSRequired && didSolicit {
remote.TLS = true
}
supportsHeaders := c.srv.supportsHeaders()
c.headers = supportsHeaders && info.Headers
@@ -1121,7 +1283,7 @@ func (c *client) processLeafnodeInfo(info *Info) {
// For both initial INFO and async INFO protocols, Possibly
// update our list of remote leafnode URLs we can connect to.
if c.leaf.remote != nil && (len(info.LeafNodeURLs) > 0 || len(info.WSConnectURLs) > 0) {
if didSolicit && (len(info.LeafNodeURLs) > 0 || len(info.WSConnectURLs) > 0) {
// Consider the incoming array as the most up-to-date
// representation of the remote cluster's list of URLs.
c.updateLeafNodeURLs(info)
@@ -1155,10 +1317,12 @@ func (c *client) processLeafnodeInfo(info *Info) {
// If this is a remote connection and this is the first INFO protocol,
// then we need to finish the connect process by sending CONNECT, etc..
if firstINFO && c.leaf.remote != nil {
if firstINFO && didSolicit {
// Clear deadline that was set in createLeafNode while waiting for the INFO.
c.nc.SetDeadline(time.Time{})
resumeConnect = true
} else if !firstINFO && didSolicit {
c.leaf.remoteAccName = info.RemoteAccount
}
// Check if we have the remote account information and if so make sure it's stored.
@@ -1179,6 +1343,67 @@ func (c *client) processLeafnodeInfo(info *Info) {
}
}
func (s *Server) negotiateLeafCompression(c *client, didSolicit bool, infoCompression string, co *CompressionOpts) (bool, error) {
// Negotiate the appropriate compression mode (or no compression)
cm, err := selectCompressionMode(co.Mode, infoCompression)
if err != nil {
return false, err
}
c.mu.Lock()
// For "auto" mode, set the initial compression mode based on RTT
if cm == CompressionS2Auto {
if c.rttStart.IsZero() {
c.rtt = computeRTT(c.start)
}
cm = selectS2AutoModeBasedOnRTT(c.rtt, co.RTTThresholds)
}
// Keep track of the negotiated compression mode.
c.leaf.compression = cm
cid := c.cid
var nonce string
if !didSolicit {
nonce = string(c.nonce)
}
c.mu.Unlock()
if !needsCompression(cm) {
return false, nil
}
// If we end-up doing compression...
// Generate an INFO with the chosen compression mode.
s.mu.Lock()
info := s.copyLeafNodeInfo()
info.Compression, info.CID, info.Nonce = compressionModeForInfoProtocol(co, cm), cid, nonce
infoProto := generateInfoJSON(info)
s.mu.Unlock()
// If we solicited, then send this INFO protocol BEFORE switching
// to compression writer. However, if we did not, we send it after.
c.mu.Lock()
if didSolicit {
c.enqueueProto(infoProto)
// Make sure it is completely flushed (the pending bytes goes to
// 0) before proceeding.
for c.out.pb > 0 && !c.isClosed() {
c.flushOutbound()
}
}
// This is to notify the readLoop that it should switch to a
// (de)compression reader.
c.in.flags.set(switchToCompression)
// Create the compress writer before queueing the INFO protocol for
// a route that did not solicit. It will make sure that that proto
// is sent with compression on.
c.out.cw = s2.NewWriter(nil, s2WriterOptions(cm)...)
if !didSolicit {
c.enqueueProto(infoProto)
}
c.mu.Unlock()
return true, nil
}
// When getting a leaf node INFO protocol, use the provided
// array of urls to update the list of possible endpoints.
func (c *client) updateLeafNodeURLs(info *Info) {
@@ -1291,6 +1516,7 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
}
myRemoteDomain := c.leaf.remoteDomain
mySrvName := c.leaf.remoteServer
remoteAccName := c.leaf.remoteAccName
myClustName := c.leaf.remoteCluster
solicited := c.leaf.remote != nil
c.mu.Unlock()
@@ -1306,7 +1532,8 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
// We have code for the loop detection elsewhere, which also delays
// attempt to reconnect.
if !ol.isSolicitedLeafNode() && ol.leaf.remoteServer == srvName &&
ol.leaf.remoteCluster == clusterName && ol.acc.Name == accName {
ol.leaf.remoteCluster == clusterName && ol.acc.Name == accName &&
remoteAccName != _EMPTY_ && ol.leaf.remoteAccName == remoteAccName {
old = ol
}
ol.mu.Unlock()
@@ -1353,16 +1580,16 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
if acc == sysAcc {
for _, d := range opts.JsAccDefaultDomain {
if d == _EMPTY_ {
// Extending Js via leaf node is mutually exclusive with a domain mapping to the empty/default domain.
// Extending JetStream via leaf node is mutually exclusive with a domain mapping to the empty/default domain.
// As soon as one mapping to "" is found, disable the ability to extend JS via a leaf node.
c.Noticef("Forcing System Account into non extend mode due to presence of empty default domain")
c.Noticef("Not extending remote JetStream domain %q due to presence of empty default domain", myRemoteDomain)
forceSysAccDeny = true
break
}
}
} else if domain, ok := opts.JsAccDefaultDomain[accName]; ok && domain == _EMPTY_ {
// for backwards compatibility with old setups that do not have a domain name set
c.Noticef("Skipping deny %q for account %q due to default domain", jsAllAPI, accName)
c.Debugf("Skipping deny %q for account %q due to default domain", jsAllAPI, accName)
return
}
}
@@ -1378,14 +1605,14 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
// If domain names mismatch always deny. This applies to system accounts as well as non system accounts.
// Not having a system account, account or JetStream disabled is considered a mismatch as well.
if acc != nil && acc == sysAcc {
c.Noticef("System Account Connected from %s", srvDecorated())
c.Noticef("JetStream Not Extended, adding denies %+v", denyAllJs)
c.Noticef("System account connected from %s", srvDecorated())
c.Noticef("JetStream not extended, domains differ")
c.mergeDenyPermissionsLocked(both, denyAllJs)
// When a remote with a system account is present in a server, unless otherwise disabled, the server will be
// started in observer mode. Now that it is clear that this not used, turn the observer mode off.
if solicited && meta != nil && meta.IsObserver() {
meta.setObserver(false, extNotExtended)
c.Noticef("Turning JetStream metadata controller Observer Mode off")
c.Debugf("Turning JetStream metadata controller Observer Mode off")
// Take note that the domain was not extended to avoid this state from startup.
writePeerState(js.config.StoreDir, meta.currentPeerState())
// Meta controller can't be leader yet.
@@ -1394,7 +1621,7 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
meta.Campaign()
}
} else {
c.Noticef("JetStream Not Extended, adding deny %+v for account %q", denyAllClientJs, accName)
c.Noticef("JetStream using domains: local %q, remote %q", opts.JetStreamDomain, myRemoteDomain)
c.mergeDenyPermissionsLocked(both, denyAllClientJs)
}
blockMappingOutgoing = true
@@ -1406,7 +1633,7 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
// Therefore, server with a remote that are not already in observer mode, need to be put into it.
if solicited && meta != nil && !meta.IsObserver() {
meta.setObserver(true, extExtended)
c.Noticef("Turning JetStream metadata controller Observer Mode on - System Account Connected")
c.Debugf("Turning JetStream metadata controller Observer Mode on - System Account Connected")
// Take note that the domain was not extended to avoid this state next startup.
writePeerState(js.config.StoreDir, meta.currentPeerState())
// If this server is the leader already, step down so a new leader can be elected (that is not an observer)
@@ -1417,7 +1644,7 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
// If the system account is shared, jsAllAPI traffic will go through the system account.
// So in order to prevent duplicate delivery (from system and actual account) suppress it on the account.
// If the system account is NOT shared, jsAllAPI traffic has no business
c.Noticef("Adding deny %+v for account %q", denyAllClientJs, accName)
c.Debugf("Adding deny %+v for account %q", denyAllClientJs, accName)
c.mergeDenyPermissionsLocked(both, denyAllClientJs)
}
// If we have a specified JetStream domain we will want to add a mapping to
@@ -1427,7 +1654,7 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
if err := acc.AddMapping(src, dest); err != nil {
c.Debugf("Error adding JetStream domain mapping: %s", err.Error())
} else {
c.Noticef("Adding JetStream Domain Mapping %q -> %s to account %q", src, dest, accName)
c.Debugf("Adding JetStream Domain Mapping %q -> %s to account %q", src, dest, accName)
}
}
if blockMappingOutgoing {
@@ -1438,7 +1665,7 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
// This guards against a hub and a spoke having the same domain name.
// But not two spokes having the same one and the request coming from the hub.
c.mergeDenyPermissionsLocked(pub, []string{src})
c.Noticef("Adding deny %q for outgoing messages to account %q", src, accName)
c.Debugf("Adding deny %q for outgoing messages to account %q", src, accName)
}
}
}
@@ -1464,8 +1691,6 @@ type leafConnectInfo struct {
Sig string `json:"sig,omitempty"`
User string `json:"user,omitempty"`
Pass string `json:"pass,omitempty"`
TLS bool `json:"tls_required"`
Comp bool `json:"compression,omitempty"`
ID string `json:"server_id,omitempty"`
Domain string `json:"domain,omitempty"`
Name string `json:"name,omitempty"`
@@ -1475,8 +1700,17 @@ type leafConnectInfo struct {
JetStream bool `json:"jetstream,omitempty"`
DenyPub []string `json:"deny_pub,omitempty"`
// There was an existing field called:
// >> Comp bool `json:"compression,omitempty"`
// that has never been used. With support for compression, we now need
// a field that is a string. So we use a different json tag:
Compression string `json:"compress_mode,omitempty"`
// Just used to detect wrong connection attempts.
Gateway string `json:"gateway,omitempty"`
// Tells the accept side which account the remote is binding to.
RemoteAccount string `json:"remote_account,omitempty"`
}
// processLeafNodeConnect will process the inbound connect args.
@@ -1545,9 +1779,21 @@ func (c *client) processLeafNodeConnect(s *Server, arg []byte, lang string) erro
// support headers and the remote has sent in the CONNECT protocol that it does
// support headers too.
c.headers = supportHeaders && proto.Headers
// If the compression level is still not set, set it based on what has been
// given to us in the CONNECT protocol.
if c.leaf.compression == _EMPTY_ {
// But if proto.Compression is _EMPTY_, set it to CompressionNotSupported
if proto.Compression == _EMPTY_ {
c.leaf.compression = CompressionNotSupported
} else {
c.leaf.compression = proto.Compression
}
}
// Remember the remote server.
c.leaf.remoteServer = proto.Name
// Remember the remote account name
c.leaf.remoteAccName = proto.RemoteAccount
// If the other side has declared itself a hub, so we will take on the spoke role.
if proto.Hub {
@@ -1623,9 +1869,7 @@ func (s *Server) sendPermsAndAccountInfo(c *client) {
info.Export = c.opts.Export
info.RemoteAccount = c.acc.Name
info.ConnectInfo = true
b, _ := json.Marshal(info)
pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
c.enqueueProto(bytes.Join(pcs, []byte(" ")))
c.enqueueProto(generateInfoJSON(info))
c.mu.Unlock()
}
@@ -1896,7 +2140,7 @@ func (c *client) updateSmap(sub *subscription, delta int32) {
}
// We will update if its a queue, if count is zero (or negative), or we were 0 and are N > 0.
update := sub.queue != nil || n == 0 || n+delta <= 0
update := sub.queue != nil || (n <= 0 && n+delta > 0) || (n > 0 && n+delta <= 0)
n += delta
if n > 0 {
c.leaf.smap[key] = n
@@ -2480,16 +2724,16 @@ func (c *client) setLeafConnectDelayIfSoliciting(delay time.Duration) (string, t
// if TLS is required, and if so, will return a clone of the TLS Config
// (since some fields will be changed during handshake), the TLS server
// name that is remembered, and the TLS timeout.
func (c *client) leafNodeGetTLSConfigForSolicit(remote *leafNodeCfg, needsLock bool) (bool, *tls.Config, string, float64) {
func (c *client) leafNodeGetTLSConfigForSolicit(remote *leafNodeCfg) (bool, *tls.Config, string, float64) {
var (
tlsConfig *tls.Config
tlsName string
tlsTimeout float64
)
if needsLock {
remote.RLock()
}
remote.RLock()
defer remote.RUnlock()
tlsRequired := remote.TLS || remote.TLSConfig != nil
if tlsRequired {
if remote.TLSConfig != nil {
@@ -2503,9 +2747,6 @@ func (c *client) leafNodeGetTLSConfigForSolicit(remote *leafNodeCfg, needsLock b
tlsTimeout = float64(TLS_TIMEOUT / time.Second)
}
}
if needsLock {
remote.RUnlock()
}
return tlsRequired, tlsConfig, tlsName, tlsTimeout
}
@@ -2526,21 +2767,12 @@ func (c *client) leafNodeSolicitWSConnection(opts *Options, rURL *url.URL, remot
compress := remote.Websocket.Compression
// By default the server will mask outbound frames, but it can be disabled with this option.
noMasking := remote.Websocket.NoMasking
tlsRequired, tlsConfig, tlsName, tlsTimeout := c.leafNodeGetTLSConfigForSolicit(remote, false)
remote.RUnlock()
// Do TLS here as needed.
if tlsRequired {
// Perform the client-side TLS handshake.
if resetTLSName, err := c.doTLSClientHandshake("leafnode", rURL, tlsConfig, tlsName, tlsTimeout, opts.LeafNode.TLSPinnedCerts); err != nil {
// Check if we need to reset the remote's TLS name.
if resetTLSName {
remote.Lock()
remote.tlsName = _EMPTY_
remote.Unlock()
}
// 0 will indicate that the connection was already closed
return nil, 0, err
}
// Will do the client-side TLS handshake if needed.
tlsRequired, err := c.leafClientHandshakeIfNeeded(remote, opts)
if err != nil {
// 0 will indicate that the connection was already closed
return nil, 0, err
}
// For http request, we need the passed URL to contain either http or https scheme.
@@ -2642,7 +2874,7 @@ func (c *client) leafNodeSolicitWSConnection(opts *Options, rURL *url.URL, remot
const connectProcessTimeout = 2 * time.Second
// This is invoked for remote LEAF remote connections after processing the INFO
// protocol. This will do the TLS handshake (if needed be)
// protocol.
func (s *Server) leafNodeResumeConnectProcess(c *client) {
clusterName := s.ClusterName()
@@ -2651,39 +2883,7 @@ func (s *Server) leafNodeResumeConnectProcess(c *client) {
c.mu.Unlock()
return
}
remote := c.leaf.remote
var tlsRequired bool
// In case of websocket, the TLS handshake has been already done.
// So check only for non websocket connections.
if !c.isWebsocket() {
var tlsConfig *tls.Config
var tlsName string
var tlsTimeout float64
// Check if TLS is required and gather TLS config variables.
tlsRequired, tlsConfig, tlsName, tlsTimeout = c.leafNodeGetTLSConfigForSolicit(remote, true)
// If TLS required, peform handshake.
if tlsRequired {
// Get the URL that was used to connect to the remote server.
rURL := remote.getCurrentURL()
// Perform the client-side TLS handshake.
if resetTLSName, err := c.doTLSClientHandshake("leafnode", rURL, tlsConfig, tlsName, tlsTimeout, c.srv.getOpts().LeafNode.TLSPinnedCerts); err != nil {
// Check if we need to reset the remote's TLS name.
if resetTLSName {
remote.Lock()
remote.tlsName = _EMPTY_
remote.Unlock()
}
c.mu.Unlock()
return
}
}
}
if err := c.sendLeafConnect(clusterName, tlsRequired, c.headers); err != nil {
if err := c.sendLeafConnect(clusterName, c.headers); err != nil {
c.mu.Unlock()
c.closeConnection(WriteError)
return
+18
View File
@@ -72,6 +72,16 @@ func (s *Server) ConfigureLogger() {
l.SetSizeLimit(opts.LogSizeLimit)
}
}
if opts.LogMaxFiles > 0 {
if l, ok := log.(*srvlog.Logger); ok {
al := int(opts.LogMaxFiles)
if int64(al) != opts.LogMaxFiles {
// set to default (no max) on overflow
al = 0
}
l.SetMaxNumFiles(al)
}
}
} else if opts.RemoteSyslog != "" {
log = srvlog.NewRemoteSysLogger(opts.RemoteSyslog, opts.Debug, opts.Trace)
} else if syslog {
@@ -217,6 +227,14 @@ func (s *Server) RateLimitWarnf(format string, v ...interface{}) {
s.Warnf("%s", statement)
}
func (s *Server) RateLimitDebugf(format string, v ...interface{}) {
statement := fmt.Sprintf(format, v...)
if _, loaded := s.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
return
}
s.Debugf("%s", statement)
}
// Fatalf logs a fatal error
func (s *Server) Fatalf(format string, v ...interface{}) {
s.executeLogCall(func(logger Logger, format string, v ...interface{}) {
+109 -12
View File
@@ -14,11 +14,14 @@
package server
import (
crand "crypto/rand"
"encoding/binary"
"fmt"
"math/rand"
"sort"
"sync"
"time"
"github.com/nats-io/nats-server/v2/server/avl"
)
// TODO(dlc) - This is a fairly simplistic approach but should do for now.
@@ -28,6 +31,7 @@ type memStore struct {
state StreamState
msgs map[uint64]*StoreMsg
fss map[string]*SimpleState
dmap avl.SequenceSet
maxp int64
scb StorageUpdateHandler
ageChk *time.Timer
@@ -48,6 +52,11 @@ func newMemStore(cfg *StreamConfig) (*memStore, error) {
maxp: cfg.MaxMsgsPer,
cfg: *cfg,
}
if cfg.FirstSeq > 0 {
if _, err := ms.purge(cfg.FirstSeq); err != nil {
return nil, err
}
}
return ms, nil
}
@@ -290,8 +299,11 @@ func (ms *memStore) GetSeqFromTime(t time.Time) uint64 {
// FilteredState will return the SimpleState associated with the filtered subject and a proposed starting sequence.
func (ms *memStore) FilteredState(sseq uint64, subj string) SimpleState {
ms.mu.RLock()
defer ms.mu.RUnlock()
// This needs to be a write lock, as filteredStateLocked can
// mutate the per-subject state.
ms.mu.Lock()
defer ms.mu.Unlock()
return ms.filteredStateLocked(sseq, subj, false)
}
@@ -503,8 +515,10 @@ func (ms *memStore) SubjectsTotals(filterSubject string) map[string]uint64 {
// NumPending will return the number of pending messages matching the filter subject starting at sequence.
func (ms *memStore) NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64) {
ms.mu.RLock()
defer ms.mu.RUnlock()
// This needs to be a write lock, as filteredStateLocked can
// mutate the per-subject state.
ms.mu.Lock()
defer ms.mu.Unlock()
ss := ms.filteredStateLocked(sseq, filter, lastPerSubject)
return ss.Msgs, ms.state.LastSeq
@@ -663,11 +677,23 @@ func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint6
// Purge will remove all messages from this store.
// Will return the number of purged messages.
func (ms *memStore) Purge() (uint64, error) {
ms.mu.RLock()
first := ms.state.LastSeq + 1
ms.mu.RUnlock()
return ms.purge(first)
}
func (ms *memStore) purge(fseq uint64) (uint64, error) {
ms.mu.Lock()
purged := uint64(len(ms.msgs))
cb := ms.scb
bytes := int64(ms.state.Bytes)
ms.state.FirstSeq = ms.state.LastSeq + 1
if fseq < ms.state.LastSeq {
ms.mu.Unlock()
return 0, fmt.Errorf("partial purges not supported on memory store")
}
ms.state.FirstSeq = fseq
ms.state.LastSeq = fseq - 1
ms.state.FirstTime = time.Time{}
ms.state.Bytes = 0
ms.state.Msgs = 0
@@ -859,8 +885,10 @@ func (ms *memStore) LoadLastMsg(subject string, smp *StoreMsg) (*StoreMsg, error
var sm *StoreMsg
var ok bool
ms.mu.RLock()
defer ms.mu.RUnlock()
// This needs to be a write lock, as filteredStateLocked can
// mutate the per-subject state.
ms.mu.Lock()
defer ms.mu.Unlock()
if subject == _EMPTY_ || subject == fwcs {
sm, ok = ms.msgs[ms.state.LastSeq]
@@ -885,8 +913,8 @@ func (ms *memStore) LoadLastMsg(subject string, smp *StoreMsg) (*StoreMsg, error
// LoadNextMsg will find the next message matching the filter subject starting at the start sequence.
// The filter subject can be a wildcard.
func (ms *memStore) LoadNextMsg(filter string, wc bool, start uint64, smp *StoreMsg) (*StoreMsg, uint64, error) {
ms.mu.RLock()
defer ms.mu.RUnlock()
ms.mu.Lock()
defer ms.mu.Unlock()
if start < ms.state.FirstSeq {
start = ms.state.FirstSeq
@@ -986,6 +1014,7 @@ func (ms *memStore) updateFirstSeq(seq uint64) {
break
}
}
oldFirst := ms.state.FirstSeq
if nsm != nil {
ms.state.FirstSeq = nsm.seq
ms.state.FirstTime = time.Unix(0, nsm.ts).UTC()
@@ -994,6 +1023,17 @@ func (ms *memStore) updateFirstSeq(seq uint64) {
ms.state.FirstSeq = ms.state.LastSeq + 1
ms.state.FirstTime = time.Time{}
}
if oldFirst == ms.state.FirstSeq-1 {
ms.dmap.Delete(oldFirst)
} else {
for seq := oldFirst; seq < ms.state.FirstSeq; seq++ {
ms.dmap.Delete(seq)
}
}
if ms.dmap.IsEmpty() {
ms.dmap.SetInitialMin(ms.state.FirstSeq)
}
}
// Remove a seq from the fss and select new first.
@@ -1023,6 +1063,7 @@ func (ms *memStore) removeSeqPerSubject(subj string, seq uint64) {
}
// Will recalulate the first sequence for this subject in this block.
// Lock should be held.
func (ms *memStore) recalculateFirstForSubj(subj string, startSeq uint64, ss *SimpleState) {
for tseq := startSeq + 1; tseq <= ss.Last; tseq++ {
if sm := ms.msgs[tseq]; sm != nil && sm.subj == subj {
@@ -1052,16 +1093,17 @@ func (ms *memStore) removeMsg(seq uint64, secure bool) bool {
}
ms.state.Bytes -= ss
}
ms.dmap.Insert(seq)
ms.updateFirstSeq(seq)
if secure {
if len(sm.hdr) > 0 {
sm.hdr = make([]byte, len(sm.hdr))
rand.Read(sm.hdr)
crand.Read(sm.hdr)
}
if len(sm.msg) > 0 {
sm.msg = make([]byte, len(sm.msg))
rand.Read(sm.msg)
crand.Read(sm.msg)
}
sm.seq, sm.ts = 0, 0
}
@@ -1209,6 +1251,61 @@ func (ms *memStore) Snapshot(_ time.Duration, _, _ bool) (*SnapshotResult, error
return nil, fmt.Errorf("no impl")
}
// Binary encoded state snapshot, >= v2.10 server.
func (ms *memStore) EncodedStreamState(failed uint64) ([]byte, error) {
ms.mu.RLock()
defer ms.mu.RUnlock()
// Quick calculate num deleted.
numDeleted := int((ms.state.LastSeq - ms.state.FirstSeq + 1) - ms.state.Msgs)
if numDeleted < 0 {
numDeleted = 0
}
// Encoded is Msgs, Bytes, FirstSeq, LastSeq, Failed, NumDeleted and optional DeletedBlocks
var buf [1024]byte
buf[0], buf[1] = streamStateMagic, streamStateVersion
n := hdrLen
n += binary.PutUvarint(buf[n:], ms.state.Msgs)
n += binary.PutUvarint(buf[n:], ms.state.Bytes)
n += binary.PutUvarint(buf[n:], ms.state.FirstSeq)
n += binary.PutUvarint(buf[n:], ms.state.LastSeq)
n += binary.PutUvarint(buf[n:], failed)
n += binary.PutUvarint(buf[n:], uint64(numDeleted))
b := buf[0:n]
if numDeleted > 0 {
buf, err := ms.dmap.Encode(nil)
if err != nil {
return nil, err
}
b = append(b, buf...)
}
return b, nil
}
// SyncDeleted will make sure this stream has same deleted state as dbs.
func (ms *memStore) SyncDeleted(dbs DeleteBlocks) {
// For now we share one dmap, so if we have one entry here check if states are the same.
// Note this will work for any DeleteBlock type, but we expect this to be a dmap too.
if len(dbs) == 1 {
ms.mu.RLock()
min, max, num := ms.dmap.State()
ms.mu.RUnlock()
if pmin, pmax, pnum := dbs[0].State(); pmin == min && pmax == max && pnum == num {
return
}
}
for _, db := range dbs {
db.Range(func(dseq uint64) bool {
ms.RemoveMsg(dseq)
return true
})
}
}
func (o *consumerMemStore) Update(state *ConsumerState) error {
// Sanity checks.
if state.AckFloor.Consumer > state.Delivered.Consumer {
+541 -97
View File
@@ -14,6 +14,7 @@
package server
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
@@ -26,6 +27,7 @@ import (
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"sort"
"strconv"
"strings"
@@ -460,7 +462,7 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) {
if auth {
cc.AuthorizedUser = cc.user
// Add in account iff not the global account.
if cc.acc != "" && (cc.acc != globalAccountName) {
if cc.acc != _EMPTY_ && (cc.acc != globalAccountName) {
cc.Account = cc.acc
}
}
@@ -559,12 +561,13 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool)
// Exclude clients that are still doing handshake so we don't block in
// ConnectionState().
if client.flags.isSet(handshakeComplete) && nc != nil {
conn := nc.(*tls.Conn)
cs := conn.ConnectionState()
ci.TLSVersion = tlsVersion(cs.Version)
ci.TLSCipher = tlsCipher(cs.CipherSuite)
if auth && len(cs.PeerCertificates) > 0 {
ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates)
if conn, ok := nc.(*tls.Conn); ok {
cs := conn.ConnectionState()
ci.TLSVersion = tlsVersion(cs.Version)
ci.TLSCipher = tlsCipher(cs.CipherSuite)
if auth && len(cs.PeerCertificates) > 0 {
ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates)
}
}
}
@@ -607,7 +610,7 @@ func (c *client) getRTT() time.Duration {
func decodeBool(w http.ResponseWriter, r *http.Request, param string) (bool, error) {
str := r.URL.Query().Get(param)
if str == "" {
if str == _EMPTY_ {
return false, nil
}
val, err := strconv.ParseBool(str)
@@ -621,7 +624,7 @@ func decodeBool(w http.ResponseWriter, r *http.Request, param string) (bool, err
func decodeUint64(w http.ResponseWriter, r *http.Request, param string) (uint64, error) {
str := r.URL.Query().Get(param)
if str == "" {
if str == _EMPTY_ {
return 0, nil
}
val, err := strconv.ParseUint(str, 10, 64)
@@ -635,7 +638,7 @@ func decodeUint64(w http.ResponseWriter, r *http.Request, param string) (uint64,
func decodeInt(w http.ResponseWriter, r *http.Request, param string) (int, error) {
str := r.URL.Query().Get(param)
if str == "" {
if str == _EMPTY_ {
return 0, nil
}
val, err := strconv.Atoi(str)
@@ -649,7 +652,7 @@ func decodeInt(w http.ResponseWriter, r *http.Request, param string) (int, error
func decodeState(w http.ResponseWriter, r *http.Request) (ConnState, error) {
str := r.URL.Query().Get("state")
if str == "" {
if str == _EMPTY_ {
return ConnOpen, nil
}
switch strings.ToLower(str) {
@@ -783,6 +786,8 @@ type RouteInfo struct {
NumSubs uint32 `json:"subscriptions"`
Subs []string `json:"subscriptions_list,omitempty"`
SubsDetail []SubDetail `json:"subscriptions_list_detail,omitempty"`
Account string `json:"account,omitempty"`
Compression string `json:"compression,omitempty"`
}
// Routez returns a Routez struct containing information about routes.
@@ -795,7 +800,7 @@ func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) {
}
s.mu.Lock()
rs.NumRoutes = len(s.routes)
rs.NumRoutes = s.numRoutes()
// copy the server id for monitoring
rs.ID = s.info.ID
@@ -807,8 +812,7 @@ func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) {
}
rs.Name = s.getOpts().ServerName
// Walk the list
for _, r := range s.routes {
addRoute := func(r *client) {
r.mu.Lock()
ri := &RouteInfo{
Rid: r.cid,
@@ -828,6 +832,8 @@ func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) {
LastActivity: r.last,
Uptime: myUptime(rs.Now.Sub(r.start)),
Idle: myUptime(rs.Now.Sub(r.last)),
Account: string(r.route.accName),
Compression: r.route.compression,
}
if len(r.subs) > 0 {
@@ -847,6 +853,11 @@ func (s *Server) Routez(routezOpts *RoutezOptions) (*Routez, error) {
r.mu.Unlock()
rs.Routes = append(rs.Routes, ri)
}
// Walk the list
s.forEachRoute(func(r *client) {
addRoute(r)
})
s.mu.Unlock()
return rs, nil
}
@@ -945,9 +956,9 @@ func (s *Server) Subsz(opts *SubszOptions) (*Subsz, error) {
subdetail bool
test bool
offset int
testSub string
filterAcc string
limit = DefaultSubListSize
testSub = ""
filterAcc = ""
)
if opts != nil {
@@ -960,14 +971,14 @@ func (s *Server) Subsz(opts *SubszOptions) (*Subsz, error) {
if limit <= 0 {
limit = DefaultSubListSize
}
if opts.Test != "" {
if opts.Test != _EMPTY_ {
testSub = opts.Test
test = true
if !IsValidLiteralSubject(testSub) {
return nil, fmt.Errorf("invalid test subject, must be valid publish subject: %s", testSub)
}
}
if opts.Account != "" {
if opts.Account != _EMPTY_ {
filterAcc = opts.Account
}
}
@@ -982,7 +993,7 @@ func (s *Server) Subsz(opts *SubszOptions) (*Subsz, error) {
subs := raw[:0]
s.accounts.Range(func(k, v interface{}) bool {
acc := v.(*Account)
if filterAcc != "" && acc.GetName() != filterAcc {
if filterAcc != _EMPTY_ && acc.GetName() != filterAcc {
return true
}
slStats.add(acc.sl.Stats())
@@ -1023,7 +1034,7 @@ func (s *Server) Subsz(opts *SubszOptions) (*Subsz, error) {
} else {
s.accounts.Range(func(k, v interface{}) bool {
acc := v.(*Account)
if filterAcc != "" && acc.GetName() != filterAcc {
if filterAcc != _EMPTY_ && acc.GetName() != filterAcc {
return true
}
slStats.add(acc.sl.Stats())
@@ -1207,6 +1218,7 @@ type Varz struct {
SystemAccount string `json:"system_account,omitempty"`
PinnedAccountFail uint64 `json:"pinned_account_fails,omitempty"`
OCSPResponseCache OCSPResponseCacheVarz `json:"ocsp_peer_cache,omitempty"`
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats"`
}
// JetStreamVarz contains basic runtime information about jetstream
@@ -1226,6 +1238,7 @@ type ClusterOptsVarz struct {
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSRequired bool `json:"tls_required,omitempty"`
TLSVerify bool `json:"tls_verify,omitempty"`
PoolSize int `json:"pool_size,omitempty"`
}
// GatewayOptsVarz contains monitoring gateway information
@@ -1325,6 +1338,14 @@ type OCSPResponseCacheVarz struct {
// Currently, there are no options defined.
type VarzOptions struct{}
// SlowConsumersStats contains information about the slow consumers from different type of connections.
type SlowConsumersStats struct {
Clients uint64 `json:"clients"`
Routes uint64 `json:"routes"`
Gateways uint64 `json:"gateways"`
Leafs uint64 `json:"leafs"`
}
func myUptime(d time.Duration) string {
// Just use total seconds for uptime, and display days / years
tsecs := d / time.Second
@@ -1497,6 +1518,7 @@ func (s *Server) createVarz(pcpu float64, rss int64) *Varz {
TLSTimeout: c.TLSTimeout,
TLSRequired: clustTlsReq,
TLSVerify: clustTlsReq,
PoolSize: opts.Cluster.PoolSize,
},
Gateway: GatewayOptsVarz{
Name: gw.Name,
@@ -1672,14 +1694,20 @@ func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64
}
v.Connections = len(s.clients)
v.TotalConnections = s.totalClients
v.Routes = len(s.routes)
v.Remotes = len(s.remotes)
v.Routes = s.numRoutes()
v.Remotes = s.numRemotes()
v.Leafs = len(s.leafs)
v.InMsgs = atomic.LoadInt64(&s.inMsgs)
v.InBytes = atomic.LoadInt64(&s.inBytes)
v.OutMsgs = atomic.LoadInt64(&s.outMsgs)
v.OutBytes = atomic.LoadInt64(&s.outBytes)
v.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
v.SlowConsumersStats = &SlowConsumersStats{
Clients: s.NumSlowConsumersClients(),
Routes: s.NumSlowConsumersRoutes(),
Gateways: s.NumSlowConsumersGateways(),
Leafs: s.NumSlowConsumersLeafs(),
}
v.PinnedAccountFail = atomic.LoadUint64(&s.pinnedAccFail)
// Make sure to reset in case we are re-using.
@@ -2136,18 +2164,19 @@ type LeafzOptions struct {
// LeafInfo has detailed information on each remote leafnode connection.
type LeafInfo struct {
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"`
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"`
}
// Leafz returns a Leafz structure containing information about leafnodes.
@@ -2158,7 +2187,7 @@ func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) {
if len(s.leafs) > 0 {
lconns = make([]*client, 0, len(s.leafs))
for _, ln := range s.leafs {
if opts != nil && opts.Account != "" {
if opts != nil && opts.Account != _EMPTY_ {
ln.mu.Lock()
ok := ln.acc.Name == opts.Account
ln.mu.Unlock()
@@ -2177,17 +2206,18 @@ func (s *Server) Leafz(opts *LeafzOptions) (*Leafz, error) {
for _, ln := range lconns {
ln.mu.Lock()
lni := &LeafInfo{
Name: ln.leaf.remoteServer,
IsSpoke: ln.isSpokeLeafNode(),
Account: ln.acc.Name,
IP: ln.host,
Port: int(ln.port),
RTT: ln.getRTT().String(),
InMsgs: atomic.LoadInt64(&ln.inMsgs),
OutMsgs: ln.outMsgs,
InBytes: atomic.LoadInt64(&ln.inBytes),
OutBytes: ln.outBytes,
NumSubs: uint32(len(ln.subs)),
Name: ln.leaf.remoteServer,
IsSpoke: ln.isSpokeLeafNode(),
Account: ln.acc.Name,
IP: ln.host,
Port: int(ln.port),
RTT: ln.getRTT().String(),
InMsgs: atomic.LoadInt64(&ln.inMsgs),
OutMsgs: ln.outMsgs,
InBytes: atomic.LoadInt64(&ln.inBytes),
OutBytes: ln.outBytes,
NumSubs: uint32(len(ln.subs)),
Compression: ln.leaf.compression,
}
if opts != nil && opts.Subscriptions {
lni.Subs = make([]string, 0, len(ln.subs))
@@ -2313,8 +2343,7 @@ func ResponseHandler(w http.ResponseWriter, r *http.Request, data []byte) {
func handleResponse(code int, w http.ResponseWriter, r *http.Request, data []byte) {
// Get callback from request
callback := r.URL.Query().Get("callback")
// If callback is not empty then
if callback != "" {
if callback != _EMPTY_ {
// Response for JSONP
w.Header().Set("Content-Type", "application/javascript")
w.WriteHeader(code)
@@ -2398,6 +2427,8 @@ func (reason ClosedState) String() string {
return "Minimum Version Required"
case ClusterNamesIdentical:
return "Cluster Names Identical"
case Kicked:
return "Kicked"
}
return "Unknown State"
@@ -2638,7 +2669,7 @@ func (s *Server) accountInfo(accName string) (*AccountInfo, error) {
mappings := ExtMap{}
for _, m := range a.mappings {
var dests []*MapDest
src := ""
var src string
if m == nil {
src = "nil"
if _, ok := mappings[src]; ok { // only set if not present (keep orig in case nil is used)
@@ -2648,7 +2679,7 @@ func (s *Server) accountInfo(accName string) (*AccountInfo, error) {
} else {
src = m.src
for _, d := range m.dests {
dests = append(dests, &MapDest{d.tr.dest, d.weight, ""})
dests = append(dests, &MapDest{d.tr.dest, d.weight, _EMPTY_})
}
for c, cd := range m.cdests {
for _, d := range cd {
@@ -2699,9 +2730,19 @@ type JSzOptions struct {
// HealthzOptions are options passed to Healthz
type HealthzOptions struct {
// Deprecated: Use JSEnabledOnly instead
JSEnabled bool `json:"js-enabled,omitempty"`
JSEnabledOnly bool `json:"js-enabled-only,omitempty"`
JSServerOnly bool `json:"js-server-only,omitempty"`
JSEnabled bool `json:"js-enabled,omitempty"`
JSEnabledOnly bool `json:"js-enabled-only,omitempty"`
JSServerOnly bool `json:"js-server-only,omitempty"`
Account string `json:"account,omitempty"`
Stream string `json:"stream,omitempty"`
Consumer string `json:"consumer,omitempty"`
Details bool `json:"details,omitempty"`
}
// ProfilezOptions are options passed to Profilez
type ProfilezOptions struct {
Name string `json:"name"`
Debug int `json:"debug"`
}
// StreamDetail shows information about the stream state and its consumers.
@@ -2762,7 +2803,7 @@ func (s *Server) accountDetail(jsa *jsAccount, optStreams, optConsumers, optCfg,
acc := jsa.account
name := acc.GetName()
id := name
if acc.nameTag != "" {
if acc.nameTag != _EMPTY_ {
name = acc.nameTag
}
jsa.usageMu.RLock()
@@ -3054,8 +3095,72 @@ func (s *Server) HandleJsz(w http.ResponseWriter, r *http.Request) {
}
type HealthStatus struct {
Status string `json:"status"`
Error string `json:"error,omitempty"`
Status string `json:"status"`
StatusCode int `json:"status_code,omitempty"`
Error string `json:"error,omitempty"`
Errors []HealthzError `json:"errors,omitempty"`
}
type HealthzError struct {
Type HealthZErrorType `json:"type"`
Account string `json:"account,omitempty"`
Stream string `json:"stream,omitempty"`
Consumer string `json:"consumer,omitempty"`
Error string `json:"error,omitempty"`
}
type HealthZErrorType int
const (
HealthzErrorConn HealthZErrorType = iota
HealthzErrorBadRequest
HealthzErrorJetStream
HealthzErrorAccount
HealthzErrorStream
HealthzErrorConsumer
)
func (t HealthZErrorType) String() string {
switch t {
case HealthzErrorConn:
return "CONNECTION"
case HealthzErrorBadRequest:
return "BAD_REQUEST"
case HealthzErrorJetStream:
return "JETSTREAM"
case HealthzErrorAccount:
return "ACCOUNT"
case HealthzErrorStream:
return "STREAM"
case HealthzErrorConsumer:
return "CONSUMER"
default:
return "unknown"
}
}
func (t HealthZErrorType) MarshalJSON() ([]byte, error) {
return json.Marshal(t.String())
}
func (t *HealthZErrorType) UnmarshalJSON(data []byte) error {
switch string(data) {
case jsonString("CONNECTION"):
*t = HealthzErrorConn
case jsonString("BAD_REQUEST"):
*t = HealthzErrorBadRequest
case jsonString("JETSTREAM"):
*t = HealthzErrorJetStream
case jsonString("ACCOUNT"):
*t = HealthzErrorAccount
case jsonString("STREAM"):
*t = HealthzErrorStream
case jsonString("CONSUMER"):
*t = HealthzErrorConsumer
default:
return fmt.Errorf("unknown healthz error type %q", data)
}
return nil
}
// https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check
@@ -3080,18 +3185,29 @@ func (s *Server) HandleHealthz(w http.ResponseWriter, r *http.Request) {
return
}
includeDetails, err := decodeBool(w, r, "details")
if err != nil {
return
}
hs := s.healthz(&HealthzOptions{
JSEnabled: jsEnabled,
JSEnabledOnly: jsEnabledOnly,
JSServerOnly: jsServerOnly,
Account: r.URL.Query().Get("account"),
Stream: r.URL.Query().Get("stream"),
Consumer: r.URL.Query().Get("consumer"),
Details: includeDetails,
})
code := http.StatusOK
if hs.Error != _EMPTY_ {
s.Warnf("Healthcheck failed: %q", hs.Error)
code = http.StatusServiceUnavailable
code = hs.StatusCode
}
// Remove StatusCode from JSON representation when responding via HTTP
// since this is already in the response.
hs.StatusCode = 0
b, err := json.Marshal(hs)
if err != nil {
s.Errorf("Error marshaling response to /healthz request: %v", err)
@@ -3108,10 +3224,65 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
if opts == nil {
opts = &HealthzOptions{}
}
details := opts.Details
defer func() {
// for response with details enabled, set status to either "error" or "ok"
if details {
if len(health.Errors) != 0 {
health.Status = "error"
} else {
health.Status = "ok"
}
}
// if no specific status code was set, set it based on the presence of errors
if health.StatusCode == 0 {
if health.Error != _EMPTY_ || len(health.Errors) != 0 {
health.StatusCode = http.StatusServiceUnavailable
} else {
health.StatusCode = http.StatusOK
}
}
}()
if opts.Account == _EMPTY_ && opts.Stream != _EMPTY_ {
health.StatusCode = http.StatusBadRequest
if !details {
health.Status = "error"
health.Error = fmt.Sprintf("%q must not be empty when checking stream health", "account")
} else {
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorBadRequest,
Error: fmt.Sprintf("%q must not be empty when checking stream health", "account"),
})
}
return health
}
if opts.Stream == _EMPTY_ && opts.Consumer != _EMPTY_ {
health.StatusCode = http.StatusBadRequest
if !details {
health.Status = "error"
health.Error = fmt.Sprintf("%q must not be empty when checking consumer health", "stream")
} else {
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorBadRequest,
Error: fmt.Sprintf("%q must not be empty when checking consumer health", "stream"),
})
}
return health
}
if err := s.readyForConnections(time.Millisecond); err != nil {
health.StatusCode = http.StatusInternalServerError
health.Status = "error"
health.Error = err.Error()
if !details {
health.Error = err.Error()
} else {
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorConn,
Error: err.Error(),
})
}
return health
}
@@ -3124,10 +3295,18 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
// Access the Jetstream state to perform additional checks.
js := s.getJetStream()
const na = "unavailable"
if !js.isEnabled() {
health.Status = "unavailable"
health.Error = NewJSNotEnabledError().Error()
health.StatusCode = http.StatusServiceUnavailable
health.Status = na
if !details {
health.Error = NewJSNotEnabledError().Error()
} else {
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorJetStream,
Error: NewJSNotEnabledError().Error(),
})
}
return health
}
// Only check if JS is enabled, skip meta and asset check.
@@ -3140,30 +3319,124 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
cc := js.cluster
js.mu.RUnlock()
const na = "unavailable"
// Currently single server we make sure the streams were recovered.
if cc == nil {
sdir := js.config.StoreDir
// Whip through account folders and pull each stream name.
fis, _ := os.ReadDir(sdir)
var accFound, streamFound, consumerFound bool
for _, fi := range fis {
if fi.Name() == snapStagingDir {
continue
}
if opts.Account != _EMPTY_ {
if fi.Name() != opts.Account {
continue
}
accFound = true
}
acc, err := s.LookupAccount(fi.Name())
if err != nil {
health.Status = na
health.Error = fmt.Sprintf("JetStream account '%s' could not be resolved", fi.Name())
return health
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream account '%s' could not be resolved", fi.Name())
return health
}
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorAccount,
Account: fi.Name(),
Error: fmt.Sprintf("JetStream account '%s' could not be resolved", fi.Name()),
})
continue
}
sfis, _ := os.ReadDir(filepath.Join(sdir, fi.Name(), "streams"))
for _, sfi := range sfis {
if opts.Stream != _EMPTY_ {
if sfi.Name() != opts.Stream {
continue
}
streamFound = true
}
stream := sfi.Name()
if _, err := acc.lookupStream(stream); err != nil {
health.Status = na
health.Error = fmt.Sprintf("JetStream stream '%s > %s' could not be recovered", acc, stream)
return health
s, err := acc.lookupStream(stream)
if err != nil {
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream stream '%s > %s' could not be recovered", acc, stream)
return health
}
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorStream,
Account: acc.Name,
Stream: stream,
Error: fmt.Sprintf("JetStream stream '%s > %s' could not be recovered", acc, stream),
})
continue
}
if streamFound {
// if consumer option is passed, verify that the consumer exists on stream
if opts.Consumer != _EMPTY_ {
for _, cons := range s.consumers {
if cons.name == opts.Consumer {
consumerFound = true
break
}
}
}
break
}
}
if accFound {
break
}
}
if opts.Account != _EMPTY_ && !accFound {
health.StatusCode = http.StatusNotFound
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream account %q not found", opts.Account)
} else {
health.Errors = []HealthzError{
{
Type: HealthzErrorAccount,
Account: opts.Account,
Error: fmt.Sprintf("JetStream account %q not found", opts.Account),
},
}
}
return health
}
if opts.Stream != _EMPTY_ && !streamFound {
health.StatusCode = http.StatusNotFound
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream stream %q not found on account %q", opts.Stream, opts.Account)
} else {
health.Errors = []HealthzError{
{
Type: HealthzErrorStream,
Account: opts.Account,
Stream: opts.Stream,
Error: fmt.Sprintf("JetStream stream %q not found on account %q", opts.Stream, opts.Account),
},
}
}
return health
}
if opts.Consumer != _EMPTY_ && !consumerFound {
health.StatusCode = http.StatusNotFound
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream consumer %q not found for stream %q on account %q", opts.Consumer, opts.Stream, opts.Account)
} else {
health.Errors = []HealthzError{
{
Type: HealthzErrorConsumer,
Account: opts.Account,
Stream: opts.Stream,
Consumer: opts.Consumer,
Error: fmt.Sprintf("JetStream consumer %q not found for stream %q on account %q", opts.Consumer, opts.Stream, opts.Account),
},
}
}
}
@@ -3178,14 +3451,32 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
// If no meta leader.
if meta == nil || meta.GroupLeader() == _EMPTY_ {
health.Status = na
health.Error = "JetStream has not established contact with a meta leader"
if !details {
health.Status = na
health.Error = "JetStream has not established contact with a meta leader"
} else {
health.Errors = []HealthzError{
{
Type: HealthzErrorJetStream,
Error: "JetStream has not established contact with a meta leader",
},
}
}
return health
}
// If we are not current with the meta leader.
if !meta.Healthy() {
health.Status = na
health.Error = "JetStream is not current with the meta leader"
if !details {
health.Status = na
health.Error = "JetStream is not current with the meta leader"
} else {
health.Errors = []HealthzError{
{
Type: HealthzErrorJetStream,
Error: "JetStream is not current with the meta leader",
},
}
}
return health
}
@@ -3199,25 +3490,124 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
ourID := meta.ID()
// Copy the meta layer so we do not need to hold the js read lock for an extended period of time.
var streams map[string]map[string]*streamAssignment
js.mu.RLock()
streams := make(map[string]map[string]*streamAssignment, len(cc.streams))
for acc, asa := range cc.streams {
if opts.Account == _EMPTY_ {
// Collect all relevant streams and consumers.
streams = make(map[string]map[string]*streamAssignment, len(cc.streams))
for acc, asa := range cc.streams {
nasa := make(map[string]*streamAssignment)
for stream, sa := range asa {
// If we are a member and we are not being restored, select for check.
if sa.Group.isMember(ourID) && sa.Restore == nil {
csa := sa.copyGroup()
csa.consumers = make(map[string]*consumerAssignment)
for consumer, ca := range sa.consumers {
if ca.Group.isMember(ourID) {
// Use original here. Not a copy.
csa.consumers[consumer] = ca
}
}
nasa[stream] = csa
}
}
streams[acc] = nasa
}
} else {
streams = make(map[string]map[string]*streamAssignment, 1)
asa, ok := cc.streams[opts.Account]
if !ok {
health.StatusCode = http.StatusNotFound
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream account %q not found", opts.Account)
} else {
health.Errors = []HealthzError{
{
Type: HealthzErrorAccount,
Account: opts.Account,
Error: fmt.Sprintf("JetStream account %q not found", opts.Account),
},
}
}
js.mu.RUnlock()
return health
}
nasa := make(map[string]*streamAssignment)
for stream, sa := range asa {
// If we are a member and we are not being restored, select for check.
if sa.Group.isMember(ourID) && sa.Restore == nil {
csa := sa.copyGroup()
csa.consumers = make(map[string]*consumerAssignment)
for consumer, ca := range sa.consumers {
if ca.Group.isMember(ourID) {
// Use original here. Not a copy.
csa.consumers[consumer] = ca
if opts.Stream != _EMPTY_ {
sa, ok := asa[opts.Stream]
if !ok || !sa.Group.isMember(ourID) {
health.StatusCode = http.StatusNotFound
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream stream %q not found on account %q", opts.Stream, opts.Account)
} else {
health.Errors = []HealthzError{
{
Type: HealthzErrorStream,
Account: opts.Account,
Stream: opts.Stream,
Error: fmt.Sprintf("JetStream stream %q not found on account %q", opts.Stream, opts.Account),
},
}
}
nasa[stream] = csa
js.mu.RUnlock()
return health
}
csa := sa.copyGroup()
csa.consumers = make(map[string]*consumerAssignment)
var consumerFound bool
for consumer, ca := range sa.consumers {
if opts.Consumer != _EMPTY_ {
if consumer != opts.Consumer || !ca.Group.isMember(ourID) {
continue
}
consumerFound = true
}
// If we are a member and we are not being restored, select for check.
if sa.Group.isMember(ourID) && sa.Restore == nil {
csa.consumers[consumer] = ca
}
if consumerFound {
break
}
}
if opts.Consumer != _EMPTY_ && !consumerFound {
health.StatusCode = http.StatusNotFound
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream consumer %q not found for stream %q on account %q", opts.Consumer, opts.Stream, opts.Account)
} else {
health.Errors = []HealthzError{
{
Type: HealthzErrorConsumer,
Account: opts.Account,
Stream: opts.Stream,
Consumer: opts.Consumer,
Error: fmt.Sprintf("JetStream consumer %q not found for stream %q on account %q", opts.Consumer, opts.Stream, opts.Account),
},
}
}
js.mu.RUnlock()
return health
}
nasa[opts.Stream] = csa
} else {
for stream, sa := range asa {
// If we are a member and we are not being restored, select for check.
if sa.Group.isMember(ourID) && sa.Restore == nil {
csa := sa.copyGroup()
csa.consumers = make(map[string]*consumerAssignment)
for consumer, ca := range sa.consumers {
if ca.Group.isMember(ourID) {
csa.consumers[consumer] = ca
}
}
nasa[stream] = csa
}
}
}
streams[acc] = nasa
streams[opts.Account] = nasa
}
js.mu.RUnlock()
@@ -3225,25 +3615,51 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
for accName, asa := range streams {
acc, err := s.LookupAccount(accName)
if err != nil && len(asa) > 0 {
health.Status = na
health.Error = fmt.Sprintf("JetStream can not lookup account %q: %v", accName, err)
return health
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream can not lookup account %q: %v", accName, err)
return health
}
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorAccount,
Account: accName,
Error: fmt.Sprintf("JetStream can not lookup account %q: %v", accName, err),
})
continue
}
for stream, sa := range asa {
// Make sure we can look up
if !js.isStreamHealthy(acc, sa) {
health.Status = na
health.Error = fmt.Sprintf("JetStream stream '%s > %s' is not current", accName, stream)
return health
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream stream '%s > %s' is not current", accName, stream)
return health
}
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorStream,
Account: accName,
Stream: stream,
Error: fmt.Sprintf("JetStream stream '%s > %s' is not current", accName, stream),
})
continue
}
mset, _ := acc.lookupStream(stream)
// Now check consumers.
for consumer, ca := range sa.consumers {
if !js.isConsumerHealthy(mset, consumer, ca) {
health.Status = na
health.Error = fmt.Sprintf("JetStream consumer '%s > %s > %s' is not current", acc, stream, consumer)
return health
if !details {
health.Status = na
health.Error = fmt.Sprintf("JetStream consumer '%s > %s > %s' is not current", acc, stream, consumer)
return health
}
health.Errors = append(health.Errors, HealthzError{
Type: HealthzErrorConsumer,
Account: accName,
Stream: stream,
Consumer: consumer,
Error: fmt.Sprintf("JetStream consumer '%s > %s > %s' is not current", acc, stream, consumer),
})
}
}
}
@@ -3251,3 +3667,31 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
// Success.
return health
}
type ProfilezStatus struct {
Profile []byte `json:"profile"`
Error string `json:"error"`
}
func (s *Server) profilez(opts *ProfilezOptions) *ProfilezStatus {
if opts.Name == _EMPTY_ {
return &ProfilezStatus{
Error: "Profile name not specified",
}
}
profile := pprof.Lookup(opts.Name)
if profile == nil {
return &ProfilezStatus{
Error: fmt.Sprintf("Profile %q not found", opts.Name),
}
}
var buffer bytes.Buffer
if err := profile.WriteTo(&buffer, opts.Debug); err != nil {
return &ProfilezStatus{
Error: fmt.Sprintf("Profile %q error: %s", opts.Name, err),
}
}
return &ProfilezStatus{
Profile: buffer.Bytes(),
}
}
+1162 -329
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -14,7 +14,7 @@
package server
import (
"crypto/rand"
crand "crypto/rand"
"encoding/base64"
)
@@ -42,6 +42,6 @@ func (s *Server) nonceRequired() bool {
func (s *Server) generateNonce(n []byte) {
var raw [nonceRawLen]byte
data := raw[:]
rand.Read(data)
crand.Read(data)
base64.RawURLEncoding.Encode(n, data)
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2021 The NATS Authors
// Copyright 2021-2023 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
+362 -94
View File
@@ -77,6 +77,9 @@ type ClusterOpts struct {
Advertise string `json:"-"`
NoAdvertise bool `json:"-"`
ConnectRetries int `json:"-"`
PoolSize int `json:"-"`
PinnedAccounts []string `json:"-"`
Compression CompressionOpts `json:"-"`
// Not exported (used in tests)
resolver netResolver
@@ -84,6 +87,21 @@ type ClusterOpts struct {
tlsConfigOpts *TLSConfigOpts
}
// CompressionOpts defines the compression mode and optional configuration.
type CompressionOpts struct {
Mode string
// If `Mode` is set to CompressionS2Auto, RTTThresholds provides the
// thresholds at which the compression level will go from
// CompressionS2Uncompressed to CompressionS2Fast, CompressionS2Better
// or CompressionS2Best. If a given level is not desired, specify 0
// for this slot. For instance, the slice []{0, 10ms, 20ms} means that
// for any RTT up to 10ms included the compression level will be
// CompressionS2Fast, then from ]10ms..20ms], the level will be selected
// as CompressionS2Better. Anything above 20ms will result in picking
// the CompressionS2Best compression level.
RTTThresholds []time.Duration
}
// GatewayOpts are options for gateways.
// NOTE: This structure is no longer used for monitoring endpoints
// and json tags are deprecated and may be removed in the future.
@@ -136,10 +154,14 @@ type LeafNodeOpts struct {
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSMap bool `json:"-"`
TLSPinnedCerts PinnedCertSet `json:"-"`
TLSHandshakeFirst bool `json:"-"`
Advertise string `json:"-"`
NoAdvertise bool `json:"-"`
ReconnectInterval time.Duration `json:"-"`
// Compression options
Compression CompressionOpts `json:"-"`
// For solicited connections to other clusters/superclusters.
Remotes []*RemoteLeafOpts `json:"remotes,omitempty"`
@@ -166,17 +188,22 @@ type SignatureHandler func([]byte) (string, []byte, error)
// RemoteLeafOpts are options for connecting to a remote server as a leaf node.
type RemoteLeafOpts struct {
LocalAccount string `json:"local_account,omitempty"`
NoRandomize bool `json:"-"`
URLs []*url.URL `json:"urls,omitempty"`
Credentials string `json:"-"`
SignatureCB SignatureHandler `json:"-"`
TLS bool `json:"-"`
TLSConfig *tls.Config `json:"-"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
Hub bool `json:"hub,omitempty"`
DenyImports []string `json:"-"`
DenyExports []string `json:"-"`
LocalAccount string `json:"local_account,omitempty"`
NoRandomize bool `json:"-"`
URLs []*url.URL `json:"urls,omitempty"`
Credentials string `json:"-"`
SignatureCB SignatureHandler `json:"-"`
TLS bool `json:"-"`
TLSConfig *tls.Config `json:"-"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSHandshakeFirst bool `json:"-"`
Hub bool `json:"hub,omitempty"`
DenyImports []string `json:"-"`
DenyExports []string `json:"-"`
// Compression options for this remote. Each remote could have a different
// setting and also be different from the LeafNode options.
Compression CompressionOpts `json:"-"`
// When an URL has the "ws" (or "wss") scheme, then the server will initiate the
// connection as a websocket connection. By default, the websocket frames will be
@@ -203,6 +230,19 @@ type JSLimitOpts struct {
Duplicates time.Duration
}
// AuthCallout option used to map external AuthN to NATS based AuthZ.
type AuthCallout struct {
// Must be a public account Nkey.
Issuer string
// Account to be used for sending requests.
Account string
// Users that will bypass auth_callout and be used for the auth service itself.
AuthUsers []string
// XKey is a public xkey for the authorization service.
// This will enable encryption for server requests and the authorization service responses.
XKey string
}
// Options block for nats-server.
// NOTE: This structure is no longer used for monitoring endpoints
// and json tags are deprecated and may be removed in the future.
@@ -235,6 +275,7 @@ type Options struct {
Username string `json:"-"`
Password string `json:"-"`
Authorization string `json:"-"`
AuthCallout *AuthCallout `json:"-"`
PingInterval time.Duration `json:"ping_interval"`
MaxPingsOut int `json:"ping_max"`
HTTPHost string `json:"http_host"`
@@ -254,11 +295,14 @@ type Options struct {
JetStreamDomain string `json:"-"`
JetStreamExtHint string `json:"-"`
JetStreamKey string `json:"-"`
JetStreamOldKey string `json:"-"`
JetStreamCipher StoreCipher `json:"-"`
JetStreamUniqueTag string
JetStreamLimits JSLimitOpts
JetStreamMaxCatchup int64
StoreDir string `json:"-"`
SyncInterval time.Duration `json:"-"`
SyncAlways bool `json:"-"`
JsAccDefaultDomain map[string]string `json:"-"` // account to domain name mapping
Websocket WebsocketOpts `json:"-"`
MQTT MQTTOpts `json:"-"`
@@ -267,6 +311,7 @@ type Options struct {
PortsFileDir string `json:"-"`
LogFile string `json:"-"`
LogSizeLimit int64 `json:"-"`
LogMaxFiles int64 `json:"-"`
Syslog bool `json:"-"`
RemoteSyslog string `json:"-"`
Routes []*url.URL `json:"-"`
@@ -307,6 +352,9 @@ type Options struct {
// CheckConfig configuration file syntax test was successful and exit.
CheckConfig bool `json:"-"`
// DisableJetStreamBanner will not print the ascii art on startup for JetStream enabled servers
DisableJetStreamBanner bool `json:"-"`
// ConnectErrorReports specifies the number of failed attempts
// at which point server should report the failure of an initial
// connection to a route, gateway or leaf node.
@@ -342,6 +390,7 @@ type Options struct {
// JetStream
maxMemSet bool
maxStoreSet bool
syncSet bool
// OCSP Cache config enables next-gen cache for OCSP features
OCSPCacheConfig *OCSPResponseCacheConfig
@@ -472,21 +521,24 @@ type MQTTOpts struct {
// Set of allowable certificates
TLSPinnedCerts PinnedCertSet
// AckWait is the amount of time after which a QoS 1 message sent to
// a client is redelivered as a DUPLICATE if the server has not
// received the PUBACK on the original Packet Identifier.
// The value has to be positive.
// Zero will cause the server to use the default value (30 seconds).
// Note that changes to this option is applied only to new MQTT subscriptions.
// AckWait is the amount of time after which a QoS 1 or 2 message sent to a
// client is redelivered as a DUPLICATE if the server has not received the
// PUBACK on the original Packet Identifier. The same value applies to
// PubRel redelivery. The value has to be positive. Zero will cause the
// server to use the default value (30 seconds). Note that changes to this
// option is applied only to new MQTT subscriptions (or sessions for
// PubRels).
AckWait time.Duration
// MaxAckPending is the amount of QoS 1 messages the server can send to
// a subscription without receiving any PUBACK for those messages.
// The valid range is [0..65535].
// MaxAckPending is the amount of QoS 1 and 2 messages (combined) the server
// can send to a subscription without receiving any PUBACK for those
// messages. The valid range is [0..65535].
//
// The total of subscriptions' MaxAckPending on a given session cannot
// exceed 65535. Attempting to create a subscription that would bring
// the total above the limit would result in the server returning 0x80
// in the SUBACK for this subscription.
// exceed 65535. Attempting to create a subscription that would bring the
// total above the limit would result in the server returning 0x80 in the
// SUBACK for this subscription.
//
// Due to how the NATS Server handles the MQTT "#" wildcard, each
// subscription ending with "#" will use 2 times the MaxAckPending value.
// Note that changes to this option is applied only to new subscriptions.
@@ -568,6 +620,8 @@ type authorization struct {
users []*User
timeout float64
defaultPermissions *Permissions
// Auth Callouts
callout *AuthCallout
}
// TLSConfigOpts holds the parsed tls config information,
@@ -580,6 +634,7 @@ type TLSConfigOpts struct {
Insecure bool
Map bool
TLSCheckKnownURLs bool
HandshakeFirst bool // Indicate that the TLS handshake should occur first, before sending the INFO protocol
Timeout float64
RateLimit int64
Ciphers []uint16
@@ -834,6 +889,8 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
o.Password = auth.pass
o.Authorization = auth.token
o.AuthTimeout = auth.timeout
o.AuthCallout = auth.callout
if (auth.user != _EMPTY_ || auth.pass != _EMPTY_) && auth.token != _EMPTY_ {
err := &configErr{tk, "Cannot have a user/pass and token"}
*errors = append(*errors, err)
@@ -928,7 +985,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
}
case "store_dir", "storedir":
// Check if JetStream configuration is also setting the storage directory.
if o.StoreDir != "" {
if o.StoreDir != _EMPTY_ {
*errors = append(*errors, &configErr{tk, "Duplicate 'store_dir' configuration"})
return
}
@@ -943,6 +1000,8 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
o.LogFile = v.(string)
case "logfile_size_limit", "log_size_limit":
o.LogSizeLimit = v.(int64)
case "logfile_max_num", "log_max_num":
o.LogMaxFiles = v.(int64)
case "syslog":
o.Syslog = v.(bool)
trackExplicitVal(o, &o.inConfig, "Syslog", o.Syslog)
@@ -1582,6 +1641,12 @@ func parseCluster(v interface{}, opts *Options, errors *[]error, warnings *[]err
*errors = append(*errors, err)
continue
}
if auth.callout != nil {
err := &configErr{tk, "Cluster authorization does not support callouts"}
*errors = append(*errors, err)
continue
}
opts.Cluster.Username = auth.user
opts.Cluster.Password = auth.pass
opts.Cluster.AuthTimeout = auth.timeout
@@ -1642,6 +1707,15 @@ func parseCluster(v interface{}, opts *Options, errors *[]error, warnings *[]err
}
// This will possibly override permissions that were define in auth block
setClusterPermissions(&opts.Cluster, perms)
case "pool_size":
opts.Cluster.PoolSize = int(mv.(int64))
case "accounts":
opts.Cluster.PinnedAccounts, _ = parseStringArray("accounts", tk, &lt, mv, errors, warnings)
case "compression":
if err := parseCompression(&opts.Cluster.Compression, CompressionS2Fast, tk, mk, mv); err != nil {
*errors = append(*errors, err)
continue
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -1658,6 +1732,50 @@ func parseCluster(v interface{}, opts *Options, errors *[]error, warnings *[]err
return nil
}
// The parameter `chosenModeForOn` indicates which compression mode to use
// when the user selects "on" (or enabled, true, etc..). This is because
// we may have different defaults depending on where the compression is used.
func parseCompression(c *CompressionOpts, chosenModeForOn string, tk token, mk string, mv interface{}) (retErr error) {
var lt token
defer convertPanicToError(&lt, &retErr)
switch mv := mv.(type) {
case string:
// Do not validate here, it will be done in NewServer.
c.Mode = mv
case bool:
if mv {
c.Mode = chosenModeForOn
} else {
c.Mode = CompressionOff
}
case map[string]interface{}:
for mk, mv := range mv {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "mode":
c.Mode = mv.(string)
case "rtt_thresholds", "thresholds", "rtts", "rtt":
for _, iv := range mv.([]interface{}) {
_, mv := unwrapValue(iv, &lt)
dur, err := time.ParseDuration(mv.(string))
if err != nil {
return &configErr{tk, err.Error()}
}
c.RTTThresholds = append(c.RTTThresholds, dur)
}
default:
if !tk.IsUsedVariable() {
return &configErr{tk, fmt.Sprintf("unknown field %q", mk)}
}
}
}
default:
return &configErr{tk, fmt.Sprintf("field %q should be a boolean or a structure, got %T", mk, mv)}
}
return nil
}
func parseURLs(a []interface{}, typ string, warnings *[]error) (urls []*url.URL, errors []error) {
urls = make([]*url.URL, 0, len(a))
var lt token
@@ -1745,6 +1863,12 @@ func parseGateway(v interface{}, o *Options, errors *[]error, warnings *[]error)
*errors = append(*errors, err)
continue
}
if auth.callout != nil {
err := &configErr{tk, "Gateway authorization does not support callouts"}
*errors = append(*errors, err)
continue
}
o.Gateway.Username = auth.user
o.Gateway.Password = auth.pass
o.Gateway.AuthTimeout = auth.timeout
@@ -1898,7 +2022,7 @@ func getStorageSize(v interface{}) (int64, error) {
return 0, fmt.Errorf("must be int64 or string")
}
if s == "" {
if s == _EMPTY_ {
return 0, nil
}
@@ -1993,6 +2117,14 @@ func parseJetStream(v interface{}, opts *Options, errors *[]error, warnings *[]e
return &configErr{tk, "Duplicate 'store_dir' configuration"}
}
opts.StoreDir = mv.(string)
case "sync", "sync_interval":
if v, ok := mv.(string); ok && strings.ToLower(v) == "always" {
opts.SyncInterval = defaultSyncInterval
opts.SyncAlways = true
} else {
opts.SyncInterval = parseDuration(mk, tk, mv, errors, warnings)
}
opts.syncSet = true
case "max_memory_store", "max_mem_store", "max_mem":
s, err := getStorageSize(mv)
if err != nil {
@@ -2013,6 +2145,8 @@ func parseJetStream(v interface{}, opts *Options, errors *[]error, warnings *[]e
doEnable = mv.(bool)
case "key", "ek", "encryption_key":
opts.JetStreamKey = mv.(string)
case "prev_key", "prev_ek", "prev_encryption_key":
opts.JetStreamOldKey = mv.(string)
case "cipher":
switch strings.ToLower(mv.(string)) {
case "chacha", "chachapoly":
@@ -2125,6 +2259,7 @@ func parseLeafNodes(v interface{}, opts *Options, errors *[]error, warnings *[]e
opts.LeafNode.TLSTimeout = tc.Timeout
opts.LeafNode.TLSMap = tc.Map
opts.LeafNode.TLSPinnedCerts = tc.PinnedCerts
opts.LeafNode.TLSHandshakeFirst = tc.HandshakeFirst
opts.LeafNode.tlsConfigOpts = tc
case "leafnode_advertise", "advertise":
opts.LeafNode.Advertise = mv.(string)
@@ -2139,6 +2274,11 @@ func parseLeafNodes(v interface{}, opts *Options, errors *[]error, warnings *[]e
continue
}
opts.LeafNode.MinVersion = version
case "compression":
if err := parseCompression(&opts.LeafNode.Compression, CompressionS2Auto, tk, mk, mv); err != nil {
*errors = append(*errors, err)
continue
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -2340,6 +2480,7 @@ func parseRemoteLeafNodes(v interface{}, errors *[]error, warnings *[]error) ([]
} else {
remote.TLSTimeout = float64(DEFAULT_LEAF_TLS_TIMEOUT) / float64(time.Second)
}
remote.TLSHandshakeFirst = tc.HandshakeFirst
remote.tlsConfigOpts = tc
case "hub":
remote.Hub = v.(bool)
@@ -2363,6 +2504,11 @@ func parseRemoteLeafNodes(v interface{}, errors *[]error, warnings *[]error) ([]
remote.Websocket.NoMasking = v.(bool)
case "jetstream_cluster_migrate", "js_cluster_migrate":
remote.JetStreamClusterMigrate = true
case "compression":
if err := parseCompression(&remote.Compression, CompressionS2Auto, tk, k, v); err != nil {
*errors = append(*errors, err)
continue
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -2617,7 +2763,7 @@ func parseAccountMappings(v interface{}, acc *Account, errors *[]error, warnings
// Now add them in..
if err := acc.AddWeightedMappings(subj, mappings...); err != nil {
err := &configErr{tk, fmt.Sprintf("Error adding mapping for %q to %q : %v", subj, v.(string), err)}
err := &configErr{tk, fmt.Sprintf("Error adding mapping for %q : %v", subj, err)}
*errors = append(*errors, err)
continue
}
@@ -2629,7 +2775,7 @@ func parseAccountMappings(v interface{}, acc *Account, errors *[]error, warnings
}
// Now add it in..
if err := acc.AddWeightedMappings(subj, mdest); err != nil {
err := &configErr{tk, fmt.Sprintf("Error adding mapping for %q to %q : %v", subj, v.(string), err)}
err := &configErr{tk, fmt.Sprintf("Error adding mapping for %q : %v", subj, err)}
*errors = append(*errors, err)
continue
}
@@ -2937,7 +3083,7 @@ func parseAccounts(v interface{}, opts *Options, errors *[]error, warnings *[]er
*errors = append(*errors, &configErr{tk, msg})
continue
}
if stream.pre != "" {
if stream.pre != _EMPTY_ {
if err := stream.acc.AddStreamImport(ta, stream.sub, stream.pre); err != nil {
msg := fmt.Sprintf("Error adding stream import %q: %v", stream.sub, err)
*errors = append(*errors, &configErr{tk, msg})
@@ -3391,16 +3537,16 @@ func parseImportStreamOrService(v interface{}, errors, warnings *[]error) (*impo
*errors = append(*errors, err)
continue
}
if accountName == "" || subject == "" {
if accountName == _EMPTY_ || subject == _EMPTY_ {
err := &configErr{tk, "Expect an account name and a subject"}
*errors = append(*errors, err)
continue
}
curStream = &importStream{an: accountName, sub: subject}
if to != "" {
if to != _EMPTY_ {
curStream.to = to
}
if pre != "" {
if pre != _EMPTY_ {
curStream.pre = pre
}
case "service":
@@ -3421,13 +3567,13 @@ func parseImportStreamOrService(v interface{}, errors, warnings *[]error) (*impo
*errors = append(*errors, err)
continue
}
if accountName == "" || subject == "" {
if accountName == _EMPTY_ || subject == _EMPTY_ {
err := &configErr{tk, "Expect an account name and a subject"}
*errors = append(*errors, err)
continue
}
curService = &importService{an: accountName, sub: subject}
if to != "" {
if to != _EMPTY_ {
curService.to = to
} else {
curService.to = subject
@@ -3445,7 +3591,7 @@ func parseImportStreamOrService(v interface{}, errors, warnings *[]error) (*impo
}
if curStream != nil {
curStream.to = to
if curStream.pre != "" {
if curStream.pre != _EMPTY_ {
err := &configErr{tk, "Stream import can not have a 'prefix' and a 'to' property"}
*errors = append(*errors, err)
continue
@@ -3534,6 +3680,13 @@ func parseAuthorization(v interface{}, opts *Options, errors *[]error, warnings
continue
}
auth.defaultPermissions = permissions
case "auth_callout", "auth_hook":
ac, err := parseAuthCallout(tk, errors, warnings)
if err != nil {
*errors = append(*errors, err)
continue
}
auth.callout = ac
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -3622,7 +3775,7 @@ func parseUsers(mv interface{}, opts *Options, errors *[]error, warnings *[]erro
// Place perms if we have them.
if perms != nil {
// nkey takes precedent.
if nkey.Nkey != "" {
if nkey.Nkey != _EMPTY_ {
nkey.Permissions = perms
} else {
user.Permissions = perms
@@ -3630,15 +3783,15 @@ func parseUsers(mv interface{}, opts *Options, errors *[]error, warnings *[]erro
}
// Check to make sure we have at least an nkey or username <password> defined.
if nkey.Nkey == "" && user.Username == "" {
if nkey.Nkey == _EMPTY_ && user.Username == _EMPTY_ {
return nil, nil, &configErr{tk, "User entry requires a user"}
} else if nkey.Nkey != "" {
} else if nkey.Nkey != _EMPTY_ {
// Make sure the nkey a proper public nkey for a user..
if !nkeys.IsValidPublicUserKey(nkey.Nkey) {
return nil, nil, &configErr{tk, "Not a valid public nkey for a user"}
}
// If we have user or password defined here that is an error.
if user.Username != "" || user.Password != "" {
if user.Username != _EMPTY_ || user.Password != _EMPTY_ {
return nil, nil, &configErr{tk, "Nkey users do not take usernames or passwords"}
}
keys = append(keys, nkey)
@@ -3662,6 +3815,66 @@ func parseAllowedConnectionTypes(tk token, lt *token, mv interface{}, errors *[]
return m
}
// Helper function to parse auth callouts.
func parseAuthCallout(mv interface{}, errors, warnings *[]error) (*AuthCallout, error) {
var (
tk token
lt token
ac = &AuthCallout{}
)
defer convertPanicToErrorList(&lt, errors)
tk, mv = unwrapValue(mv, &lt)
pm, ok := mv.(map[string]interface{})
if !ok {
return nil, &configErr{tk, fmt.Sprintf("Expected authorization callout to be a map/struct, got %+v", mv)}
}
for k, v := range pm {
tk, mv = unwrapValue(v, &lt)
switch strings.ToLower(k) {
case "issuer":
ac.Issuer = mv.(string)
if !nkeys.IsValidPublicAccountKey(ac.Issuer) {
return nil, &configErr{tk, fmt.Sprintf("Expected callout user to be a valid public account nkey, got %q", ac.Issuer)}
}
case "account", "acc":
ac.Account = mv.(string)
case "auth_users", "users":
aua, ok := mv.([]interface{})
if !ok {
return nil, &configErr{tk, fmt.Sprintf("Expected auth_users field to be an array, got %T", v)}
}
for _, uv := range aua {
_, uv = unwrapValue(uv, &lt)
ac.AuthUsers = append(ac.AuthUsers, uv.(string))
}
case "xkey", "key":
ac.XKey = mv.(string)
if !nkeys.IsValidPublicCurveKey(ac.XKey) {
return nil, &configErr{tk, fmt.Sprintf("Expected callout xkey to be a valid public xkey, got %q", ac.XKey)}
}
default:
if !tk.IsUsedVariable() {
err := &configErr{tk, fmt.Sprintf("Unknown field %q parsing authorization callout", k)}
*errors = append(*errors, err)
}
}
}
// Make sure we have all defined. All fields are required.
// If no account specified, selet $G.
if ac.Account == _EMPTY_ {
ac.Account = globalAccountName
}
if ac.Issuer == _EMPTY_ {
return nil, &configErr{tk, "Authorization callouts require an issuer to be specified"}
}
if len(ac.AuthUsers) == 0 {
return nil, &configErr{tk, "Authorization callouts require authorized users to be specified"}
}
return ac, nil
}
// Helper function to parse user/account permissions
func parseUserPermissions(mv interface{}, errors, warnings *[]error) (*Permissions, error) {
var (
@@ -4092,6 +4305,8 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error)
return nil, &configErr{tk, certstore.ErrBadCertMatchField.Error()}
}
tc.CertMatch = certMatch
case "handshake_first", "first", "immediate":
tc.HandshakeFirst = mv.(bool)
case "ocsp_peer":
switch vv := mv.(type) {
case bool:
@@ -4431,7 +4646,7 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
config.ClientAuth = tls.RequireAndVerifyClientCert
}
// Add in CAs if applicable.
if tc.CaFile != "" {
if tc.CaFile != _EMPTY_ {
rootPEM, err := os.ReadFile(tc.CaFile)
if err != nil || rootPEM == nil {
return nil, err
@@ -4462,28 +4677,28 @@ func MergeOptions(fileOpts, flagOpts *Options) *Options {
if flagOpts.Port != 0 {
opts.Port = flagOpts.Port
}
if flagOpts.Host != "" {
if flagOpts.Host != _EMPTY_ {
opts.Host = flagOpts.Host
}
if flagOpts.DontListen {
opts.DontListen = flagOpts.DontListen
}
if flagOpts.ClientAdvertise != "" {
if flagOpts.ClientAdvertise != _EMPTY_ {
opts.ClientAdvertise = flagOpts.ClientAdvertise
}
if flagOpts.Username != "" {
if flagOpts.Username != _EMPTY_ {
opts.Username = flagOpts.Username
}
if flagOpts.Password != "" {
if flagOpts.Password != _EMPTY_ {
opts.Password = flagOpts.Password
}
if flagOpts.Authorization != "" {
if flagOpts.Authorization != _EMPTY_ {
opts.Authorization = flagOpts.Authorization
}
if flagOpts.HTTPPort != 0 {
opts.HTTPPort = flagOpts.HTTPPort
}
if flagOpts.HTTPBasePath != "" {
if flagOpts.HTTPBasePath != _EMPTY_ {
opts.HTTPBasePath = flagOpts.HTTPBasePath
}
if flagOpts.Debug {
@@ -4495,19 +4710,19 @@ func MergeOptions(fileOpts, flagOpts *Options) *Options {
if flagOpts.Logtime {
opts.Logtime = true
}
if flagOpts.LogFile != "" {
if flagOpts.LogFile != _EMPTY_ {
opts.LogFile = flagOpts.LogFile
}
if flagOpts.PidFile != "" {
if flagOpts.PidFile != _EMPTY_ {
opts.PidFile = flagOpts.PidFile
}
if flagOpts.PortsFileDir != "" {
if flagOpts.PortsFileDir != _EMPTY_ {
opts.PortsFileDir = flagOpts.PortsFileDir
}
if flagOpts.ProfPort != 0 {
opts.ProfPort = flagOpts.ProfPort
}
if flagOpts.Cluster.ListenStr != "" {
if flagOpts.Cluster.ListenStr != _EMPTY_ {
opts.Cluster.ListenStr = flagOpts.Cluster.ListenStr
}
if flagOpts.Cluster.NoAdvertise {
@@ -4516,10 +4731,10 @@ func MergeOptions(fileOpts, flagOpts *Options) *Options {
if flagOpts.Cluster.ConnectRetries != 0 {
opts.Cluster.ConnectRetries = flagOpts.Cluster.ConnectRetries
}
if flagOpts.Cluster.Advertise != "" {
if flagOpts.Cluster.Advertise != _EMPTY_ {
opts.Cluster.Advertise = flagOpts.Cluster.Advertise
}
if flagOpts.RoutesStr != "" {
if flagOpts.RoutesStr != _EMPTY_ {
mergeRoutes(&opts, flagOpts)
}
if flagOpts.JetStream {
@@ -4635,10 +4850,10 @@ func getInterfaceIPs() ([]net.IP, error) {
func setBaselineOptions(opts *Options) {
// Setup non-standard Go defaults
if opts.Host == "" {
if opts.Host == _EMPTY_ {
opts.Host = DEFAULT_HOST
}
if opts.HTTPHost == "" {
if opts.HTTPHost == _EMPTY_ {
// Default to same bind from server if left undefined
opts.HTTPHost = opts.Host
}
@@ -4663,8 +4878,8 @@ func setBaselineOptions(opts *Options) {
if opts.AuthTimeout == 0 {
opts.AuthTimeout = getDefaultAuthTimeout(opts.TLSConfig, opts.TLSTimeout)
}
if opts.Cluster.Port != 0 {
if opts.Cluster.Host == "" {
if opts.Cluster.Port != 0 || opts.Cluster.ListenStr != _EMPTY_ {
if opts.Cluster.Host == _EMPTY_ {
opts.Cluster.Host = DEFAULT_HOST
}
if opts.Cluster.TLSTimeout == 0 {
@@ -4673,9 +4888,43 @@ func setBaselineOptions(opts *Options) {
if opts.Cluster.AuthTimeout == 0 {
opts.Cluster.AuthTimeout = getDefaultAuthTimeout(opts.Cluster.TLSConfig, opts.Cluster.TLSTimeout)
}
if opts.Cluster.PoolSize == 0 {
opts.Cluster.PoolSize = DEFAULT_ROUTE_POOL_SIZE
}
// Unless pooling/accounts are disabled (by PoolSize being set to -1),
// check for Cluster.Accounts. Add the system account if not present and
// unless we have a configuration that disabled it.
if opts.Cluster.PoolSize > 0 {
sysAccName := opts.SystemAccount
if sysAccName == _EMPTY_ && !opts.NoSystemAccount {
sysAccName = DEFAULT_SYSTEM_ACCOUNT
}
if sysAccName != _EMPTY_ {
var found bool
for _, acc := range opts.Cluster.PinnedAccounts {
if acc == sysAccName {
found = true
break
}
}
if !found {
opts.Cluster.PinnedAccounts = append(opts.Cluster.PinnedAccounts, sysAccName)
}
}
}
// Default to compression "accept", which means that compression is not
// initiated, but if the remote selects compression, this server will
// use the same.
if c := &opts.Cluster.Compression; c.Mode == _EMPTY_ {
if testDefaultClusterCompression != _EMPTY_ {
c.Mode = testDefaultClusterCompression
} else {
c.Mode = CompressionAccept
}
}
}
if opts.LeafNode.Port != 0 {
if opts.LeafNode.Host == "" {
if opts.LeafNode.Host == _EMPTY_ {
opts.LeafNode.Host = DEFAULT_HOST
}
if opts.LeafNode.TLSTimeout == 0 {
@@ -4684,15 +4933,31 @@ func setBaselineOptions(opts *Options) {
if opts.LeafNode.AuthTimeout == 0 {
opts.LeafNode.AuthTimeout = getDefaultAuthTimeout(opts.LeafNode.TLSConfig, opts.LeafNode.TLSTimeout)
}
// Default to compression "s2_auto".
if c := &opts.LeafNode.Compression; c.Mode == _EMPTY_ {
if testDefaultLeafNodeCompression != _EMPTY_ {
c.Mode = testDefaultLeafNodeCompression
} else {
c.Mode = CompressionS2Auto
}
}
}
// Set baseline connect port for remotes.
for _, r := range opts.LeafNode.Remotes {
if r != nil {
for _, u := range r.URLs {
if u.Port() == "" {
if u.Port() == _EMPTY_ {
u.Host = net.JoinHostPort(u.Host, strconv.Itoa(DEFAULT_LEAFNODE_PORT))
}
}
// Default to compression "s2_auto".
if c := &r.Compression; c.Mode == _EMPTY_ {
if testDefaultLeafNodeCompression != _EMPTY_ {
c.Mode = testDefaultLeafNodeCompression
} else {
c.Mode = CompressionS2Auto
}
}
}
}
@@ -4723,7 +4988,7 @@ func setBaselineOptions(opts *Options) {
opts.LameDuckGracePeriod = DEFAULT_LAME_DUCK_GRACE_PERIOD
}
if opts.Gateway.Port != 0 {
if opts.Gateway.Host == "" {
if opts.Gateway.Host == _EMPTY_ {
opts.Gateway.Host = DEFAULT_HOST
}
if opts.Gateway.TLSTimeout == 0 {
@@ -4740,12 +5005,12 @@ func setBaselineOptions(opts *Options) {
opts.ReconnectErrorReports = DEFAULT_RECONNECT_ERROR_REPORTS
}
if opts.Websocket.Port != 0 {
if opts.Websocket.Host == "" {
if opts.Websocket.Host == _EMPTY_ {
opts.Websocket.Host = DEFAULT_HOST
}
}
if opts.MQTT.Port != 0 {
if opts.MQTT.Host == "" {
if opts.MQTT.Host == _EMPTY_ {
opts.MQTT.Host = DEFAULT_HOST
}
if opts.MQTT.TLSTimeout == 0 {
@@ -4759,6 +5024,9 @@ func setBaselineOptions(opts *Options) {
if opts.JetStreamMaxStore == 0 && !opts.maxStoreSet {
opts.JetStreamMaxStore = -1
}
if opts.SyncInterval == 0 && !opts.syncSet {
opts.SyncInterval = defaultSyncInterval
}
}
func getDefaultAuthTimeout(tls *tls.Config, tlsTimeout float64) float64 {
@@ -4793,13 +5061,13 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
fs.BoolVar(&showHelp, "help", false, "Show this message.")
fs.IntVar(&opts.Port, "port", 0, "Port to listen on.")
fs.IntVar(&opts.Port, "p", 0, "Port to listen on.")
fs.StringVar(&opts.ServerName, "n", "", "Server name.")
fs.StringVar(&opts.ServerName, "name", "", "Server name.")
fs.StringVar(&opts.ServerName, "server_name", "", "Server name.")
fs.StringVar(&opts.Host, "addr", "", "Network host to listen on.")
fs.StringVar(&opts.Host, "a", "", "Network host to listen on.")
fs.StringVar(&opts.Host, "net", "", "Network host to listen on.")
fs.StringVar(&opts.ClientAdvertise, "client_advertise", "", "Client URL to advertise to other servers.")
fs.StringVar(&opts.ServerName, "n", _EMPTY_, "Server name.")
fs.StringVar(&opts.ServerName, "name", _EMPTY_, "Server name.")
fs.StringVar(&opts.ServerName, "server_name", _EMPTY_, "Server name.")
fs.StringVar(&opts.Host, "addr", _EMPTY_, "Network host to listen on.")
fs.StringVar(&opts.Host, "a", _EMPTY_, "Network host to listen on.")
fs.StringVar(&opts.Host, "net", _EMPTY_, "Network host to listen on.")
fs.StringVar(&opts.ClientAdvertise, "client_advertise", _EMPTY_, "Client URL to advertise to other servers.")
fs.BoolVar(&opts.Debug, "D", false, "Enable Debug logging.")
fs.BoolVar(&opts.Debug, "debug", false, "Enable Debug logging.")
fs.BoolVar(&opts.Trace, "V", false, "Enable Trace logging.")
@@ -4817,8 +5085,8 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
fs.IntVar(&opts.HTTPPort, "http_port", 0, "HTTP Port for /varz, /connz endpoints.")
fs.IntVar(&opts.HTTPSPort, "ms", 0, "HTTPS Port for /varz, /connz endpoints.")
fs.IntVar(&opts.HTTPSPort, "https_port", 0, "HTTPS Port for /varz, /connz endpoints.")
fs.StringVar(&configFile, "c", "", "Configuration file.")
fs.StringVar(&configFile, "config", "", "Configuration file.")
fs.StringVar(&configFile, "c", _EMPTY_, "Configuration file.")
fs.StringVar(&configFile, "config", _EMPTY_, "Configuration file.")
fs.BoolVar(&opts.CheckConfig, "t", false, "Check configuration and exit.")
fs.StringVar(&signal, "sl", "", "Send signal to nats-server process (ldm, stop, quit, term, reopen, reload).")
fs.StringVar(&signal, "signal", "", "Send signal to nats-server process (ldm, stop, quit, term, reopen, reload).")
@@ -4830,29 +5098,29 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
fs.Int64Var(&opts.LogSizeLimit, "log_size_limit", 0, "Logfile size limit being auto-rotated")
fs.BoolVar(&opts.Syslog, "s", false, "Enable syslog as log method.")
fs.BoolVar(&opts.Syslog, "syslog", false, "Enable syslog as log method.")
fs.StringVar(&opts.RemoteSyslog, "r", "", "Syslog server addr (udp://127.0.0.1:514).")
fs.StringVar(&opts.RemoteSyslog, "remote_syslog", "", "Syslog server addr (udp://127.0.0.1:514).")
fs.StringVar(&opts.RemoteSyslog, "r", _EMPTY_, "Syslog server addr (udp://127.0.0.1:514).")
fs.StringVar(&opts.RemoteSyslog, "remote_syslog", _EMPTY_, "Syslog server addr (udp://127.0.0.1:514).")
fs.BoolVar(&showVersion, "version", false, "Print version information.")
fs.BoolVar(&showVersion, "v", false, "Print version information.")
fs.IntVar(&opts.ProfPort, "profile", 0, "Profiling HTTP port.")
fs.StringVar(&opts.RoutesStr, "routes", "", "Routes to actively solicit a connection.")
fs.StringVar(&opts.Cluster.ListenStr, "cluster", "", "Cluster url from which members can solicit routes.")
fs.StringVar(&opts.Cluster.ListenStr, "cluster_listen", "", "Cluster url from which members can solicit routes.")
fs.StringVar(&opts.Cluster.Advertise, "cluster_advertise", "", "Cluster URL to advertise to other servers.")
fs.StringVar(&opts.RoutesStr, "routes", _EMPTY_, "Routes to actively solicit a connection.")
fs.StringVar(&opts.Cluster.ListenStr, "cluster", _EMPTY_, "Cluster url from which members can solicit routes.")
fs.StringVar(&opts.Cluster.ListenStr, "cluster_listen", _EMPTY_, "Cluster url from which members can solicit routes.")
fs.StringVar(&opts.Cluster.Advertise, "cluster_advertise", _EMPTY_, "Cluster URL to advertise to other servers.")
fs.BoolVar(&opts.Cluster.NoAdvertise, "no_advertise", false, "Advertise known cluster IPs to clients.")
fs.IntVar(&opts.Cluster.ConnectRetries, "connect_retries", 0, "For implicit routes, number of connect retries.")
fs.StringVar(&opts.Cluster.Name, "cluster_name", "", "Cluster Name, if not set one will be dynamically generated.")
fs.StringVar(&opts.Cluster.Name, "cluster_name", _EMPTY_, "Cluster Name, if not set one will be dynamically generated.")
fs.BoolVar(&showTLSHelp, "help_tls", false, "TLS help.")
fs.BoolVar(&opts.TLS, "tls", false, "Enable TLS.")
fs.BoolVar(&opts.TLSVerify, "tlsverify", false, "Enable TLS with client verification.")
fs.StringVar(&opts.TLSCert, "tlscert", "", "Server certificate file.")
fs.StringVar(&opts.TLSKey, "tlskey", "", "Private key for server certificate.")
fs.StringVar(&opts.TLSCaCert, "tlscacert", "", "Client certificate CA for verification.")
fs.StringVar(&opts.TLSCert, "tlscert", _EMPTY_, "Server certificate file.")
fs.StringVar(&opts.TLSKey, "tlskey", _EMPTY_, "Private key for server certificate.")
fs.StringVar(&opts.TLSCaCert, "tlscacert", _EMPTY_, "Client certificate CA for verification.")
fs.IntVar(&opts.MaxTracedMsgLen, "max_traced_msg_len", 0, "Maximum printable length for traced messages. 0 for unlimited.")
fs.BoolVar(&opts.JetStream, "js", false, "Enable JetStream.")
fs.BoolVar(&opts.JetStream, "jetstream", false, "Enable JetStream.")
fs.StringVar(&opts.StoreDir, "sd", "", "Storage directory.")
fs.StringVar(&opts.StoreDir, "store_dir", "", "Storage directory.")
fs.StringVar(&opts.StoreDir, "sd", _EMPTY_, "Storage directory.")
fs.StringVar(&opts.StoreDir, "store_dir", _EMPTY_, "Storage directory.")
// The flags definition above set "default" values to some of the options.
// Calling Parse() here will override the default options with any value
@@ -5003,7 +5271,7 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
flagErr = overrideCluster(opts)
case "routes":
// Keep in mind that the flag has updated opts.RoutesStr at this point.
if opts.RoutesStr == "" {
if opts.RoutesStr == _EMPTY_ {
// Set routes array to nil since routes string is empty
opts.Routes = nil
return
@@ -5028,7 +5296,7 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
// If we don't have cluster defined in the configuration
// file and no cluster listen string override, but we do
// have a routes override, we need to report misconfiguration.
if opts.RoutesStr != "" && opts.Cluster.ListenStr == "" && opts.Cluster.Host == "" && opts.Cluster.Port == 0 {
if opts.RoutesStr != _EMPTY_ && opts.Cluster.ListenStr == _EMPTY_ && opts.Cluster.Host == _EMPTY_ && opts.Cluster.Port == 0 {
return nil, errors.New("solicited routes require cluster capabilities, e.g. --cluster")
}
@@ -5048,10 +5316,10 @@ func normalizeBasePath(p string) string {
// overrideTLS is called when at least "-tls=true" has been set.
func overrideTLS(opts *Options) error {
if opts.TLSCert == "" {
if opts.TLSCert == _EMPTY_ {
return errors.New("TLS Server certificate must be present and valid")
}
if opts.TLSKey == "" {
if opts.TLSKey == _EMPTY_ {
return errors.New("TLS Server private key must be present and valid")
}
@@ -5071,7 +5339,7 @@ func overrideTLS(opts *Options) error {
// has explicitly be set in the command line. If it is set to empty string, it will
// clear the Cluster options.
func overrideCluster(opts *Options) error {
if opts.Cluster.ListenStr == "" {
if opts.Cluster.ListenStr == _EMPTY_ {
// This one is enough to disable clustering.
opts.Cluster.Port = 0
return nil
@@ -5114,8 +5382,8 @@ func overrideCluster(opts *Options) error {
} else {
// Since we override from flag and there is no user/pwd, make
// sure we clear what we may have gotten from config file.
opts.Cluster.Username = ""
opts.Cluster.Password = ""
opts.Cluster.Username = _EMPTY_
opts.Cluster.Password = _EMPTY_
}
return nil
@@ -5155,9 +5423,9 @@ func homeDir() (string, error) {
userProfile := os.Getenv("USERPROFILE")
home := filepath.Join(homeDrive, homePath)
if homeDrive == "" || homePath == "" {
if userProfile == "" {
return "", errors.New("nats: failed to get home dir, require %HOMEDRIVE% and %HOMEPATH% or %USERPROFILE%")
if homeDrive == _EMPTY_ || homePath == _EMPTY_ {
if userProfile == _EMPTY_ {
return _EMPTY_, errors.New("nats: failed to get home dir, require %HOMEDRIVE% and %HOMEPATH% or %USERPROFILE%")
}
home = userProfile
}
@@ -5166,8 +5434,8 @@ func homeDir() (string, error) {
}
home := os.Getenv("HOME")
if home == "" {
return "", errors.New("failed to get home dir, require $HOME")
if home == _EMPTY_ {
return _EMPTY_, errors.New("failed to get home dir, require $HOME")
}
return home, nil
}
@@ -5181,7 +5449,7 @@ func expandPath(p string) (string, error) {
home, err := homeDir()
if err != nil {
return "", err
return _EMPTY_, err
}
return filepath.Join(home, p[1:]), nil
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2022 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.
//
// Copied from pse_openbsd.go
package pse
import (
"fmt"
"os"
"os/exec"
)
// ProcUsage returns CPU usage
func ProcUsage(pcpu *float64, rss, vss *int64) error {
pidStr := fmt.Sprintf("%d", os.Getpid())
out, err := exec.Command("ps", "o", "pcpu=,rss=,vsz=", "-p", pidStr).Output()
if err != nil {
*rss, *vss = -1, -1
return fmt.Errorf("ps call failed:%v", err)
}
fmt.Sscanf(string(out), "%f %d %d", pcpu, rss, vss)
*rss *= 1024 // 1k blocks, want bytes.
*vss *= 1024 // 1k blocks, want bytes.
return nil
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2023 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.
//go:build zos
// +build zos
package pse
// This is a placeholder for now.
func ProcUsage(pcpu *float64, rss, vss *int64) error {
*pcpu = 0.0
*rss = 0
*vss = 0
return nil
}
+6 -5
View File
@@ -343,7 +343,7 @@ func (s *Server) bootstrapRaftNode(cfg *RaftConfig, knownPeers []string, allPeer
}
// startRaftNode will start the raft node.
func (s *Server) startRaftNode(accName string, cfg *RaftConfig) (RaftNode, error) {
func (s *Server) startRaftNode(accName string, cfg *RaftConfig, labels pprofLabels) (RaftNode, error) {
if cfg == nil {
return nil, errNilCfg
}
@@ -497,8 +497,9 @@ func (s *Server) startRaftNode(accName string, cfg *RaftConfig) (RaftNode, error
n.llqrt = time.Now()
n.Unlock()
labels["group"] = n.group
s.registerRaftNode(n.group, n)
s.startGoRoutine(n.run)
s.startGoRoutine(n.run, labels)
s.startGoRoutine(n.fileWriter)
return n, nil
@@ -653,7 +654,7 @@ func (n *raft) Propose(data []byte) error {
n.RLock()
if n.state != Leader {
n.RUnlock()
n.debug("Proposal ignored, not leader")
n.debug("Proposal ignored, not leader (state: %v)", n.state)
return errNotLeader
}
// Error if we had a previous write error.
@@ -674,7 +675,7 @@ func (n *raft) ProposeDirect(entries []*Entry) error {
n.RLock()
if n.state != Leader {
n.RUnlock()
n.debug("Proposal ignored, not leader")
n.debug("Direct proposal ignored, not leader (state: %v)", n.state)
return errNotLeader
}
// Error if we had a previous write error.
@@ -1689,7 +1690,7 @@ func (n *raft) run() {
gw := s.gateway
for {
s.mu.Lock()
ready := len(s.routes)+len(s.leafs) > 0
ready := s.numRemotes()+len(s.leafs) > 0
if !ready && gw.enabled {
gw.RLock()
ready = len(gw.out)+len(gw.in) > 0
+540 -84
View File
@@ -24,7 +24,10 @@ import (
"sync/atomic"
"time"
"github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nuid"
)
// FlagSnapshot captures the server options as specified by CLI flags at
@@ -57,6 +60,10 @@ type option interface {
// cluster permissions.
IsClusterPermsChange() bool
// IsClusterPoolSizeOrAccountsChange indicates if this option requires
// special handling for changes in cluster's pool size or accounts list.
IsClusterPoolSizeOrAccountsChange() bool
// IsJetStreamChange inidicates a change in the servers config for JetStream.
// Account changes will be handled separately in reloadAuthorization.
IsJetStreamChange() bool
@@ -88,6 +95,10 @@ func (n noopOption) IsClusterPermsChange() bool {
return false
}
func (n noopOption) IsClusterPoolSizeOrAccountsChange() bool {
return false
}
func (n noopOption) IsJetStreamChange() bool {
return false
}
@@ -347,8 +358,12 @@ func (u *nkeysOption) Apply(server *Server) {
// clusterOption implements the option interface for the `cluster` setting.
type clusterOption struct {
authOption
newValue ClusterOpts
permsChanged bool
newValue ClusterOpts
permsChanged bool
accsAdded []string
accsRemoved []string
poolSizeChanged bool
compressChanged bool
}
// Apply the cluster change.
@@ -367,10 +382,44 @@ func (c *clusterOption) Apply(s *Server) {
s.routeInfo.WSConnectURLs = s.websocket.connectURLs
}
s.setRouteInfoHostPortAndIP()
var routes []*client
if c.compressChanged {
co := &s.getOpts().Cluster.Compression
newMode := co.Mode
s.forEachRoute(func(r *client) {
r.mu.Lock()
// Skip routes that are "not supported" (because they will never do
// compression) or the routes that have already the new compression
// mode.
if r.route.compression == CompressionNotSupported || r.route.compression == newMode {
r.mu.Unlock()
return
}
// We need to close the route if it had compression "off" or the new
// mode is compression "off", or if the new mode is "accept", because
// these require negotiation.
if r.route.compression == CompressionOff || newMode == CompressionOff || newMode == CompressionAccept {
routes = append(routes, r)
} else if newMode == CompressionS2Auto {
// If the mode is "s2_auto", we need to check if there is really
// need to change, and at any rate, we want to save the actual
// compression level here, not s2_auto.
r.updateS2AutoCompressionLevel(co, &r.route.compression)
} else {
// Simply change the compression writer
r.out.cw = s2.NewWriter(nil, s2WriterOptions(newMode)...)
r.route.compression = newMode
}
r.mu.Unlock()
})
}
s.mu.Unlock()
if c.newValue.Name != "" && c.newValue.Name != s.ClusterName() {
s.setClusterName(c.newValue.Name)
}
for _, r := range routes {
r.closeConnection(ClientClosed)
}
s.Noticef("Reloaded: cluster")
if tlsRequired && c.newValue.TLSConfig.InsecureSkipVerify {
s.Warnf(clusterTLSInsecureWarning)
@@ -381,6 +430,32 @@ func (c *clusterOption) IsClusterPermsChange() bool {
return c.permsChanged
}
func (c *clusterOption) IsClusterPoolSizeOrAccountsChange() bool {
return c.poolSizeChanged || len(c.accsAdded) > 0 || len(c.accsRemoved) > 0
}
func (c *clusterOption) diffPoolAndAccounts(old *ClusterOpts) {
c.poolSizeChanged = c.newValue.PoolSize != old.PoolSize
addLoop:
for _, na := range c.newValue.PinnedAccounts {
for _, oa := range old.PinnedAccounts {
if na == oa {
continue addLoop
}
}
c.accsAdded = append(c.accsAdded, na)
}
removeLoop:
for _, oa := range old.PinnedAccounts {
for _, na := range c.newValue.PinnedAccounts {
if oa == na {
continue removeLoop
}
}
c.accsRemoved = append(c.accsRemoved, oa)
}
}
// routesOption implements the option interface for the cluster `routes`
// setting.
type routesOption struct {
@@ -392,12 +467,12 @@ type routesOption struct {
// Apply the route changes by adding and removing the necessary routes.
func (r *routesOption) Apply(server *Server) {
server.mu.Lock()
routes := make([]*client, len(server.routes))
routes := make([]*client, server.numRoutes())
i := 0
for _, client := range server.routes {
routes[i] = client
server.forEachRoute(func(r *client) {
routes[i] = r
i++
}
})
// If there was a change, notify monitoring code that it should
// update the route URLs if /varz endpoint is inspected.
if len(r.add)+len(r.remove) > 0 {
@@ -425,7 +500,7 @@ func (r *routesOption) Apply(server *Server) {
// Add routes.
server.mu.Lock()
server.solicitRoutes(r.add)
server.solicitRoutes(r.add, server.getOpts().Cluster.PinnedAccounts)
server.mu.Unlock()
server.Noticef("Reloaded: cluster routes")
@@ -730,6 +805,84 @@ func (o *mqttInactiveThresholdReload) Apply(s *Server) {
s.Noticef("Reloaded: MQTT consumer_inactive_threshold = %v", o.newValue)
}
type leafNodeOption struct {
noopOption
tlsFirstChanged bool
compressionChanged bool
}
func (l *leafNodeOption) Apply(s *Server) {
opts := s.getOpts()
if l.tlsFirstChanged {
s.Noticef("Reloaded: LeafNode TLS HandshakeFirst value is: %v", opts.LeafNode.TLSHandshakeFirst)
for _, r := range opts.LeafNode.Remotes {
s.Noticef("Reloaded: LeafNode Remote to %v TLS HandshakeFirst value is: %v", r.URLs, r.TLSHandshakeFirst)
}
}
if l.compressionChanged {
var leafs []*client
acceptSideCompOpts := &opts.LeafNode.Compression
s.mu.RLock()
// First, update our internal leaf remote configurations with the new
// compress options.
// Since changing the remotes (as in adding/removing) is currently not
// supported, we know that we should have the same number in Options
// than in leafRemoteCfgs, but to be sure, use the max size.
max := len(opts.LeafNode.Remotes)
if l := len(s.leafRemoteCfgs); l < max {
max = l
}
for i := 0; i < max; i++ {
lr := s.leafRemoteCfgs[i]
lr.Lock()
lr.Compression = opts.LeafNode.Remotes[i].Compression
lr.Unlock()
}
for _, l := range s.leafs {
var co *CompressionOpts
l.mu.Lock()
if r := l.leaf.remote; r != nil {
co = &r.Compression
} else {
co = acceptSideCompOpts
}
newMode := co.Mode
// Skip leaf connections that are "not supported" (because they
// will never do compression) or the ones that have already the
// new compression mode.
if l.leaf.compression == CompressionNotSupported || l.leaf.compression == newMode {
l.mu.Unlock()
continue
}
// We need to close the connections if it had compression "off" or the new
// mode is compression "off", or if the new mode is "accept", because
// these require negotiation.
if l.leaf.compression == CompressionOff || newMode == CompressionOff || newMode == CompressionAccept {
leafs = append(leafs, l)
} else if newMode == CompressionS2Auto {
// If the mode is "s2_auto", we need to check if there is really
// need to change, and at any rate, we want to save the actual
// compression level here, not s2_auto.
l.updateS2AutoCompressionLevel(co, &l.leaf.compression)
} else {
// Simply change the compression writer
l.out.cw = s2.NewWriter(nil, s2WriterOptions(newMode)...)
l.leaf.compression = newMode
}
l.mu.Unlock()
}
s.mu.RUnlock()
// Close the connections for which negotiation is required.
for _, l := range leafs {
l.closeConnection(ClientClosed)
}
s.Noticef("Reloaded: LeafNode compression settings")
}
}
// Compares options and disconnects clients that are no longer listed in pinned certs. Lock must not be held.
func (s *Server) recheckPinnedCerts(curOpts *Options, newOpts *Options) {
s.mu.Lock()
@@ -765,14 +918,22 @@ func (s *Server) recheckPinnedCerts(curOpts *Options, newOpts *Options) {
checkClients(LEAF, s.leafs, newOpts.LeafNode.TLSPinnedCerts)
}
if !reflect.DeepEqual(newOpts.Cluster.TLSPinnedCerts, curOpts.Cluster.TLSPinnedCerts) {
checkClients(ROUTER, s.routes, newOpts.Cluster.TLSPinnedCerts)
s.forEachRoute(func(c *client) {
if !c.matchesPinnedCert(newOpts.Cluster.TLSPinnedCerts) {
disconnectClients = append(disconnectClients, c)
}
})
}
if reflect.DeepEqual(newOpts.Gateway.TLSPinnedCerts, curOpts.Gateway.TLSPinnedCerts) {
for _, c := range s.remotes {
if s.gateway.enabled && reflect.DeepEqual(newOpts.Gateway.TLSPinnedCerts, curOpts.Gateway.TLSPinnedCerts) {
gw := s.gateway
gw.RLock()
for _, c := range gw.out {
if !c.matchesPinnedCert(newOpts.Gateway.TLSPinnedCerts) {
disconnectClients = append(disconnectClients, c)
}
}
checkClients(GATEWAY, gw.in, newOpts.Gateway.TLSPinnedCerts)
gw.RUnlock()
}
s.mu.Unlock()
if len(disconnectClients) > 0 {
@@ -824,6 +985,13 @@ func (s *Server) ReloadOptions(newOpts *Options) error {
s.mu.Unlock()
// In case "-cluster ..." was provided through the command line, this will
// properly set the Cluster.Host/Port etc...
if l := curOpts.Cluster.ListenStr; l != _EMPTY_ {
newOpts.Cluster.ListenStr = l
overrideCluster(newOpts)
}
// Apply flags over config file settings.
newOpts = MergeOptions(newOpts, FlagSnapshot)
@@ -962,6 +1130,7 @@ func imposeOrder(value interface{}) error {
*URLAccResolver, *MemAccResolver, *DirAccResolver, *CacheDirAccResolver, Authentication, MQTTOpts, jwt.TagList,
*OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig:
// explicitly skipped types
case *AuthCallout:
default:
// this will fail during unit tests
return fmt.Errorf("OnReload, sort or explicitly skip type: %s",
@@ -1063,8 +1232,28 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
if err := validateClusterOpts(oldClusterOpts, newClusterOpts); err != nil {
return nil, err
}
permsChanged := !reflect.DeepEqual(newClusterOpts.Permissions, oldClusterOpts.Permissions)
diffOpts = append(diffOpts, &clusterOption{newValue: newClusterOpts, permsChanged: permsChanged})
co := &clusterOption{
newValue: newClusterOpts,
permsChanged: !reflect.DeepEqual(newClusterOpts.Permissions, oldClusterOpts.Permissions),
compressChanged: !reflect.DeepEqual(oldClusterOpts.Compression, newClusterOpts.Compression),
}
co.diffPoolAndAccounts(&oldClusterOpts)
// If there are added accounts, first make sure that we can look them up.
// If we can't let's fail the reload.
for _, acc := range co.accsAdded {
if _, err := s.LookupAccount(acc); err != nil {
return nil, fmt.Errorf("unable to add account %q to the list of dedicated routes: %v", acc, err)
}
}
// If pool_size has been set to negative (but was not before), then let's
// add the system account to the list of removed accounts (we don't have
// to check if already there, duplicates are ok in that case).
if newClusterOpts.PoolSize < 0 && oldClusterOpts.PoolSize >= 0 {
if sys := s.SystemAccount(); sys != nil {
co.accsRemoved = append(co.accsRemoved, sys.GetName())
}
}
diffOpts = append(diffOpts, co)
case "routes":
add, remove := diffRoutes(oldValue.([]*url.URL), newValue.([]*url.URL))
diffOpts = append(diffOpts, &routesOption{add: add, remove: remove})
@@ -1137,6 +1326,38 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
tmpNew.TLSConfig = nil
tmpOld.tlsConfigOpts = nil
tmpNew.tlsConfigOpts = nil
// We will allow TLSHandshakeFirst to me config reloaded. First,
// we just want to detect if there was a change in the leafnodes{}
// block, and if not, we will check the remotes.
handshakeFirstChanged := tmpOld.TLSHandshakeFirst != tmpNew.TLSHandshakeFirst
// If changed, set them (in the temporary variables) to false so that the
// rest of the comparison does not fail.
if handshakeFirstChanged {
tmpOld.TLSHandshakeFirst, tmpNew.TLSHandshakeFirst = false, false
} else if len(tmpOld.Remotes) == len(tmpNew.Remotes) {
// Since we don't support changes in the remotes, we will do a
// simple pass to see if there was a change of this field.
for i := 0; i < len(tmpOld.Remotes); i++ {
if tmpOld.Remotes[i].TLSHandshakeFirst != tmpNew.Remotes[i].TLSHandshakeFirst {
handshakeFirstChanged = true
break
}
}
}
// 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)
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) {
compressionChanged = true
break
}
}
}
// Need to do the same for remote leafnodes' TLS configs.
// But we can't just set remotes' TLSConfig to nil otherwise this
@@ -1225,6 +1446,11 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
return nil, fmt.Errorf("config reload not supported for %s: old=%v, new=%v",
field.Name, oldValue, newValue)
}
diffOpts = append(diffOpts, &leafNodeOption{
tlsFirstChanged: handshakeFirstChanged,
compressionChanged: compressionChanged,
})
case "jetstream":
new := newValue.(bool)
old := oldValue.(bool)
@@ -1413,12 +1639,15 @@ func copyRemoteLNConfigForReloadCompare(current []*RemoteLeafOpts) []*RemoteLeaf
cp := *rcfg
cp.TLSConfig = nil
cp.tlsConfigOpts = nil
cp.TLSHandshakeFirst = false
// This is set only when processing a CONNECT, so reset here so that we
// don't fail the DeepEqual comparison.
cp.TLS = false
// For now, remove DenyImports/Exports since those get modified at runtime
// to add JS APIs.
cp.DenyImports, cp.DenyExports = nil, nil
// Remove compression mode
cp.Compression = CompressionOpts{}
rlns = append(rlns, &cp)
}
return rlns
@@ -1434,6 +1663,7 @@ func (s *Server) applyOptions(ctx *reloadContext, opts []option) {
jsEnabled = false
reloadTLS = false
isStatszChange = false
co *clusterOption
)
for _, opt := range opts {
opt.Apply(s)
@@ -1449,6 +1679,9 @@ func (s *Server) applyOptions(ctx *reloadContext, opts []option) {
if opt.IsTLSChange() {
reloadTLS = true
}
if opt.IsClusterPoolSizeOrAccountsChange() {
co = opt.(*clusterOption)
}
if opt.IsClusterPermsChange() {
reloadClusterPerms = true
}
@@ -1473,7 +1706,11 @@ func (s *Server) applyOptions(ctx *reloadContext, opts []option) {
if reloadClusterPerms {
s.reloadClusterPermissions(ctx.oldClusterPerms)
}
newOpts := s.getOpts()
// If we need to reload cluster pool/per-account, then co will be not nil
if co != nil {
s.reloadClusterPoolAndAccounts(co, newOpts)
}
if reloadJetstream {
if !jsEnabled {
s.DisableJetStream()
@@ -1492,7 +1729,6 @@ func (s *Server) applyOptions(ctx *reloadContext, opts []option) {
// For remote gateways and leafnodes, make sure that their TLS configuration
// is updated (since the config is "captured" early and changes would otherwise
// not be visible).
newOpts := s.getOpts()
if s.gateway.enabled {
s.gateway.updateRemotesTLSConfig(newOpts)
}
@@ -1539,7 +1775,7 @@ func (s *Server) reloadClientTraceLevel() {
// Update their trace level when not holding server or gateway lock
s.mu.Lock()
clientCnt := 1 + len(s.clients) + len(s.grTmpClients) + len(s.routes) + len(s.leafs)
clientCnt := 1 + len(s.clients) + len(s.grTmpClients) + s.numRoutes() + len(s.leafs)
s.mu.Unlock()
s.gateway.RLock()
@@ -1553,12 +1789,15 @@ func (s *Server) reloadClientTraceLevel() {
clients = append(clients, s.sys.client)
}
cMaps := []map[uint64]*client{s.clients, s.grTmpClients, s.routes, s.leafs}
cMaps := []map[uint64]*client{s.clients, s.grTmpClients, s.leafs}
for _, m := range cMaps {
for _, c := range m {
clients = append(clients, c)
}
}
s.forEachRoute(func(c *client) {
clients = append(clients, c)
})
s.mu.Unlock()
s.gateway.RLock()
@@ -1671,9 +1910,9 @@ func (s *Server) reloadAuthorization() {
clients = append(clients, client)
}
}
for _, route := range s.routes {
s.forEachRoute(func(route *client) {
routes = append(routes, route)
}
})
// Check here for any system/internal clients which will not be in the servers map of normal clients.
if s.sys != nil && s.sys.account != nil && !opts.NoSystemAccount {
s.accounts.Store(s.sys.account.Name, s.sys.account)
@@ -1770,11 +2009,13 @@ func (s *Server) clientHasMovedToDifferentAccount(c *client) bool {
nu *NkeyUser
u *User
)
if c.opts.Nkey != "" {
c.mu.Lock()
defer c.mu.Unlock()
if c.opts.Nkey != _EMPTY_ {
if s.nkeys != nil {
nu = s.nkeys[c.opts.Nkey]
}
} else if c.opts.Username != "" {
} else if c.opts.Username != _EMPTY_ {
if s.users != nil {
u = s.users[c.opts.Username]
}
@@ -1782,12 +2023,10 @@ func (s *Server) clientHasMovedToDifferentAccount(c *client) bool {
return false
}
// Get the current account name
c.mu.Lock()
var curAccName string
if c.acc != nil {
curAccName = c.acc.Name
}
c.mu.Unlock()
if nu != nil && nu.Account != nil {
return curAccName != nu.Account.Name
} else if u != nil && u.Account != nil {
@@ -1804,22 +2043,14 @@ func (s *Server) clientHasMovedToDifferentAccount(c *client) bool {
// import subjects.
func (s *Server) reloadClusterPermissions(oldPerms *RoutePermissions) {
s.mu.Lock()
var (
infoJSON []byte
newPerms = s.opts.Cluster.Permissions
routes = make(map[uint64]*client, len(s.routes))
withNewProto int
)
newPerms := s.getOpts().Cluster.Permissions
routes := make(map[uint64]*client, s.numRoutes())
// Get all connected routes
for i, route := range s.routes {
// Count the number of routes that can understand receiving INFO updates.
s.forEachRoute(func(route *client) {
route.mu.Lock()
if route.opts.Protocol >= RouteProtoInfo {
withNewProto++
}
routes[route.cid] = route
route.mu.Unlock()
routes[i] = route
}
})
// If new permissions is nil, then clear routeInfo import/export
if newPerms == nil {
s.routeInfo.Import = nil
@@ -1828,23 +2059,23 @@ func (s *Server) reloadClusterPermissions(oldPerms *RoutePermissions) {
s.routeInfo.Import = newPerms.Import
s.routeInfo.Export = newPerms.Export
}
// Regenerate route INFO
s.generateRouteInfoJSON()
infoJSON = s.routeInfoJSON
gacc := s.gacc
infoJSON := generateInfoJSON(&s.routeInfo)
s.mu.Unlock()
// If there were no route, we are done
if len(routes) == 0 {
return
// Close connections for routes that don't understand async INFO.
for _, route := range routes {
route.mu.Lock()
close := route.opts.Protocol < RouteProtoInfo
cid := route.cid
route.mu.Unlock()
if close {
route.closeConnection(RouteRemoved)
delete(routes, cid)
}
}
// If only older servers, simply close all routes and they will do the right
// thing on reconnect.
if withNewProto == 0 {
for _, route := range routes {
route.closeConnection(RouteRemoved)
}
// If there are no route left, we are done
if len(routes) == 0 {
return
}
@@ -1856,42 +2087,69 @@ func (s *Server) reloadClusterPermissions(oldPerms *RoutePermissions) {
var (
_localSubs [4096]*subscription
localSubs = _localSubs[:0]
subsNeedSUB []*subscription
subsNeedUNSUB []*subscription
subsNeedSUB = map[*client][]*subscription{}
subsNeedUNSUB = map[*client][]*subscription{}
deleteRoutedSubs []*subscription
)
// FIXME(dlc) - Change for accounts.
gacc.sl.localSubs(&localSubs, false)
// Go through all local subscriptions
for _, sub := range localSubs {
// Get all subs that can now be imported
subj := string(sub.subject)
couldImportThen := oldPermsTester.canImport(subj)
canImportNow := newPermsTester.canImport(subj)
if canImportNow {
// If we could not before, then will need to send a SUB protocol.
if !couldImportThen {
subsNeedSUB = append(subsNeedSUB, sub)
getRouteForAccount := func(accName string, poolIdx int) *client {
for _, r := range routes {
r.mu.Lock()
ok := (poolIdx >= 0 && poolIdx == r.route.poolIdx) || (string(r.route.accName) == accName) || r.route.noPool
r.mu.Unlock()
if ok {
return r
}
} else if couldImportThen {
// We were previously able to import this sub, but now
// we can't so we need to send an UNSUB protocol
subsNeedUNSUB = append(subsNeedUNSUB, sub)
}
return nil
}
// First set the new permissions on all routes.
for _, route := range routes {
route.mu.Lock()
// If route is to older server, simply close connection.
if route.opts.Protocol < RouteProtoInfo {
route.mu.Unlock()
route.closeConnection(RouteRemoved)
continue
}
route.setRoutePermissions(newPerms)
for _, sub := range route.subs {
route.mu.Unlock()
}
// Then, go over all accounts and gather local subscriptions that need to be
// sent over as SUB or removed as UNSUB, and routed subscriptions that need
// to be dropped due to export permissions.
s.accounts.Range(func(_, v interface{}) bool {
acc := v.(*Account)
acc.mu.RLock()
accName, sl, poolIdx := acc.Name, acc.sl, acc.routePoolIdx
acc.mu.RUnlock()
// Get the route handling this account. If no route or sublist, bail out.
route := getRouteForAccount(accName, poolIdx)
if route == nil || sl == nil {
return true
}
localSubs := _localSubs[:0]
sl.localSubs(&localSubs, false)
// Go through all local subscriptions
for _, sub := range localSubs {
// Get all subs that can now be imported
subj := string(sub.subject)
couldImportThen := oldPermsTester.canImport(subj)
canImportNow := newPermsTester.canImport(subj)
if canImportNow {
// If we could not before, then will need to send a SUB protocol.
if !couldImportThen {
subsNeedSUB[route] = append(subsNeedSUB[route], sub)
}
} else if couldImportThen {
// We were previously able to import this sub, but now
// we can't so we need to send an UNSUB protocol
subsNeedUNSUB[route] = append(subsNeedUNSUB[route], sub)
}
}
deleteRoutedSubs = deleteRoutedSubs[:0]
route.mu.Lock()
for key, sub := range route.subs {
if an := strings.Fields(key)[0]; an != accName {
continue
}
// If we can't export, we need to drop the subscriptions that
// we have on behalf of this route.
subj := string(sub.subject)
@@ -1900,22 +2158,220 @@ func (s *Server) reloadClusterPermissions(oldPerms *RoutePermissions) {
deleteRoutedSubs = append(deleteRoutedSubs, sub)
}
}
// Send an update INFO, which will allow remote server to show
// our current route config in monitoring and resend subscriptions
// that we now possibly allow with a change of Export permissions.
route.mu.Unlock()
// Remove as a batch all the subs that we have removed from each route.
sl.RemoveBatch(deleteRoutedSubs)
return true
})
// Send an update INFO, which will allow remote server to show
// our current route config in monitoring and resend subscriptions
// that we now possibly allow with a change of Export permissions.
for _, route := range routes {
route.mu.Lock()
route.enqueueProto(infoJSON)
// Now send SUB and UNSUB protocols as needed.
route.sendRouteSubProtos(subsNeedSUB, false, nil)
route.sendRouteUnSubProtos(subsNeedUNSUB, false, nil)
if subs, ok := subsNeedSUB[route]; ok && len(subs) > 0 {
route.sendRouteSubProtos(subs, false, nil)
}
if unsubs, ok := subsNeedUNSUB[route]; ok && len(unsubs) > 0 {
route.sendRouteUnSubProtos(unsubs, false, nil)
}
route.mu.Unlock()
}
// Remove as a batch all the subs that we have removed from each route.
// FIXME(dlc) - Change for accounts.
gacc.sl.RemoveBatch(deleteRoutedSubs)
}
// validateClusterOpts ensures the new ClusterOpts does not change host or
// port, which do not support reload.
func (s *Server) reloadClusterPoolAndAccounts(co *clusterOption, opts *Options) {
s.mu.Lock()
// Prevent adding new routes until we are ready to do so.
s.routesReject = true
var ch chan struct{}
// For accounts that have been added to the list of dedicated routes,
// send a protocol to their current assigned routes to allow the
// other side to prepare for the changes.
if len(co.accsAdded) > 0 {
protosSent := 0
s.accAddedReqID = nuid.Next()
for _, an := range co.accsAdded {
if s.accRoutes == nil {
s.accRoutes = make(map[string]map[string]*client)
}
// In case a config reload was first done on another server,
// we may have already switched this account to a dedicated route.
// But we still want to send the protocol over the routes that
// would have otherwise handled it.
if _, ok := s.accRoutes[an]; !ok {
s.accRoutes[an] = make(map[string]*client)
}
if a, ok := s.accounts.Load(an); ok {
acc := a.(*Account)
acc.mu.Lock()
sl := acc.sl
// Get the current route pool index before calling setRouteInfo.
rpi := acc.routePoolIdx
// Switch to per-account route if not already done.
if rpi >= 0 {
s.setRouteInfo(acc)
} else {
// If it was transitioning, make sure we set it to the state
// that indicates that it has a dedicated route
if rpi == accTransitioningToDedicatedRoute {
acc.routePoolIdx = accDedicatedRoute
}
// Otherwise get the route pool index it would have been before
// the move so we can send the protocol to those routes.
rpi = s.computeRoutePoolIdx(acc)
}
acc.mu.Unlock()
// Generate the INFO protocol to send indicating that this account
// is being moved to a dedicated route.
ri := Info{
RoutePoolSize: s.routesPoolSize,
RouteAccount: an,
RouteAccReqID: s.accAddedReqID,
}
proto := generateInfoJSON(&ri)
// Go over each remote's route at pool index `rpi` and remove
// remote subs for this account and send the protocol.
s.forEachRouteIdx(rpi, func(r *client) bool {
r.mu.Lock()
// Exclude routes to servers that don't support pooling.
if !r.route.noPool {
if subs := r.removeRemoteSubsForAcc(an); len(subs) > 0 {
sl.RemoveBatch(subs)
}
r.enqueueProto(proto)
protosSent++
}
r.mu.Unlock()
return true
})
}
}
if protosSent > 0 {
s.accAddedCh = make(chan struct{}, protosSent)
ch = s.accAddedCh
}
}
// Collect routes that need to be closed.
routes := make(map[*client]struct{})
// Collect the per-account routes that need to be closed.
if len(co.accsRemoved) > 0 {
for _, an := range co.accsRemoved {
if remotes, ok := s.accRoutes[an]; ok && remotes != nil {
for _, r := range remotes {
if r != nil {
r.setNoReconnect()
routes[r] = struct{}{}
}
}
}
}
}
// If the pool size has changed, we need to close all pooled routes.
if co.poolSizeChanged {
s.forEachNonPerAccountRoute(func(r *client) {
routes[r] = struct{}{}
})
}
// If there are routes to close, we need to release the server lock.
// Same if we need to wait on responses from the remotes when
// processing new per-account routes.
if len(routes) > 0 || len(ch) > 0 {
s.mu.Unlock()
for done := false; !done && len(ch) > 0; {
select {
case <-ch:
case <-time.After(2 * time.Second):
s.Warnf("Timed out waiting for confirmation from all routes regarding per-account routes changes")
done = true
}
}
for r := range routes {
r.closeConnection(RouteRemoved)
}
s.mu.Lock()
}
// Clear the accAddedCh/ReqID fields in case they were set.
s.accAddedReqID, s.accAddedCh = _EMPTY_, nil
// Now that per-account routes that needed to be closed are closed,
// remove them from s.accRoutes. Doing so before would prevent
// removeRoute() to do proper cleanup because the route would not
// be found in s.accRoutes.
for _, an := range co.accsRemoved {
delete(s.accRoutes, an)
// Do not lookup and call setRouteInfo() on the accounts here.
// We need first to set the new s.routesPoolSize value and
// anyway, there is no need to do here if the pool size has
// changed (since it will be called for all accounts).
}
// We have already added the accounts to s.accRoutes that needed to
// be added.
// We should always have at least the system account with a dedicated route,
// but in case we have a configuration that disables pooling and without
// a system account, possibly set the accRoutes to nil.
if len(opts.Cluster.PinnedAccounts) == 0 {
s.accRoutes = nil
}
// Now deal with pool size updates.
if ps := opts.Cluster.PoolSize; ps > 0 {
s.routesPoolSize = ps
s.routeInfo.RoutePoolSize = ps
} else {
s.routesPoolSize = 1
s.routeInfo.RoutePoolSize = 0
}
// If the pool size has changed, we need to recompute all accounts' route
// pool index. Note that the added/removed accounts will be reset there
// too, but that's ok (we could use a map to exclude them, but not worth it).
if co.poolSizeChanged {
s.accounts.Range(func(_, v interface{}) bool {
acc := v.(*Account)
acc.mu.Lock()
s.setRouteInfo(acc)
acc.mu.Unlock()
return true
})
} else if len(co.accsRemoved) > 0 {
// For accounts that no longer have a dedicated route, we need to send
// the subsriptions on the existing pooled routes for those accounts.
for _, an := range co.accsRemoved {
if a, ok := s.accounts.Load(an); ok {
acc := a.(*Account)
acc.mu.Lock()
// First call this which will assign a new route pool index.
s.setRouteInfo(acc)
// Get the value so we can send the subscriptions interest
// on all routes with this pool index.
rpi := acc.routePoolIdx
acc.mu.Unlock()
s.forEachRouteIdx(rpi, func(r *client) bool {
// We have the guarantee that if the route exists, it
// is not a new one that would have been created when
// we released the server lock if some routes needed
// to be closed, because we have set s.routesReject
// to `true` at the top of this function.
s.sendSubsToRoute(r, rpi, an)
return true
})
}
}
}
// Allow routes to be accepted now.
s.routesReject = false
// If there is a pool size change or added accounts, solicit routes now.
if co.poolSizeChanged || len(co.accsAdded) > 0 {
s.solicitRoutes(opts.Routes, co.accsAdded)
}
s.mu.Unlock()
}
// validateClusterOpts ensures the new ClusterOpts does not change some of the
// fields that do not support reload.
func validateClusterOpts(old, new ClusterOpts) error {
if old.Host != new.Host {
return fmt.Errorf("config reload not supported for cluster host: old=%s, new=%s",
+1089 -256
View File
File diff suppressed because it is too large Load Diff
+485 -55
View File
@@ -21,12 +21,14 @@ import (
"errors"
"flag"
"fmt"
"hash/fnv"
"io"
"log"
"math/rand"
"net"
"net/http"
"regexp"
"runtime/pprof"
// Allow dynamic profiling.
_ "net/http/pprof"
@@ -40,6 +42,7 @@ import (
"sync/atomic"
"time"
"github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
"github.com/nats-io/nuid"
@@ -83,6 +86,7 @@ type Info struct {
ClientConnectURLs []string `json:"connect_urls,omitempty"` // Contains URLs a client can connect to.
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"`
// Route Specific
Import *SubjectPermission `json:"import,omitempty"`
@@ -90,6 +94,10 @@ type Info struct {
LNOC bool `json:"lnoc,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"`
RouteAccReqID string `json:"route_acc_add_reqid,omitempty"`
// Gateways Specific
Gateway string `json:"gateway,omitempty"` // Name of the origin Gateway (sent by gateway's INFO)
@@ -103,6 +111,8 @@ type Info struct {
// 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.
XKey string `json:"xkey,omitempty"` // Public server's x25519 key.
}
// Server is our main struct.
@@ -112,8 +122,11 @@ type Server struct {
// How often user logon fails due to the issuer account not being pinned.
pinnedAccFail uint64
stats
scStats
mu sync.RWMutex
kp nkeys.KeyPair
xkp nkeys.KeyPair
xpub string
info Info
configFile string
optsMu sync.RWMutex
@@ -130,9 +143,14 @@ type Server struct {
activeAccounts int32
accResolver AccountResolver
clients map[uint64]*client
routes map[uint64]*client
routesByHash sync.Map
remotes map[string]*client
routes map[string][]*client
routesPoolSize int // Configured pool size
routesReject bool // During reload, we may want to reject adding routes until some conditions are met
routesNoPool int // Number of routes that don't use pooling (connecting to older server for instance)
accRoutes map[string]map[string]*client // Key is account name, value is key=remoteID/value=route connection
accRouteByHash sync.Map // Key is account name, value is nil or a pool index
accAddedCh chan struct{}
accAddedReqID string
leafs map[uint64]*client
users map[string]*User
nkeys map[string]*NkeyUser
@@ -148,7 +166,6 @@ type Server struct {
routeListener net.Listener
routeListenerErr error
routeInfo Info
routeInfoJSON []byte
routeResolver netResolver
routesToSelf map[string]struct{}
routeTLSName string
@@ -300,16 +317,17 @@ type Server struct {
// For tracking JS nodes.
type nodeInfo struct {
name string
version string
cluster string
domain string
id string
tags jwt.TagList
cfg *JetStreamConfig
stats *JetStreamStats
offline bool
js bool
name string
version string
cluster string
domain string
id string
tags jwt.TagList
cfg *JetStreamConfig
stats *JetStreamStats
offline bool
js bool
binarySnapshots bool
}
// Make sure all are 64bits for atomic use
@@ -321,6 +339,251 @@ type stats struct {
slowConsumers int64
}
// scStats includes the total and per connection counters of Slow Consumers.
type scStats 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
var (
testDefaultClusterCompression string
testDefaultLeafNodeCompression string
)
// Compression modes.
const (
CompressionNotSupported = "not supported"
CompressionOff = "off"
CompressionAccept = "accept"
CompressionS2Auto = "s2_auto"
CompressionS2Uncompressed = "s2_uncompressed"
CompressionS2Fast = "s2_fast"
CompressionS2Better = "s2_better"
CompressionS2Best = "s2_best"
)
// defaultCompressionS2AutoRTTThresholds is the default of RTT thresholds for
// the CompressionS2Auto mode.
var defaultCompressionS2AutoRTTThresholds = []time.Duration{
// [0..10ms] -> CompressionS2Uncompressed
10 * time.Millisecond,
// ]10ms..50ms] -> CompressionS2Fast
50 * time.Millisecond,
// ]50ms..100ms] -> CompressionS2Better
100 * time.Millisecond,
// ]100ms..] -> CompressionS2Best
}
// For a given user provided string, matches to one of the compression mode
// constant and updates the provided string to that constant. Returns an
// error if the provided compression mode is not known.
// The parameter `chosenModeForOn` indicates which compression mode to use
// when the user selects "on" (or enabled, true, etc..). This is because
// we may have different defaults depending on where the compression is used.
func validateAndNormalizeCompressionOption(c *CompressionOpts, chosenModeForOn string) error {
if c == nil {
return nil
}
cmtl := strings.ToLower(c.Mode)
// First, check for the "on" case so that we set to the default compression
// mode for that. The other switch/case will finish setup if needed (for
// instance if the default mode is s2Auto).
switch cmtl {
case "on", "enabled", "true":
cmtl = chosenModeForOn
default:
}
// Check (again) with the proper mode.
switch cmtl {
case "not supported", "not_supported":
c.Mode = CompressionNotSupported
case "disabled", "off", "false":
c.Mode = CompressionOff
case "accept":
c.Mode = CompressionAccept
case "auto", "s2_auto":
var rtts []time.Duration
if len(c.RTTThresholds) == 0 {
rtts = defaultCompressionS2AutoRTTThresholds
} else {
for _, n := range c.RTTThresholds {
// Do not error on negative, but simply set to 0
if n < 0 {
n = 0
}
// Make sure they are properly ordered. However, it is possible
// to have a "0" anywhere in the list to indicate that this
// compression level should not be used.
if l := len(rtts); l > 0 && n != 0 {
for _, v := range rtts {
if n < v {
return fmt.Errorf("RTT threshold values %v should be in ascending order", c.RTTThresholds)
}
}
}
rtts = append(rtts, n)
}
if len(rtts) > 0 {
// Trim 0 that are at the end.
stop := -1
for i := len(rtts) - 1; i >= 0; i-- {
if rtts[i] != 0 {
stop = i
break
}
}
rtts = rtts[:stop+1]
}
if len(rtts) > 4 {
// There should be at most values for "uncompressed", "fast",
// "better" and "best" (when some 0 are present).
return fmt.Errorf("compression mode %q should have no more than 4 RTT thresholds: %v", c.Mode, c.RTTThresholds)
} else if len(rtts) == 0 {
// But there should be at least 1 if the user provided the slice.
// We would be here only if it was provided by say with values
// being a single or all zeros.
return fmt.Errorf("compression mode %q requires at least one RTT threshold", c.Mode)
}
}
c.Mode = CompressionS2Auto
c.RTTThresholds = rtts
case "fast", "s2_fast":
c.Mode = CompressionS2Fast
case "better", "s2_better":
c.Mode = CompressionS2Better
case "best", "s2_best":
c.Mode = CompressionS2Best
default:
return fmt.Errorf("unsupported compression mode %q", c.Mode)
}
return nil
}
// Returns `true` if the compression mode `m` indicates that the server
// will negotiate compression with the remote server, `false` otherwise.
// Note that the provided compression mode is assumed to have been
// normalized and validated.
func needsCompression(m string) bool {
return m != _EMPTY_ && m != CompressionOff && m != CompressionNotSupported
}
// Compression is asymmetric, meaning that one side can have a different
// compression level than the other. However, we need to check for cases
// when this server `scm` or the remote `rcm` do not support compression
// (say older server, or test to make it behave as it is not), or have
// the compression off.
// Note that `scm` is assumed to not be "off" or "not supported".
func selectCompressionMode(scm, rcm string) (mode string, err error) {
if rcm == CompressionNotSupported || rcm == _EMPTY_ {
return CompressionNotSupported, nil
}
switch rcm {
case CompressionOff:
// If the remote explicitly disables compression, then we won't
// use compression.
return CompressionOff, nil
case CompressionAccept:
// If the remote is ok with compression (but is not initiating it),
// and if we too are in this mode, then it means no compression.
if scm == CompressionAccept {
return CompressionOff, nil
}
// Otherwise use our compression mode.
return scm, nil
case CompressionS2Auto, CompressionS2Uncompressed, CompressionS2Fast, CompressionS2Better, CompressionS2Best:
// This case is here to make sure that if we don't recognize a
// compression setting, we error out.
if scm == CompressionAccept {
// If our compression mode is "accept", then we will use the remote
// compression mode, except if it is "auto", in which case we will
// default to "fast". This is not a configuration (auto in one
// side and accept in the other) that would be recommended.
if rcm == CompressionS2Auto {
return CompressionS2Fast, nil
}
// Use their compression mode.
return rcm, nil
}
// Otherwise use our compression mode.
return scm, nil
default:
return _EMPTY_, fmt.Errorf("unsupported route compression mode %q", rcm)
}
}
// If the configured compression mode is "auto" then will return that,
// otherwise will return the given `cm` compression mode.
func compressionModeForInfoProtocol(co *CompressionOpts, cm string) string {
if co.Mode == CompressionS2Auto {
return CompressionS2Auto
}
return cm
}
// Given a connection RTT and a list of thresholds durations, this
// function will return an S2 compression level such as "uncompressed",
// "fast", "better" or "best". For instance, with the following slice:
// [5ms, 10ms, 15ms, 20ms], a RTT of up to 5ms will result
// in the compression level "uncompressed", ]5ms..10ms] will result in
// "fast" compression, etc..
// However, the 0 value allows for disabling of some compression levels.
// For instance, the following slice: [0, 0, 20, 30] means that a RTT of
// [0..20ms] would result in the "better" compression - effectively disabling
// the use of "uncompressed" and "fast", then anything above 20ms would
// result in the use of "best" level (the 30 in the list has no effect
// and the list could have been simplified to [0, 0, 20]).
func selectS2AutoModeBasedOnRTT(rtt time.Duration, rttThresholds []time.Duration) string {
var idx int
var found bool
for i, d := range rttThresholds {
if rtt <= d {
idx = i
found = true
break
}
}
if !found {
// If we did not find but we have all levels, then use "best",
// otherwise use the last one in array.
if l := len(rttThresholds); l >= 3 {
idx = 3
} else {
idx = l - 1
}
}
switch idx {
case 0:
return CompressionS2Uncompressed
case 1:
return CompressionS2Fast
case 2:
return CompressionS2Better
}
return CompressionS2Best
}
// 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
// this is more versatile.
func s2WriterOptions(cm string) []s2.WriterOption {
switch cm {
case CompressionS2Uncompressed:
return []s2.WriterOption{s2.WriterUncompressed()}
case CompressionS2Best:
return []s2.WriterOption{s2.WriterBestCompression()}
case CompressionS2Better:
return []s2.WriterOption{s2.WriterBetterCompression()}
default:
return nil
}
}
// New will setup a new server struct after parsing the options.
// DEPRECATED: Use NewServer(opts)
func New(opts *Options) *Server {
@@ -337,10 +600,14 @@ func NewServer(opts *Options) (*Server, error) {
tlsReq := opts.TLSConfig != nil
verify := (tlsReq && opts.TLSConfig.ClientAuth == tls.RequireAndVerifyClientCert)
// Created server's nkey identity.
// Create our server's nkey identity.
kp, _ := nkeys.CreateServer()
pub, _ := kp.PublicKey()
// Create an xkey for encrypting messages from this server.
xkp, _ := nkeys.CreateCurveKeys()
xpub, _ := xkp.PublicKey()
serverName := pub
if opts.ServerName != _EMPTY_ {
serverName = opts.ServerName
@@ -358,6 +625,7 @@ func NewServer(opts *Options) (*Server, error) {
info := Info{
ID: pub,
XKey: xpub,
Version: VERSION,
Proto: PROTO,
GitCommit: gitCommit,
@@ -383,6 +651,8 @@ func NewServer(opts *Options) (*Server, error) {
s := &Server{
kp: kp,
xkp: xkp,
xpub: xpub,
configFile: opts.ConfigFile,
info: info,
opts: opts,
@@ -442,7 +712,7 @@ func NewServer(opts *Options) (*Server, error) {
opts.Tags,
&JetStreamConfig{MaxMemory: opts.JetStreamMaxMemory, MaxStore: opts.JetStreamMaxStore, CompressOK: true},
nil,
false, true,
false, true, true,
})
}
@@ -497,8 +767,7 @@ func NewServer(opts *Options) (*Server, error) {
s.grTmpClients = make(map[uint64]*client)
// For tracking routes and their remote ids
s.routes = make(map[uint64]*client)
s.remotes = make(map[string]*client)
s.initRouteStructures(opts)
// For tracking leaf nodes.
s.leafs = make(map[uint64]*client)
@@ -564,6 +833,27 @@ func NewServer(opts *Options) (*Server, error) {
return s, nil
}
// Initializes route structures based on pooling and/or per-account routes.
//
// Server lock is held on entry
func (s *Server) initRouteStructures(opts *Options) {
s.routes = make(map[string][]*client)
if ps := opts.Cluster.PoolSize; ps > 0 {
s.routesPoolSize = ps
} else {
s.routesPoolSize = 1
}
// If we have per-account routes, we create accRoutes and initialize it
// with nil values. The presence of an account as the key will allow us
// to know if a given account is supposed to have dedicated routes.
if l := len(opts.Cluster.PinnedAccounts); l > 0 {
s.accRoutes = make(map[string]map[string]*client, l)
for _, acc := range opts.Cluster.PinnedAccounts {
s.accRoutes[acc] = make(map[string]*client)
}
}
}
func (s *Server) logRejectedTLSConns() {
defer s.grWG.Done()
t := time.NewTicker(time.Second)
@@ -608,8 +898,6 @@ func (s *Server) setClusterName(name string) {
s.info.Cluster = name
s.routeInfo.Cluster = name
// Regenerate the info byte array
s.generateRouteInfoJSON()
// Need to close solicited leaf nodes. The close has to be done outside of the server lock.
var leafs []*client
for _, c := range s.leafs {
@@ -658,6 +946,11 @@ func (s *Server) ClientURL() string {
}
func validateCluster(o *Options) error {
if o.Cluster.Compression.Mode != _EMPTY_ {
if err := validateAndNormalizeCompressionOption(&o.Cluster.Compression, CompressionS2Fast); err != nil {
return err
}
}
if err := validatePinnedCerts(o.Cluster.TLSPinnedCerts); err != nil {
return fmt.Errorf("cluster: %v", err)
}
@@ -669,6 +962,18 @@ func validateCluster(o *Options) error {
// Set this here so we do not consider it dynamic.
o.Cluster.Name = o.Gateway.Name
}
if l := len(o.Cluster.PinnedAccounts); l > 0 {
if o.Cluster.PoolSize < 0 {
return fmt.Errorf("pool_size cannot be negative if pinned accounts are specified")
}
m := make(map[string]struct{}, l)
for _, a := range o.Cluster.PinnedAccounts {
if _, exists := m[a]; exists {
return fmt.Errorf("found duplicate account name %q in pinned accounts list %q", a, o.Cluster.PinnedAccounts)
}
m[a] = struct{}{}
}
}
return nil
}
@@ -816,7 +1121,7 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
a.mu.Lock()
acc.shallowCopy(a)
a.mu.Unlock()
// Will be a no-op in case of the global account since it is alrady registered.
// Will be a no-op in case of the global account since it is already registered.
s.registerAccountNoLock(a)
}
// The `acc` account is stored in options, not in the server, and these can be cleared.
@@ -921,10 +1226,6 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
if err == nil && s.sys != nil && acc != s.sys.account {
// sys.account.clients (including internal client)/respmap/etc... are transferred separately
s.sys.account = acc
s.mu.Unlock()
// acquires server lock separately
s.addSystemAccountExports(acc)
s.mu.Lock()
}
if err != nil {
return awcsti, fmt.Errorf("error resolving system account: %v", err)
@@ -954,6 +1255,13 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
}
}
// Add any required exports from system account.
if s.sys != nil {
s.mu.Unlock()
s.addSystemAccountExports(s.sys.account)
s.mu.Lock()
}
return awcsti, nil
}
@@ -1011,12 +1319,6 @@ func (s *Server) checkResolvePreloads() {
}
}
func (s *Server) generateRouteInfoJSON() {
b, _ := json.Marshal(s.routeInfo)
pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
s.routeInfoJSON = bytes.Join(pcs, []byte(" "))
}
// Determines if we are in pre NATS 2.0 setup with no accounts.
func (s *Server) globalAccountOnly() bool {
var hasOthers bool
@@ -1460,6 +1762,7 @@ func (s *Server) registerAccountNoLock(acc *Account) *Account {
s.setAccountSublist(acc)
acc.mu.Lock()
s.setRouteInfo(acc)
if acc.clients == nil {
acc.clients = make(map[*client]struct{})
}
@@ -1514,6 +1817,47 @@ func (s *Server) registerAccountNoLock(acc *Account) *Account {
return nil
}
// Sets the account's routePoolIdx depending on presence or not of
// pooling or per-account routes. Also updates a map used by
// gateway code to retrieve a route based on some route hash.
//
// Both Server and Account lock held on entry.
func (s *Server) setRouteInfo(acc *Account) {
// If there is a dedicated route configured for this account
if _, ok := s.accRoutes[acc.Name]; ok {
// We want the account name to be in the map, but we don't
// need a value (we could store empty string)
s.accRouteByHash.Store(acc.Name, nil)
// Set the route pool index to -1 so that it is easy when
// ranging over accounts to exclude those accounts when
// trying to get accounts for a given pool index.
acc.routePoolIdx = accDedicatedRoute
} else {
// If pool size more than 1, we will compute a hash code and
// use modulo to assign to an index of the pool slice. For 1
// and below, all accounts will be bound to the single connection
// at index 0.
acc.routePoolIdx = s.computeRoutePoolIdx(acc)
if s.routesPoolSize > 1 {
s.accRouteByHash.Store(acc.Name, acc.routePoolIdx)
}
}
}
// Returns a route pool index for this account based on the given pool size.
// Account lock is held on entry (account's name is accessed but immutable
// so could be called without account's lock).
// Server lock held on entry.
func (s *Server) computeRoutePoolIdx(acc *Account) int {
if s.routesPoolSize <= 1 {
return 0
}
h := fnv.New32a()
h.Write([]byte(acc.Name))
sum32 := h.Sum32()
return int((sum32 % uint32(s.routesPoolSize)))
}
// lookupAccount is a function to return the account structure
// associated with an account name.
// Lock MUST NOT be held upon entry.
@@ -1554,11 +1898,14 @@ func (s *Server) LookupAccount(name string) (*Account, error) {
// This will fetch new claims and if found update the account with new claims.
// Lock MUST NOT be held upon entry.
func (s *Server) updateAccount(acc *Account) error {
acc.mu.RLock()
// TODO(dlc) - Make configurable
if !acc.incomplete && time.Since(acc.updated) < time.Second {
acc.mu.RUnlock()
s.Debugf("Requested account update for [%s] ignored, too soon", acc.Name)
return ErrAccountResolverUpdateTooSoon
}
acc.mu.RUnlock()
claimJWT, err := s.fetchRawAccountClaims(acc.Name)
if err != nil {
return err
@@ -1864,11 +2211,14 @@ func (s *Server) Start() {
s.Fatalf("Not allowed to enable JetStream on the system account")
}
cfg := &JetStreamConfig{
StoreDir: opts.StoreDir,
MaxMemory: opts.JetStreamMaxMemory,
MaxStore: opts.JetStreamMaxStore,
Domain: opts.JetStreamDomain,
CompressOK: true,
StoreDir: opts.StoreDir,
SyncInterval: opts.SyncInterval,
SyncAlways: opts.SyncAlways,
MaxMemory: opts.JetStreamMaxMemory,
MaxStore: opts.JetStreamMaxStore,
Domain: opts.JetStreamDomain,
CompressOK: true,
UniqueTag: opts.JetStreamUniqueTag,
}
if err := s.EnableJetStream(cfg); err != nil {
s.Fatalf("Can't start JetStream: %v", err)
@@ -2051,9 +2401,11 @@ func (s *Server) Shutdown() {
}
s.grMu.Unlock()
// Copy off the routes
for i, r := range s.routes {
conns[i] = r
}
s.forEachRoute(func(r *client) {
r.mu.Lock()
conns[r.cid] = r
r.mu.Unlock()
})
// Copy off the gateways
s.getAllGatewayConnections(conns)
@@ -2347,9 +2699,6 @@ func (s *Server) StartProfiler() {
s.profiler = l
s.profilingServer = srv
// Enable blocking profile
runtime.SetBlockProfileRate(1)
go func() {
// if this errors out, it's probably because the server is being shutdown
err := srv.Serve(l)
@@ -2840,6 +3189,7 @@ func (s *Server) saveClosedClient(c *client, nc net.Conn, reason ClosedState) {
// Adds to the list of client and websocket clients connect URLs.
// If there was a change, an INFO protocol is sent to registered clients
// that support async INFO protocols.
// Server lock held on entry.
func (s *Server) addConnectURLsAndSendINFOToClients(curls, wsurls []string) {
s.updateServerINFOAndSendINFOToClients(curls, wsurls, true)
}
@@ -2847,16 +3197,15 @@ func (s *Server) addConnectURLsAndSendINFOToClients(curls, wsurls []string) {
// Removes from the list of client and websocket clients connect URLs.
// If there was a change, an INFO protocol is sent to registered clients
// that support async INFO protocols.
// Server lock held on entry.
func (s *Server) removeConnectURLsAndSendINFOToClients(curls, wsurls []string) {
s.updateServerINFOAndSendINFOToClients(curls, wsurls, false)
}
// Updates the list of client and websocket clients connect URLs and if any change
// sends an async INFO update to clients that support it.
// Server lock held on entry.
func (s *Server) updateServerINFOAndSendINFOToClients(curls, wsurls []string, add bool) {
s.mu.Lock()
defer s.mu.Unlock()
remove := !add
// Will return true if we need alter the server's Info object.
updateMap := func(urls []string, m refCountedUrlSet) bool {
@@ -2990,8 +3339,17 @@ func (s *Server) addToTempClients(cid uint64, c *client) bool {
// NumRoutes will report the number of registered routes.
func (s *Server) NumRoutes() int {
s.mu.RLock()
nr := len(s.routes)
s.mu.RUnlock()
defer s.mu.RUnlock()
return s.numRoutes()
}
// numRoutes will report the number of registered routes.
// Server lock held on entry
func (s *Server) numRoutes() int {
var nr int
s.forEachRoute(func(c *client) {
nr++
})
return nr
}
@@ -2999,7 +3357,13 @@ func (s *Server) NumRoutes() int {
func (s *Server) NumRemotes() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.remotes)
return s.numRemotes()
}
// numRemotes will report number of registered remotes.
// Server lock held on entry
func (s *Server) numRemotes() int {
return len(s.routes)
}
// NumLeafNodes will report number of leaf node connections.
@@ -3059,6 +3423,26 @@ func (s *Server) NumSlowConsumers() int64 {
return atomic.LoadInt64(&s.slowConsumers)
}
// NumSlowConsumersClients will report the number of slow consumers clients.
func (s *Server) NumSlowConsumersClients() uint64 {
return s.scStats.clients.Load()
}
// NumSlowConsumersRoutes will report the number of slow consumers routes.
func (s *Server) NumSlowConsumersRoutes() uint64 {
return s.scStats.routes.Load()
}
// NumSlowConsumersGateways will report the number of slow consumers leafs.
func (s *Server) NumSlowConsumersGateways() uint64 {
return s.scStats.gateways.Load()
}
// NumSlowConsumersLeafs will report the number of slow consumers leafs.
func (s *Server) NumSlowConsumersLeafs() uint64 {
return s.scStats.leafs.Load()
}
// ConfigTime will report the last time the server configuration was loaded.
func (s *Server) ConfigTime() time.Time {
s.mu.RLock()
@@ -3204,15 +3588,28 @@ func (s *Server) String() string {
return s.info.Name
}
func (s *Server) startGoRoutine(f func()) bool {
type pprofLabels map[string]string
func (s *Server) startGoRoutine(f func(), tags ...pprofLabels) bool {
var started bool
s.grMu.Lock()
defer s.grMu.Unlock()
if s.grRunning {
var labels []string
for _, m := range tags {
for k, v := range m {
labels = append(labels, k, v)
}
}
s.grWG.Add(1)
go f()
go func() {
pprof.SetGoroutineLabels(
pprof.WithLabels(context.Background(), pprof.Labels(labels...)),
)
f()
}()
started = true
}
s.grMu.Unlock()
return started
}
@@ -3679,12 +4076,12 @@ func (s *Server) lameDuckMode() {
// Server lock is held on entry.
func (s *Server) sendLDMToRoutes() {
s.routeInfo.LameDuckMode = true
s.generateRouteInfoJSON()
for _, r := range s.routes {
infoJSON := generateInfoJSON(&s.routeInfo)
s.forEachRemote(func(r *client) {
r.mu.Lock()
r.enqueueProto(s.routeInfoJSON)
r.enqueueProto(infoJSON)
r.mu.Unlock()
}
})
// Clear now so that we notify only once, should we have to send other INFOs.
s.routeInfo.LameDuckMode = false
}
@@ -3858,3 +4255,36 @@ func (s *Server) changeRateLimitLogInterval(d time.Duration) {
default:
}
}
// DisconnectClientByID disconnects a client by connection ID
func (s *Server) DisconnectClientByID(id uint64) error {
client := s.clients[id]
if client != nil {
client.closeConnection(Kicked)
return nil
}
return errors.New("no such client id")
}
// LDMClientByID sends a Lame Duck Mode info message to a client by connection ID
func (s *Server) LDMClientByID(id uint64) error {
info := s.copyInfo()
info.LameDuckMode = true
c := s.clients[id]
if c != nil {
c.mu.Lock()
defer c.mu.Unlock()
if c.opts.Protocol >= ClientProtoInfo &&
c.flags.isSet(firstPongSent) {
// sendInfo takes care of checking if the connection is still
// valid or not, so don't duplicate tests here.
c.Debugf("sending Lame Duck Mode info to client")
c.enqueueProto(c.generateClientInfoJSON(info))
return nil
} else {
return errors.New("ClientProtoInfo < ClientOps.Protocol or first pong not sent")
}
}
return errors.New("no such client id")
}
+9 -1
View File
@@ -42,6 +42,7 @@ type winServiceWrapper struct {
}
var dockerized = false
var startupDelay = 10 * time.Second
func init() {
if v, exists := os.LookupEnv("NATS_DOCKERIZED"); exists && v == "1" {
@@ -66,8 +67,15 @@ func (w *winServiceWrapper) Execute(args []string, changes <-chan svc.ChangeRequ
status <- svc.Status{State: svc.StartPending}
go w.server.Start()
if v, exists := os.LookupEnv("NATS_STARTUP_DELAY"); exists {
if delay, err := time.ParseDuration(v); err == nil {
startupDelay = delay
} else {
w.server.Errorf("Failed to parse \"%v\" as a duration for startup: %s", v, err)
}
}
// Wait for accept loop(s) to be started
if !w.server.ReadyForConnections(10 * time.Second) {
if !w.server.ReadyForConnections(startupDelay) {
// Failed to start.
return false, 1
}
+63 -35
View File
@@ -82,54 +82,82 @@ func (s *Server) handleSignals() {
// ProcessSignal sends the given signal command to the given process. If pidStr
// is empty, this will send the signal to the single running instance of
// nats-server. If multiple instances are running, it returns an error. This returns
// an error if the given process is not running or the command is invalid.
func ProcessSignal(command Command, pidStr string) error {
var pid int
if pidStr == "" {
pids, err := resolvePids()
if err != nil {
return err
}
if len(pids) == 0 {
return fmt.Errorf("no %s processes running", processName)
}
if len(pids) > 1 {
errStr := fmt.Sprintf("multiple %s processes running:\n", processName)
prefix := ""
for _, p := range pids {
errStr += fmt.Sprintf("%s%d", prefix, p)
prefix = "\n"
}
return errors.New(errStr)
}
pid = pids[0]
} else {
p, err := strconv.Atoi(pidStr)
if err != nil {
// nats-server. If multiple instances are running, pidStr can be a globular
// expression ending with '*'. This returns an error if the given process is
// not running or the command is invalid.
func ProcessSignal(command Command, pidExpr string) error {
var (
err error
errStr string
pids = make([]int, 1)
pidStr = strings.TrimSuffix(pidExpr, "*")
isGlob = strings.HasSuffix(pidExpr, "*")
)
// Validate input if given
if pidStr != "" {
if pids[0], err = strconv.Atoi(pidStr); err != nil {
return fmt.Errorf("invalid pid: %s", pidStr)
}
pid = p
}
// Gather all PIDs unless the input is specific
if pidStr == "" || isGlob {
if pids, err = resolvePids(); err != nil {
return err
}
}
// Multiple instances are running and the input is not an expression
if len(pids) > 1 && !isGlob {
errStr = fmt.Sprintf("multiple %s processes running:", processName)
for _, p := range pids {
errStr += fmt.Sprintf("\n%d", p)
}
return errors.New(errStr)
}
// No instances are running
if len(pids) == 0 {
return fmt.Errorf("no %s processes running", processName)
}
var err error
var signum syscall.Signal
if signum, err = CommandToSignal(command); err != nil {
return err
}
for _, pid := range pids {
if _pidStr := strconv.Itoa(pid); _pidStr != pidStr && pidStr != "" {
if !isGlob || !strings.HasPrefix(_pidStr, pidStr) {
continue
}
}
if err = kill(pid, signum); err != nil {
errStr += fmt.Sprintf("\nsignal %q %d: %s", command, pid, err)
}
}
if errStr != "" {
return errors.New(errStr)
}
return nil
}
// Translates a command to a signal number
func CommandToSignal(command Command) (syscall.Signal, error) {
switch command {
case CommandStop:
err = kill(pid, syscall.SIGKILL)
return syscall.SIGKILL, nil
case CommandQuit:
err = kill(pid, syscall.SIGINT)
return syscall.SIGINT, nil
case CommandReopen:
err = kill(pid, syscall.SIGUSR1)
return syscall.SIGUSR1, nil
case CommandReload:
err = kill(pid, syscall.SIGHUP)
return syscall.SIGHUP, nil
case commandLDMode:
err = kill(pid, syscall.SIGUSR2)
return syscall.SIGUSR2, nil
case commandTerm:
err = kill(pid, syscall.SIGTERM)
return syscall.SIGTERM, nil
default:
err = fmt.Errorf("unknown signal %q", command)
return 0, fmt.Errorf("unknown signal %q", command)
}
return err
}
// resolvePids returns the pids for all running nats-server processes.
+157
View File
@@ -21,6 +21,8 @@ import (
"io"
"strings"
"time"
"github.com/nats-io/nats-server/v2/server/avl"
)
// StorageType determines how messages are stored for retention.
@@ -61,6 +63,8 @@ var (
ErrInvalidSequence = errors.New("invalid sequence")
// ErrSequenceMismatch is returned when storing a raw message and the expected sequence is wrong.
ErrSequenceMismatch = errors.New("expected sequence does not match store")
// ErrCorruptStreamState
ErrCorruptStreamState = errors.New("stream state snapshot is corrupt")
)
// StoreMsg is the stored message format for messages that are retained by the Store layer.
@@ -97,6 +101,8 @@ type StreamStore interface {
NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64)
State() StreamState
FastState(*StreamState)
EncodedStreamState(failed uint64) (enc []byte, err error)
SyncDeleted(dbs DeleteBlocks)
Type() StorageType
RegisterStorageUpdates(StorageUpdateHandler)
UpdateConfig(cfg *StreamConfig) error
@@ -171,6 +177,157 @@ type SnapshotResult struct {
State StreamState
}
const (
// Magic is used to identify stream state encodings.
streamStateMagic = uint8(42)
// Version
streamStateVersion = uint8(1)
// Magic / Identifier for run length encodings.
runLengthMagic = uint8(33)
// Magic / Identifier for AVL seqsets.
seqSetMagic = uint8(22)
)
// Interface for DeleteBlock.
// These will be of three types:
// 1. AVL seqsets.
// 2. Run length encoding of a deleted range.
// 3. Legacy []uint64
type DeleteBlock interface {
State() (first, last, num uint64)
Range(f func(uint64) bool)
}
type DeleteBlocks []DeleteBlock
// StreamReplicatedState represents what is encoded in a binary stream snapshot used
// for stream replication in an NRG.
type StreamReplicatedState struct {
Msgs uint64
Bytes uint64
FirstSeq uint64
LastSeq uint64
Failed uint64
Deleted DeleteBlocks
}
// Determine if this is an encoded stream state.
func IsEncodedStreamState(buf []byte) bool {
return len(buf) >= hdrLen && buf[0] == streamStateMagic && buf[1] == streamStateVersion
}
var ErrBadStreamStateEncoding = errors.New("bad stream state encoding")
func DecodeStreamState(buf []byte) (*StreamReplicatedState, error) {
ss := &StreamReplicatedState{}
if len(buf) < hdrLen || buf[0] != streamStateMagic || buf[1] != streamStateVersion {
return nil, ErrBadStreamStateEncoding
}
var bi = hdrLen
readU64 := func() uint64 {
if bi < 0 || bi >= len(buf) {
bi = -1
return 0
}
num, n := binary.Uvarint(buf[bi:])
if n <= 0 {
bi = -1
return 0
}
bi += n
return num
}
parserFailed := func() bool {
return bi < 0
}
ss.Msgs = readU64()
ss.Bytes = readU64()
ss.FirstSeq = readU64()
ss.LastSeq = readU64()
ss.Failed = readU64()
if parserFailed() {
return nil, ErrCorruptStreamState
}
if numDeleted := readU64(); numDeleted > 0 {
// If we have some deleted blocks.
for l := len(buf); l > bi; {
switch buf[bi] {
case seqSetMagic:
dmap, n, err := avl.Decode(buf[bi:])
if err != nil {
return nil, ErrCorruptStreamState
}
bi += n
ss.Deleted = append(ss.Deleted, dmap)
case runLengthMagic:
bi++
var rl DeleteRange
rl.First = readU64()
rl.Num = readU64()
if parserFailed() {
return nil, ErrCorruptStreamState
}
ss.Deleted = append(ss.Deleted, &rl)
default:
return nil, ErrCorruptStreamState
}
}
}
return ss, nil
}
// DeleteRange is a run length encoded delete range.
type DeleteRange struct {
First uint64
Num uint64
}
func (dr *DeleteRange) State() (first, last, num uint64) {
return dr.First, dr.First + dr.Num, dr.Num
}
// Range will range over all the deleted sequences represented by this block.
func (dr *DeleteRange) Range(f func(uint64) bool) {
for seq := dr.First; seq <= dr.First+dr.Num; seq++ {
if !f(seq) {
return
}
}
}
// Legacy []uint64
type DeleteSlice []uint64
func (ds DeleteSlice) State() (first, last, num uint64) {
if len(ds) == 0 {
return 0, 0, 0
}
return ds[0], ds[len(ds)-1], uint64(len(ds))
}
// Range will range over all the deleted sequences represented by this []uint64.
func (ds DeleteSlice) Range(f func(uint64) bool) {
for _, seq := range ds {
if !f(seq) {
return
}
}
}
func (dbs DeleteBlocks) NumDeleted() (total uint64) {
for _, db := range dbs {
_, _, num := db.State()
total += num
}
return total
}
// ConsumerStore stores state on consumers for streams.
type ConsumerStore interface {
SetStarting(sseq uint64) error
File diff suppressed because it is too large Load Diff
+563
View File
@@ -0,0 +1,563 @@
// Copyright 2023 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 (
"fmt"
"hash/fnv"
"regexp"
"strconv"
"strings"
)
// Subject mapping and transform setups.
var (
commaSeparatorRegEx = regexp.MustCompile(`,\s*`)
partitionMappingFunctionRegEx = regexp.MustCompile(`{{\s*[pP]artition\s*\((.*)\)\s*}}`)
wildcardMappingFunctionRegEx = regexp.MustCompile(`{{\s*[wW]ildcard\s*\((.*)\)\s*}}`)
splitFromLeftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit[fF]rom[lL]eft\s*\((.*)\)\s*}}`)
splitFromRightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit[fF]rom[rR]ight\s*\((.*)\)\s*}}`)
sliceFromLeftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]lice[fF]rom[lL]eft\s*\((.*)\)\s*}}`)
sliceFromRightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]lice[fF]rom[rR]ight\s*\((.*)\)\s*}}`)
splitMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit\s*\((.*)\)\s*}}`)
)
// Enum for the subject mapping subjectTransform function types
const (
NoTransform int16 = iota
BadTransform
Partition
Wildcard
SplitFromLeft
SplitFromRight
SliceFromLeft
SliceFromRight
Split
)
// Transforms for arbitrarily mapping subjects from one to another for maps, tees and filters.
// These can also be used for proper mapping on wildcard exports/imports.
// These will be grouped and caching and locking are assumed to be in the upper layers.
type subjectTransform struct {
src, dest string
dtoks []string // destination tokens
stoks []string // source tokens
dtokmftypes []int16 // destination token mapping function types
dtokmftokindexesargs [][]int // destination token mapping function array of source token index arguments
dtokmfintargs []int32 // destination token mapping function int32 arguments
dtokmfstringargs []string // destination token mapping function string arguments
}
// SubjectTransformer transforms subjects using mappings
//
// This API is not part of the public API and not subject to SemVer protections
type SubjectTransformer interface {
// TODO(dlc) - We could add in client here to allow for things like foo -> foo.$ACCOUNT
Match(string) (string, error)
TransformSubject(subject string) string
TransformTokenizedSubject(tokens []string) string
}
func NewSubjectTransformWithStrict(src, dest string, strict bool) (*subjectTransform, error) {
// strict = true for import subject mappings that need to be reversible
// (meaning can only use the Wildcard function and must use all the pwcs that are present in the source)
// No source given is equivalent to the source being ">"
if dest == _EMPTY_ {
return nil, nil
}
if src == _EMPTY_ {
src = fwcs
}
// Both entries need to be valid subjects.
sv, stokens, npwcs, hasFwc := subjectInfo(src)
dv, dtokens, dnpwcs, dHasFwc := subjectInfo(dest)
// Make sure both are valid, match fwc if present and there are no pwcs in the dest subject.
if !sv || !dv || dnpwcs > 0 || hasFwc != dHasFwc {
return nil, ErrBadSubject
}
var dtokMappingFunctionTypes []int16
var dtokMappingFunctionTokenIndexes [][]int
var dtokMappingFunctionIntArgs []int32
var dtokMappingFunctionStringArgs []string
// If the src has partial wildcards then the dest needs to have the token place markers.
if npwcs > 0 || hasFwc {
// We need to count to make sure that the dest has token holders for the pwcs.
sti := make(map[int]int)
for i, token := range stokens {
if len(token) == 1 && token[0] == pwc {
sti[len(sti)+1] = i
}
}
nphs := 0
for _, token := range dtokens {
tranformType, transformArgWildcardIndexes, transfomArgInt, transformArgString, err := indexPlaceHolders(token)
if err != nil {
return nil, err
}
if strict {
if tranformType != NoTransform && tranformType != Wildcard {
return nil, &mappingDestinationErr{token, ErrMappingDestinationNotSupportedForImport}
}
}
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 {
nphs += len(transformArgWildcardIndexes)
// Now build up our runtime mapping from dest to source tokens.
var stis []int
for _, wildcardIndex := range transformArgWildcardIndexes {
if wildcardIndex > npwcs {
return nil, &mappingDestinationErr{fmt.Sprintf("%s: [%d]", token, wildcardIndex), ErrMappingDestinationIndexOutOfRange}
}
stis = append(stis, sti[wildcardIndex])
}
dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, tranformType)
dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, stis)
dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, transfomArgInt)
dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, transformArgString)
}
}
if strict && nphs < npwcs {
// not all wildcards are being used in the destination
return nil, &mappingDestinationErr{dest, ErrMappingDestinationNotUsingAllWildcards}
}
} 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)
if err != nil {
return nil, err
}
if tranformType != NoTransform {
return nil, &mappingDestinationErr{token, ErrMappingDestinationIndexOutOfRange}
}
}
}
return &subjectTransform{
src: src,
dest: dest,
dtoks: dtokens,
stoks: stokens,
dtokmftypes: dtokMappingFunctionTypes,
dtokmftokindexesargs: dtokMappingFunctionTokenIndexes,
dtokmfintargs: dtokMappingFunctionIntArgs,
dtokmfstringargs: dtokMappingFunctionStringArgs,
}, nil
}
func NewSubjectTransform(src, dest string) (*subjectTransform, error) {
return NewSubjectTransformWithStrict(src, dest, false)
}
func NewSubjectTransformStrict(src, dest string) (*subjectTransform, error) {
return NewSubjectTransformWithStrict(src, dest, true)
}
func getMappingFunctionArgs(functionRegEx *regexp.Regexp, token string) []string {
commandStrings := functionRegEx.FindStringSubmatch(token)
if len(commandStrings) > 1 {
return commaSeparatorRegEx.Split(commandStrings[1], -1)
}
return nil
}
// Helper for mapping functions that take a wildcard index and an integer as arguments
func transformIndexIntArgsHelper(token string, args []string, transformType int16) (int16, []int, int32, string, error) {
if len(args) < 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs}
}
if len(args) > 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationTooManyArgs}
}
i, err := strconv.Atoi(strings.Trim(args[0], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
}
mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[1], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
}
return transformType, []int{i}, int32(mappingFunctionIntArg), _EMPTY_, nil
}
// Helper to ingest and index the subjectTransform destination token (e.g. $x or {{}}) in the token
// returns a transformation type, and three function arguments: an array of source subject token indexes,
// and a single number (e.g. number of partitions, or a slice size), and a string (e.g.a split delimiter)
func indexPlaceHolders(token string) (int16, []int, int32, string, error) {
length := len(token)
if length > 1 {
// old $1, $2, etc... mapping format still supported to maintain backwards compatibility
if token[0] == '$' { // simple non-partition mapping
tp, err := strconv.Atoi(token[1:])
if err != nil {
// other things rely on tokens starting with $ so not an error just leave it as is
return NoTransform, []int{-1}, -1, _EMPTY_, nil
}
return Wildcard, []int{tp}, -1, _EMPTY_, nil
}
// New 'mustache' style mapping
if length > 4 && token[0] == '{' && token[1] == '{' && token[length-2] == '}' && token[length-1] == '}' {
// wildcard(wildcard token index) (equivalent to $)
args := getMappingFunctionArgs(wildcardMappingFunctionRegEx, token)
if args != nil {
if len(args) == 1 && args[0] == _EMPTY_ {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs}
}
if len(args) == 1 {
tokenIndex, err := strconv.Atoi(strings.Trim(args[0], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
}
return Wildcard, []int{tokenIndex}, -1, _EMPTY_, nil
} else {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationTooManyArgs}
}
}
// partition(number of partitions, token1, token2, ...)
args = getMappingFunctionArgs(partitionMappingFunctionRegEx, token)
if args != nil {
if len(args) < 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs}
}
if len(args) >= 2 {
mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
}
var numPositions = len(args[1:])
tokenIndexes := make([]int, numPositions)
for ti, t := range args[1:] {
i, err := strconv.Atoi(strings.Trim(t, " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
}
tokenIndexes[ti] = i
}
return Partition, tokenIndexes, int32(mappingFunctionIntArg), _EMPTY_, nil
}
}
// SplitFromLeft(token, position)
args = getMappingFunctionArgs(splitFromLeftMappingFunctionRegEx, token)
if args != nil {
return transformIndexIntArgsHelper(token, args, SplitFromLeft)
}
// SplitFromRight(token, position)
args = getMappingFunctionArgs(splitFromRightMappingFunctionRegEx, token)
if args != nil {
return transformIndexIntArgsHelper(token, args, SplitFromRight)
}
// SliceFromLeft(token, position)
args = getMappingFunctionArgs(sliceFromLeftMappingFunctionRegEx, token)
if args != nil {
return transformIndexIntArgsHelper(token, args, SliceFromLeft)
}
// SliceFromRight(token, position)
args = getMappingFunctionArgs(sliceFromRightMappingFunctionRegEx, token)
if args != nil {
return transformIndexIntArgsHelper(token, args, SliceFromRight)
}
// split(token, deliminator)
args = getMappingFunctionArgs(splitMappingFunctionRegEx, token)
if args != nil {
if len(args) < 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationNotEnoughArgs}
}
if len(args) > 2 {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationTooManyArgs}
}
i, err := strconv.Atoi(strings.Trim(args[0], " "))
if err != nil {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrMappingDestinationInvalidArg}
}
if strings.Contains(args[1], " ") || strings.Contains(args[1], tsep) {
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token: token, err: ErrMappingDestinationInvalidArg}
}
return Split, []int{i}, -1, args[1], nil
}
return BadTransform, []int{}, -1, _EMPTY_, &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction}
}
}
return NoTransform, []int{-1}, -1, _EMPTY_, nil
}
// Helper function to tokenize subjects with partial wildcards into formal transform destinations.
// e.g. "foo.*.*" -> "foo.$1.$2"
func transformTokenize(subject string) string {
// We need to make the appropriate markers for the wildcards etc.
i := 1
var nda []string
for _, token := range strings.Split(subject, tsep) {
if token == pwcs {
nda = append(nda, fmt.Sprintf("$%d", i))
i++
} else {
nda = append(nda, token)
}
}
return strings.Join(nda, tsep)
}
// Helper function to go from transform destination to a subject with partial wildcards and ordered list of placeholders
// E.g.:
//
// "bar" -> "bar", []
// "foo.$2.$1" -> "foo.*.*", ["$2","$1"]
// "foo.{{wildcard(2)}}.{{wildcard(1)}}" -> "foo.*.*", ["{{wildcard(2)}}","{{wildcard(1)}}"]
func transformUntokenize(subject string) (string, []string) {
var phs []string
var nda []string
for _, token := range strings.Split(subject, tsep) {
if args := getMappingFunctionArgs(wildcardMappingFunctionRegEx, token); (len(token) > 1 && token[0] == '$' && token[1] >= '1' && token[1] <= '9') || (len(args) == 1 && args[0] != _EMPTY_) {
phs = append(phs, token)
nda = append(nda, pwcs)
} else {
nda = append(nda, token)
}
}
return strings.Join(nda, tsep), phs
}
func tokenizeSubject(subject string) []string {
// Tokenize the subject.
tsa := [32]string{}
tts := tsa[:0]
start := 0
for i := 0; i < len(subject); i++ {
if subject[i] == btsep {
tts = append(tts, subject[start:i])
start = i + 1
}
}
tts = append(tts, subject[start:])
return tts
}
// Match will take a literal published subject that is associated with a client and will match and subjectTransform
// the subject if possible.
//
// This API is not part of the public API and not subject to SemVer protections
func (tr *subjectTransform) Match(subject string) (string, error) {
// Special case: matches any and no no-op subjectTransform. May not be legal config for some features
// but specific validations made at subjectTransform create time
if (tr.src == fwcs || tr.src == _EMPTY_) && (tr.dest == fwcs || tr.dest == _EMPTY_) {
return subject, nil
}
tts := tokenizeSubject(subject)
// TODO(jnm): optimization -> not sure this is actually needed but was there in initial code
if !isValidLiteralSubject(tts) {
return _EMPTY_, ErrBadSubject
}
if (tr.src == _EMPTY_ || tr.src == fwcs) || isSubsetMatch(tts, tr.src) {
return tr.TransformTokenizedSubject(tts), nil
}
return _EMPTY_, ErrNoTransforms
}
// TransformSubject transforms a subject
//
// This API is not part of the public API and not subject to SemVer protection
func (tr *subjectTransform) TransformSubject(subject string) string {
return tr.TransformTokenizedSubject(tokenizeSubject(subject))
}
func (tr *subjectTransform) getHashPartition(key []byte, numBuckets int) string {
h := fnv.New32a()
_, _ = h.Write(key)
return strconv.Itoa(int(h.Sum32() % uint32(numBuckets)))
}
// Do a subjectTransform on the subject to the dest subject.
func (tr *subjectTransform) TransformTokenizedSubject(tokens []string) string {
if len(tr.dtokmftypes) == 0 {
return tr.dest
}
var b strings.Builder
// We need to walk destination tokens and create the mapped subject pulling tokens or mapping functions
li := len(tr.dtokmftypes) - 1
for i, mfType := range tr.dtokmftypes {
if mfType == NoTransform {
// Break if fwc
if len(tr.dtoks[i]) == 1 && tr.dtoks[i][0] == fwc {
break
}
b.WriteString(tr.dtoks[i])
} else {
switch mfType {
case Partition:
var (
_buffer [64]byte
keyForHashing = _buffer[:0]
)
for _, sourceToken := range tr.dtokmftokindexesargs[i] {
keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...)
}
b.WriteString(tr.getHashPartition(keyForHashing, int(tr.dtokmfintargs[i])))
case Wildcard: // simple substitution
b.WriteString(tokens[tr.dtokmftokindexesargs[i][0]])
case SplitFromLeft:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
position := int(tr.dtokmfintargs[i])
if position > 0 && position < sourceTokenLen {
b.WriteString(sourceToken[:position])
b.WriteString(tsep)
b.WriteString(sourceToken[position:])
} else { // too small to split at the requested position: don't split
b.WriteString(sourceToken)
}
case SplitFromRight:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
position := int(tr.dtokmfintargs[i])
if position > 0 && position < sourceTokenLen {
b.WriteString(sourceToken[:sourceTokenLen-position])
b.WriteString(tsep)
b.WriteString(sourceToken[sourceTokenLen-position:])
} else { // too small to split at the requested position: don't split
b.WriteString(sourceToken)
}
case SliceFromLeft:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
sliceSize := int(tr.dtokmfintargs[i])
if sliceSize > 0 && sliceSize < sourceTokenLen {
for i := 0; i+sliceSize <= sourceTokenLen; i += sliceSize {
if i != 0 {
b.WriteString(tsep)
}
b.WriteString(sourceToken[i : i+sliceSize])
if i+sliceSize != sourceTokenLen && i+sliceSize+sliceSize > sourceTokenLen {
b.WriteString(tsep)
b.WriteString(sourceToken[i+sliceSize:])
break
}
}
} else { // too small to slice at the requested size: don't slice
b.WriteString(sourceToken)
}
case SliceFromRight:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
sliceSize := int(tr.dtokmfintargs[i])
if sliceSize > 0 && sliceSize < sourceTokenLen {
remainder := sourceTokenLen % sliceSize
if remainder > 0 {
b.WriteString(sourceToken[:remainder])
b.WriteString(tsep)
}
for i := remainder; i+sliceSize <= sourceTokenLen; i += sliceSize {
b.WriteString(sourceToken[i : i+sliceSize])
if i+sliceSize < sourceTokenLen {
b.WriteString(tsep)
}
}
} else { // too small to slice at the requested size: don't slice
b.WriteString(sourceToken)
}
case Split:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
splits := strings.Split(sourceToken, tr.dtokmfstringargs[i])
for j, split := range splits {
if split != _EMPTY_ {
b.WriteString(split)
}
if j < len(splits)-1 && splits[j+1] != _EMPTY_ && !(j == 0 && split == _EMPTY_) {
b.WriteString(tsep)
}
}
}
}
if i < li {
b.WriteByte(btsep)
}
}
// We may have more source tokens available. This happens with ">".
if tr.dtoks[len(tr.dtoks)-1] == fwcs {
for sli, i := len(tokens)-1, len(tr.stoks)-1; i < len(tokens); i++ {
b.WriteString(tokens[i])
if i < sli {
b.WriteByte(btsep)
}
}
}
return b.String()
}
// Reverse a subjectTransform.
func (tr *subjectTransform) reverse() *subjectTransform {
if len(tr.dtokmftokindexesargs) == 0 {
rtr, _ := NewSubjectTransformStrict(tr.dest, tr.src)
return rtr
}
// If we are here we need to dynamically get the correct reverse
// of this subjectTransform.
nsrc, phs := transformUntokenize(tr.dest)
var nda []string
for _, token := range tr.stoks {
if token == pwcs {
if len(phs) == 0 {
// TODO(dlc) - Should not happen
return nil
}
nda = append(nda, phs[0])
phs = phs[1:]
} else {
nda = append(nda, token)
}
}
ndest := strings.Join(nda, tsep)
rtr, _ := NewSubjectTransformStrict(nsrc, ndest)
return rtr
}
+3
View File
@@ -1142,6 +1142,9 @@ func isValidLiteralSubject(tokens []string) bool {
// ValidateMappingDestination returns nil error if the subject is a valid subject mapping destination subject
func ValidateMappingDestination(subject string) error {
if subject == _EMPTY_ {
return nil
}
subjectTokens := strings.Split(subject, tsep)
sfwc := false
for _, t := range subjectTokens {
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2022 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.
//go:build zos
// +build zos
package sysmem
func Memory() int64 {
// TODO: We don't know the system memory
return 0
}
+9
View File
@@ -14,7 +14,9 @@
package server
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"math"
@@ -334,3 +336,10 @@ func copyStrings(src []string) []string {
copy(dst, src)
return dst
}
// Returns a byte slice for the INFO protocol.
func generateInfoJSON(info *Info) []byte {
b, _ := json.Marshal(info)
pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
return bytes.Join(pcs, []byte(" "))
}
+8 -6
View File
@@ -15,7 +15,7 @@ package server
import (
"bytes"
"crypto/rand"
crand "crypto/rand"
"crypto/sha1"
"crypto/tls"
"encoding/base64"
@@ -545,7 +545,7 @@ func wsFillFrameHeader(fh []byte, useMasking, first, final, compressed bool, fra
var key []byte
if useMasking {
var keyBuf [4]byte
if _, err := io.ReadFull(rand.Reader, keyBuf[:4]); err != nil {
if _, err := io.ReadFull(crand.Reader, keyBuf[:4]); err != nil {
kv := mrand.Int31()
binary.LittleEndian.PutUint32(keyBuf[:4], uint32(kv))
}
@@ -958,7 +958,7 @@ func wsAcceptKey(key string) string {
func wsMakeChallengeKey() (string, error) {
p := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, p); err != nil {
if _, err := io.ReadFull(crand.Reader, p); err != nil {
return _EMPTY_, err
}
return base64.StdEncoding.EncodeToString(p), nil
@@ -1234,12 +1234,14 @@ func (s *Server) createWSClient(conn net.Conn, ws *websocket) *client {
return nil
}
s.clients[c.cid] = c
// Websocket clients do TLS in the websocket http server.
// So no TLS here...
s.mu.Unlock()
c.mu.Lock()
// Websocket clients do TLS in the websocket http server.
// So no TLS initiation here...
if _, ok := conn.(*tls.Conn); ok {
c.flags.set(handshakeComplete)
}
if c.isClosed() {
c.mu.Unlock()
+3
View File
@@ -8,3 +8,6 @@ issues:
- linters:
- errcheck
text: "msg.Ack"
- linters:
- errcheck
text: "watcher.Stop"
+1 -1
View File
@@ -29,7 +29,7 @@ When using or transitioning to Go modules support:
```bash
# Go client latest or explicit version
go get github.com/nats-io/nats.go/@latest
go get github.com/nats-io/nats.go/@v1.28.0
go get github.com/nats-io/nats.go/@v1.29.0
# For latest NATS Server, add /v2 at the end
go get github.com/nats-io/nats-server/v2
+30 -27
View File
@@ -396,7 +396,7 @@ func DirectGetNext(subject string) JSOpt {
}
// StreamListFilter is an option that can be used to configure `StreamsInfo()` and `StreamNames()` requests.
// It allows filtering the retured streams by subject associated with each stream.
// It allows filtering the returned streams by subject associated with each stream.
// Wildcards can be used. For example, `StreamListFilter(FOO.*.A) will return
// all streams which have at least one subject matching the provided pattern (e.g. FOO.TEST.A).
func StreamListFilter(subject string) JSOpt {
@@ -698,6 +698,10 @@ func (js *js) resetPendingAcksOnReconnect() {
paf.err = ErrDisconnected
}
js.pafs = nil
if js.dch != nil {
close(js.dch)
js.dch = nil
}
js.mu.Unlock()
}
}
@@ -1822,6 +1826,17 @@ func (js *js) subscribe(subj, queue string, cb MsgHandler, ch chan *Msg, isSync,
return sub, nil
}
// InitialConsumerPending returns the number of messages pending to be
// delivered to the consumer when the subscription was created.
func (sub *Subscription) InitialConsumerPending() (uint64, error) {
sub.mu.Lock()
defer sub.mu.Unlock()
if sub.jsi == nil || sub.jsi.consumer == _EMPTY_ {
return 0, fmt.Errorf("%w: not a JetStream subscription", ErrTypeSubscription)
}
return sub.jsi.pending, nil
}
// This long-lived routine is used per ChanSubscription to check
// on the number of delivered messages and check for flow control response.
func (sub *Subscription) chanSubcheckForFlowControlResponse() {
@@ -1915,7 +1930,7 @@ func (sub *Subscription) checkOrderedMsgs(m *Msg) bool {
if err != nil {
return false
}
sseq, dseq := parser.ParseNum(tokens[ackStreamSeqTokenPos]), parser.ParseNum(tokens[ackConsumerSeqTokenPos])
sseq, dseq := parser.ParseNum(tokens[parser.AckStreamSeqTokenPos]), parser.ParseNum(tokens[parser.AckConsumerSeqTokenPos])
jsi := sub.jsi
if dseq != jsi.dseq {
@@ -2029,7 +2044,7 @@ func (sub *Subscription) resetOrderedConsumer(sseq uint64) {
cinfo, err := js.upsertConsumer(jsi.stream, consName, cfg)
if err != nil {
var apiErr *APIError
if errors.Is(err, ErrJetStreamNotEnabled) || errors.Is(err, ErrTimeout) {
if errors.Is(err, ErrJetStreamNotEnabled) || errors.Is(err, ErrTimeout) || errors.Is(err, context.DeadlineExceeded) {
// if creating consumer failed, retry
return
} else if errors.As(err, &apiErr) && apiErr.ErrorCode == JSErrCodeInsufficientResourcesErr {
@@ -2157,7 +2172,7 @@ func (nc *Conn) checkForSequenceMismatch(msg *Msg, s *Subscription, jsi *jsSub)
// Consumer sequence.
var ldseq string
dseq := tokens[ackConsumerSeqTokenPos]
dseq := tokens[parser.AckConsumerSeqTokenPos]
hdr := msg.Header[lastConsumerSeqHdr]
if len(hdr) == 1 {
ldseq = hdr[0]
@@ -2168,7 +2183,7 @@ func (nc *Conn) checkForSequenceMismatch(msg *Msg, s *Subscription, jsi *jsSub)
if ldseq != dseq {
// Dispatch async error including details such as
// from where the consumer could be restarted.
sseq := parser.ParseNum(tokens[ackStreamSeqTokenPos])
sseq := parser.ParseNum(tokens[parser.AckStreamSeqTokenPos])
if ordered {
s.mu.Lock()
s.resetOrderedConsumer(jsi.sseq + 1)
@@ -2211,7 +2226,7 @@ type subOpts struct {
skipCInfo bool
}
// SkipConsumerLookup will omit lookipng up consumer when [Bind], [Durable]
// SkipConsumerLookup will omit looking up consumer when [Bind], [Durable]
// or [ConsumerName] are provided.
//
// NOTE: This setting may cause an existing consumer to be overwritten. Also,
@@ -3280,18 +3295,6 @@ type MsgMetadata struct {
Domain string
}
const (
ackDomainTokenPos = 2
ackAccHashTokenPos = 3
ackStreamTokenPos = 4
ackConsumerTokenPos = 5
ackNumDeliveredTokenPos = 6
ackStreamSeqTokenPos = 7
ackConsumerSeqTokenPos = 8
ackTimestampSeqTokenPos = 9
ackNumPendingTokenPos = 10
)
// Metadata retrieves the metadata from a JetStream message. This method will
// return an error for non-JetStream Msgs.
func (m *Msg) Metadata() (*MsgMetadata, error) {
@@ -3305,15 +3308,15 @@ func (m *Msg) Metadata() (*MsgMetadata, error) {
}
meta := &MsgMetadata{
Domain: tokens[ackDomainTokenPos],
NumDelivered: parser.ParseNum(tokens[ackNumDeliveredTokenPos]),
NumPending: parser.ParseNum(tokens[ackNumPendingTokenPos]),
Timestamp: time.Unix(0, int64(parser.ParseNum(tokens[ackTimestampSeqTokenPos]))),
Stream: tokens[ackStreamTokenPos],
Consumer: tokens[ackConsumerTokenPos],
Domain: tokens[parser.AckDomainTokenPos],
NumDelivered: parser.ParseNum(tokens[parser.AckNumDeliveredTokenPos]),
NumPending: parser.ParseNum(tokens[parser.AckNumPendingTokenPos]),
Timestamp: time.Unix(0, int64(parser.ParseNum(tokens[parser.AckTimestampSeqTokenPos]))),
Stream: tokens[parser.AckStreamTokenPos],
Consumer: tokens[parser.AckConsumerTokenPos],
}
meta.Sequence.Stream = parser.ParseNum(tokens[ackStreamSeqTokenPos])
meta.Sequence.Consumer = parser.ParseNum(tokens[ackConsumerSeqTokenPos])
meta.Sequence.Stream = parser.ParseNum(tokens[parser.AckStreamSeqTokenPos])
meta.Sequence.Consumer = parser.ParseNum(tokens[parser.AckConsumerSeqTokenPos])
return meta, nil
}
@@ -3363,7 +3366,7 @@ func (p AckPolicy) MarshalJSON() ([]byte, error) {
case AckExplicitPolicy:
return json.Marshal("explicit")
default:
return nil, fmt.Errorf("nats: unknown acknowlegement policy %v", p)
return nil, fmt.Errorf("nats: unknown acknowledgement policy %v", p)
}
}
+38 -12
View File
@@ -123,6 +123,8 @@ type watchOpts struct {
ignoreDeletes bool
// Include all history per subject, not just last one.
includeHistory bool
// Include only updates for keys.
updatesOnly bool
// retrieve only the meta data of the entry
metaOnly bool
}
@@ -136,11 +138,25 @@ func (opt watchOptFn) configureWatcher(opts *watchOpts) error {
// IncludeHistory instructs the key watcher to include historical values as well.
func IncludeHistory() WatchOpt {
return watchOptFn(func(opts *watchOpts) error {
if opts.updatesOnly {
return errors.New("nats: include history can not be used with updates only")
}
opts.includeHistory = true
return nil
})
}
// UpdatesOnly instructs the key watcher to only include updates on values (without latest values when started).
func UpdatesOnly() WatchOpt {
return watchOptFn(func(opts *watchOpts) error {
if opts.includeHistory {
return errors.New("nats: updates only can not be used with include history")
}
opts.updatesOnly = true
return nil
})
}
// IgnoreDeletes will have the key watcher not pass any deleted keys.
func IgnoreDeletes() WatchOpt {
return watchOptFn(func(opts *watchOpts) error {
@@ -622,7 +638,7 @@ func (kv *kvs) PutString(key string, value string) (revision uint64, err error)
return kv.Put(key, []byte(value))
}
// Create will add the key/value pair iff it does not exist.
// Create will add the key/value pair if it does not exist.
func (kv *kvs) Create(key string, value []byte) (revision uint64, err error) {
v, err := kv.Update(key, value, 0)
if err == nil {
@@ -645,7 +661,7 @@ func (kv *kvs) Create(key string, value []byte) (revision uint64, err error) {
return 0, err
}
// Update will update the value iff the latest revision matches.
// Update will update the value if the latest revision matches.
func (kv *kvs) Update(key string, value []byte, revision uint64) (uint64, error) {
if !keyValid(key) {
return 0, ErrInvalidKey
@@ -909,7 +925,7 @@ func (kv *kvs) Watch(keys string, opts ...WatchOpt) (KeyWatcher, error) {
op = KeyValuePurge
}
}
delta := parser.ParseNum(tokens[ackNumPendingTokenPos])
delta := parser.ParseNum(tokens[parser.AckNumPendingTokenPos])
w.mu.Lock()
defer w.mu.Unlock()
if !o.ignoreDeletes || (op != KeyValueDelete && op != KeyValuePurge) {
@@ -917,14 +933,15 @@ func (kv *kvs) Watch(keys string, opts ...WatchOpt) (KeyWatcher, error) {
bucket: kv.name,
key: subj,
value: m.Data,
revision: parser.ParseNum(tokens[ackStreamSeqTokenPos]),
created: time.Unix(0, int64(parser.ParseNum(tokens[ackTimestampSeqTokenPos]))),
revision: parser.ParseNum(tokens[parser.AckStreamSeqTokenPos]),
created: time.Unix(0, int64(parser.ParseNum(tokens[parser.AckTimestampSeqTokenPos]))),
delta: delta,
op: op,
}
w.updates <- entry
}
// Check if done and initial values.
// Skip if UpdatesOnly() is set, since there will never be updates initially.
if !w.initDone {
w.received++
// We set this on the first trip through..
@@ -943,6 +960,9 @@ func (kv *kvs) Watch(keys string, opts ...WatchOpt) (KeyWatcher, error) {
if !o.includeHistory {
subOpts = append(subOpts, DeliverLastPerSubject())
}
if o.updatesOnly {
subOpts = append(subOpts, DeliverNew())
}
if o.metaOnly {
subOpts = append(subOpts, HeadersOnly())
}
@@ -961,12 +981,18 @@ func (kv *kvs) Watch(keys string, opts ...WatchOpt) (KeyWatcher, error) {
sub.mu.Lock()
// If there were no pending messages at the time of the creation
// of the consumer, send the marker.
if sub.jsi != nil && sub.jsi.pending == 0 {
// Skip if UpdatesOnly() is set, since there will never be updates initially.
if !o.updatesOnly {
if sub.jsi != nil && sub.jsi.pending == 0 {
w.initDone = true
w.updates <- nil
}
} else {
// if UpdatesOnly was used, mark initialization as complete
w.initDone = true
w.updates <- nil
}
// Set us up to close when the waitForMessages func returns.
sub.pDone = func() {
sub.pDone = func(_ string) {
close(w.updates)
}
sub.mu.Unlock()
@@ -1020,16 +1046,16 @@ func (kv *kvs) Status() (KeyValueStatus, error) {
// KeyValueStoreNames is used to retrieve a list of key value store names
func (js *js) KeyValueStoreNames() <-chan string {
ch := make(chan string)
l := &streamLister{js: js}
l := &streamNamesLister{js: js}
l.js.opts.streamListSubject = fmt.Sprintf(kvSubjectsTmpl, "*")
go func() {
defer close(ch)
for l.Next() {
for _, info := range l.Page() {
if !strings.HasPrefix(info.Config.Name, kvBucketNamePre) {
for _, name := range l.Page() {
if !strings.HasPrefix(name, kvBucketNamePre) {
continue
}
ch <- info.Config.Name
ch <- name
}
}
}()
+18 -3
View File
@@ -47,7 +47,7 @@ import (
// Default Constants
const (
Version = "1.28.0"
Version = "1.29.0"
DefaultURL = "nats://127.0.0.1:4222"
DefaultPort = 4222
DefaultMaxReconnect = 60
@@ -61,6 +61,7 @@ const (
DefaultReconnectBufSize = 8 * 1024 * 1024 // 8MB
RequestChanLen = 8
DefaultDrainTimeout = 30 * time.Second
DefaultFlusherTimeout = time.Minute
LangString = "go"
)
@@ -154,6 +155,7 @@ func GetDefaultOptions() Options {
SubChanLen: DefaultMaxChanLen,
ReconnectBufSize: DefaultReconnectBufSize,
DrainTimeout: DefaultDrainTimeout,
FlusherTimeout: DefaultFlusherTimeout,
}
}
@@ -356,6 +358,7 @@ type Options struct {
// FlusherTimeout is the maximum time to wait for write operations
// to the underlying connection to complete (including the flusher loop).
// Defaults to 1m.
FlusherTimeout time.Duration
// PingInterval is the period at which the client will be sending ping
@@ -613,7 +616,7 @@ type Subscription struct {
pHead *Msg
pTail *Msg
pCond *sync.Cond
pDone func()
pDone func(subject string)
// Pending stats, async subscriptions, high-speed etc.
pMsgs int
@@ -946,6 +949,7 @@ func ReconnectWait(t time.Duration) Option {
}
// MaxReconnects is an Option to set the maximum number of reconnect attempts.
// If negative, it will never stop trying to reconnect.
// Defaults to 60.
func MaxReconnects(max int) Option {
return func(o *Options) error {
@@ -2500,6 +2504,9 @@ func (nc *Conn) sendConnect() error {
// Construct the CONNECT protocol string
cProto, err := nc.connectProto()
if err != nil {
if !nc.initc && nc.Opts.AsyncErrorCB != nil {
nc.ach.push(func() { nc.Opts.AsyncErrorCB(nc, nil, err) })
}
return err
}
@@ -3025,7 +3032,7 @@ func (nc *Conn) waitForMsgs(s *Subscription) {
s.mu.Unlock()
if done != nil {
done()
done(s.Subject)
}
}
@@ -4450,6 +4457,14 @@ func (s *Subscription) AutoUnsubscribe(max int) error {
return conn.unsubscribe(s, max, false)
}
// SetClosedHandler will set the closed handler for when a subscription
// is closed (either unsubscribed or drained).
func (s *Subscription) SetClosedHandler(handler func(subject string)) {
s.mu.Lock()
s.pDone = handler
s.mu.Unlock()
}
// unsubscribe performs the low level unsubscribe to the server.
// Use Subscription.Unsubscribe()
func (nc *Conn) unsubscribe(sub *Subscription, max int, drainMode bool) error {
+23 -18
View File
@@ -34,10 +34,6 @@ import (
)
// ObjectStoreManager creates, loads and deletes Object Stores
//
// Notice: Experimental Preview
//
// This functionality is EXPERIMENTAL and may be changed in later releases.
type ObjectStoreManager interface {
// ObjectStore will look up and bind to an existing object store instance.
ObjectStore(bucket string) (ObjectStore, error)
@@ -53,10 +49,6 @@ type ObjectStoreManager interface {
// ObjectStore is a blob store capable of storing large objects efficiently in
// JetStream streams
//
// Notice: Experimental Preview
//
// This functionality is EXPERIMENTAL and may be changed in later releases.
type ObjectStore interface {
// Put will place the contents from the reader into a new object.
Put(obj *ObjectMeta, reader io.Reader, opts ...ObjectOpt) (*ObjectInfo, error)
@@ -150,13 +142,13 @@ var (
// ObjectStoreConfig is the config for the object store.
type ObjectStoreConfig struct {
Bucket string
Description string
TTL time.Duration
MaxBytes int64
Storage StorageType
Replicas int
Placement *Placement
Bucket string `json:"bucket"`
Description string `json:"description,omitempty"`
TTL time.Duration `json:"max_age,omitempty"`
MaxBytes int64 `json:"max_bytes,omitempty"`
Storage StorageType `json:"storage,omitempty"`
Replicas int `json:"num_replicas,omitempty"`
Placement *Placement `json:"placement,omitempty"`
}
type ObjectStoreStatus interface {
@@ -658,7 +650,7 @@ func (obs *obs) Get(name string, opts ...GetObjectOpt) (ObjectResult, error) {
result.digest.Write(m.Data)
// Check if we are done.
if tokens[ackNumPendingTokenPos] == objNoPending {
if tokens[parser.AckNumPendingTokenPos] == objNoPending {
pw.Close()
m.Sub.Unsubscribe()
}
@@ -1056,6 +1048,8 @@ func (obs *obs) Watch(opts ...WatchOpt) (ObjectWatcher, error) {
w.updates <- &info
}
// if UpdatesOnly is set, no not send nil to the channel
// as it would always be triggered after initializing the watcher
if !initDoneMarker && meta.NumPending == 0 {
initDoneMarker = true
w.updates <- nil
@@ -1064,9 +1058,17 @@ func (obs *obs) Watch(opts ...WatchOpt) (ObjectWatcher, error) {
allMeta := fmt.Sprintf(objAllMetaPreTmpl, obs.name)
_, err := obs.js.GetLastMsg(obs.stream, allMeta)
if err == ErrMsgNotFound {
// if there are no messages on the stream and we are not watching
// updates only, send nil to the channel to indicate that the initial
// watch is done
if !o.updatesOnly {
if errors.Is(err, ErrMsgNotFound) {
initDoneMarker = true
w.updates <- nil
}
} else {
// if UpdatesOnly was used, mark initialization as complete
initDoneMarker = true
w.updates <- nil
}
// Used ordered consumer to deliver results.
@@ -1074,6 +1076,9 @@ func (obs *obs) Watch(opts ...WatchOpt) (ObjectWatcher, error) {
if !o.includeHistory {
subOpts = append(subOpts, DeliverLastPerSubject())
}
if o.updatesOnly {
subOpts = append(subOpts, DeliverNew())
}
sub, err := obs.js.Subscribe(allMeta, update, subOpts...)
if err != nil {
return nil, err
+25 -3
View File
@@ -6,15 +6,34 @@ release:
name_template: '{{.Tag}}'
draft: true
builds:
- main: ./nk/main.go
- id: nk
main: ./nk/main.go
ldflags: "-X main.Version={{.Tag}}_{{.Commit}}"
binary: nk
goos:
- linux
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- arm
- arm64
- 386
- mips64le
- s390x
goarm:
- 6
- 7
ignore:
- goos: darwin
goarch: 386
- goos: freebsd
goarch: arm
- goos: freebsd
goarch: arm64
- goos: freebsd
goarch: 386
dist: build
@@ -23,6 +42,9 @@ archives:
}}v{{ .Arm }}{{ end }}'
wrap_in_directory: true
format: zip
files:
- README.md
- LICENSE
checksum:
name_template: '{{ .ProjectName }}-v{{ .Version }}-checksums.txt'
+1 -1
View File
@@ -19,7 +19,7 @@ package nkeys
import "io"
// Version is our current version
const Version = "0.4.4"
const Version = "0.4.5"
// KeyPair provides the central interface to nkeys.
type KeyPair interface {
+5 -9
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2022 The NATS Authors
// Copyright 2018-2023 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
@@ -137,22 +137,18 @@ func decode(src []byte) ([]byte, error) {
}
raw = raw[:n]
if len(raw) < 4 {
if n < 4 {
return nil, ErrInvalidEncoding
}
var crc uint16
checksum := bytes.NewReader(raw[len(raw)-2:])
if err := binary.Read(checksum, binary.LittleEndian, &crc); err != nil {
return nil, err
}
crc := binary.LittleEndian.Uint16(raw[n-2:])
// ensure checksum is valid
if err := validate(raw[0:len(raw)-2], crc); err != nil {
if err := validate(raw[0:n-2], crc); err != nil {
return nil, err
}
return raw[:len(raw)-2], nil
return raw[:n-2], nil
}
// Decode will decode the base32 string and check crc16 and enforce the prefix is what is expected.