mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-03 09:20:50 -05:00
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:
committed by
Ralf Haferkamp
parent
a1b7dc34cd
commit
502ec695f1
+29
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
+1190
-420
File diff suppressed because it is too large
Load Diff
+9
-11
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
consumer’s 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 client’s
|
||||
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 manager’s sublist’s
|
||||
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 session’s 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 don’t 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 session’s 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 NATS’s `“*”` wildcard, however, MQTT’s 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
+589
-163
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
+1890
-1024
File diff suppressed because it is too large
Load Diff
+42
-15
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
+278
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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
@@ -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
@@ -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, <, 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(<, &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, <)
|
||||
switch strings.ToLower(mk) {
|
||||
case "mode":
|
||||
c.Mode = mv.(string)
|
||||
case "rtt_thresholds", "thresholds", "rtts", "rtt":
|
||||
for _, iv := range mv.([]interface{}) {
|
||||
_, mv := unwrapValue(iv, <)
|
||||
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(<, errors)
|
||||
|
||||
tk, mv = unwrapValue(mv, <)
|
||||
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, <)
|
||||
|
||||
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, <)
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+485
-55
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+668
-350
File diff suppressed because it is too large
Load Diff
+563
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -8,3 +8,6 @@ issues:
|
||||
- linters:
|
||||
- errcheck
|
||||
text: "msg.Ack"
|
||||
- linters:
|
||||
- errcheck
|
||||
text: "watcher.Stop"
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user