mirror of
https://github.com/hanzomsg/nats-server.git
synced 2026-10-30 20:27:34 +00:00
The test `TestLeafNodePubAllowedPruning` would sometimes report (after all go routines calling c.pubAllowed() being done) that the cache size was greater than the max allowed (128). @MauriceVanVeen did fix the test with a correct explanation of why this test flapped, but I think we can improve a tiny bit the code so that the cache is much likely to be pruned enough. In this PR, I make `prunePubPermsCache()` attempts up to 5 times to prune enough the cache, locking/releasing at each attempt to improve the odds that another routine takes a shot at it. In my experiments, I am seeing only up to 2 attempts to be below the max size (and less than 10ms execution time). That being said, with artificial sleep before the release of the lock, it could take way more attempts. So for very slow machines, although I have reverted the changes to the test done in PR#6636, I did add a little protection to call one more time `c.pubAllowed()` if we first detect that we are over the limit. If we are still after that the test will fail and would need another look. Signed-off-by: Ivan Kozlovic <[email protected]>
9862 lines
263 KiB
Go
9862 lines
263 KiB
Go
// Copyright 2019-2025 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nkeys"
|
|
|
|
"github.com/klauspost/compress/s2"
|
|
jwt "github.com/nats-io/jwt/v2"
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/nats-io/nats-server/v2/internal/fastrand"
|
|
"github.com/nats-io/nats-server/v2/internal/testhelper"
|
|
)
|
|
|
|
type captureLeafNodeRandomIPLogger struct {
|
|
DummyLogger
|
|
ch chan struct{}
|
|
ips [3]int
|
|
}
|
|
|
|
func (c *captureLeafNodeRandomIPLogger) Debugf(format string, v ...any) {
|
|
msg := fmt.Sprintf(format, v...)
|
|
if strings.Contains(msg, "hostname_to_resolve") {
|
|
ippos := strings.Index(msg, "127.0.0.")
|
|
if ippos != -1 {
|
|
n := int(msg[ippos+8] - '1')
|
|
c.ips[n]++
|
|
for _, v := range c.ips {
|
|
if v < 2 {
|
|
return
|
|
}
|
|
}
|
|
// All IPs got at least some hit, we are done.
|
|
c.ch <- struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeRandomIP(t *testing.T) {
|
|
u, err := url.Parse("nats://hostname_to_resolve:1234")
|
|
if err != nil {
|
|
t.Fatalf("Error parsing: %v", err)
|
|
}
|
|
|
|
resolver := &myDummyDNSResolver{ips: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}}
|
|
|
|
o := DefaultOptions()
|
|
o.Host = "127.0.0.1"
|
|
o.Port = -1
|
|
o.LeafNode.Port = 0
|
|
o.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
|
|
o.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
o.LeafNode.resolver = resolver
|
|
o.LeafNode.dialTimeout = 15 * time.Millisecond
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
l := &captureLeafNodeRandomIPLogger{ch: make(chan struct{})}
|
|
s.SetLogger(l, true, true)
|
|
|
|
select {
|
|
case <-l.ch:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatalf("Does not seem to have used random IPs")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeRandomRemotes(t *testing.T) {
|
|
// 16! possible permutations.
|
|
orderedURLs := make([]*url.URL, 0, 16)
|
|
for i := 0; i < cap(orderedURLs); i++ {
|
|
orderedURLs = append(orderedURLs, &url.URL{
|
|
Scheme: "nats-leaf",
|
|
Host: fmt.Sprintf("host%d:7422", i),
|
|
})
|
|
}
|
|
|
|
o := DefaultOptions()
|
|
o.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{NoRandomize: true},
|
|
{NoRandomize: false},
|
|
}
|
|
o.LeafNode.Remotes[0].URLs = make([]*url.URL, cap(orderedURLs))
|
|
copy(o.LeafNode.Remotes[0].URLs, orderedURLs)
|
|
o.LeafNode.Remotes[1].URLs = make([]*url.URL, cap(orderedURLs))
|
|
copy(o.LeafNode.Remotes[1].URLs, orderedURLs)
|
|
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
s.mu.Lock()
|
|
r1 := s.leafRemoteCfgs[0]
|
|
r2 := s.leafRemoteCfgs[1]
|
|
s.mu.Unlock()
|
|
|
|
r1.RLock()
|
|
gotOrdered := r1.urls
|
|
r1.RUnlock()
|
|
if got, want := len(gotOrdered), len(orderedURLs); got != want {
|
|
t.Fatalf("Unexpected rem0 len URLs, got %d, want %d", got, want)
|
|
}
|
|
|
|
// These should be IN order.
|
|
for i := range orderedURLs {
|
|
if got, want := gotOrdered[i].String(), orderedURLs[i].String(); got != want {
|
|
t.Fatalf("Unexpected ordered url, got %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
r2.RLock()
|
|
gotRandom := r2.urls
|
|
r2.RUnlock()
|
|
if got, want := len(gotRandom), len(orderedURLs); got != want {
|
|
t.Fatalf("Unexpected rem1 len URLs, got %d, want %d", got, want)
|
|
}
|
|
|
|
// These should be OUT of order.
|
|
var random bool
|
|
for i := range orderedURLs {
|
|
if gotRandom[i].String() != orderedURLs[i].String() {
|
|
random = true
|
|
break
|
|
}
|
|
}
|
|
if !random {
|
|
t.Fatal("Expected urls to be random")
|
|
}
|
|
}
|
|
|
|
type testLoopbackResolver struct{}
|
|
|
|
func (r *testLoopbackResolver) LookupHost(ctx context.Context, host string) ([]string, error) {
|
|
return []string{"127.0.0.1"}, nil
|
|
}
|
|
|
|
func TestLeafNodeTLSWithCerts(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(`
|
|
port: -1
|
|
leaf {
|
|
listen: "127.0.0.1:-1"
|
|
tls {
|
|
ca_file: "../test/configs/certs/tlsauth/ca.pem"
|
|
cert_file: "../test/configs/certs/tlsauth/server.pem"
|
|
key_file: "../test/configs/certs/tlsauth/server-key.pem"
|
|
timeout: 2
|
|
}
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
u, err := url.Parse(fmt.Sprintf("nats://localhost:%d", o1.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leaf {
|
|
remotes [
|
|
{
|
|
url: "%s"
|
|
tls {
|
|
ca_file: "../test/configs/certs/tlsauth/ca.pem"
|
|
cert_file: "../test/configs/certs/tlsauth/client.pem"
|
|
key_file: "../test/configs/certs/tlsauth/client-key.pem"
|
|
timeout: 2
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`, u.String())))
|
|
o2, err := ProcessConfigFile(conf2)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
o2.NoLog, o2.NoSigs = true, true
|
|
o2.LeafNode.resolver = &testLoopbackResolver{}
|
|
s2 := RunServer(o2)
|
|
defer s2.Shutdown()
|
|
|
|
checkFor(t, 3*time.Second, 10*time.Millisecond, func() error {
|
|
if nln := s1.NumLeafNodes(); nln != 1 {
|
|
return fmt.Errorf("Number of leaf nodes is %d", nln)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestLeafNodeTLSRemoteWithNoCerts(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(`
|
|
port: -1
|
|
leaf {
|
|
listen: "127.0.0.1:-1"
|
|
tls {
|
|
ca_file: "../test/configs/certs/tlsauth/ca.pem"
|
|
cert_file: "../test/configs/certs/tlsauth/server.pem"
|
|
key_file: "../test/configs/certs/tlsauth/server-key.pem"
|
|
timeout: 2
|
|
}
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
u, err := url.Parse(fmt.Sprintf("nats://localhost:%d", o1.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leaf {
|
|
remotes [
|
|
{
|
|
url: "%s"
|
|
tls {
|
|
ca_file: "../test/configs/certs/tlsauth/ca.pem"
|
|
timeout: 5
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`, u.String())))
|
|
o2, err := ProcessConfigFile(conf2)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
|
|
if len(o2.LeafNode.Remotes) == 0 {
|
|
t.Fatal("Expected at least a single leaf remote")
|
|
}
|
|
|
|
var (
|
|
got float64 = o2.LeafNode.Remotes[0].TLSTimeout
|
|
expected float64 = 5
|
|
)
|
|
if got != expected {
|
|
t.Fatalf("Expected %v, got: %v", expected, got)
|
|
}
|
|
o2.NoLog, o2.NoSigs = true, true
|
|
o2.LeafNode.resolver = &testLoopbackResolver{}
|
|
s2 := RunServer(o2)
|
|
defer s2.Shutdown()
|
|
|
|
checkFor(t, 3*time.Second, 10*time.Millisecond, func() error {
|
|
if nln := s1.NumLeafNodes(); nln != 1 {
|
|
return fmt.Errorf("Number of leaf nodes is %d", nln)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Here we only process options without starting the server
|
|
// and without a root CA for the remote.
|
|
conf3 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leaf {
|
|
remotes [
|
|
{
|
|
url: "%s"
|
|
tls {
|
|
timeout: 10
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`, u.String())))
|
|
o3, err := ProcessConfigFile(conf3)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
|
|
if len(o3.LeafNode.Remotes) == 0 {
|
|
t.Fatal("Expected at least a single leaf remote")
|
|
}
|
|
got = o3.LeafNode.Remotes[0].TLSTimeout
|
|
expected = 10
|
|
if got != expected {
|
|
t.Fatalf("Expected %v, got: %v", expected, got)
|
|
}
|
|
|
|
// Here we only process options without starting the server
|
|
// and check the default for leafnode remotes.
|
|
conf4 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leaf {
|
|
remotes [
|
|
{
|
|
url: "%s"
|
|
tls {
|
|
ca_file: "../test/configs/certs/tlsauth/ca.pem"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`, u.String())))
|
|
o4, err := ProcessConfigFile(conf4)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
|
|
if len(o4.LeafNode.Remotes) == 0 {
|
|
t.Fatal("Expected at least a single leaf remote")
|
|
}
|
|
got = o4.LeafNode.Remotes[0].TLSTimeout
|
|
expected = float64(DEFAULT_LEAF_TLS_TIMEOUT) / float64(time.Second)
|
|
if int(got) != int(expected) {
|
|
t.Fatalf("Expected %v, got: %v", expected, got)
|
|
}
|
|
}
|
|
|
|
type captureErrorLogger struct {
|
|
DummyLogger
|
|
errCh chan string
|
|
}
|
|
|
|
func (l *captureErrorLogger) Errorf(format string, v ...any) {
|
|
select {
|
|
case l.errCh <- fmt.Sprintf(format, v...):
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeAccountNotFound(t *testing.T) {
|
|
ob := DefaultOptions()
|
|
ob.LeafNode.Host = "127.0.0.1"
|
|
ob.LeafNode.Port = -1
|
|
sb := RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
u, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ob.LeafNode.Port))
|
|
|
|
oa := DefaultOptions()
|
|
oa.Cluster.Name = "xyz"
|
|
oa.LeafNode.ReconnectInterval = 10 * time.Millisecond
|
|
oa.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
LocalAccount: "foo",
|
|
URLs: []*url.URL{u},
|
|
},
|
|
}
|
|
// Expected to fail
|
|
if _, err := NewServer(oa); err == nil || !strings.Contains(err.Error(), "local account") {
|
|
t.Fatalf("Expected server to fail with error about no local account, got %v", err)
|
|
}
|
|
oa.Accounts = []*Account{NewAccount("foo")}
|
|
sa := RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 1)}
|
|
sa.SetLogger(l, false, false)
|
|
|
|
checkLeafNodeConnected(t, sa)
|
|
|
|
// Now simulate account is removed with config reload, or it expires.
|
|
sa.accounts.Delete("foo")
|
|
|
|
// Restart B (with same Port)
|
|
sb.Shutdown()
|
|
sb = RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
// Wait for report of error
|
|
select {
|
|
case e := <-l.errCh:
|
|
if !strings.Contains(e, "Unable to lookup account") {
|
|
t.Fatalf("Expected error about no local account, got %s", e)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Did not get the error")
|
|
}
|
|
|
|
// TODO below test is bogus. Instead add the account, do a reload, and make sure the connection works.
|
|
|
|
// For now, sa would try to recreate the connection for ever.
|
|
// Check that lid is increasing...
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if lid := atomic.LoadUint64(&sa.gcid); lid < 3 {
|
|
return fmt.Errorf("Seems like connection was not retried, lid currently only at %d", lid)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// This test ensures that we can connect using proper user/password
|
|
// to a LN URL that was discovered through the INFO protocol.
|
|
// We also check that the password doesn't leak to debug/trace logs.
|
|
func TestLeafNodeBasicAuthFailover(t *testing.T) {
|
|
// Something a little longer than "pwd" to prevent false positives amongst many log lines;
|
|
// don't make it complex enough to be subject to %-escaping, we want a simple needle search.
|
|
fatalPassword := "pwdfatal"
|
|
|
|
content := `
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: "abc"
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
authorization {
|
|
user: foo
|
|
password: %s
|
|
timeout: 1
|
|
}
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(content, "", fatalPassword)))
|
|
|
|
sb1, ob1 := RunServerWithConfig(conf)
|
|
defer sb1.Shutdown()
|
|
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(content, fmt.Sprintf("routes: [nats://127.0.0.1:%d]", ob1.Cluster.Port), fatalPassword)))
|
|
|
|
sb2, _ := RunServerWithConfig(conf)
|
|
defer sb2.Shutdown()
|
|
|
|
checkClusterFormed(t, sb1, sb2)
|
|
|
|
content = `
|
|
port: -1
|
|
accounts {
|
|
foo {}
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
remotes [
|
|
{
|
|
account: "foo"
|
|
url: "nats://foo:%[email protected]:%d"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(content, fatalPassword, ob1.LeafNode.Port)))
|
|
|
|
sa, _ := RunServerWithConfig(conf)
|
|
defer sa.Shutdown()
|
|
|
|
l := testhelper.NewDummyLogger(100)
|
|
sa.SetLogger(l, true, true) // we want debug & trace logs, to check for passwords in them
|
|
|
|
checkLeafNodeConnected(t, sa)
|
|
|
|
// Shutdown sb1, sa should reconnect to sb2
|
|
sb1.Shutdown()
|
|
|
|
// Wait a bit to make sure there was a disconnect and attempt to reconnect.
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
// Should be able to reconnect
|
|
checkLeafNodeConnected(t, sa)
|
|
|
|
// Look at all our logs for the password; at time of writing it doesn't appear
|
|
// but we want to safe-guard against it.
|
|
l.CheckForProhibited(t, "fatal password", fatalPassword)
|
|
}
|
|
|
|
func TestLeafNodeRTT(t *testing.T) {
|
|
ob := DefaultOptions()
|
|
ob.PingInterval = 15 * time.Millisecond
|
|
ob.LeafNode.Host = "127.0.0.1"
|
|
ob.LeafNode.Port = -1
|
|
sb := RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
lnBURL, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ob.LeafNode.Port))
|
|
oa := DefaultOptions()
|
|
oa.Cluster.Name = "xyz"
|
|
oa.PingInterval = 15 * time.Millisecond
|
|
oa.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{lnBURL}}}
|
|
sa := RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sa)
|
|
|
|
checkRTT := func(t *testing.T, s *Server) time.Duration {
|
|
t.Helper()
|
|
var ln *client
|
|
s.mu.Lock()
|
|
for _, l := range s.leafs {
|
|
ln = l
|
|
break
|
|
}
|
|
s.mu.Unlock()
|
|
var rtt time.Duration
|
|
checkFor(t, 2*firstPingInterval, 15*time.Millisecond, func() error {
|
|
ln.mu.Lock()
|
|
rtt = ln.rtt
|
|
ln.mu.Unlock()
|
|
if rtt == 0 {
|
|
return fmt.Errorf("RTT not tracked")
|
|
}
|
|
return nil
|
|
})
|
|
return rtt
|
|
}
|
|
|
|
prevA := checkRTT(t, sa)
|
|
prevB := checkRTT(t, sb)
|
|
|
|
// Wait to see if RTT is updated
|
|
checkUpdated := func(t *testing.T, s *Server, prev time.Duration) {
|
|
attempts := 0
|
|
timeout := time.Now().Add(2 * firstPingInterval)
|
|
for time.Now().Before(timeout) {
|
|
if rtt := checkRTT(t, s); rtt != prev {
|
|
return
|
|
}
|
|
attempts++
|
|
if attempts == 5 {
|
|
s.mu.Lock()
|
|
for _, ln := range s.leafs {
|
|
ln.mu.Lock()
|
|
ln.rtt = 0
|
|
ln.mu.Unlock()
|
|
break
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
time.Sleep(15 * time.Millisecond)
|
|
}
|
|
t.Fatalf("RTT probably not updated")
|
|
}
|
|
checkUpdated(t, sa, prevA)
|
|
checkUpdated(t, sb, prevB)
|
|
|
|
sa.Shutdown()
|
|
sb.Shutdown()
|
|
|
|
// Now check that initial RTT is computed prior to first PingInterval
|
|
// Get new options to avoid possible race changing the ping interval.
|
|
ob = DefaultOptions()
|
|
ob.Cluster.Name = "xyz"
|
|
ob.PingInterval = time.Minute
|
|
ob.LeafNode.Host = "127.0.0.1"
|
|
ob.LeafNode.Port = -1
|
|
sb = RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
lnBURL, _ = url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ob.LeafNode.Port))
|
|
oa = DefaultOptions()
|
|
oa.PingInterval = time.Minute
|
|
oa.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{lnBURL}}}
|
|
sa = RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sa)
|
|
|
|
checkRTT(t, sa)
|
|
checkRTT(t, sb)
|
|
}
|
|
|
|
func TestLeafNodeValidateAuthOptions(t *testing.T) {
|
|
opts := DefaultOptions()
|
|
opts.LeafNode.Username = "user1"
|
|
opts.LeafNode.Password = "pwd"
|
|
opts.LeafNode.Users = []*User{{Username: "user", Password: "pwd"}}
|
|
if _, err := NewServer(opts); err == nil || !strings.Contains(err.Error(),
|
|
"can not have a single user/pass and a users array") {
|
|
t.Fatalf("Expected error about mixing single/multi users, got %v", err)
|
|
}
|
|
|
|
// Check duplicate user names
|
|
opts.LeafNode.Username = _EMPTY_
|
|
opts.LeafNode.Password = _EMPTY_
|
|
opts.LeafNode.Users = append(opts.LeafNode.Users, &User{Username: "user", Password: "pwd"})
|
|
if _, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "duplicate user") {
|
|
t.Fatalf("Expected error about duplicate user, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeBasicAuthSingleton(t *testing.T) {
|
|
opts := DefaultOptions()
|
|
opts.LeafNode.Port = -1
|
|
opts.LeafNode.Account = "unknown"
|
|
if s, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "cannot find") {
|
|
if s != nil {
|
|
s.Shutdown()
|
|
}
|
|
t.Fatalf("Expected error about account not found, got %v", err)
|
|
}
|
|
|
|
template := `
|
|
port: -1
|
|
accounts: {
|
|
ACC1: { users = [{user: "user1", password: "user1"}] }
|
|
ACC2: { users = [{user: "user2", password: "user2"}] }
|
|
}
|
|
leafnodes: {
|
|
port: -1
|
|
authorization {
|
|
%s
|
|
account: "ACC1"
|
|
}
|
|
}
|
|
`
|
|
for iter, test := range []struct {
|
|
name string
|
|
userSpec string
|
|
lnURLCreds string
|
|
shouldFail bool
|
|
}{
|
|
{"no user creds required and no user so binds to ACC1", "", "", false},
|
|
{"no user creds required and pick user2 associated to ACC2", "", "user2:user2@", false},
|
|
{"no user creds required and unknown user should fail", "", "unknown:user@", true},
|
|
{"user creds required so binds to ACC1", "user: \"ln\"\npass: \"pwd\"", "ln:pwd@", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(template, test.userSpec)))
|
|
s1, o1 := RunServerWithConfig(conf)
|
|
defer s1.Shutdown()
|
|
|
|
// Create a sub on "foo" for account ACC1 (user user1), which is the one
|
|
// bound to the accepted LN connection.
|
|
ncACC1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o1.Host, o1.Port))
|
|
defer ncACC1.Close()
|
|
sub1 := natsSubSync(t, ncACC1, "foo")
|
|
natsFlush(t, ncACC1)
|
|
|
|
// Create a sub on "foo" for account ACC2 (user user2). This one should
|
|
// not receive any message.
|
|
ncACC2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o1.Host, o1.Port))
|
|
defer ncACC2.Close()
|
|
sub2 := natsSubSync(t, ncACC2, "foo")
|
|
natsFlush(t, ncACC2)
|
|
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes: {
|
|
remotes = [ { url: "nats-leaf://%s%s:%d" } ]
|
|
}
|
|
`, test.lnURLCreds, o1.LeafNode.Host, o1.LeafNode.Port)))
|
|
s2, _ := RunServerWithConfig(conf)
|
|
defer s2.Shutdown()
|
|
|
|
if test.shouldFail {
|
|
// Wait a bit and ensure that there is no leaf node connection
|
|
time.Sleep(100 * time.Millisecond)
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if n := s1.NumLeafNodes(); n != 0 {
|
|
return fmt.Errorf("Expected no leafnode connection, got %v", n)
|
|
}
|
|
return nil
|
|
})
|
|
return
|
|
}
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
nc := natsConnect(t, s2.ClientURL())
|
|
defer nc.Close()
|
|
natsPub(t, nc, "foo", []byte("hello"))
|
|
// If url contains known user, even when there is no credentials
|
|
// required, the connection will be bound to the user's account.
|
|
if iter == 1 {
|
|
// Should not receive on "ACC1", but should on "ACC2"
|
|
if _, err := sub1.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
|
|
t.Fatalf("Expected timeout error, got %v", err)
|
|
}
|
|
natsNexMsg(t, sub2, time.Second)
|
|
} else {
|
|
// Should receive on "ACC1"...
|
|
natsNexMsg(t, sub1, time.Second)
|
|
// but not received on "ACC2" since leafnode bound to account "ACC1".
|
|
if _, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
|
|
t.Fatalf("Expected timeout error, got %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeBasicAuthMultiple(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
port: -1
|
|
accounts: {
|
|
S1ACC1: { users = [{user: "user1", password: "user1"}] }
|
|
S1ACC2: { users = [{user: "user2", password: "user2"}] }
|
|
}
|
|
leafnodes: {
|
|
port: -1
|
|
authorization {
|
|
users = [
|
|
{user: "ln1", password: "ln1", account: "S1ACC1"}
|
|
{user: "ln2", password: "ln2", account: "S1ACC2"}
|
|
{user: "ln3", password: "ln3"}
|
|
]
|
|
}
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf)
|
|
defer s1.Shutdown()
|
|
|
|
// Make sure that we reject a LN connection if user does not match
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes: {
|
|
remotes = [{url: "nats-leaf://wron:user@%s:%d"}]
|
|
}
|
|
`, o1.LeafNode.Host, o1.LeafNode.Port)))
|
|
s2, _ := RunServerWithConfig(conf)
|
|
defer s2.Shutdown()
|
|
// Give a chance for s2 to attempt to connect and make sure that s1
|
|
// did not register a LN connection.
|
|
time.Sleep(100 * time.Millisecond)
|
|
if n := s1.NumLeafNodes(); n != 0 {
|
|
t.Fatalf("Expected no leafnode connection, got %v", n)
|
|
}
|
|
s2.Shutdown()
|
|
|
|
ncACC1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o1.Host, o1.Port))
|
|
defer ncACC1.Close()
|
|
sub1 := natsSubSync(t, ncACC1, "foo")
|
|
natsFlush(t, ncACC1)
|
|
|
|
ncACC2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o1.Host, o1.Port))
|
|
defer ncACC2.Close()
|
|
sub2 := natsSubSync(t, ncACC2, "foo")
|
|
natsFlush(t, ncACC2)
|
|
|
|
// We will start s2 with 2 LN connections that should bind local account S2ACC1
|
|
// to account S1ACC1 and S2ACC2 to account S1ACC2 on s1.
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
accounts {
|
|
S2ACC1 { users = [{user: "user1", password: "user1"}] }
|
|
S2ACC2 { users = [{user: "user2", password: "user2"}] }
|
|
}
|
|
leafnodes: {
|
|
remotes = [
|
|
{
|
|
url: "nats-leaf://ln1:ln1@%s:%d"
|
|
account: "S2ACC1"
|
|
}
|
|
{
|
|
url: "nats-leaf://ln2:ln2@%s:%d"
|
|
account: "S2ACC2"
|
|
}
|
|
]
|
|
}
|
|
`, o1.LeafNode.Host, o1.LeafNode.Port, o1.LeafNode.Host, o1.LeafNode.Port)))
|
|
s2, o2 := RunServerWithConfig(conf)
|
|
defer s2.Shutdown()
|
|
|
|
checkFor(t, 5*time.Second, 100*time.Millisecond, func() error {
|
|
if nln := s2.NumLeafNodes(); nln != 2 {
|
|
return fmt.Errorf("Expected 2 connected leafnodes for server %q, got %d", s2.ID(), nln)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Create a user connection on s2 that binds to S2ACC1 (use user1).
|
|
nc1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o2.Host, o2.Port))
|
|
defer nc1.Close()
|
|
|
|
// Create an user connection on s2 that binds to S2ACC2 (use user2).
|
|
nc2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o2.Host, o2.Port))
|
|
defer nc2.Close()
|
|
|
|
// Now if a message is published from nc1, sub1 should receive it since
|
|
// their account are bound together.
|
|
natsPub(t, nc1, "foo", []byte("hello"))
|
|
natsNexMsg(t, sub1, time.Second)
|
|
// But sub2 should not receive it since different account.
|
|
if _, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
|
|
t.Fatalf("Expected timeout error, got %v", err)
|
|
}
|
|
|
|
// Now use nc2 (S2ACC2) to publish
|
|
natsPub(t, nc2, "foo", []byte("hello"))
|
|
// Expect sub2 to receive and sub1 not to.
|
|
natsNexMsg(t, sub2, time.Second)
|
|
if _, err := sub1.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout {
|
|
t.Fatalf("Expected timeout error, got %v", err)
|
|
}
|
|
|
|
// Now check that we don't panic if no account is specified for
|
|
// a given user.
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes: {
|
|
remotes = [
|
|
{ url: "nats-leaf://ln3:ln3@%s:%d" }
|
|
]
|
|
}
|
|
`, o1.LeafNode.Host, o1.LeafNode.Port)))
|
|
s3, _ := RunServerWithConfig(conf)
|
|
defer s3.Shutdown()
|
|
}
|
|
|
|
type loopDetectedLogger struct {
|
|
DummyLogger
|
|
ch chan string
|
|
}
|
|
|
|
func (l *loopDetectedLogger) Errorf(format string, v ...any) {
|
|
msg := fmt.Sprintf(format, v...)
|
|
if strings.Contains(msg, "Loop") {
|
|
select {
|
|
case l.ch <- msg:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeLoop(t *testing.T) {
|
|
test := func(t *testing.T, cluster bool) {
|
|
// This test requires that we set the port to known value because
|
|
// we want A point to B and B to A.
|
|
oa := DefaultOptions()
|
|
oa.ServerName = "A"
|
|
if !cluster {
|
|
oa.Cluster.Port = 0
|
|
oa.Cluster.Name = _EMPTY_
|
|
}
|
|
oa.LeafNode.ReconnectInterval = 10 * time.Millisecond
|
|
oa.LeafNode.Port = 1234
|
|
ub, _ := url.Parse("nats://127.0.0.1:5678")
|
|
oa.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ub}}}
|
|
oa.LeafNode.connDelay = 50 * time.Millisecond
|
|
sa := RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
la := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
sa.SetLogger(la, false, false)
|
|
|
|
ob := DefaultOptions()
|
|
ob.ServerName = "B"
|
|
if !cluster {
|
|
ob.Cluster.Port = 0
|
|
ob.Cluster.Name = _EMPTY_
|
|
} else {
|
|
ob.Cluster.Name = "xyz"
|
|
}
|
|
ob.LeafNode.ReconnectInterval = 10 * time.Millisecond
|
|
ob.LeafNode.Port = 5678
|
|
ua, _ := url.Parse("nats://127.0.0.1:1234")
|
|
ob.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ua}}}
|
|
ob.LeafNode.connDelay = 50 * time.Millisecond
|
|
sb := RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
lb := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
sb.SetLogger(lb, false, false)
|
|
|
|
select {
|
|
case <-la.ch:
|
|
// OK!
|
|
case <-lb.ch:
|
|
// OK!
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("Did not get any error regarding loop")
|
|
}
|
|
|
|
sb.Shutdown()
|
|
ob.Port = -1
|
|
ob.Cluster.Port = -1
|
|
ob.LeafNode.Remotes = nil
|
|
sb = RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sa)
|
|
}
|
|
t.Run("standalone", func(t *testing.T) { test(t, false) })
|
|
t.Run("cluster", func(t *testing.T) { test(t, true) })
|
|
}
|
|
|
|
func TestLeafNodeLoopFromDAG(t *testing.T) {
|
|
// We want B & C to point to A, A itself does not point to any other server.
|
|
// We need to cancel clustering since now this will suppress on its own.
|
|
oa := DefaultOptions()
|
|
oa.ServerName = "A"
|
|
oa.LeafNode.connDelay = 50 * time.Millisecond
|
|
oa.LeafNode.ReconnectInterval = 10 * time.Millisecond
|
|
oa.LeafNode.Port = -1
|
|
oa.Cluster = ClusterOpts{}
|
|
sa := RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
ua, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oa.LeafNode.Port))
|
|
|
|
// B will point to A
|
|
ob := DefaultOptions()
|
|
ob.ServerName = "B"
|
|
ob.LeafNode.connDelay = 50 * time.Millisecond
|
|
ob.LeafNode.ReconnectInterval = 10 * time.Millisecond
|
|
ob.LeafNode.Port = -1
|
|
ob.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ua}}}
|
|
ob.Cluster = ClusterOpts{}
|
|
sb := RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
ub, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ob.LeafNode.Port))
|
|
|
|
checkLeafNodeConnected(t, sa)
|
|
checkLeafNodeConnected(t, sb)
|
|
|
|
// C will point to A and B
|
|
oc := DefaultOptions()
|
|
oc.ServerName = "C"
|
|
oc.LeafNode.connDelay = 50 * time.Millisecond
|
|
oc.LeafNode.ReconnectInterval = 10 * time.Millisecond
|
|
oc.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ua}}, {URLs: []*url.URL{ub}}}
|
|
oc.LeafNode.connDelay = 100 * time.Millisecond // Allow logger to be attached before connecting.
|
|
oc.Cluster = ClusterOpts{}
|
|
sc := RunServer(oc)
|
|
|
|
lc := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
sc.SetLogger(lc, false, false)
|
|
|
|
// We should get an error.
|
|
select {
|
|
case <-lc.ch:
|
|
// OK
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Did not get any error regarding loop")
|
|
}
|
|
|
|
// C should not be connected to anything.
|
|
checkLeafNodeConnectedCount(t, sc, 0)
|
|
// A and B are connected to each other.
|
|
checkLeafNodeConnectedCount(t, sa, 1)
|
|
checkLeafNodeConnectedCount(t, sb, 1)
|
|
|
|
// Shutdown C and restart without the loop.
|
|
sc.Shutdown()
|
|
oc.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{ub}}}
|
|
|
|
sc = RunServer(oc)
|
|
defer sc.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, sa, 1)
|
|
checkLeafNodeConnectedCount(t, sb, 2)
|
|
checkLeafNodeConnectedCount(t, sc, 1)
|
|
}
|
|
|
|
func TestLeafNodeCloseTLSConnection(t *testing.T) {
|
|
opts := DefaultOptions()
|
|
opts.DisableShortFirstPing = true
|
|
opts.LeafNode.Host = "127.0.0.1"
|
|
opts.LeafNode.Port = -1
|
|
opts.LeafNode.TLSTimeout = 100
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "./configs/certs/server.pem",
|
|
KeyFile: "./configs/certs/key.pem",
|
|
Insecure: true,
|
|
}
|
|
tlsConf, err := GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error generating tls config: %v", err)
|
|
}
|
|
opts.LeafNode.TLSConfig = tlsConf
|
|
opts.NoLog = true
|
|
opts.NoSigs = true
|
|
s := RunServer(opts)
|
|
defer s.Shutdown()
|
|
|
|
endpoint := fmt.Sprintf("%s:%d", opts.LeafNode.Host, opts.LeafNode.Port)
|
|
conn, err := net.DialTimeout("tcp", endpoint, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error on dial: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
br := bufio.NewReaderSize(conn, 100)
|
|
if _, err := br.ReadString('\n'); err != nil {
|
|
t.Fatalf("Unexpected error reading INFO: %v", err)
|
|
}
|
|
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
defer tlsConn.Close()
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
t.Fatalf("Unexpected error during handshake: %v", err)
|
|
}
|
|
connectOp := []byte("CONNECT {\"name\":\"leaf\",\"verbose\":false,\"pedantic\":false}\r\n")
|
|
if _, err := tlsConn.Write(connectOp); err != nil {
|
|
t.Fatalf("Unexpected error writing CONNECT: %v", err)
|
|
}
|
|
if _, err := tlsConn.Write([]byte("PING\r\n")); err != nil {
|
|
t.Fatalf("Unexpected error writing PING: %v", err)
|
|
}
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
|
|
// Get leaf connection
|
|
var leaf *client
|
|
s.mu.Lock()
|
|
for _, l := range s.leafs {
|
|
leaf = l
|
|
break
|
|
}
|
|
s.mu.Unlock()
|
|
// Fill the buffer. We want to timeout on write so that nc.Close()
|
|
// would block due to a write that cannot complete.
|
|
buf := make([]byte, 64*1024)
|
|
done := false
|
|
for !done {
|
|
leaf.nc.SetWriteDeadline(time.Now().Add(time.Second))
|
|
if _, err := leaf.nc.Write(buf); err != nil {
|
|
done = true
|
|
}
|
|
leaf.nc.SetWriteDeadline(time.Time{})
|
|
}
|
|
ch := make(chan bool)
|
|
go func() {
|
|
select {
|
|
case <-ch:
|
|
return
|
|
case <-time.After(3 * time.Second):
|
|
fmt.Println("!!!! closeConnection is blocked, test will hang !!!")
|
|
return
|
|
}
|
|
}()
|
|
// Close the route
|
|
leaf.closeConnection(SlowConsumerWriteDeadline)
|
|
ch <- true
|
|
}
|
|
|
|
func TestLeafNodeTLSSaveName(t *testing.T) {
|
|
opts := DefaultOptions()
|
|
opts.LeafNode.Host = "127.0.0.1"
|
|
opts.LeafNode.Port = -1
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/server-noip.pem",
|
|
KeyFile: "../test/configs/certs/server-key-noip.pem",
|
|
Insecure: true,
|
|
}
|
|
tlsConf, err := GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error generating tls config: %v", err)
|
|
}
|
|
opts.LeafNode.TLSConfig = tlsConf
|
|
s := RunServer(opts)
|
|
defer s.Shutdown()
|
|
|
|
lo := DefaultOptions()
|
|
u, _ := url.Parse(fmt.Sprintf("nats://localhost:%d", opts.LeafNode.Port))
|
|
lo.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
|
|
lo.LeafNode.ReconnectInterval = 15 * time.Millisecond
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
// We know connection will fail, but it should not fail because of error such as:
|
|
// "cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs"
|
|
// This would mean that we are not saving the hostname to use during the TLS handshake.
|
|
|
|
le := &captureErrorLogger{errCh: make(chan string, 100)}
|
|
ln.SetLogger(le, false, false)
|
|
|
|
tm := time.NewTimer(time.Second)
|
|
var done bool
|
|
for !done {
|
|
select {
|
|
case err := <-le.errCh:
|
|
if strings.Contains(err, "doesn't contain any IP SANs") {
|
|
t.Fatalf("Got this error: %q", err)
|
|
}
|
|
case <-tm.C:
|
|
done = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeRemoteWrongPort(t *testing.T) {
|
|
for _, test1 := range []struct {
|
|
name string
|
|
clusterAdvertise bool
|
|
leafnodeAdvertise bool
|
|
}{
|
|
{"advertise_on", false, false},
|
|
{"cluster_no_advertise", true, false},
|
|
{"leafnode_no_advertise", false, true},
|
|
} {
|
|
t.Run(test1.name, func(t *testing.T) {
|
|
oa := DefaultOptions()
|
|
// Make sure we have all ports (client, route, gateway) and we will try
|
|
// to create a leafnode to connection to each and make sure we get the error.
|
|
oa.Cluster.NoAdvertise = test1.clusterAdvertise
|
|
oa.Cluster.Name = "A"
|
|
oa.Cluster.Host = "127.0.0.1"
|
|
oa.Cluster.Port = -1
|
|
oa.Gateway.Host = "127.0.0.1"
|
|
oa.Gateway.Port = -1
|
|
oa.Gateway.Name = "A"
|
|
oa.LeafNode.Host = "127.0.0.1"
|
|
oa.LeafNode.Port = -1
|
|
oa.LeafNode.NoAdvertise = test1.leafnodeAdvertise
|
|
oa.Accounts = []*Account{NewAccount("sys")}
|
|
oa.SystemAccount = "sys"
|
|
sa := RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
ob := DefaultOptions()
|
|
ob.Cluster.NoAdvertise = test1.clusterAdvertise
|
|
ob.Cluster.Name = "A"
|
|
ob.Cluster.Host = "127.0.0.1"
|
|
ob.Cluster.Port = -1
|
|
ob.Routes = RoutesFromStr(fmt.Sprintf("nats://%s:%d", oa.Cluster.Host, oa.Cluster.Port))
|
|
ob.Gateway.Host = "127.0.0.1"
|
|
ob.Gateway.Port = -1
|
|
ob.Gateway.Name = "A"
|
|
ob.LeafNode.Host = "127.0.0.1"
|
|
ob.LeafNode.Port = -1
|
|
ob.LeafNode.NoAdvertise = test1.leafnodeAdvertise
|
|
ob.Accounts = []*Account{NewAccount("sys")}
|
|
ob.SystemAccount = "sys"
|
|
sb := RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
checkClusterFormed(t, sa, sb)
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
port int
|
|
}{
|
|
{"client", oa.Port},
|
|
{"cluster", oa.Cluster.Port},
|
|
{"gateway", oa.Gateway.Port},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
oc := DefaultOptions()
|
|
// Server with the wrong config against non leafnode port.
|
|
leafURL, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", test.port))
|
|
oc.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{leafURL}}}
|
|
oc.LeafNode.ReconnectInterval = 5 * time.Millisecond
|
|
sc := RunServer(oc)
|
|
defer sc.Shutdown()
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
sc.SetLogger(l, true, true)
|
|
|
|
select {
|
|
case e := <-l.errCh:
|
|
if strings.Contains(e, ErrConnectedToWrongPort.Error()) {
|
|
return
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Did not get any error about connecting to wrong port for %q - %q",
|
|
test1.name, test.name)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeRemoteIsHub(t *testing.T) {
|
|
oa := testDefaultOptionsForGateway("A")
|
|
oa.Accounts = []*Account{NewAccount("sys")}
|
|
oa.SystemAccount = "sys"
|
|
sa := RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
lno := DefaultOptions()
|
|
lno.LeafNode.Host = "127.0.0.1"
|
|
lno.LeafNode.Port = -1
|
|
ln := RunServer(lno)
|
|
defer ln.Shutdown()
|
|
|
|
ob1 := testGatewayOptionsFromToWithServers(t, "B", "A", sa)
|
|
ob1.Accounts = []*Account{NewAccount("sys")}
|
|
ob1.SystemAccount = "sys"
|
|
ob1.Cluster.Host = "127.0.0.1"
|
|
ob1.Cluster.Port = -1
|
|
ob1.LeafNode.Host = "127.0.0.1"
|
|
ob1.LeafNode.Port = -1
|
|
u, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", lno.LeafNode.Port))
|
|
ob1.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{u},
|
|
Hub: true,
|
|
},
|
|
}
|
|
sb1 := RunServer(ob1)
|
|
defer sb1.Shutdown()
|
|
|
|
waitForOutboundGateways(t, sb1, 1, 2*time.Second)
|
|
waitForInboundGateways(t, sb1, 1, 2*time.Second)
|
|
waitForOutboundGateways(t, sa, 1, 2*time.Second)
|
|
waitForInboundGateways(t, sa, 1, 2*time.Second)
|
|
|
|
checkLeafNodeConnected(t, sb1)
|
|
|
|
// For now, due to issue 977, let's restart the leafnode so that the
|
|
// leafnode connect is propagated in the super-cluster.
|
|
ln.Shutdown()
|
|
ln = RunServer(lno)
|
|
defer ln.Shutdown()
|
|
checkLeafNodeConnected(t, sb1)
|
|
|
|
// Connect another server in cluster B
|
|
ob2 := testGatewayOptionsFromToWithServers(t, "B", "A", sa)
|
|
ob2.Accounts = []*Account{NewAccount("sys")}
|
|
ob2.SystemAccount = "sys"
|
|
ob2.Cluster.Host = "127.0.0.1"
|
|
ob2.Cluster.Port = -1
|
|
ob2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", ob1.Cluster.Port))
|
|
sb2 := RunServer(ob2)
|
|
defer sb2.Shutdown()
|
|
|
|
checkClusterFormed(t, sb1, sb2)
|
|
waitForOutboundGateways(t, sb2, 1, 2*time.Second)
|
|
|
|
expectedSubs := ln.NumSubscriptions() + 2
|
|
|
|
// Create sub on "foo" connected to sa
|
|
ncA := natsConnect(t, sa.ClientURL())
|
|
defer ncA.Close()
|
|
subFoo := natsSubSync(t, ncA, "foo")
|
|
|
|
// Create sub on "bar" connected to sb2
|
|
ncB2 := natsConnect(t, sb2.ClientURL())
|
|
defer ncB2.Close()
|
|
subBar := natsSubSync(t, ncB2, "bar")
|
|
|
|
// Make sure subscriptions have propagated to the leafnode.
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
if subs := ln.NumSubscriptions(); subs < expectedSubs {
|
|
return fmt.Errorf("Number of subs is %d", subs)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Create pub connection on leafnode
|
|
ncLN := natsConnect(t, ln.ClientURL())
|
|
defer ncLN.Close()
|
|
|
|
// Publish on foo and make sure it is received.
|
|
natsPub(t, ncLN, "foo", []byte("msg"))
|
|
natsNexMsg(t, subFoo, time.Second)
|
|
|
|
// Publish on foo and make sure it is received.
|
|
natsPub(t, ncLN, "bar", []byte("msg"))
|
|
natsNexMsg(t, subBar, time.Second)
|
|
}
|
|
|
|
func TestLeafNodePermissions(t *testing.T) {
|
|
lo1 := DefaultOptions()
|
|
lo1.LeafNode.Host = "127.0.0.1"
|
|
lo1.LeafNode.Port = -1
|
|
ln1 := RunServer(lo1)
|
|
defer ln1.Shutdown()
|
|
|
|
errLog := &captureErrorLogger{errCh: make(chan string, 1)}
|
|
ln1.SetLogger(errLog, false, false)
|
|
|
|
u, _ := url.Parse(fmt.Sprintf("nats://%s:%d", lo1.LeafNode.Host, lo1.LeafNode.Port))
|
|
lo2 := DefaultOptions()
|
|
lo2.Cluster.Name = "xyz"
|
|
lo2.LeafNode.ReconnectInterval = 5 * time.Millisecond
|
|
lo2.LeafNode.connDelay = 100 * time.Millisecond
|
|
lo2.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{u},
|
|
DenyExports: []string{"export.*", "export"},
|
|
DenyImports: []string{"import.*", "import"},
|
|
},
|
|
}
|
|
ln2 := RunServer(lo2)
|
|
defer ln2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, ln1)
|
|
|
|
// Create clients on ln1 and ln2
|
|
nc1, err := nats.Connect(ln1.ClientURL())
|
|
if err != nil {
|
|
t.Fatalf("Error creating client: %v", err)
|
|
}
|
|
defer nc1.Close()
|
|
nc2, err := nats.Connect(ln2.ClientURL())
|
|
if err != nil {
|
|
t.Fatalf("Error creating client: %v", err)
|
|
}
|
|
defer nc2.Close()
|
|
|
|
checkSubs := func(acc *Account, expected int) {
|
|
t.Helper()
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if n := acc.TotalSubs(); n != expected {
|
|
return fmt.Errorf("Expected %d subs, got %v", expected, n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Create a sub on ">" on LN1
|
|
subAll := natsSubSync(t, nc1, ">")
|
|
// this should be registered in LN2 (there is 1 sub for LN1 $LDS subject) + SYS IMPORTS
|
|
checkSubs(ln2.globalAccount(), 12)
|
|
|
|
// Check deny export clause from messages published from LN2
|
|
for _, test := range []struct {
|
|
name string
|
|
subject string
|
|
received bool
|
|
}{
|
|
{"do not send on export.bat", "export.bat", false},
|
|
{"do not send on export", "export", false},
|
|
{"send on foo", "foo", true},
|
|
{"send on export.this.one", "export.this.one", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
nc2.Publish(test.subject, []byte("msg"))
|
|
if test.received {
|
|
natsNexMsg(t, subAll, time.Second)
|
|
} else {
|
|
if _, err := subAll.NextMsg(50 * time.Millisecond); err == nil {
|
|
t.Fatalf("Should not have received message on %q", test.subject)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
subAll.Unsubscribe()
|
|
// Goes down by 1.
|
|
checkSubs(ln2.globalAccount(), 11)
|
|
|
|
// We used to make sure we would not do subscriptions however that
|
|
// was incorrect. We need to check publishes, not the subscriptions.
|
|
// For instance if we can publish across a leafnode to foo, and the
|
|
// other side has a subsxcription for '*' the message should cross
|
|
// the leafnode. The old way would not allow this.
|
|
|
|
// Now check deny import clause.
|
|
// As of now, we don't suppress forwarding of subscriptions on LN2 that
|
|
// match the deny import clause to be forwarded to LN1. However, messages
|
|
// should still not be able to come back to LN2.
|
|
for _, test := range []struct {
|
|
name string
|
|
subSubject string
|
|
pubSubject string
|
|
ok bool
|
|
}{
|
|
{"reject import on import.*", "import.*", "import.bad", false},
|
|
{"reject import on import", "import", "import", false},
|
|
{"accepts import on foo", "foo", "foo", true},
|
|
{"accepts import on import.this.one.ok", "import.*.>", "import.this.one.ok", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
sub := natsSubSync(t, nc2, test.subSubject)
|
|
checkSubs(ln2.globalAccount(), 12)
|
|
|
|
if !test.ok {
|
|
nc1.Publish(test.pubSubject, []byte("msg"))
|
|
if _, err := sub.NextMsg(50 * time.Millisecond); err == nil {
|
|
t.Fatalf("Did not expect to get the message")
|
|
}
|
|
} else {
|
|
checkSubs(ln1.globalAccount(), 11)
|
|
nc1.Publish(test.pubSubject, []byte("msg"))
|
|
natsNexMsg(t, sub, time.Second)
|
|
}
|
|
sub.Unsubscribe()
|
|
checkSubs(ln1.globalAccount(), 10)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodePermissionsConcurrentAccess(t *testing.T) {
|
|
lo1 := DefaultOptions()
|
|
lo1.LeafNode.Host = "127.0.0.1"
|
|
lo1.LeafNode.Port = -1
|
|
ln1 := RunServer(lo1)
|
|
defer ln1.Shutdown()
|
|
|
|
nc1 := natsConnect(t, ln1.ClientURL())
|
|
defer nc1.Close()
|
|
|
|
natsSub(t, nc1, "_INBOX.>", func(_ *nats.Msg) {})
|
|
natsFlush(t, nc1)
|
|
|
|
ch := make(chan struct{}, 1)
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(2)
|
|
|
|
publish := func(nc *nats.Conn) {
|
|
defer wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-ch:
|
|
return
|
|
default:
|
|
nc.Publish(nats.NewInbox(), []byte("hello"))
|
|
}
|
|
}
|
|
}
|
|
|
|
go publish(nc1)
|
|
|
|
u, _ := url.Parse(fmt.Sprintf("nats://%s:%d", lo1.LeafNode.Host, lo1.LeafNode.Port))
|
|
lo2 := DefaultOptions()
|
|
lo2.Cluster.Name = "xyz"
|
|
lo2.LeafNode.ReconnectInterval = 5 * time.Millisecond
|
|
lo2.LeafNode.connDelay = 500 * time.Millisecond
|
|
lo2.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{u},
|
|
DenyExports: []string{"foo"},
|
|
DenyImports: []string{"bar"},
|
|
},
|
|
}
|
|
ln2 := RunServer(lo2)
|
|
defer ln2.Shutdown()
|
|
|
|
nc2 := natsConnect(t, ln2.ClientURL())
|
|
defer nc2.Close()
|
|
|
|
natsSub(t, nc2, "_INBOX.>", func(_ *nats.Msg) {})
|
|
natsFlush(t, nc2)
|
|
|
|
go publish(nc2)
|
|
|
|
checkLeafNodeConnected(t, ln1)
|
|
checkLeafNodeConnected(t, ln2)
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
close(ch)
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestLeafNodePubAllowedPruning(t *testing.T) {
|
|
c := &client{}
|
|
c.setPermissions(&Permissions{Publish: &SubjectPermission{Allow: []string{"foo"}}})
|
|
|
|
gr := 100
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(gr)
|
|
for i := 0; i < gr; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 100; i++ {
|
|
c.pubAllowed(nats.NewInbox())
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
// The cache prune function does try for a bit to make sure the cache
|
|
// is below the maxPermCacheSize value, but depending on the machine
|
|
// this runs on, it may be that it is still a bit over. If so, run
|
|
// pubAllowed one more time and we must get below.
|
|
if n := int(atomic.LoadInt32(&c.perms.pcsz)); n > maxPermCacheSize {
|
|
c.pubAllowed(nats.NewInbox())
|
|
}
|
|
if n := int(atomic.LoadInt32(&c.perms.pcsz)); n > maxPermCacheSize {
|
|
t.Fatalf("Expected size to be less than %v, got %v", maxPermCacheSize, n)
|
|
}
|
|
if n := atomic.LoadInt32(&c.perms.prun); n != 0 {
|
|
t.Fatalf("c.perms.prun should be 0, was %v", n)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeExportPermissionsNotForSpecialSubs(t *testing.T) {
|
|
lo1 := DefaultOptions()
|
|
lo1.Accounts = []*Account{NewAccount("SYS")}
|
|
lo1.SystemAccount = "SYS"
|
|
lo1.Cluster.Name = "A"
|
|
lo1.Gateway.Name = "A"
|
|
lo1.Gateway.Port = -1
|
|
lo1.LeafNode.Host = "127.0.0.1"
|
|
lo1.LeafNode.Port = -1
|
|
ln1 := RunServer(lo1)
|
|
defer ln1.Shutdown()
|
|
|
|
u, _ := url.Parse(fmt.Sprintf("nats://%s:%d", lo1.LeafNode.Host, lo1.LeafNode.Port))
|
|
lo2 := DefaultOptions()
|
|
lo2.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{u},
|
|
DenyExports: []string{">"},
|
|
},
|
|
}
|
|
ln2 := RunServer(lo2)
|
|
defer ln2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, ln1)
|
|
|
|
// The deny is totally restrictive, but make sure that we still accept the $LDS, $GR and _GR_ go from LN1.
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
// We should have registered the 3 subs from the accepting leafnode.
|
|
if n := ln2.globalAccount().TotalSubs(); n != 9 {
|
|
return fmt.Errorf("Expected %d subs, got %v", 9, n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Make sure that if the node that detects the loop (and sends the error and
|
|
// close the connection) is the accept side, the remote node (the one that solicits)
|
|
// properly use the reconnect delay.
|
|
func TestLeafNodeLoopDetectedOnAcceptSide(t *testing.T) {
|
|
bo := DefaultOptions()
|
|
bo.LeafNode.Host = "127.0.0.1"
|
|
bo.LeafNode.Port = -1
|
|
b := RunServer(bo)
|
|
defer b.Shutdown()
|
|
|
|
l := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
b.SetLogger(l, false, false)
|
|
|
|
u, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", bo.LeafNode.Port))
|
|
|
|
ao := testDefaultOptionsForGateway("A")
|
|
ao.Accounts = []*Account{NewAccount("SYS")}
|
|
ao.SystemAccount = "SYS"
|
|
ao.LeafNode.ReconnectInterval = 5 * time.Millisecond
|
|
ao.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{u},
|
|
Hub: true,
|
|
},
|
|
}
|
|
a := RunServer(ao)
|
|
defer a.Shutdown()
|
|
|
|
co := testGatewayOptionsFromToWithServers(t, "C", "A", a)
|
|
co.Accounts = []*Account{NewAccount("SYS")}
|
|
co.SystemAccount = "SYS"
|
|
co.LeafNode.ReconnectInterval = 5 * time.Millisecond
|
|
co.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{u},
|
|
Hub: true,
|
|
},
|
|
}
|
|
c := RunServer(co)
|
|
defer c.Shutdown()
|
|
|
|
for i := 0; i < 2; i++ {
|
|
select {
|
|
case <-l.ch:
|
|
// OK
|
|
case <-time.After(200 * time.Millisecond):
|
|
// We are likely to detect from each A and C servers,
|
|
// but consider a failure if we did not receive any.
|
|
if i == 0 {
|
|
t.Fatalf("Should have detected loop")
|
|
}
|
|
}
|
|
}
|
|
|
|
// The reconnect attempt is set to 5ms, but the default loop delay
|
|
// is 30 seconds, so we should not get any new error for that long.
|
|
// Check if we are getting more errors..
|
|
select {
|
|
case e := <-l.ch:
|
|
t.Fatalf("Should not have gotten another error, got %q", e)
|
|
case <-time.After(50 * time.Millisecond):
|
|
// OK!
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeHubWithGateways(t *testing.T) {
|
|
ao := DefaultOptions()
|
|
ao.ServerName = "A"
|
|
ao.LeafNode.Host = "127.0.0.1"
|
|
ao.LeafNode.Port = -1
|
|
a := RunServer(ao)
|
|
defer a.Shutdown()
|
|
|
|
ua, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ao.LeafNode.Port))
|
|
|
|
bo := testDefaultOptionsForGateway("B")
|
|
bo.ServerName = "B"
|
|
bo.Accounts = []*Account{NewAccount("SYS")}
|
|
bo.SystemAccount = "SYS"
|
|
bo.LeafNode.ReconnectInterval = 5 * time.Millisecond
|
|
bo.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{ua},
|
|
Hub: true,
|
|
},
|
|
}
|
|
b := RunServer(bo)
|
|
defer b.Shutdown()
|
|
|
|
do := DefaultOptions()
|
|
do.ServerName = "D"
|
|
do.LeafNode.Host = "127.0.0.1"
|
|
do.LeafNode.Port = -1
|
|
d := RunServer(do)
|
|
defer d.Shutdown()
|
|
|
|
ud, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", do.LeafNode.Port))
|
|
|
|
co := testGatewayOptionsFromToWithServers(t, "C", "B", b)
|
|
co.ServerName = "C"
|
|
co.Accounts = []*Account{NewAccount("SYS")}
|
|
co.SystemAccount = "SYS"
|
|
co.LeafNode.ReconnectInterval = 5 * time.Millisecond
|
|
co.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{ud},
|
|
Hub: true,
|
|
},
|
|
}
|
|
c := RunServer(co)
|
|
defer c.Shutdown()
|
|
|
|
waitForInboundGateways(t, b, 1, 2*time.Second)
|
|
waitForInboundGateways(t, c, 1, 2*time.Second)
|
|
checkLeafNodeConnected(t, a)
|
|
checkLeafNodeConnected(t, d)
|
|
|
|
// Create a responder on D
|
|
ncD := natsConnect(t, d.ClientURL())
|
|
defer ncD.Close()
|
|
|
|
ncD.Subscribe("service", func(m *nats.Msg) {
|
|
m.Respond([]byte("reply"))
|
|
})
|
|
ncD.Flush()
|
|
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
acc := a.globalAccount()
|
|
if r := acc.sl.Match("service"); r != nil && len(r.psubs) == 1 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("subscription still not registered")
|
|
})
|
|
|
|
// Create requestor on A and send the request, expect a reply.
|
|
ncA := natsConnect(t, a.ClientURL())
|
|
defer ncA.Close()
|
|
if msg, err := ncA.Request("service", []byte("request"), time.Second); err != nil {
|
|
t.Fatalf("Failed to get reply: %v", err)
|
|
} else if string(msg.Data) != "reply" {
|
|
t.Fatalf("Unexpected reply: %q", msg.Data)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeTmpClients(t *testing.T) {
|
|
ao := DefaultOptions()
|
|
ao.LeafNode.Host = "127.0.0.1"
|
|
ao.LeafNode.Port = -1
|
|
a := RunServer(ao)
|
|
defer a.Shutdown()
|
|
|
|
c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", ao.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error connecting: %v", err)
|
|
}
|
|
defer c.Close()
|
|
// Read info
|
|
br := bufio.NewReader(c)
|
|
br.ReadLine()
|
|
|
|
checkTmp := func(expected int) {
|
|
t.Helper()
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
a.grMu.Lock()
|
|
l := len(a.grTmpClients)
|
|
a.grMu.Unlock()
|
|
if l != expected {
|
|
return fmt.Errorf("Expected tmp map to have %v entries, got %v", expected, l)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
checkTmp(1)
|
|
|
|
// Close client and wait check that it is removed.
|
|
c.Close()
|
|
checkTmp(0)
|
|
|
|
// Check with normal leafnode connection that once connected,
|
|
// the tmp map is also emptied.
|
|
bo := DefaultOptions()
|
|
bo.Cluster.Name = "xyz"
|
|
bo.LeafNode.ReconnectInterval = 5 * time.Millisecond
|
|
u, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", ao.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error creating url: %v", err)
|
|
}
|
|
bo.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
|
|
b := RunServer(bo)
|
|
defer b.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b)
|
|
checkTmp(0)
|
|
}
|
|
|
|
func TestLeafNodeTLSVerifyAndMap(t *testing.T) {
|
|
accName := "MyAccount"
|
|
acc := NewAccount(accName)
|
|
certUserName := "CN=example.com,OU=NATS.io"
|
|
users := []*User{{Username: certUserName, Account: acc}}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
leafUsers bool
|
|
provideCert bool
|
|
}{
|
|
{"no users override, provides cert", false, true},
|
|
{"no users override, does not provide cert", false, false},
|
|
{"users override, provides cert", true, true},
|
|
{"users override, does not provide cert", true, false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.Accounts = []*Account{acc}
|
|
o.LeafNode.Host = "127.0.0.1"
|
|
o.LeafNode.Port = -1
|
|
if test.leafUsers {
|
|
o.LeafNode.Users = users
|
|
} else {
|
|
o.Users = users
|
|
}
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/tlsauth/server.pem",
|
|
KeyFile: "../test/configs/certs/tlsauth/server-key.pem",
|
|
CaFile: "../test/configs/certs/tlsauth/ca.pem",
|
|
Verify: true,
|
|
}
|
|
tlsc, err := GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error creating tls config: %v", err)
|
|
}
|
|
o.LeafNode.TLSConfig = tlsc
|
|
o.LeafNode.TLSMap = true
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
slo := DefaultOptions()
|
|
slo.Cluster.Name = "xyz"
|
|
|
|
sltlsc := &tls.Config{}
|
|
if test.provideCert {
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/tlsauth/client.pem",
|
|
KeyFile: "../test/configs/certs/tlsauth/client-key.pem",
|
|
}
|
|
var err error
|
|
sltlsc, err = GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error generating tls config: %v", err)
|
|
}
|
|
}
|
|
sltlsc.InsecureSkipVerify = true
|
|
u, _ := url.Parse(fmt.Sprintf("nats://%s:%d", o.LeafNode.Host, o.LeafNode.Port))
|
|
slo.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
TLSConfig: sltlsc,
|
|
URLs: []*url.URL{u},
|
|
},
|
|
}
|
|
sl := RunServer(slo)
|
|
defer sl.Shutdown()
|
|
|
|
if !test.provideCert {
|
|
// Wait a bit and make sure we are not connecting
|
|
time.Sleep(100 * time.Millisecond)
|
|
checkLeafNodeConnectedCount(t, s, 0)
|
|
return
|
|
}
|
|
checkLeafNodeConnected(t, s)
|
|
|
|
var uname string
|
|
var accname string
|
|
s.mu.Lock()
|
|
for _, c := range s.leafs {
|
|
c.mu.Lock()
|
|
uname = c.opts.Username
|
|
if c.acc != nil {
|
|
accname = c.acc.GetName()
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
s.mu.Unlock()
|
|
if uname != certUserName {
|
|
t.Fatalf("Expected username %q, got %q", certUserName, uname)
|
|
}
|
|
if accname != accName {
|
|
t.Fatalf("Expected account %q, got %v", accName, accname)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type chanLogger struct {
|
|
DummyLogger
|
|
triggerChan chan string
|
|
}
|
|
|
|
func (l *chanLogger) Warnf(format string, v ...any) {
|
|
l.triggerChan <- fmt.Sprintf(format, v...)
|
|
}
|
|
|
|
func (l *chanLogger) Errorf(format string, v ...any) {
|
|
l.triggerChan <- fmt.Sprintf(format, v...)
|
|
}
|
|
|
|
const (
|
|
testLeafNodeTLSVerifyAndMapSrvA = `
|
|
listen: 127.0.0.1:-1
|
|
leaf {
|
|
listen: "127.0.0.1:-1"
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
timeout: 2
|
|
verify_and_map: true
|
|
}
|
|
authorization {
|
|
users [{
|
|
user: "%s"
|
|
}]
|
|
}
|
|
}
|
|
`
|
|
testLeafNodeTLSVerifyAndMapSrvB = `
|
|
listen: -1
|
|
leaf {
|
|
remotes [
|
|
{
|
|
url: "tls://user-provided-in-url@localhost:%d"
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
}
|
|
}
|
|
]
|
|
}`
|
|
)
|
|
|
|
func TestLeafNodeTLSVerifyAndMapCfgPass(t *testing.T) {
|
|
l := &chanLogger{triggerChan: make(chan string, 100)}
|
|
defer close(l.triggerChan)
|
|
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(testLeafNodeTLSVerifyAndMapSrvA, "localhost")))
|
|
srvA, optsA := RunServerWithConfig(confA)
|
|
defer srvA.Shutdown()
|
|
srvA.SetLogger(l, true, true)
|
|
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(testLeafNodeTLSVerifyAndMapSrvB, optsA.LeafNode.Port)))
|
|
ob := LoadConfig(confB)
|
|
ob.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
srvB := RunServer(ob)
|
|
defer srvB.Shutdown()
|
|
|
|
// Now make sure that the leaf node connection is up and the correct account was picked
|
|
checkFor(t, 10*time.Second, 10*time.Millisecond, func() error {
|
|
for _, srv := range []*Server{srvA, srvB} {
|
|
if nln := srv.NumLeafNodes(); nln != 1 {
|
|
return fmt.Errorf("Number of leaf nodes is %d", nln)
|
|
}
|
|
if leafz, err := srv.Leafz(nil); err != nil {
|
|
if len(leafz.Leafs) != 1 {
|
|
return fmt.Errorf("Number of leaf nodes returned by LEAFZ is not one: %d", len(leafz.Leafs))
|
|
} else if leafz.Leafs[0].Account != DEFAULT_GLOBAL_ACCOUNT {
|
|
return fmt.Errorf("Account used is not $G: %s", leafz.Leafs[0].Account)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
// Make sure that the user name in the url was ignored and a warning printed
|
|
for {
|
|
select {
|
|
case w := <-l.triggerChan:
|
|
if w == `User "user-provided-in-url" found in connect proto, but user required from cert` {
|
|
return
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Did not get expected warning")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeTLSVerifyAndMapCfgFail(t *testing.T) {
|
|
l := &chanLogger{triggerChan: make(chan string, 100)}
|
|
defer close(l.triggerChan)
|
|
|
|
// use certificate with SAN localhost, but configure the server to not accept it
|
|
// instead provide a name matching the user (to be matched by failed
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(testLeafNodeTLSVerifyAndMapSrvA, "user-provided-in-url")))
|
|
srvA, optsA := RunServerWithConfig(confA)
|
|
defer srvA.Shutdown()
|
|
srvA.SetLogger(l, true, true)
|
|
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(testLeafNodeTLSVerifyAndMapSrvB, optsA.LeafNode.Port)))
|
|
ob := LoadConfig(confB)
|
|
ob.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
srvB := RunServer(ob)
|
|
defer srvB.Shutdown()
|
|
|
|
// Now make sure that the leaf node connection is down
|
|
checkFor(t, 10*time.Second, 10*time.Millisecond, func() error {
|
|
for _, srv := range []*Server{srvA, srvB} {
|
|
if nln := srv.NumLeafNodes(); nln != 0 {
|
|
return fmt.Errorf("Number of leaf nodes is %d", nln)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
// Make sure that the connection was closed for the right reason
|
|
for {
|
|
select {
|
|
case w := <-l.triggerChan:
|
|
if strings.Contains(w, ErrAuthentication.Error()) {
|
|
return
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Did not get expected warning")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeOriginClusterInfo(t *testing.T) {
|
|
hopts := DefaultOptions()
|
|
hopts.ServerName = "hub"
|
|
hopts.LeafNode.Port = -1
|
|
|
|
hub := RunServer(hopts)
|
|
defer hub.Shutdown()
|
|
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leaf {
|
|
remotes [ { url: "nats://127.0.0.1:%d" } ]
|
|
}
|
|
`, hopts.LeafNode.Port)))
|
|
|
|
opts, err := ProcessConfigFile(conf)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
opts.NoLog, opts.NoSigs = true, true
|
|
|
|
s := RunServer(opts)
|
|
defer s.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
|
|
// Check the info on the leadnode client in the hub.
|
|
grabLeaf := func() *client {
|
|
var l *client
|
|
hub.mu.Lock()
|
|
for _, l = range hub.leafs {
|
|
break
|
|
}
|
|
hub.mu.Unlock()
|
|
return l
|
|
}
|
|
|
|
l := grabLeaf()
|
|
if rc := l.remoteCluster(); rc != "" {
|
|
t.Fatalf("Expected an empty remote cluster, got %q", rc)
|
|
}
|
|
|
|
s.Shutdown()
|
|
|
|
// Now make our leafnode part of a cluster.
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leaf {
|
|
remotes [ { url: "nats://127.0.0.1:%d" } ]
|
|
}
|
|
cluster {
|
|
name: "xyz"
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`, hopts.LeafNode.Port)))
|
|
|
|
opts, err = ProcessConfigFile(conf)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
opts.NoLog, opts.NoSigs = true, true
|
|
|
|
s = RunServer(opts)
|
|
defer s.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
|
|
l = grabLeaf()
|
|
if rc := l.remoteCluster(); rc != "xyz" {
|
|
t.Fatalf("Expected a remote cluster name of \"xyz\", got %q", rc)
|
|
}
|
|
pcid := l.cid
|
|
|
|
// Now make sure that if we update our cluster name, simulating the settling
|
|
// of dynamic cluster names between competing servers.
|
|
s.setClusterName("xyz")
|
|
// Make sure we disconnect and reconnect.
|
|
checkLeafNodeConnectedCount(t, s, 0)
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, hub)
|
|
|
|
l = grabLeaf()
|
|
if rc := l.remoteCluster(); rc != "xyz" {
|
|
t.Fatalf("Expected a remote cluster name of \"xyz\", got %q", rc)
|
|
}
|
|
// Make sure we reconnected and have a new CID.
|
|
if l.cid == pcid {
|
|
t.Fatalf("Expected a different id, got the same")
|
|
}
|
|
}
|
|
|
|
type proxyAcceptDetectFailureLate struct {
|
|
sync.Mutex
|
|
wg sync.WaitGroup
|
|
acceptPort int
|
|
l net.Listener
|
|
srvs []net.Conn
|
|
leaf net.Conn
|
|
startChan chan struct{}
|
|
}
|
|
|
|
func (p *proxyAcceptDetectFailureLate) run(t *testing.T) int {
|
|
return p.runEx(t, false)
|
|
}
|
|
|
|
func (p *proxyAcceptDetectFailureLate) runEx(t *testing.T, needStart bool) int {
|
|
l, err := natsListen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("Error on listen: %v", err)
|
|
}
|
|
p.Lock()
|
|
var startChan chan struct{}
|
|
if needStart {
|
|
startChan = make(chan struct{})
|
|
p.startChan = startChan
|
|
}
|
|
p.l = l
|
|
p.Unlock()
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
p.wg.Add(1)
|
|
go func() {
|
|
defer p.wg.Done()
|
|
defer l.Close()
|
|
defer func() {
|
|
p.Lock()
|
|
for _, c := range p.srvs {
|
|
c.Close()
|
|
}
|
|
p.Unlock()
|
|
}()
|
|
if startChan != nil {
|
|
<-startChan
|
|
}
|
|
for {
|
|
c, err := l.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
srv, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", p.acceptPort))
|
|
if err != nil {
|
|
return
|
|
}
|
|
p.Lock()
|
|
p.leaf = c
|
|
p.srvs = append(p.srvs, srv)
|
|
p.Unlock()
|
|
|
|
transfer := func(c1, c2 net.Conn) {
|
|
var buf [1024]byte
|
|
for {
|
|
n, err := c1.Read(buf[:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, err := c2.Write(buf[:n]); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
go transfer(srv, c)
|
|
go transfer(c, srv)
|
|
}
|
|
}()
|
|
return port
|
|
}
|
|
|
|
func (p *proxyAcceptDetectFailureLate) start() {
|
|
p.Lock()
|
|
if p.startChan != nil {
|
|
close(p.startChan)
|
|
p.startChan = nil
|
|
}
|
|
p.Unlock()
|
|
}
|
|
|
|
func (p *proxyAcceptDetectFailureLate) close() {
|
|
p.Lock()
|
|
if p.startChan != nil {
|
|
close(p.startChan)
|
|
p.startChan = nil
|
|
}
|
|
p.l.Close()
|
|
p.Unlock()
|
|
|
|
p.wg.Wait()
|
|
}
|
|
|
|
type oldConnReplacedLogger struct {
|
|
DummyLogger
|
|
errCh chan string
|
|
warnCh chan string
|
|
}
|
|
|
|
func (l *oldConnReplacedLogger) Errorf(format string, v ...any) {
|
|
select {
|
|
case l.errCh <- fmt.Sprintf(format, v...):
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (l *oldConnReplacedLogger) Warnf(format string, v ...any) {
|
|
select {
|
|
case l.warnCh <- fmt.Sprintf(format, v...):
|
|
default:
|
|
}
|
|
}
|
|
|
|
// This test will simulate that the accept side does not detect the connection
|
|
// has been closed early enough. The soliciting side will attempt to reconnect
|
|
// and we should not be getting the "loop detected" error.
|
|
func TestLeafNodeLoopDetectedDueToReconnect(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Host = "127.0.0.1"
|
|
o.LeafNode.Port = -1
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
l := &oldConnReplacedLogger{errCh: make(chan string, 10), warnCh: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
p := &proxyAcceptDetectFailureLate{acceptPort: o.LeafNode.Port}
|
|
defer p.close()
|
|
port := p.run(t)
|
|
|
|
aurl, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
ol := DefaultOptions()
|
|
ol.Cluster.Name = "cde"
|
|
ol.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
ol.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{aurl}}}
|
|
sl := RunServer(ol)
|
|
defer sl.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, sl)
|
|
|
|
// Cause disconnect client side...
|
|
p.Lock()
|
|
p.leaf.Close()
|
|
p.Unlock()
|
|
|
|
// Make sure we did not get the loop detected error
|
|
select {
|
|
case e := <-l.errCh:
|
|
if strings.Contains(e, "Loop detected") {
|
|
t.Fatalf("Loop detected: %v", e)
|
|
}
|
|
case <-time.After(250 * time.Millisecond):
|
|
// We are ok
|
|
}
|
|
|
|
// Now make sure we got the warning
|
|
select {
|
|
case w := <-l.warnCh:
|
|
if !strings.Contains(w, "Replacing connection from same server") {
|
|
t.Fatalf("Unexpected warning: %v", w)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Did not get expected warning")
|
|
}
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, sl)
|
|
}
|
|
|
|
func TestLeafNodeTwoRemotesBindToSameHubAccount(t *testing.T) {
|
|
opts := DefaultOptions()
|
|
opts.LeafNode.Host = "127.0.0.1"
|
|
opts.LeafNode.Port = -1
|
|
s := RunServer(opts)
|
|
defer s.Shutdown()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
account string
|
|
fail bool
|
|
}{
|
|
{"different local accounts", "b", false},
|
|
{"same local accounts", "a", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf := `
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1 }
|
|
accounts {
|
|
a { users [ {user: a, password: a} ]}
|
|
b { users [ {user: b, password: b} ]}
|
|
}
|
|
leafnodes {
|
|
remotes = [
|
|
{
|
|
url: nats-leaf://127.0.0.1:%[1]d
|
|
account: a
|
|
}
|
|
{
|
|
url: nats-leaf://127.0.0.1:%[1]d
|
|
account: %s
|
|
}
|
|
]
|
|
}
|
|
`
|
|
lconf := createConfFile(t, []byte(fmt.Sprintf(conf, opts.LeafNode.Port, test.account)))
|
|
|
|
lopts, err := ProcessConfigFile(lconf)
|
|
if err != nil {
|
|
t.Fatalf("Error loading config file: %v", err)
|
|
}
|
|
lopts.NoLog = false
|
|
ln, err := NewServer(lopts)
|
|
if err != nil {
|
|
t.Fatalf("Error creating server: %v", err)
|
|
}
|
|
defer ln.Shutdown()
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
ln.SetLogger(l, false, false)
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
ln.Start()
|
|
}()
|
|
|
|
select {
|
|
case err := <-l.errCh:
|
|
if test.fail && !strings.Contains(err, DuplicateRemoteLeafnodeConnection.String()) {
|
|
t.Fatalf("Did not get expected duplicate connection error: %v", err)
|
|
} else if !test.fail && strings.Contains(err, DuplicateRemoteLeafnodeConnection.String()) {
|
|
t.Fatalf("Incorrectly detected a duplicate connection: %v", err)
|
|
}
|
|
case <-time.After(250 * time.Millisecond):
|
|
if test.fail {
|
|
t.Fatal("Did not get expected error")
|
|
}
|
|
}
|
|
ln.Shutdown()
|
|
wg.Wait()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeNoDuplicateWithinCluster(t *testing.T) {
|
|
// This set the cluster name to "abc"
|
|
oSrv1 := DefaultOptions()
|
|
oSrv1.ServerName = "srv1"
|
|
oSrv1.LeafNode.Host = "127.0.0.1"
|
|
oSrv1.LeafNode.Port = -1
|
|
srv1 := RunServer(oSrv1)
|
|
defer srv1.Shutdown()
|
|
|
|
u, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oSrv1.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
|
|
oLeaf1 := DefaultOptions()
|
|
oLeaf1.ServerName = "leaf1"
|
|
oLeaf1.Cluster.Name = "xyz"
|
|
oLeaf1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
|
|
leaf1 := RunServer(oLeaf1)
|
|
defer leaf1.Shutdown()
|
|
|
|
leaf1ClusterURL := fmt.Sprintf("nats://127.0.0.1:%d", oLeaf1.Cluster.Port)
|
|
|
|
oLeaf2 := DefaultOptions()
|
|
oLeaf2.ServerName = "leaf2"
|
|
oLeaf2.Cluster.Name = "xyz"
|
|
oLeaf2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
|
|
oLeaf2.Routes = RoutesFromStr(leaf1ClusterURL)
|
|
leaf2 := RunServer(oLeaf2)
|
|
defer leaf2.Shutdown()
|
|
|
|
checkClusterFormed(t, leaf1, leaf2)
|
|
|
|
checkLeafNodeConnectedCount(t, srv1, 2)
|
|
checkLeafNodeConnected(t, leaf1)
|
|
checkLeafNodeConnected(t, leaf2)
|
|
|
|
ncSrv1 := natsConnect(t, srv1.ClientURL())
|
|
defer ncSrv1.Close()
|
|
natsQueueSub(t, ncSrv1, "foo", "queue", func(m *nats.Msg) {
|
|
m.Data = []byte("from srv1")
|
|
m.RespondMsg(m)
|
|
})
|
|
|
|
ncLeaf1 := natsConnect(t, leaf1.ClientURL())
|
|
defer ncLeaf1.Close()
|
|
natsQueueSub(t, ncLeaf1, "foo", "queue", func(m *nats.Msg) {
|
|
m.Data = []byte("from leaf1")
|
|
m.RespondMsg(m)
|
|
})
|
|
|
|
ncLeaf2 := natsConnect(t, leaf2.ClientURL())
|
|
defer ncLeaf2.Close()
|
|
|
|
// Check that "foo" interest is available everywhere.
|
|
for _, s := range []*Server{srv1, leaf1, leaf2} {
|
|
gacc := s.GlobalAccount()
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if n := gacc.Interest("foo"); n != 2 {
|
|
return fmt.Errorf("Expected interest for %q to be 2, got %v", "foo", n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Send requests (from leaf2). For this test to make sure that
|
|
// there is no duplicate, we want to make sure that we check for
|
|
// multiple replies and that the reply subject subscription has
|
|
// been propagated everywhere.
|
|
sub := natsSubSync(t, ncLeaf2, "reply_subj")
|
|
natsFlush(t, ncLeaf2)
|
|
|
|
// Here we have a single sub on "reply_subj" so using checkSubInterest is ok.
|
|
checkSubInterest(t, srv1, globalAccountName, "reply_subj", time.Second)
|
|
checkSubInterest(t, leaf1, globalAccountName, "reply_subj", time.Second)
|
|
checkSubInterest(t, leaf2, globalAccountName, "reply_subj", time.Second)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
// Now send the request
|
|
reqID := fmt.Sprintf("req.%d", i)
|
|
msg := nats.NewMsg("foo")
|
|
msg.Data = []byte("req")
|
|
msg.Header.Set("ReqId", reqID)
|
|
msg.Reply = sub.Subject
|
|
if err := ncLeaf2.PublishMsg(msg); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
// Check that we get the reply
|
|
replyMsg := natsNexMsg(t, sub, time.Second)
|
|
// But make sure no duplicate. We do so by checking that the reply's
|
|
// header ReqId matches our current reqID.
|
|
if respReqID := replyMsg.Header.Get("ReqId"); respReqID != reqID {
|
|
t.Fatalf("Current request is %q, got duplicate with %q", reqID, respReqID)
|
|
}
|
|
// We also should have preferred the queue sub that is in the leaf cluster.
|
|
if string(replyMsg.Data) != "from leaf1" {
|
|
t.Fatalf("Expected reply from leaf1, got %q", replyMsg.Data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeLMsgSplit(t *testing.T) {
|
|
// This set the cluster name to "abc"
|
|
oSrv1 := DefaultOptions()
|
|
oSrv1.LeafNode.Host = "127.0.0.1"
|
|
oSrv1.LeafNode.Port = -1
|
|
srv1 := RunServer(oSrv1)
|
|
defer srv1.Shutdown()
|
|
|
|
oSrv2 := DefaultOptions()
|
|
oSrv2.LeafNode.Host = "127.0.0.1"
|
|
oSrv2.LeafNode.Port = -1
|
|
oSrv2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", oSrv1.Cluster.Port))
|
|
srv2 := RunServer(oSrv2)
|
|
defer srv2.Shutdown()
|
|
|
|
checkClusterFormed(t, srv1, srv2)
|
|
|
|
u1, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oSrv1.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
u2, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oSrv2.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
|
|
oLeaf1 := DefaultOptions()
|
|
oLeaf1.Cluster.Name = "xyz"
|
|
oLeaf1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u1, u2}}}
|
|
leaf1 := RunServer(oLeaf1)
|
|
defer leaf1.Shutdown()
|
|
|
|
oLeaf2 := DefaultOptions()
|
|
oLeaf2.Cluster.Name = "xyz"
|
|
oLeaf2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u1, u2}}}
|
|
oLeaf2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", oLeaf1.Cluster.Port))
|
|
leaf2 := RunServer(oLeaf2)
|
|
defer leaf2.Shutdown()
|
|
|
|
checkClusterFormed(t, leaf1, leaf2)
|
|
|
|
checkLeafNodeConnected(t, leaf1)
|
|
checkLeafNodeConnected(t, leaf2)
|
|
|
|
ncSrv2 := natsConnect(t, srv2.ClientURL())
|
|
defer ncSrv2.Close()
|
|
natsQueueSub(t, ncSrv2, "foo", "queue", func(m *nats.Msg) {
|
|
m.Respond([]byte("from srv2"))
|
|
})
|
|
|
|
// Check that "foo" interest is available everywhere.
|
|
checkSubInterest(t, srv1, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, srv2, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, leaf1, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, leaf2, globalAccountName, "foo", time.Second)
|
|
|
|
// Not required, but have a request payload that is more than 100 bytes
|
|
reqPayload := make([]byte, 150)
|
|
for i := 0; i < len(reqPayload); i++ {
|
|
reqPayload[i] = byte((i % 26)) + 'A'
|
|
}
|
|
|
|
// Send repeated requests (from scratch) from leaf-2:
|
|
sendReq := func() {
|
|
t.Helper()
|
|
|
|
ncLeaf2 := natsConnect(t, leaf2.ClientURL())
|
|
defer ncLeaf2.Close()
|
|
|
|
if _, err := ncLeaf2.Request("foo", reqPayload, time.Second); err != nil {
|
|
t.Fatalf("Did not receive reply: %v", err)
|
|
}
|
|
}
|
|
for i := 0; i < 100; i++ {
|
|
sendReq()
|
|
}
|
|
}
|
|
|
|
type parseRouteLSUnsubLogger struct {
|
|
DummyLogger
|
|
gotTrace chan struct{}
|
|
gotErr chan error
|
|
}
|
|
|
|
func (l *parseRouteLSUnsubLogger) Errorf(format string, v ...any) {
|
|
err := fmt.Errorf(format, v...)
|
|
select {
|
|
case l.gotErr <- err:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (l *parseRouteLSUnsubLogger) Tracef(format string, v ...any) {
|
|
trace := fmt.Sprintf(format, v...)
|
|
if strings.Contains(trace, "LS- xyz $G foo bar") {
|
|
l.gotTrace <- struct{}{}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeRouteParseLSUnsub(t *testing.T) {
|
|
// This set the cluster name to "abc"
|
|
oSrv1 := DefaultOptions()
|
|
oSrv1.LeafNode.Host = "127.0.0.1"
|
|
oSrv1.LeafNode.Port = -1
|
|
srv1 := RunServer(oSrv1)
|
|
defer srv1.Shutdown()
|
|
|
|
l := &parseRouteLSUnsubLogger{gotTrace: make(chan struct{}, 1), gotErr: make(chan error, 1)}
|
|
srv1.SetLogger(l, true, true)
|
|
|
|
oSrv2 := DefaultOptions()
|
|
oSrv2.LeafNode.Host = "127.0.0.1"
|
|
oSrv2.LeafNode.Port = -1
|
|
oSrv2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", oSrv1.Cluster.Port))
|
|
srv2 := RunServer(oSrv2)
|
|
defer srv2.Shutdown()
|
|
|
|
checkClusterFormed(t, srv1, srv2)
|
|
|
|
u2, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", oSrv2.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
|
|
oLeaf2 := DefaultOptions()
|
|
oLeaf2.Cluster.Name = "xyz"
|
|
oLeaf2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u2}}}
|
|
leaf2 := RunServer(oLeaf2)
|
|
defer leaf2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, srv2)
|
|
checkLeafNodeConnected(t, leaf2)
|
|
|
|
ncLeaf2 := natsConnect(t, leaf2.ClientURL())
|
|
defer ncLeaf2.Close()
|
|
|
|
sub := natsQueueSubSync(t, ncLeaf2, "foo", "bar")
|
|
// The issue was with the unsubscribe of this queue subscription
|
|
natsUnsub(t, sub)
|
|
|
|
// We should get the trace
|
|
select {
|
|
case <-l.gotTrace:
|
|
// OK!
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatalf("Did not get LS- trace")
|
|
}
|
|
// And no error...
|
|
select {
|
|
case e := <-l.gotErr:
|
|
t.Fatalf("There was an error on server 1: %q", e.Error())
|
|
case <-time.After(100 * time.Millisecond):
|
|
// OK!
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeOperatorBadCfg(t *testing.T) {
|
|
sysAcc, err := nkeys.CreateAccount()
|
|
require_NoError(t, err)
|
|
sysAccPk, err := sysAcc.PublicKey()
|
|
require_NoError(t, err)
|
|
tmpDir := t.TempDir()
|
|
|
|
configTmpl := `
|
|
port: -1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: {
|
|
type: cache
|
|
dir: '%s'
|
|
}
|
|
leafnodes: {
|
|
%s
|
|
}
|
|
`
|
|
|
|
cases := []struct {
|
|
name string
|
|
errorText string
|
|
cfg string
|
|
}{
|
|
{
|
|
name: "Operator with Leafnode",
|
|
errorText: "operator mode does not allow specifying users in leafnode config",
|
|
cfg: `
|
|
port: -1
|
|
authorization {
|
|
users = [{user: "u", password: "p"}]
|
|
}`,
|
|
},
|
|
{
|
|
name: "Operator with NKey",
|
|
errorText: "operator mode and non account nkeys are incompatible",
|
|
cfg: `
|
|
port: -1
|
|
authorization {
|
|
account: notankey
|
|
}`,
|
|
},
|
|
{
|
|
name: "Operator remote account NKeys",
|
|
errorText: "operator mode requires account nkeys in remotes. " +
|
|
"Please add an `account` key to each remote in your `leafnodes` section, to assign it to an account. " +
|
|
"Each account value should be a 56 character public key, starting with the letter 'A'",
|
|
cfg: `remotes: [{url: u}]`,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(configTmpl, ojwt, sysAccPk, tmpDir, c.cfg)))
|
|
opts := LoadConfig(conf)
|
|
s, err := NewServer(opts)
|
|
if err == nil {
|
|
s.Shutdown()
|
|
t.Fatal("Expected an error")
|
|
}
|
|
// Since the server cannot be stopped, since it did not start,
|
|
// let's manually close the account resolver to avoid leaking go routines.
|
|
opts.AccountResolver.Close()
|
|
if err.Error() != c.errorText {
|
|
t.Fatalf("Expected error %q but got %q", c.errorText, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeTLSConfigReload(t *testing.T) {
|
|
template := `
|
|
listen: 127.0.0.1:-1
|
|
leaf {
|
|
listen: "127.0.0.1:-1"
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
%s
|
|
timeout: 2
|
|
verify: true
|
|
}
|
|
}
|
|
`
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(template, "")))
|
|
|
|
srvA, optsA := RunServerWithConfig(confA)
|
|
defer srvA.Shutdown()
|
|
|
|
lg := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
srvA.SetLogger(lg, false, false)
|
|
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: -1
|
|
leaf {
|
|
remotes [
|
|
{
|
|
url: "tls://127.0.0.1:%d"
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`, optsA.LeafNode.Port)))
|
|
|
|
optsB, err := ProcessConfigFile(confB)
|
|
if err != nil {
|
|
t.Fatalf("Error processing config file: %v", err)
|
|
}
|
|
optsB.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
optsB.NoLog, optsB.NoSigs = true, true
|
|
|
|
srvB := RunServer(optsB)
|
|
defer srvB.Shutdown()
|
|
|
|
// Wait for the error
|
|
select {
|
|
case err := <-lg.errCh:
|
|
// Since Go 1.18, we had to regenerate certs to not have to use GODEBUG="x509sha1=1"
|
|
// But on macOS, with our test CA certs, no SCTs included, it will fail
|
|
// for the reason "x509: “localhost” certificate is not standards compliant"
|
|
// instead of "unknown authority".
|
|
if !strings.Contains(err, "unknown") && !strings.Contains(err, "compliant") {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Did not get TLS error")
|
|
}
|
|
|
|
// Add the CA to srvA
|
|
reloadUpdateConfig(t, srvA, confA, fmt.Sprintf(template, `ca_file: "../test/configs/certs/ca.pem"`))
|
|
|
|
// Now make sure that srvB can create a LN connection.
|
|
checkFor(t, 3*time.Second, 10*time.Millisecond, func() error {
|
|
if nln := srvB.NumLeafNodes(); nln != 1 {
|
|
return fmt.Errorf("Number of leaf nodes is %d", nln)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestLeafNodeTLSConfigReloadForRemote(t *testing.T) {
|
|
confA := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
leaf {
|
|
listen: "127.0.0.1:-1"
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
timeout: 2
|
|
verify: true
|
|
}
|
|
}
|
|
`))
|
|
|
|
srvA, optsA := RunServerWithConfig(confA)
|
|
defer srvA.Shutdown()
|
|
|
|
lg := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
srvA.SetLogger(lg, false, false)
|
|
|
|
template := `
|
|
listen: -1
|
|
leaf {
|
|
remotes [
|
|
{
|
|
url: "tls://127.0.0.1:%d"
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
%s
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(template, optsA.LeafNode.Port, "")))
|
|
|
|
srvB, _ := RunServerWithConfig(confB)
|
|
defer srvB.Shutdown()
|
|
|
|
// Wait for the error
|
|
select {
|
|
case err := <-lg.errCh:
|
|
if !strings.Contains(err, "bad certificate") {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("Did not get TLS error")
|
|
}
|
|
|
|
// Add the CA to srvB
|
|
reloadUpdateConfig(t, srvB, confB, fmt.Sprintf(template, optsA.LeafNode.Port, `ca_file: "../test/configs/certs/ca.pem"`))
|
|
|
|
// Now make sure that srvB can create a LN connection.
|
|
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
|
|
if nln := srvB.NumLeafNodes(); nln != 1 {
|
|
return fmt.Errorf("Number of leaf nodes is %d", nln)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func testDefaultLeafNodeWSOptions() *Options {
|
|
o := DefaultOptions()
|
|
o.Websocket.Host = "127.0.0.1"
|
|
o.Websocket.Port = -1
|
|
o.Websocket.NoTLS = true
|
|
o.LeafNode.Host = "127.0.0.1"
|
|
o.LeafNode.Port = -1
|
|
return o
|
|
}
|
|
|
|
func testDefaultRemoteLeafNodeWSOptions(t *testing.T, o *Options, tls bool) *Options {
|
|
// Use some path in the URL.. we don't use that, but internally
|
|
// the server will prefix the path with /leafnode so that the
|
|
// WS webserver knows that it needs to create a LEAF connection.
|
|
u, _ := url.Parse(fmt.Sprintf("ws://127.0.0.1:%d/some/path", o.Websocket.Port))
|
|
lo := DefaultOptions()
|
|
lo.Cluster.Name = "LN"
|
|
remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
|
|
if tls {
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/server-cert.pem",
|
|
KeyFile: "../test/configs/certs/server-key.pem",
|
|
CaFile: "../test/configs/certs/ca.pem",
|
|
}
|
|
tlsConf, err := GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error generating TLS config: %v", err)
|
|
}
|
|
// GenTLSConfig sets the CA in ClientCAs, but since here we act
|
|
// as a client, set RootCAs...
|
|
tlsConf.RootCAs = tlsConf.ClientCAs
|
|
remote.TLSConfig = tlsConf
|
|
}
|
|
lo.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
return lo
|
|
}
|
|
|
|
func TestLeafNodeWSMixURLs(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
urls []string
|
|
}{
|
|
{"mix 1", []string{"nats://127.0.0.1:1234", "ws://127.0.0.1:5678", "wss://127.0.0.1:9012"}},
|
|
{"mix 2", []string{"ws://127.0.0.1:1234", "nats://127.0.0.1:5678", "wss://127.0.0.1:9012"}},
|
|
{"mix 3", []string{"wss://127.0.0.1:1234", "ws://127.0.0.1:5678", "nats://127.0.0.1:9012"}},
|
|
{"mix 4", []string{"ws://127.0.0.1:1234", "nats://127.0.0.1:9012"}},
|
|
{"mix 5", []string{"nats://127.0.0.1:1234", "ws://127.0.0.1:9012"}},
|
|
{"mix 6", []string{"wss://127.0.0.1:1234", "nats://127.0.0.1:9012"}},
|
|
{"mix 7", []string{"nats://127.0.0.1:1234", "wss://127.0.0.1:9012"}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := DefaultOptions()
|
|
remote := &RemoteLeafOpts{}
|
|
urls := make([]*url.URL, 0, 3)
|
|
for _, ustr := range test.urls {
|
|
u, err := url.Parse(ustr)
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
urls = append(urls, u)
|
|
}
|
|
remote.URLs = urls
|
|
o.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
s, err := NewServer(o)
|
|
if err == nil || !strings.Contains(err.Error(), "mix") {
|
|
if s != nil {
|
|
s.Shutdown()
|
|
}
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type testConnTrackSize struct {
|
|
sync.Mutex
|
|
net.Conn
|
|
sz int
|
|
}
|
|
|
|
func (c *testConnTrackSize) Write(p []byte) (int, error) {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
n, err := c.Conn.Write(p)
|
|
c.sz += n
|
|
return n, err
|
|
}
|
|
|
|
func TestLeafNodeWSBasic(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
masking bool
|
|
tls bool
|
|
acceptCompression bool
|
|
remoteCompression bool
|
|
}{
|
|
{"masking plain no compression", true, false, false, false},
|
|
{"masking plain compression", true, false, true, true},
|
|
{"masking plain compression disagree", true, false, false, true},
|
|
{"masking plain compression disagree 2", true, false, true, false},
|
|
{"masking tls no compression", true, true, false, false},
|
|
{"masking tls compression", true, true, true, true},
|
|
{"masking tls compression disagree", true, true, false, true},
|
|
{"masking tls compression disagree 2", true, true, true, false},
|
|
{"no masking plain no compression", false, false, false, false},
|
|
{"no masking plain compression", false, false, true, true},
|
|
{"no masking plain compression disagree", false, false, false, true},
|
|
{"no masking plain compression disagree 2", false, false, true, false},
|
|
{"no masking tls no compression", false, true, false, false},
|
|
{"no masking tls compression", false, true, true, true},
|
|
{"no masking tls compression disagree", false, true, false, true},
|
|
{"no masking tls compression disagree 2", false, true, true, false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := testDefaultLeafNodeWSOptions()
|
|
o.Websocket.NoTLS = !test.tls
|
|
if test.tls {
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/server-cert.pem",
|
|
KeyFile: "../test/configs/certs/server-key.pem",
|
|
CaFile: "../test/configs/certs/ca.pem",
|
|
}
|
|
tlsConf, err := GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error generating TLS config: %v", err)
|
|
}
|
|
o.Websocket.TLSConfig = tlsConf
|
|
}
|
|
o.Websocket.Compression = test.acceptCompression
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
lo := testDefaultRemoteLeafNodeWSOptions(t, o, test.tls)
|
|
lo.LeafNode.Remotes[0].Websocket.Compression = test.remoteCompression
|
|
lo.LeafNode.Remotes[0].Websocket.NoMasking = !test.masking
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
var trackSizeConn *testConnTrackSize
|
|
if !test.tls {
|
|
var cln *client
|
|
ln.mu.Lock()
|
|
for _, l := range ln.leafs {
|
|
cln = l
|
|
break
|
|
}
|
|
ln.mu.Unlock()
|
|
cln.mu.Lock()
|
|
trackSizeConn = &testConnTrackSize{Conn: cln.nc}
|
|
cln.nc = trackSizeConn
|
|
cln.mu.Unlock()
|
|
}
|
|
|
|
nc1 := natsConnect(t, s.ClientURL())
|
|
defer nc1.Close()
|
|
sub1 := natsSubSync(t, nc1, "foo")
|
|
natsFlush(t, nc1)
|
|
|
|
checkSubInterest(t, ln, globalAccountName, "foo", time.Second)
|
|
|
|
nc2 := natsConnect(t, ln.ClientURL())
|
|
defer nc2.Close()
|
|
msg1Payload := make([]byte, 2048)
|
|
for i := 0; i < len(msg1Payload); i++ {
|
|
msg1Payload[i] = 'A'
|
|
}
|
|
natsPub(t, nc2, "foo", msg1Payload)
|
|
|
|
msg := natsNexMsg(t, sub1, time.Second)
|
|
if !bytes.Equal(msg.Data, msg1Payload) {
|
|
t.Fatalf("Invalid message: %q", msg.Data)
|
|
}
|
|
|
|
sub2 := natsSubSync(t, nc2, "bar")
|
|
natsFlush(t, nc2)
|
|
|
|
checkSubInterest(t, s, globalAccountName, "bar", time.Second)
|
|
|
|
msg2Payload := make([]byte, 2048)
|
|
for i := 0; i < len(msg2Payload); i++ {
|
|
msg2Payload[i] = 'B'
|
|
}
|
|
natsPub(t, nc1, "bar", msg2Payload)
|
|
|
|
msg = natsNexMsg(t, sub2, time.Second)
|
|
if !bytes.Equal(msg.Data, msg2Payload) {
|
|
t.Fatalf("Invalid message: %q", msg.Data)
|
|
}
|
|
|
|
if !test.tls {
|
|
trackSizeConn.Lock()
|
|
size := trackSizeConn.sz
|
|
trackSizeConn.Unlock()
|
|
|
|
if test.acceptCompression && test.remoteCompression {
|
|
if size >= 1024 {
|
|
t.Fatalf("Seems that there was no compression: size=%v", size)
|
|
}
|
|
} else if size < 2048 {
|
|
t.Fatalf("Seems compression was on while it should not: size=%v", size)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeWSRemoteCompressAndMaskingOptions(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
compress bool
|
|
compStr string
|
|
noMasking bool
|
|
noMaskStr string
|
|
}{
|
|
{"compression masking", true, "true", false, "false"},
|
|
{"compression no masking", true, "true", true, "true"},
|
|
{"no compression masking", false, "false", false, "false"},
|
|
{"no compression no masking", false, "false", true, "true"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes {
|
|
remotes [
|
|
{url: "ws://127.0.0.1:1234", ws_compression: %s, ws_no_masking: %s}
|
|
]
|
|
}
|
|
`, test.compStr, test.noMaskStr)))
|
|
o, err := ProcessConfigFile(conf)
|
|
if err != nil {
|
|
t.Fatalf("Error loading conf: %v", err)
|
|
}
|
|
if nr := len(o.LeafNode.Remotes); nr != 1 {
|
|
t.Fatalf("Expected 1 remote, got %v", nr)
|
|
}
|
|
r := o.LeafNode.Remotes[0]
|
|
if cur := r.Websocket.Compression; cur != test.compress {
|
|
t.Fatalf("Expected compress to be %v, got %v", test.compress, cur)
|
|
}
|
|
if cur := r.Websocket.NoMasking; cur != test.noMasking {
|
|
t.Fatalf("Expected ws_masking to be %v, got %v", test.noMasking, cur)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeWSNoMaskingRejected(t *testing.T) {
|
|
wsTestRejectNoMasking = true
|
|
defer func() { wsTestRejectNoMasking = false }()
|
|
|
|
o := testDefaultLeafNodeWSOptions()
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
lo := testDefaultRemoteLeafNodeWSOptions(t, o, false)
|
|
lo.LeafNode.Remotes[0].Websocket.NoMasking = true
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
var cln *client
|
|
ln.mu.Lock()
|
|
for _, l := range ln.leafs {
|
|
cln = l
|
|
break
|
|
}
|
|
ln.mu.Unlock()
|
|
|
|
cln.mu.Lock()
|
|
maskWrite := cln.ws.maskwrite
|
|
cln.mu.Unlock()
|
|
|
|
if !maskWrite {
|
|
t.Fatal("Leafnode remote connection should mask writes, it does not")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeWSSubPath(t *testing.T) {
|
|
o := testDefaultLeafNodeWSOptions()
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
lo := testDefaultRemoteLeafNodeWSOptions(t, o, false)
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
// Confirm that it can connect using the subpath.
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
// Add another leafnode that tries to connect to the subpath
|
|
// but intercept the attempt for the test.
|
|
o2 := testDefaultLeafNodeWSOptions()
|
|
lo2 := testDefaultRemoteLeafNodeWSOptions(t, o2, false)
|
|
attempts := make(chan string, 2)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
attempts <- r.URL.String()
|
|
}))
|
|
defer ts.Close()
|
|
u, _ := url.Parse(fmt.Sprintf("%v/some/path", ts.URL))
|
|
u.Scheme = "ws"
|
|
lo2.LeafNode.Remotes = []*RemoteLeafOpts{
|
|
{
|
|
URLs: []*url.URL{u},
|
|
},
|
|
}
|
|
ln2 := RunServer(lo2)
|
|
defer ln2.Shutdown()
|
|
|
|
expected := "/some/path/leafnode"
|
|
select {
|
|
case got := <-attempts:
|
|
if got != expected {
|
|
t.Fatalf("Expected: %v, got: %v", expected, got)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for leaf ws connect attempt")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeWSFailedConnection(t *testing.T) {
|
|
o := testDefaultLeafNodeWSOptions()
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
lo := testDefaultRemoteLeafNodeWSOptions(t, o, true)
|
|
lo.LeafNode.ReconnectInterval = 100 * time.Millisecond
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
el := &captureErrorLogger{errCh: make(chan string, 100)}
|
|
ln.SetLogger(el, false, false)
|
|
|
|
select {
|
|
case err := <-el.errCh:
|
|
if !strings.Contains(err, "handshake error") {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("No error reported!")
|
|
}
|
|
ln.Shutdown()
|
|
s.Shutdown()
|
|
|
|
lst, err := natsListen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("Error starting listener: %v", err)
|
|
}
|
|
defer lst.Close()
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(2)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
c, err := lst.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
|
|
if rand.Intn(2) == 1 {
|
|
c.Write([]byte("something\r\n"))
|
|
}
|
|
c.Close()
|
|
}
|
|
}()
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
port := lst.Addr().(*net.TCPAddr).Port
|
|
u, _ := url.Parse(fmt.Sprintf("ws://127.0.0.1:%d", port))
|
|
lo = DefaultOptions()
|
|
lo.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}}}
|
|
lo.LeafNode.ReconnectInterval = 10 * time.Millisecond
|
|
ln, _ = NewServer(lo)
|
|
el = &captureErrorLogger{errCh: make(chan string, 100)}
|
|
ln.SetLogger(el, false, false)
|
|
|
|
go func() {
|
|
ln.Start()
|
|
wg.Done()
|
|
}()
|
|
|
|
timeout := time.NewTimer(time.Second)
|
|
for i := 0; i < 10; i++ {
|
|
select {
|
|
case err := <-el.errCh:
|
|
if !strings.Contains(err, "Error soliciting") {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
case <-timeout.C:
|
|
t.Fatal("No error reported!")
|
|
}
|
|
}
|
|
ln.Shutdown()
|
|
lst.Close()
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestLeafNodeWSAuth(t *testing.T) {
|
|
template := `
|
|
port: -1
|
|
authorization {
|
|
users [
|
|
{user: "user", pass: "puser", connection_types: ["%s"]}
|
|
{user: "leaf", pass: "pleaf", connection_types: ["%s"%s]}
|
|
]
|
|
}
|
|
websocket {
|
|
port: -1
|
|
no_tls: true
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`
|
|
s, o, conf := runReloadServerWithContent(t,
|
|
[]byte(fmt.Sprintf(template, jwt.ConnectionTypeStandard, jwt.ConnectionTypeLeafnode, "")))
|
|
defer s.Shutdown()
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
lo := testDefaultRemoteLeafNodeWSOptions(t, o, false)
|
|
u, _ := url.Parse(fmt.Sprintf("ws://leaf:[email protected]:%d", o.Websocket.Port))
|
|
remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
|
|
lo.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
lo.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
var lasterr string
|
|
tm := time.NewTimer(2 * time.Second)
|
|
for done := false; !done; {
|
|
select {
|
|
case lasterr = <-l.errCh:
|
|
if strings.Contains(lasterr, "authentication") {
|
|
done = true
|
|
}
|
|
case <-tm.C:
|
|
t.Fatalf("Expected auth error, got %v", lasterr)
|
|
}
|
|
}
|
|
|
|
ws := fmt.Sprintf(`, "%s"`, jwt.ConnectionTypeLeafnodeWS)
|
|
reloadUpdateConfig(t, s, conf, fmt.Sprintf(template,
|
|
jwt.ConnectionTypeStandard, jwt.ConnectionTypeLeafnode, ws))
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
nc1 := natsConnect(t, fmt.Sprintf("nats://user:[email protected]:%d", o.Port))
|
|
defer nc1.Close()
|
|
|
|
sub := natsSubSync(t, nc1, "foo")
|
|
natsFlush(t, nc1)
|
|
|
|
checkSubInterest(t, ln, globalAccountName, "foo", time.Second)
|
|
|
|
nc2 := natsConnect(t, ln.ClientURL())
|
|
defer nc2.Close()
|
|
|
|
natsPub(t, nc2, "foo", []byte("msg1"))
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
|
|
if md := string(msg.Data); md != "msg1" {
|
|
t.Fatalf("Invalid message: %q", md)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeWSGossip(t *testing.T) {
|
|
o1 := testDefaultLeafNodeWSOptions()
|
|
s1 := RunServer(o1)
|
|
defer s1.Shutdown()
|
|
|
|
// Now connect from a server that knows only about s1
|
|
lo := testDefaultRemoteLeafNodeWSOptions(t, o1, false)
|
|
lo.LeafNode.ReconnectInterval = 15 * time.Millisecond
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s1)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
// Now add a routed server to s1
|
|
o2 := testDefaultLeafNodeWSOptions()
|
|
o2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", o1.Cluster.Port))
|
|
s2 := RunServer(o2)
|
|
defer s2.Shutdown()
|
|
|
|
// Wait for cluster to form
|
|
checkClusterFormed(t, s1, s2)
|
|
|
|
// Now shutdown s1 and check that ln is able to reconnect to s2.
|
|
s1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
// Make sure that the reconnection was as a WS connection, not simply to
|
|
// the regular LN port.
|
|
var s2lc *client
|
|
s2.mu.Lock()
|
|
for _, l := range s2.leafs {
|
|
s2lc = l
|
|
break
|
|
}
|
|
s2.mu.Unlock()
|
|
|
|
s2lc.mu.Lock()
|
|
isWS := s2lc.isWebsocket()
|
|
s2lc.mu.Unlock()
|
|
|
|
if !isWS {
|
|
t.Fatal("Leafnode connection is not websocket!")
|
|
}
|
|
}
|
|
|
|
// This test was showing an issue if one set maxBufSize to very small value,
|
|
// such as maxBufSize = 10. With such small value, we would get a corruption
|
|
// in that LMSG would arrive with missing bytes. We are now always making
|
|
// a copy when dealing with messages that are bigger than maxBufSize.
|
|
func TestLeafNodeWSNoBufferCorruption(t *testing.T) {
|
|
o := testDefaultLeafNodeWSOptions()
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
lo1 := testDefaultRemoteLeafNodeWSOptions(t, o, false)
|
|
lo1.LeafNode.ReconnectInterval = 15 * time.Millisecond
|
|
ln1 := RunServer(lo1)
|
|
defer ln1.Shutdown()
|
|
|
|
lo2 := DefaultOptions()
|
|
lo2.Cluster.Name = "LN"
|
|
lo2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo1.Cluster.Port))
|
|
ln2 := RunServer(lo2)
|
|
defer ln2.Shutdown()
|
|
|
|
checkClusterFormed(t, ln1, ln2)
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, ln1)
|
|
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
sub := natsSubSync(t, nc, "foo")
|
|
|
|
nc1 := natsConnect(t, ln1.ClientURL())
|
|
defer nc1.Close()
|
|
|
|
nc2 := natsConnect(t, ln2.ClientURL())
|
|
defer nc2.Close()
|
|
sub2 := natsSubSync(t, nc2, "foo")
|
|
|
|
checkSubInterest(t, s, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, ln2, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, ln1, globalAccountName, "foo", time.Second)
|
|
|
|
payload := make([]byte, 100*1024)
|
|
for i := 0; i < len(payload); i++ {
|
|
payload[i] = 'A'
|
|
}
|
|
natsPub(t, nc1, "foo", payload)
|
|
|
|
checkMsgRcv := func(sub *nats.Subscription) {
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
if !bytes.Equal(msg.Data, payload) {
|
|
t.Fatalf("Invalid message content: %q", msg.Data)
|
|
}
|
|
}
|
|
checkMsgRcv(sub2)
|
|
checkMsgRcv(sub)
|
|
}
|
|
|
|
func TestLeafNodeWSRemoteNoTLSBlockWithWSSProto(t *testing.T) {
|
|
o := testDefaultLeafNodeWSOptions()
|
|
o.Websocket.NoTLS = false
|
|
tc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/server-cert.pem",
|
|
KeyFile: "../test/configs/certs/server-key.pem",
|
|
CaFile: "../test/configs/certs/ca.pem",
|
|
}
|
|
tlsConf, err := GenTLSConfig(tc)
|
|
if err != nil {
|
|
t.Fatalf("Error generating TLS config: %v", err)
|
|
}
|
|
o.Websocket.TLSConfig = tlsConf
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
// The test will make sure that if the protocol is "wss://", a TLS handshake must
|
|
// be initiated, regardless of the presence of a TLS config block in config file
|
|
// or here directly.
|
|
// A bug was causing the absence of TLS config block to initiate a non TLS connection
|
|
// even if "wss://" proto was specified, which would lead to "invalid websocket connection"
|
|
// errors in the log.
|
|
// With the fix, the connection will fail because the remote will fail to verify
|
|
// the root CA, but at least, we will make sure that this is not an "invalid websocket connection"
|
|
|
|
u, _ := url.Parse(fmt.Sprintf("wss://127.0.0.1:%d/some/path", o.Websocket.Port))
|
|
lo := DefaultOptions()
|
|
lo.Cluster.Name = "LN"
|
|
remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
|
|
lo.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
lo.LeafNode.ReconnectInterval = 100 * time.Millisecond
|
|
ln := RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
ln.SetLogger(l, false, false)
|
|
|
|
select {
|
|
case e := <-l.errCh:
|
|
if strings.Contains(e, "invalid websocket connection") {
|
|
t.Fatalf("The remote did not try to create a TLS connection: %v", e)
|
|
}
|
|
// OK!
|
|
return
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Connection should fail")
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeWSNoAuthUser(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
port: -1
|
|
accounts {
|
|
A { users [ {user: a, password: a} ]}
|
|
B { users [ {user: b, password: b} ]}
|
|
}
|
|
websocket {
|
|
port: -1
|
|
no_tls: true
|
|
no_auth_user: a
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
nc1 := natsConnect(t, fmt.Sprintf("nats://a:[email protected]:%d", o.Port))
|
|
defer nc1.Close()
|
|
|
|
lconf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
accounts {
|
|
A { users [ {user: a, password: a} ]}
|
|
B { users [ {user: b, password: b} ]}
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "ws://127.0.0.1:%d"
|
|
account: A
|
|
}
|
|
]
|
|
}
|
|
`, o.Websocket.Port)))
|
|
|
|
ln, lo := RunServerWithConfig(lconf)
|
|
defer ln.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
nc2 := natsConnect(t, fmt.Sprintf("nats://a:[email protected]:%d", lo.Port))
|
|
defer nc2.Close()
|
|
|
|
sub := natsSubSync(t, nc2, "foo")
|
|
natsFlush(t, nc2)
|
|
|
|
checkSubInterest(t, s, "A", "foo", time.Second)
|
|
|
|
natsPub(t, nc1, "foo", []byte("msg1"))
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
|
|
if md := string(msg.Data); md != "msg1" {
|
|
t.Fatalf("Invalid message: %q", md)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeStreamImport(t *testing.T) {
|
|
o1 := DefaultOptions()
|
|
o1.LeafNode.Port = -1
|
|
accA := NewAccount("A")
|
|
o1.Accounts = []*Account{accA}
|
|
o1.Users = []*User{{Username: "a", Password: "a", Account: accA}}
|
|
o1.LeafNode.Account = "A"
|
|
o1.NoAuthUser = "a"
|
|
s1 := RunServer(o1)
|
|
defer s1.Shutdown()
|
|
|
|
o2 := DefaultOptions()
|
|
o2.LeafNode.Port = -1
|
|
o2.Cluster.Name = "xyz"
|
|
|
|
accB := NewAccount("B")
|
|
if err := accB.AddStreamExport(">", nil); err != nil {
|
|
t.Fatalf("Error adding stream export: %v", err)
|
|
}
|
|
|
|
accC := NewAccount("C")
|
|
if err := accC.AddStreamImport(accB, ">", ""); err != nil {
|
|
t.Fatalf("Error adding stream import: %v", err)
|
|
}
|
|
|
|
o2.Accounts = []*Account{accB, accC}
|
|
o2.Users = []*User{{Username: "b", Password: "b", Account: accB}, {Username: "c", Password: "c", Account: accC}}
|
|
o2.NoAuthUser = "b"
|
|
u, err := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", o1.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
o2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u}, LocalAccount: "C"}}
|
|
s2 := RunServer(o2)
|
|
defer s2.Shutdown()
|
|
|
|
nc1 := natsConnect(t, s1.ClientURL())
|
|
defer nc1.Close()
|
|
|
|
sub := natsSubSync(t, nc1, "a")
|
|
|
|
checkSubInterest(t, s2, "C", "a", time.Second)
|
|
|
|
nc2 := natsConnect(t, s2.ClientURL())
|
|
defer nc2.Close()
|
|
|
|
natsPub(t, nc2, "a", []byte("hello?"))
|
|
|
|
natsNexMsg(t, sub, time.Second)
|
|
}
|
|
|
|
func TestLeafNodeRouteSubWithOrigin(t *testing.T) {
|
|
lo1 := DefaultOptions()
|
|
lo1.LeafNode.Host = "127.0.0.1"
|
|
lo1.LeafNode.Port = -1
|
|
lo1.Cluster.Name = "local"
|
|
lo1.Cluster.Host = "127.0.0.1"
|
|
lo1.Cluster.Port = -1
|
|
l1 := RunServer(lo1)
|
|
defer l1.Shutdown()
|
|
|
|
lo2 := DefaultOptions()
|
|
lo2.LeafNode.Host = "127.0.0.1"
|
|
lo2.LeafNode.Port = -1
|
|
lo2.Cluster.Name = "local"
|
|
lo2.Cluster.Host = "127.0.0.1"
|
|
lo2.Cluster.Port = -1
|
|
lo2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo1.Cluster.Port))
|
|
l2 := RunServer(lo2)
|
|
defer l2.Shutdown()
|
|
|
|
checkClusterFormed(t, l1, l2)
|
|
|
|
u1, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", lo1.LeafNode.Port))
|
|
urls := []*url.URL{u1}
|
|
|
|
ro1 := DefaultOptions()
|
|
ro1.Cluster.Name = "remote"
|
|
ro1.Cluster.Host = "127.0.0.1"
|
|
ro1.Cluster.Port = -1
|
|
ro1.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
ro1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: urls}}
|
|
r1 := RunServer(ro1)
|
|
defer r1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, r1)
|
|
|
|
nc := natsConnect(t, r1.ClientURL(), nats.NoReconnect())
|
|
defer nc.Close()
|
|
natsSubSync(t, nc, "foo")
|
|
natsQueueSubSync(t, nc, "bar", "baz")
|
|
checkSubInterest(t, l2, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, l2, globalAccountName, "bar", time.Second)
|
|
|
|
// Now shutdown the leafnode and check that any subscription for $G on l2 are gone.
|
|
r1.Shutdown()
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
acc := l2.GlobalAccount()
|
|
if n := acc.TotalSubs(); n != 5 {
|
|
return fmt.Errorf("Account %q should have 5 subs, got %v", acc.GetName(), n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestLeafNodeLoopDetectionWithMultipleClusters(t *testing.T) {
|
|
lo1 := DefaultOptions()
|
|
lo1.LeafNode.Host = "127.0.0.1"
|
|
lo1.LeafNode.Port = -1
|
|
lo1.Cluster.Name = "local"
|
|
lo1.Cluster.Host = "127.0.0.1"
|
|
lo1.Cluster.Port = -1
|
|
l1 := RunServer(lo1)
|
|
defer l1.Shutdown()
|
|
|
|
lo2 := DefaultOptions()
|
|
lo2.LeafNode.Host = "127.0.0.1"
|
|
lo2.LeafNode.Port = -1
|
|
lo2.Cluster.Name = "local"
|
|
lo2.Cluster.Host = "127.0.0.1"
|
|
lo2.Cluster.Port = -1
|
|
lo2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo1.Cluster.Port))
|
|
l2 := RunServer(lo2)
|
|
defer l2.Shutdown()
|
|
|
|
checkClusterFormed(t, l1, l2)
|
|
|
|
ro1 := DefaultOptions()
|
|
ro1.Cluster.Name = "remote"
|
|
ro1.Cluster.Host = "127.0.0.1"
|
|
ro1.Cluster.Port = -1
|
|
ro1.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
ro1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{
|
|
{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", lo1.LeafNode.Port)},
|
|
{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", lo2.LeafNode.Port)},
|
|
}}}
|
|
r1 := RunServer(ro1)
|
|
defer r1.Shutdown()
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 100)}
|
|
r1.SetLogger(l, false, false)
|
|
|
|
ro2 := DefaultOptions()
|
|
ro2.Cluster.Name = "remote"
|
|
ro2.Cluster.Host = "127.0.0.1"
|
|
ro2.Cluster.Port = -1
|
|
ro2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", ro1.Cluster.Port))
|
|
ro2.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
ro2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{
|
|
{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", lo1.LeafNode.Port)},
|
|
{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", lo2.LeafNode.Port)},
|
|
}}}
|
|
r2 := RunServer(ro2)
|
|
defer r2.Shutdown()
|
|
|
|
checkClusterFormed(t, r1, r2)
|
|
checkLeafNodeConnected(t, r1)
|
|
checkLeafNodeConnected(t, r2)
|
|
|
|
l1.Shutdown()
|
|
|
|
// Now wait for r1 and r2 to reconnect, they should not have a problem of loop detection.
|
|
checkLeafNodeConnected(t, r1)
|
|
checkLeafNodeConnected(t, r2)
|
|
|
|
// Wait and make sure we don't have a loop error
|
|
timeout := time.NewTimer(500 * time.Millisecond)
|
|
for {
|
|
select {
|
|
case err := <-l.errCh:
|
|
if strings.Contains(err, "Loop detected") {
|
|
t.Fatal(err)
|
|
}
|
|
case <-timeout.C:
|
|
// OK, we are done.
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeUnsubOnRouteDisconnect(t *testing.T) {
|
|
lo1 := DefaultOptions()
|
|
lo1.LeafNode.Host = "127.0.0.1"
|
|
lo1.LeafNode.Port = -1
|
|
lo1.Cluster.Name = "local"
|
|
lo1.Cluster.Host = "127.0.0.1"
|
|
lo1.Cluster.Port = -1
|
|
l1 := RunServer(lo1)
|
|
defer l1.Shutdown()
|
|
|
|
lo2 := DefaultOptions()
|
|
lo2.LeafNode.Host = "127.0.0.1"
|
|
lo2.LeafNode.Port = -1
|
|
lo2.Cluster.Name = "local"
|
|
lo2.Cluster.Host = "127.0.0.1"
|
|
lo2.Cluster.Port = -1
|
|
lo2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo1.Cluster.Port))
|
|
l2 := RunServer(lo2)
|
|
defer l2.Shutdown()
|
|
|
|
checkClusterFormed(t, l1, l2)
|
|
|
|
u1, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", lo1.LeafNode.Port))
|
|
u2, _ := url.Parse(fmt.Sprintf("nats://127.0.0.1:%d", lo2.LeafNode.Port))
|
|
urls := []*url.URL{u1, u2}
|
|
|
|
ro1 := DefaultOptions()
|
|
// DefaultOptions sets a cluster name, so make sure they are different.
|
|
// Also, we don't have r1 and r2 clustered in this test, so set port to 0.
|
|
ro1.Cluster.Name = _EMPTY_
|
|
ro1.Cluster.Port = 0
|
|
ro1.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
ro1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: urls}}
|
|
r1 := RunServer(ro1)
|
|
defer r1.Shutdown()
|
|
|
|
ro2 := DefaultOptions()
|
|
ro1.Cluster.Name = _EMPTY_
|
|
ro2.Cluster.Port = 0
|
|
ro2.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
// Have this one point only to l2
|
|
ro2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{u2}}}
|
|
r2 := RunServer(ro2)
|
|
defer r2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, r1)
|
|
checkLeafNodeConnected(t, r2)
|
|
|
|
// Create a subscription on r1.
|
|
nc := natsConnect(t, r1.ClientURL())
|
|
defer nc.Close()
|
|
sub := natsSubSync(t, nc, "foo")
|
|
natsFlush(t, nc)
|
|
|
|
checkSubInterest(t, l2, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, r2, globalAccountName, "foo", time.Second)
|
|
|
|
nc2 := natsConnect(t, r2.ClientURL())
|
|
defer nc2.Close()
|
|
natsPub(t, nc, "foo", []byte("msg1"))
|
|
|
|
// Check message received
|
|
natsNexMsg(t, sub, time.Second)
|
|
|
|
// Now shutdown l1, l2 should update subscription interest to r2.
|
|
// When r1 reconnects to l2, subscription should be updated too.
|
|
l1.Shutdown()
|
|
|
|
// Wait a bit (so that the check of interest is not OK just because
|
|
// the route would not have been yet detected as broken), and check
|
|
// interest still present on r2, l2.
|
|
time.Sleep(100 * time.Millisecond)
|
|
checkSubInterest(t, l2, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, r2, globalAccountName, "foo", time.Second)
|
|
|
|
// Check again that message received ok
|
|
natsPub(t, nc, "foo", []byte("msg2"))
|
|
natsNexMsg(t, sub, time.Second)
|
|
|
|
// Now close client. Interest should disappear on r2. Due to a bug,
|
|
// it was not.
|
|
nc.Close()
|
|
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
acc := r2.GlobalAccount()
|
|
if n := acc.Interest("foo"); n != 0 {
|
|
return fmt.Errorf("Still interest on subject: %v", n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestLeafNodeNoPingBeforeConnect(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.AuthTimeout = 0.5
|
|
// For this test we need to disable compression, because we do use
|
|
// the ping timer instead of the auth timer before the negotiation
|
|
// is complete.
|
|
o.LeafNode.Compression.Mode = CompressionOff
|
|
s := RunServer(o)
|
|
defer s.Shutdown()
|
|
|
|
addr := fmt.Sprintf("127.0.0.1:%d", o.LeafNode.Port)
|
|
c, err := net.Dial("tcp", addr)
|
|
if err != nil {
|
|
t.Fatalf("Error on dial: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
// Read the info
|
|
br := bufio.NewReader(c)
|
|
c.SetReadDeadline(time.Now().Add(time.Second))
|
|
l, _, err := br.ReadLine()
|
|
if err != nil {
|
|
t.Fatalf("Error on read: %v", err)
|
|
}
|
|
if !strings.HasPrefix(string(l), "INFO") {
|
|
t.Fatalf("Wrong proto: %q", l)
|
|
}
|
|
|
|
var leaf *client
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
s.grMu.Lock()
|
|
for _, l := range s.grTmpClients {
|
|
leaf = l
|
|
break
|
|
}
|
|
s.grMu.Unlock()
|
|
if leaf == nil {
|
|
return fmt.Errorf("No leaf connection found")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Make sure that ping timer is not set
|
|
leaf.mu.Lock()
|
|
ptmrSet := leaf.ping.tmr != nil
|
|
leaf.mu.Unlock()
|
|
|
|
if ptmrSet {
|
|
t.Fatal("Ping timer was set before CONNECT was processed")
|
|
}
|
|
|
|
// Send CONNECT
|
|
if _, err := c.Write([]byte("CONNECT {}\r\n")); err != nil {
|
|
t.Fatalf("Error writing connect: %v", err)
|
|
}
|
|
|
|
// Check that we correctly set the timer now
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
leaf.mu.Lock()
|
|
ptmrSet := leaf.ping.tmr != nil
|
|
leaf.mu.Unlock()
|
|
if !ptmrSet {
|
|
return fmt.Errorf("Timer still not set")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Reduce the first ping..
|
|
leaf.mu.Lock()
|
|
leaf.ping.tmr.Reset(15 * time.Millisecond)
|
|
leaf.mu.Unlock()
|
|
|
|
// Now consume that PING (we may get LS+, etc..)
|
|
for {
|
|
c.SetReadDeadline(time.Now().Add(time.Second))
|
|
l, _, err = br.ReadLine()
|
|
if err != nil {
|
|
t.Fatalf("Error on read: %v", err)
|
|
}
|
|
if strings.HasPrefix(string(l), "PING") {
|
|
checkLeafNodeConnected(t, s)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeNoMsgLoop(t *testing.T) {
|
|
hubConf := `
|
|
listen: "127.0.0.1:-1"
|
|
accounts {
|
|
FOO {
|
|
users [
|
|
{username: leaf, password: pass}
|
|
{username: user, password: pass}
|
|
]
|
|
}
|
|
}
|
|
cluster {
|
|
name: "hub"
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
authorization {
|
|
account: FOO
|
|
}
|
|
}
|
|
`
|
|
configS1 := createConfFile(t, []byte(fmt.Sprintf(hubConf, "")))
|
|
s1, o1 := RunServerWithConfig(configS1)
|
|
defer s1.Shutdown()
|
|
|
|
configS2S3 := createConfFile(t, []byte(fmt.Sprintf(hubConf, fmt.Sprintf(`routes: ["nats://127.0.0.1:%d"]`, o1.Cluster.Port))))
|
|
s2, o2 := RunServerWithConfig(configS2S3)
|
|
defer s2.Shutdown()
|
|
|
|
s3, _ := RunServerWithConfig(configS2S3)
|
|
defer s3.Shutdown()
|
|
|
|
checkClusterFormed(t, s1, s2, s3)
|
|
|
|
contentLN := `
|
|
listen: "127.0.0.1:%d"
|
|
accounts {
|
|
FOO {
|
|
users [
|
|
{username: leaf, password: pass}
|
|
{username: user, password: pass}
|
|
]
|
|
}
|
|
}
|
|
leafnodes {
|
|
remotes = [
|
|
{
|
|
url: "nats://leaf:[email protected]:%d"
|
|
account: FOO
|
|
}
|
|
]
|
|
}
|
|
`
|
|
lnconf := createConfFile(t, []byte(fmt.Sprintf(contentLN, -1, o1.LeafNode.Port)))
|
|
sl1, slo1 := RunServerWithConfig(lnconf)
|
|
defer sl1.Shutdown()
|
|
|
|
sl2, slo2 := RunServerWithConfig(lnconf)
|
|
defer sl2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sl1)
|
|
checkLeafNodeConnected(t, sl2)
|
|
|
|
// Create users on each leafnode
|
|
nc1, err := nats.Connect(fmt.Sprintf("nats://user:[email protected]:%d", slo1.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error on connect: %v", err)
|
|
}
|
|
defer nc1.Close()
|
|
|
|
rch := make(chan struct{}, 1)
|
|
nc2, err := nats.Connect(
|
|
fmt.Sprintf("nats://user:[email protected]:%d", slo2.Port),
|
|
nats.ReconnectWait(50*time.Millisecond),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
rch <- struct{}{}
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Error on connect: %v", err)
|
|
}
|
|
defer nc2.Close()
|
|
|
|
// Create queue subs on sl2
|
|
nc2.QueueSubscribe("foo", "bar", func(_ *nats.Msg) {})
|
|
nc2.QueueSubscribe("foo", "bar", func(_ *nats.Msg) {})
|
|
nc2.Flush()
|
|
|
|
// Wait for interest to propagate to sl1
|
|
checkSubInterest(t, sl1, "FOO", "foo", 250*time.Millisecond)
|
|
|
|
// Create sub on sl1
|
|
ch := make(chan *nats.Msg, 10)
|
|
nc1.Subscribe("foo", func(m *nats.Msg) {
|
|
select {
|
|
case ch <- m:
|
|
default:
|
|
}
|
|
})
|
|
nc1.Flush()
|
|
|
|
checkSubInterest(t, sl2, "FOO", "foo", 250*time.Millisecond)
|
|
|
|
// Produce from sl1
|
|
nc1.Publish("foo", []byte("msg1"))
|
|
|
|
// Check message is received by plain sub
|
|
select {
|
|
case <-ch:
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("Did not receive message")
|
|
}
|
|
|
|
// Restart leaf node, this time make sure we connect to 2nd server.
|
|
sl2.Shutdown()
|
|
|
|
// Use config file but this time reuse the client port and set the 2nd server for
|
|
// the remote leaf node port.
|
|
lnconf = createConfFile(t, []byte(fmt.Sprintf(contentLN, slo2.Port, o2.LeafNode.Port)))
|
|
sl2, _ = RunServerWithConfig(lnconf)
|
|
defer sl2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sl2)
|
|
|
|
// Wait for client to reconnect
|
|
select {
|
|
case <-rch:
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("Did not reconnect")
|
|
}
|
|
|
|
// Produce a new messages
|
|
for i := 0; i < 10; i++ {
|
|
nc1.Publish("foo", []byte(fmt.Sprintf("msg%d", 2+i)))
|
|
|
|
// Check sub receives 1 message
|
|
select {
|
|
case <-ch:
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("Did not receive message")
|
|
}
|
|
// Check that there is no more...
|
|
select {
|
|
case m := <-ch:
|
|
t.Fatalf("Loop: received second message %s", m.Data)
|
|
case <-time.After(50 * time.Millisecond):
|
|
// OK
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeInterestPropagationDaisychain(t *testing.T) {
|
|
aTmpl := `
|
|
port: %d
|
|
leafnodes {
|
|
port: %d
|
|
}
|
|
`
|
|
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(aTmpl, -1, -1)))
|
|
sA, _ := RunServerWithConfig(confA)
|
|
defer sA.Shutdown()
|
|
|
|
aPort := sA.opts.Port
|
|
aLeafPort := sA.opts.LeafNode.Port
|
|
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
remotes = [{
|
|
url:"nats://127.0.0.1:%d"
|
|
}]
|
|
}`, aLeafPort)))
|
|
sB, _ := RunServerWithConfig(confB)
|
|
defer sB.Shutdown()
|
|
|
|
confC := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
remotes = [{url:"nats://127.0.0.1:%d"}]
|
|
}`, sB.opts.LeafNode.Port)))
|
|
sC, _ := RunServerWithConfig(confC)
|
|
defer sC.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, sC, 1)
|
|
checkLeafNodeConnectedCount(t, sB, 2)
|
|
checkLeafNodeConnectedCount(t, sA, 1)
|
|
|
|
ncC := natsConnect(t, sC.ClientURL())
|
|
defer ncC.Close()
|
|
_, err := ncC.SubscribeSync("foo")
|
|
require_NoError(t, err)
|
|
require_NoError(t, ncC.Flush())
|
|
|
|
checkSubInterest(t, sC, "$G", "foo", time.Second)
|
|
checkSubInterest(t, sB, "$G", "foo", time.Second)
|
|
checkSubInterest(t, sA, "$G", "foo", time.Second)
|
|
|
|
ncA := natsConnect(t, sA.ClientURL())
|
|
defer ncA.Close()
|
|
|
|
sA.Shutdown()
|
|
sA.WaitForShutdown()
|
|
|
|
confAA := createConfFile(t, []byte(fmt.Sprintf(aTmpl, aPort, aLeafPort)))
|
|
sAA, _ := RunServerWithConfig(confAA)
|
|
defer sAA.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, sAA, 1)
|
|
checkLeafNodeConnectedCount(t, sB, 2)
|
|
checkLeafNodeConnectedCount(t, sC, 1)
|
|
|
|
checkSubInterest(t, sC, "$G", "foo", time.Second)
|
|
checkSubInterest(t, sB, "$G", "foo", time.Second)
|
|
checkSubInterest(t, sAA, "$G", "foo", time.Second) // failure issue 2448
|
|
}
|
|
|
|
func TestLeafNodeQueueGroupDistribution(t *testing.T) {
|
|
hc := createClusterWithName(t, "HUB", 3)
|
|
defer hc.shutdown()
|
|
|
|
// Now have a cluster of leafnodes with each one connecting to corresponding HUB(n) node.
|
|
c1 := `
|
|
server_name: LEAF1
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1 }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf1 := createConfFile(t, []byte(fmt.Sprintf(c1, hc.opts[0].LeafNode.Port)))
|
|
ln1, lopts1 := RunServerWithConfig(lconf1)
|
|
defer ln1.Shutdown()
|
|
|
|
c2 := `
|
|
server_name: LEAF2
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1, routes = [ nats-route://127.0.0.1:%d] }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf2 := createConfFile(t, []byte(fmt.Sprintf(c2, lopts1.Cluster.Port, hc.opts[1].LeafNode.Port)))
|
|
ln2, _ := RunServerWithConfig(lconf2)
|
|
defer ln2.Shutdown()
|
|
|
|
c3 := `
|
|
server_name: LEAF3
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1, routes = [ nats-route://127.0.0.1:%d] }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf3 := createConfFile(t, []byte(fmt.Sprintf(c3, lopts1.Cluster.Port, hc.opts[2].LeafNode.Port)))
|
|
ln3, _ := RunServerWithConfig(lconf3)
|
|
defer ln3.Shutdown()
|
|
|
|
// Check leaf cluster is formed and all connected to the HUB.
|
|
lnServers := []*Server{ln1, ln2, ln3}
|
|
checkClusterFormed(t, lnServers...)
|
|
for _, s := range lnServers {
|
|
checkLeafNodeConnected(t, s)
|
|
}
|
|
// Check each node in the hub has 1 connection from the leaf cluster.
|
|
for i := 0; i < 3; i++ {
|
|
checkLeafNodeConnectedCount(t, hc.servers[i], 1)
|
|
}
|
|
|
|
// Create a client and qsub on LEAF1 and LEAF2.
|
|
nc1 := natsConnect(t, ln1.ClientURL())
|
|
defer nc1.Close()
|
|
var qsub1Count atomic.Int32
|
|
natsQueueSub(t, nc1, "foo", "queue1", func(_ *nats.Msg) {
|
|
qsub1Count.Add(1)
|
|
})
|
|
natsFlush(t, nc1)
|
|
|
|
nc2 := natsConnect(t, ln2.ClientURL())
|
|
defer nc2.Close()
|
|
var qsub2Count atomic.Int32
|
|
natsQueueSub(t, nc2, "foo", "queue1", func(_ *nats.Msg) {
|
|
qsub2Count.Add(1)
|
|
})
|
|
natsFlush(t, nc2)
|
|
|
|
// Make sure that the propagation interest is done before sending.
|
|
for _, s := range hc.servers {
|
|
gacc := s.GlobalAccount()
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if n := gacc.Interest("foo"); n != 2 {
|
|
return fmt.Errorf("Expected interest for %q to be 2, got %v", "foo", n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
sendAndCheck := func(idx int) {
|
|
t.Helper()
|
|
nchub := natsConnect(t, hc.servers[idx].ClientURL())
|
|
defer nchub.Close()
|
|
total := 1000
|
|
for i := 0; i < total; i++ {
|
|
natsPub(t, nchub, "foo", []byte("from hub"))
|
|
}
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if trecv := int(qsub1Count.Load() + qsub2Count.Load()); trecv != total {
|
|
return fmt.Errorf("Expected %v messages, got %v", total, trecv)
|
|
}
|
|
return nil
|
|
})
|
|
// Now that we have made sure that all messages were received,
|
|
// check that qsub1 and qsub2 are getting at least some.
|
|
if n := int(qsub1Count.Load()); n <= total/10 {
|
|
t.Fatalf("Expected qsub1 to get some messages, but got %v", n)
|
|
}
|
|
if n := int(qsub2Count.Load()); n <= total/10 {
|
|
t.Fatalf("Expected qsub2 to get some messages, but got %v", n)
|
|
}
|
|
// Reset the counters.
|
|
qsub1Count.Store(0)
|
|
qsub2Count.Store(0)
|
|
}
|
|
// Send from HUB1
|
|
sendAndCheck(0)
|
|
// Send from HUB2
|
|
sendAndCheck(1)
|
|
// Send from HUB3
|
|
sendAndCheck(2)
|
|
}
|
|
|
|
func TestLeafNodeQueueGroupDistributionVariant(t *testing.T) {
|
|
hc := createClusterWithName(t, "HUB", 3)
|
|
defer hc.shutdown()
|
|
|
|
// Now have a cluster of leafnodes with LEAF1 and LEAF2 connecting to HUB1.
|
|
c1 := `
|
|
server_name: LEAF1
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1 }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf1 := createConfFile(t, []byte(fmt.Sprintf(c1, hc.opts[0].LeafNode.Port)))
|
|
ln1, lopts1 := RunServerWithConfig(lconf1)
|
|
defer ln1.Shutdown()
|
|
|
|
c2 := `
|
|
server_name: LEAF2
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1, routes = [ nats-route://127.0.0.1:%d] }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf2 := createConfFile(t, []byte(fmt.Sprintf(c2, lopts1.Cluster.Port, hc.opts[0].LeafNode.Port)))
|
|
ln2, _ := RunServerWithConfig(lconf2)
|
|
defer ln2.Shutdown()
|
|
|
|
// And LEAF3 to HUB3
|
|
c3 := `
|
|
server_name: LEAF3
|
|
listen: 127.0.0.1:-1
|
|
cluster { name: ln22, listen: 127.0.0.1:-1, routes = [ nats-route://127.0.0.1:%d] }
|
|
leafnodes { remotes = [{ url: nats-leaf://127.0.0.1:%d }] }
|
|
`
|
|
lconf3 := createConfFile(t, []byte(fmt.Sprintf(c3, lopts1.Cluster.Port, hc.opts[2].LeafNode.Port)))
|
|
ln3, _ := RunServerWithConfig(lconf3)
|
|
defer ln3.Shutdown()
|
|
|
|
// Check leaf cluster is formed and all connected to the HUB.
|
|
lnServers := []*Server{ln1, ln2, ln3}
|
|
checkClusterFormed(t, lnServers...)
|
|
for _, s := range lnServers {
|
|
checkLeafNodeConnected(t, s)
|
|
}
|
|
// Check that HUB1 has 2 leaf connections, HUB2 has 0 and HUB3 has 1.
|
|
checkLeafNodeConnectedCount(t, hc.servers[0], 2)
|
|
checkLeafNodeConnectedCount(t, hc.servers[1], 0)
|
|
checkLeafNodeConnectedCount(t, hc.servers[2], 1)
|
|
|
|
// Create a client and qsub on LEAF1 and LEAF2.
|
|
nc1 := natsConnect(t, ln1.ClientURL())
|
|
defer nc1.Close()
|
|
var qsub1Count atomic.Int32
|
|
natsQueueSub(t, nc1, "foo", "queue1", func(_ *nats.Msg) {
|
|
qsub1Count.Add(1)
|
|
})
|
|
natsFlush(t, nc1)
|
|
|
|
nc2 := natsConnect(t, ln2.ClientURL())
|
|
defer nc2.Close()
|
|
var qsub2Count atomic.Int32
|
|
natsQueueSub(t, nc2, "foo", "queue1", func(_ *nats.Msg) {
|
|
qsub2Count.Add(1)
|
|
})
|
|
natsFlush(t, nc2)
|
|
|
|
// Make sure that the propagation interest is done before sending.
|
|
for i, s := range hc.servers {
|
|
gacc := s.GlobalAccount()
|
|
var ei int
|
|
switch i {
|
|
case 0:
|
|
ei = 2
|
|
default:
|
|
ei = 1
|
|
}
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if n := gacc.Interest("foo"); n != ei {
|
|
return fmt.Errorf("Expected interest for %q to be %d, got %v", "foo", ei, n)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
sendAndCheck := func(idx int) {
|
|
t.Helper()
|
|
nchub := natsConnect(t, hc.servers[idx].ClientURL())
|
|
defer nchub.Close()
|
|
total := 1000
|
|
for i := 0; i < total; i++ {
|
|
natsPub(t, nchub, "foo", []byte("from hub"))
|
|
}
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
if trecv := int(qsub1Count.Load() + qsub2Count.Load()); trecv != total {
|
|
return fmt.Errorf("Expected %v messages, got %v", total, trecv)
|
|
}
|
|
return nil
|
|
})
|
|
// Now that we have made sure that all messages were received,
|
|
// check that qsub1 and qsub2 are getting at least some.
|
|
if n := int(qsub1Count.Load()); n <= total/10 {
|
|
t.Fatalf("Expected qsub1 to get some messages, but got %v (qsub2=%v)", n, qsub2Count.Load())
|
|
}
|
|
if n := int(qsub2Count.Load()); n <= total/10 {
|
|
t.Fatalf("Expected qsub2 to get some messages, but got %v (qsub1=%v)", n, qsub1Count.Load())
|
|
}
|
|
// Reset the counters.
|
|
qsub1Count.Store(0)
|
|
qsub2Count.Store(0)
|
|
}
|
|
// Send from HUB1
|
|
sendAndCheck(0)
|
|
// Send from HUB2
|
|
sendAndCheck(1)
|
|
// Send from HUB3
|
|
sendAndCheck(2)
|
|
}
|
|
|
|
func TestLeafNodeQueueGroupDistributionWithDaisyChainAndGateway(t *testing.T) {
|
|
SetGatewaysSolicitDelay(0)
|
|
defer ResetGatewaysSolicitDelay()
|
|
|
|
// We create a sort of a ladder of servers with connections that look like this:
|
|
//
|
|
// D1 <--- route ---> D2
|
|
// | |
|
|
// GW GW
|
|
// | |
|
|
// C1 <--- route ---> C2
|
|
// | |
|
|
// Leaf Leaf
|
|
// | |
|
|
// B1 <--- route ---> B2
|
|
// | |
|
|
// Leaf Leaf
|
|
// | |
|
|
// A1 <--- route ---> A2
|
|
//
|
|
// We will then place queue subscriptions (different sub-tests) on A1, A2
|
|
// B1, B2, D1 and D2.
|
|
|
|
accs := `
|
|
accounts {
|
|
SYS: {users: [{user:sys, password: pwd}]}
|
|
USER: {users: [{user:user, password: pwd}]}
|
|
}
|
|
system_account: SYS
|
|
`
|
|
dConf := `
|
|
%s
|
|
server_name: %s
|
|
port: -1
|
|
cluster {
|
|
name: "D"
|
|
port: -1
|
|
%s
|
|
}
|
|
gateway {
|
|
name: "D"
|
|
port: -1
|
|
}
|
|
`
|
|
d1Conf := createConfFile(t, []byte(fmt.Sprintf(dConf, accs, "GW1", _EMPTY_)))
|
|
d1, d1Opts := RunServerWithConfig(d1Conf)
|
|
defer d1.Shutdown()
|
|
|
|
d2Conf := createConfFile(t, []byte(fmt.Sprintf(dConf, accs, "GW2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", d1Opts.Cluster.Port))))
|
|
d2, d2Opts := RunServerWithConfig(d2Conf)
|
|
defer d2.Shutdown()
|
|
|
|
checkClusterFormed(t, d1, d2)
|
|
|
|
leafCConf := `
|
|
%s
|
|
server_name: %s
|
|
port: -1
|
|
cluster {
|
|
name: C
|
|
port: -1
|
|
%s
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
gateway {
|
|
name: C
|
|
port: -1
|
|
gateways [
|
|
{
|
|
name: D
|
|
url: "nats://127.0.0.1:%d"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
c1Conf := createConfFile(t, []byte(fmt.Sprintf(leafCConf, accs, "C1", _EMPTY_, d1Opts.Gateway.Port)))
|
|
c1, c1Opts := RunServerWithConfig(c1Conf)
|
|
defer c1.Shutdown()
|
|
|
|
waitForOutboundGateways(t, c1, 1, time.Second)
|
|
waitForInboundGateways(t, d1, 1, time.Second)
|
|
|
|
c2Conf := createConfFile(t, []byte(fmt.Sprintf(leafCConf, accs, "C2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", c1Opts.Cluster.Port), d2Opts.Gateway.Port)))
|
|
c2, c2Opts := RunServerWithConfig(c2Conf)
|
|
defer c2.Shutdown()
|
|
|
|
waitForOutboundGateways(t, c2, 1, time.Second)
|
|
waitForInboundGateways(t, d2, 1, time.Second)
|
|
|
|
checkClusterFormed(t, c1, c2)
|
|
|
|
leafABConf := `
|
|
%s
|
|
server_name: %s
|
|
port: -1
|
|
cluster {
|
|
name: %s
|
|
port: -1
|
|
%s
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
b1Conf := createConfFile(t, []byte(fmt.Sprintf(leafABConf, accs, "B1", "B", _EMPTY_, c1Opts.LeafNode.Port)))
|
|
b1, b1Opts := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b1)
|
|
checkLeafNodeConnected(t, c1)
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(leafABConf, accs, "B2", "B",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port), c2Opts.LeafNode.Port)))
|
|
b2, b2Opts := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, c2)
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
a1Conf := createConfFile(t, []byte(fmt.Sprintf(leafABConf, accs, "A1", "A", _EMPTY_, b1Opts.LeafNode.Port)))
|
|
a1, a1Opts := RunServerWithConfig(a1Conf)
|
|
defer a1.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, b1, 2)
|
|
checkLeafNodeConnected(t, a1)
|
|
|
|
a2Conf := createConfFile(t, []byte(fmt.Sprintf(leafABConf, accs, "A2", "A",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", a1Opts.Cluster.Port), b2Opts.LeafNode.Port)))
|
|
a2, _ := RunServerWithConfig(a2Conf)
|
|
defer a2.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, b2, 2)
|
|
checkLeafNodeConnected(t, a2)
|
|
checkClusterFormed(t, a1, a2)
|
|
|
|
// Create our client connections to all servers where we may need to have
|
|
// queue subscriptions.
|
|
ncD1 := natsConnect(t, d1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncD1.Close()
|
|
ncD2 := natsConnect(t, d2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncD2.Close()
|
|
ncB1 := natsConnect(t, b1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB1.Close()
|
|
ncB2 := natsConnect(t, b2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB2.Close()
|
|
ncA1 := natsConnect(t, a1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA1.Close()
|
|
ncA2 := natsConnect(t, a2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA2.Close()
|
|
|
|
// Helper to check that the interest is propagated to all servers
|
|
checkInterest := func(t *testing.T, subj string) {
|
|
t.Helper()
|
|
for _, s := range []*Server{a1, a2, b1, b2, c1, c2, d1, d2} {
|
|
acc, err := s.LookupAccount("USER")
|
|
require_NoError(t, err)
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
if acc.Interest(subj) != 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Still no interest on %q in server %q", subj, s)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper to send messages on given subject. We are always sending
|
|
// from cluster B in this test, but we pick randomly between B1 and B2.
|
|
total := 1000
|
|
send := func(t *testing.T, subj string) {
|
|
for i := 0; i < total; i++ {
|
|
var nc *nats.Conn
|
|
if fastrand.Uint32n(2) == 0 {
|
|
nc = ncB1
|
|
} else {
|
|
nc = ncB2
|
|
}
|
|
natsPub(t, nc, subj, []byte(fmt.Sprintf("msg_%d", i+1)))
|
|
}
|
|
}
|
|
|
|
const queue = "queue"
|
|
|
|
for i, test := range []struct {
|
|
name string
|
|
a1 bool
|
|
a2 bool
|
|
b1 bool
|
|
b2 bool
|
|
d1 bool
|
|
d2 bool
|
|
}{
|
|
// Cases with QSubs in A, B and D
|
|
{"A1 __ B1 __ D1 __", true, false, true, false, true, false},
|
|
{"A1 __ B1 __ __ D2", true, false, true, false, false, true},
|
|
{"A1 __ B1 __ D1 D2", true, false, true, false, true, true},
|
|
|
|
{"A1 __ __ B2 D1 __", true, false, false, true, true, false},
|
|
{"A1 __ __ B2 __ D2", true, false, false, true, false, true},
|
|
{"A1 __ __ B2 D1 D2", true, false, false, true, true, true},
|
|
|
|
{"A1 __ B1 B2 D1 __", true, false, true, true, true, false},
|
|
{"A1 __ B1 B2 __ D2", true, false, true, true, false, true},
|
|
{"A1 __ B1 B2 D1 D2", true, false, true, true, true, true},
|
|
|
|
{"__ A2 B1 __ D1 __", false, true, true, false, true, false},
|
|
{"__ A2 B1 __ __ D2", false, true, true, false, false, true},
|
|
{"__ A2 B1 __ D1 D2", false, true, true, false, true, true},
|
|
|
|
{"__ A2 __ B2 D1 __", false, true, false, true, true, false},
|
|
{"__ A2 __ B2 __ D2", false, true, false, true, false, true},
|
|
{"__ A2 __ B2 D1 D2", false, true, false, true, true, true},
|
|
|
|
{"__ A2 B1 B2 D1 __", false, true, true, true, true, false},
|
|
{"__ A2 B1 B2 __ D2", false, true, true, true, false, true},
|
|
{"__ A2 B1 B2 D1 D2", false, true, true, true, true, true},
|
|
|
|
{"A1 A2 B1 __ D1 __", true, true, true, false, true, false},
|
|
{"A1 A2 B1 __ __ D2", true, true, true, false, false, true},
|
|
{"A1 A2 B1 __ D1 D2", true, true, true, false, true, true},
|
|
|
|
{"A1 A2 __ B2 D1 __", true, true, false, true, true, false},
|
|
{"A1 A2 __ B2 __ D2", true, true, false, true, false, true},
|
|
{"A1 A2 __ B2 D1 D2", true, true, false, true, true, true},
|
|
|
|
{"A1 A2 B1 B2 D1 __", true, true, true, true, true, false},
|
|
{"A1 A2 B1 B2 __ D2", true, true, true, true, false, true},
|
|
{"A1 A2 B1 B2 D1 D2", true, true, true, true, true, true},
|
|
|
|
// Now without any QSub in B cluster (so just A and D)
|
|
{"A1 __ __ __ D1 __", true, false, false, false, true, false},
|
|
{"A1 __ __ __ __ D2", true, false, false, false, false, true},
|
|
{"A1 __ __ __ D1 D2", true, false, false, false, true, true},
|
|
|
|
{"__ A2 __ __ D1 __", false, true, false, false, true, false},
|
|
{"__ A2 __ __ __ D2", false, true, false, false, false, true},
|
|
{"__ A2 __ __ D1 D2", false, true, false, false, true, true},
|
|
|
|
{"A1 A2 __ __ D1 __", true, true, false, false, true, false},
|
|
{"A1 A2 __ __ __ D2", true, true, false, false, false, true},
|
|
{"A1 A2 __ __ D1 D2", true, true, false, false, true, true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
subj := fmt.Sprintf("foo.%d", i+1)
|
|
var aCount, bCount, dCount atomic.Int32
|
|
if test.a1 {
|
|
qsA1 := natsQueueSub(t, ncA1, subj, queue, func(_ *nats.Msg) {
|
|
aCount.Add(1)
|
|
})
|
|
defer qsA1.Unsubscribe()
|
|
}
|
|
if test.a2 {
|
|
qsA2 := natsQueueSub(t, ncA2, subj, queue, func(_ *nats.Msg) {
|
|
aCount.Add(1)
|
|
})
|
|
defer qsA2.Unsubscribe()
|
|
}
|
|
if test.b1 {
|
|
qsB1 := natsQueueSub(t, ncB1, subj, queue, func(_ *nats.Msg) {
|
|
bCount.Add(1)
|
|
})
|
|
defer qsB1.Unsubscribe()
|
|
}
|
|
if test.b2 {
|
|
qsB2 := natsQueueSub(t, ncB2, subj, queue, func(_ *nats.Msg) {
|
|
bCount.Add(1)
|
|
})
|
|
defer qsB2.Unsubscribe()
|
|
}
|
|
if test.d1 {
|
|
qsD1 := natsQueueSub(t, ncD1, subj, queue, func(_ *nats.Msg) {
|
|
dCount.Add(1)
|
|
})
|
|
defer qsD1.Unsubscribe()
|
|
}
|
|
if test.d2 {
|
|
qsD2 := natsQueueSub(t, ncD2, subj, queue, func(_ *nats.Msg) {
|
|
dCount.Add(1)
|
|
})
|
|
defer qsD2.Unsubscribe()
|
|
}
|
|
checkInterest(t, subj)
|
|
|
|
// Now send messages
|
|
send(t, subj)
|
|
|
|
// Check that appropriate queue subs receive all messages.
|
|
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
|
|
n := aCount.Load() + bCount.Load() + dCount.Load()
|
|
if n == int32(total) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Got only %v/%v messages (a=%v b=%v d=%v)", n, total, aCount.Load(), bCount.Load(), dCount.Load())
|
|
})
|
|
// When there is (are) qsub(s) on b, then only B should
|
|
// get the messages. Otherwise, it should be between A and D
|
|
if test.b1 || test.b2 {
|
|
require_Equal(t, aCount.Load(), 0)
|
|
require_Equal(t, dCount.Load(), 0)
|
|
} else {
|
|
require_Equal(t, bCount.Load(), 0)
|
|
// We should have receive some on A and D
|
|
require_True(t, aCount.Load() > 0)
|
|
require_True(t, dCount.Load() > 0)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeAndGatewaysSingleMsgPerQueueGroup(t *testing.T) {
|
|
SetGatewaysSolicitDelay(0)
|
|
defer ResetGatewaysSolicitDelay()
|
|
|
|
accs := `
|
|
accounts {
|
|
SYS: {users: [{user:sys, password: pwd}]}
|
|
USER: {users: [{user:user, password: pwd}]}
|
|
}
|
|
system_account: SYS
|
|
`
|
|
gwUSConfTmpl := `
|
|
%s
|
|
server_name: GW_US
|
|
listen: "127.0.0.1:-1"
|
|
gateway {
|
|
name: US
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
gwUSConf := createConfFile(t, []byte(fmt.Sprintf(gwUSConfTmpl, accs)))
|
|
gwUS, gwUSOpts := RunServerWithConfig(gwUSConf)
|
|
defer gwUS.Shutdown()
|
|
|
|
gwEUConfTmpl := `
|
|
%s
|
|
server_name: GW_EU
|
|
listen: "127.0.0.1:-1"
|
|
gateway {
|
|
name: EU
|
|
listen: "127.0.0.1:-1"
|
|
gateways [
|
|
{
|
|
name: US
|
|
url: "nats://127.0.0.1:%d"
|
|
}
|
|
]
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
gwEUConf := createConfFile(t, []byte(fmt.Sprintf(gwEUConfTmpl, accs, gwUSOpts.Gateway.Port)))
|
|
gwEU, gwEUOpts := RunServerWithConfig(gwEUConf)
|
|
defer gwEU.Shutdown()
|
|
|
|
waitForOutboundGateways(t, gwUS, 1, time.Second)
|
|
waitForOutboundGateways(t, gwEU, 1, time.Second)
|
|
waitForInboundGateways(t, gwUS, 1, time.Second)
|
|
waitForInboundGateways(t, gwEU, 1, time.Second)
|
|
|
|
leafConfTmpl := `
|
|
%s
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
leafUSConf := createConfFile(t, []byte(fmt.Sprintf(leafConfTmpl, accs, "LEAF_US", gwUSOpts.LeafNode.Port)))
|
|
leafUS, _ := RunServerWithConfig(leafUSConf)
|
|
defer leafUS.Shutdown()
|
|
checkLeafNodeConnected(t, leafUS)
|
|
|
|
leafEUConf := createConfFile(t, []byte(fmt.Sprintf(leafConfTmpl, accs, "LEAF_EU", gwEUOpts.LeafNode.Port)))
|
|
leafEU, _ := RunServerWithConfig(leafEUConf)
|
|
defer leafEU.Shutdown()
|
|
checkLeafNodeConnected(t, leafEU)
|
|
|
|
// Order is important! (see rest of test to understand why)
|
|
var usLeafQ, usLeafPS, usGWQ, usGWPS, euGWQ, euGWPS, euLeafQ, euLeafPS, euLeafQBaz atomic.Int32
|
|
counters := []*atomic.Int32{&usLeafQ, &usLeafPS, &usGWQ, &usGWPS, &euGWQ, &euGWPS, &euLeafQ, &euLeafPS, &euLeafQBaz}
|
|
counterNames := []string{"usLeafQ", "usLeafPS", "usGWQ", "usGWPS", "euGWQ", "euGWPS", "euLeafQ", "euLeafPS", "euLeafQBaz"}
|
|
if len(counters) != len(counterNames) {
|
|
panic("Fix test!")
|
|
}
|
|
resetCounters := func() {
|
|
for _, a := range counters {
|
|
a.Store(0)
|
|
}
|
|
}
|
|
|
|
// This test will always produce from the US leaf.
|
|
ncProd := natsConnect(t, leafUS.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncProd.Close()
|
|
|
|
total := int32(1)
|
|
check := func(t *testing.T, expected []int32) {
|
|
time.Sleep(50 * time.Millisecond)
|
|
resetCounters()
|
|
for i := 0; i < int(total); i++ {
|
|
natsPub(t, ncProd, "foo.1", []byte("hello"))
|
|
}
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
for i := 0; i < len(expected); i++ {
|
|
if n := counters[i].Load(); n != expected[i] {
|
|
return fmt.Errorf("Expected counter %q to be %v, got %v", counterNames[i], expected[i], n)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// "usLeafQ", "usLeafPS", "usGWQ", "usGWPS", "euGWQ", "euGWPS", "euLeafQ", "euLeafPS", "euLeafQBaz"
|
|
for _, test := range []struct {
|
|
subs []int
|
|
expected []int32
|
|
}{
|
|
// We will always have the qsub on leaf EU, and have some permutations
|
|
// of queue and plain subs on other server(s) and check we get the
|
|
// expected distribution.
|
|
|
|
// Simple test firs, qsubs on leaf US and leaf EU, all messages stay in leaf US.
|
|
{
|
|
[]int{1, 0, 0, 0, 0, 0, 1, 0, 0},
|
|
[]int32{total, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
// Move the queue sub from leaf US to GW US.
|
|
{
|
|
[]int{0, 0, 1, 0, 0, 0, 1, 0, 0},
|
|
[]int32{0, 0, total, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
// Now move it to GW EU.
|
|
{
|
|
[]int{0, 0, 0, 0, 1, 0, 1, 0, 0},
|
|
[]int32{0, 0, 0, 0, total, 0, 0, 0, 0},
|
|
},
|
|
|
|
// More combinations...
|
|
{
|
|
[]int{1, 1, 0, 0, 0, 0, 1, 0, 0},
|
|
[]int32{total, total, 0, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{0, 1, 1, 0, 0, 0, 1, 0, 0},
|
|
[]int32{0, total, total, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{0, 1, 1, 1, 0, 0, 1, 0, 0},
|
|
[]int32{0, total, total, total, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{0, 1, 0, 1, 1, 0, 1, 0, 0},
|
|
[]int32{0, total, 0, total, total, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{0, 1, 0, 1, 1, 1, 1, 0, 0},
|
|
[]int32{0, total, 0, total, total, total, 0, 0, 0},
|
|
},
|
|
// If we have the qsub in leaf US, does not matter if we have
|
|
// qsubs in GW US and EU, only leaf US should receive the messages,
|
|
// but plain sub in GW servers should get them too.
|
|
{
|
|
[]int{1, 1, 1, 0, 0, 0, 1, 0, 0},
|
|
[]int32{total, total, 0, 0, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 0, 0, 1, 0, 0},
|
|
[]int32{total, total, 0, total, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 1, 0, 1, 0, 0},
|
|
[]int32{total, total, 0, total, 0, 0, 0, 0, 0},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 1, 1, 1, 0, 0},
|
|
[]int32{total, total, 0, total, 0, total, 0, 0, 0},
|
|
},
|
|
// Now back to a qsub on leaf US and leaf EU, but introduce plain sub
|
|
// interest in leaf EU
|
|
{
|
|
[]int{1, 0, 0, 0, 0, 0, 1, 1, 0},
|
|
[]int32{total, 0, 0, 0, 0, 0, 0, total, 0},
|
|
},
|
|
// And add a different queue group in leaf EU and it should get the messages too.
|
|
{
|
|
[]int{1, 0, 0, 0, 0, 0, 1, 1, 1},
|
|
[]int32{total, 0, 0, 0, 0, 0, 0, total, total},
|
|
},
|
|
// Keep plain and baz queue sub interests in leaf EU and add more combinations.
|
|
{
|
|
[]int{1, 1, 0, 0, 0, 0, 1, 1, 1},
|
|
[]int32{total, total, 0, 0, 0, 0, 0, total, total},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 0, 0, 0, 1, 1, 1},
|
|
[]int32{total, total, 0, 0, 0, 0, 0, total, total},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 0, 0, 1, 1, 1},
|
|
[]int32{total, total, 0, total, 0, 0, 0, total, total},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 1, 0, 1, 1, 1},
|
|
[]int32{total, total, 0, total, 0, 0, 0, total, total},
|
|
},
|
|
{
|
|
[]int{1, 1, 1, 1, 1, 1, 1, 1, 1},
|
|
[]int32{total, total, 0, total, 0, total, 0, total, total},
|
|
},
|
|
} {
|
|
t.Run(_EMPTY_, func(t *testing.T) {
|
|
if len(test.subs) != len(counters) || len(test.expected) != len(counters) {
|
|
panic("Fix test")
|
|
}
|
|
|
|
ncUS := natsConnect(t, leafUS.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncUS.Close()
|
|
|
|
ncGWUS := natsConnect(t, gwUS.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncGWUS.Close()
|
|
|
|
ncGWEU := natsConnect(t, gwEU.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncGWEU.Close()
|
|
|
|
ncEU := natsConnect(t, leafEU.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncEU.Close()
|
|
|
|
if test.subs[0] > 0 {
|
|
natsQueueSub(t, ncUS, "foo.*", "bar", func(_ *nats.Msg) {
|
|
usLeafQ.Add(1)
|
|
})
|
|
}
|
|
if test.subs[1] > 0 {
|
|
natsSub(t, ncUS, "foo.>", func(_ *nats.Msg) {
|
|
usLeafPS.Add(1)
|
|
})
|
|
}
|
|
natsFlush(t, ncUS)
|
|
if test.subs[2] > 0 {
|
|
natsQueueSub(t, ncGWUS, "foo.*", "bar", func(_ *nats.Msg) {
|
|
usGWQ.Add(1)
|
|
})
|
|
}
|
|
if test.subs[3] > 0 {
|
|
natsSub(t, ncGWUS, "foo.>", func(_ *nats.Msg) {
|
|
usGWPS.Add(1)
|
|
})
|
|
}
|
|
natsFlush(t, ncGWUS)
|
|
if test.subs[4] > 0 {
|
|
natsQueueSub(t, ncGWEU, "foo.*", "bar", func(_ *nats.Msg) {
|
|
euGWQ.Add(1)
|
|
})
|
|
}
|
|
if test.subs[5] > 0 {
|
|
natsSub(t, ncGWEU, "foo.>", func(_ *nats.Msg) {
|
|
euGWPS.Add(1)
|
|
})
|
|
}
|
|
natsFlush(t, ncGWEU)
|
|
if test.subs[6] > 0 {
|
|
natsQueueSub(t, ncEU, "foo.*", "bar", func(_ *nats.Msg) {
|
|
euLeafQ.Add(1)
|
|
})
|
|
}
|
|
if test.subs[7] > 0 {
|
|
natsSub(t, ncEU, "foo.>", func(_ *nats.Msg) {
|
|
euLeafPS.Add(1)
|
|
})
|
|
}
|
|
if test.subs[8] > 0 {
|
|
// Create on different group, called "baz"
|
|
natsQueueSub(t, ncEU, "foo.*", "baz", func(_ *nats.Msg) {
|
|
euLeafQBaz.Add(1)
|
|
})
|
|
}
|
|
natsFlush(t, ncEU)
|
|
|
|
// Check that we have what we expect.
|
|
check(t, test.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeQueueGroupWeightCorrectOnConnectionCloseInSuperCluster(t *testing.T) {
|
|
SetGatewaysSolicitDelay(0)
|
|
defer ResetGatewaysSolicitDelay()
|
|
|
|
//
|
|
// D
|
|
// |
|
|
// Leaf
|
|
// |
|
|
// v
|
|
// C
|
|
// ^ ^
|
|
// / \
|
|
// GW GW
|
|
// / \
|
|
// v \
|
|
// B1 <--- route ---> B2 <----*----------*
|
|
// ^ <---* | |
|
|
// | | Leaf Leaf
|
|
// Leaf *-- Leaf ---* | |
|
|
// | | | |
|
|
// A1 <--- route ---> A2 OTHER1 OTHER2
|
|
//
|
|
|
|
accs := `
|
|
accounts {
|
|
SYS: {users: [{user:sys, password: pwd}]}
|
|
USER: {users: [{user:user, password: pwd}]}
|
|
}
|
|
system_account: SYS
|
|
`
|
|
bConf := `
|
|
%s
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: "B"
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
%s
|
|
}
|
|
gateway {
|
|
name: "B"
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
sb1Conf := createConfFile(t, []byte(fmt.Sprintf(bConf, accs, "B1", _EMPTY_)))
|
|
sb1, sb1o := RunServerWithConfig(sb1Conf)
|
|
defer sb1.Shutdown()
|
|
|
|
sb2Conf := createConfFile(t, []byte(fmt.Sprintf(bConf, accs, "B2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", sb1o.Cluster.Port))))
|
|
sb2, sb2o := RunServerWithConfig(sb2Conf)
|
|
defer sb2.Shutdown()
|
|
|
|
checkClusterFormed(t, sb1, sb2)
|
|
|
|
cConf := `
|
|
%s
|
|
server_name: C
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: "C"
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
gateway {
|
|
name: "C"
|
|
listen: "127.0.0.1:-1"
|
|
gateways [
|
|
{
|
|
name: B
|
|
url: "nats://127.0.0.1:%d"
|
|
}
|
|
]
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
scConf := createConfFile(t, []byte(fmt.Sprintf(cConf, accs, sb1o.Gateway.Port)))
|
|
sc, sco := RunServerWithConfig(scConf)
|
|
defer sc.Shutdown()
|
|
|
|
waitForOutboundGateways(t, sc, 1, 2*time.Second)
|
|
waitForOutboundGateways(t, sb1, 1, 2*time.Second)
|
|
waitForOutboundGateways(t, sb2, 1, 2*time.Second)
|
|
waitForInboundGateways(t, sc, 2, 2*time.Second)
|
|
waitForInboundGateways(t, sb1, 1, 2*time.Second)
|
|
|
|
dConf := `
|
|
%s
|
|
server_name: D
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: "D"
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
sdConf := createConfFile(t, []byte(fmt.Sprintf(dConf, accs, sco.LeafNode.Port)))
|
|
sd, _ := RunServerWithConfig(sdConf)
|
|
defer sd.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sc)
|
|
checkLeafNodeConnected(t, sd)
|
|
|
|
aConf := `
|
|
%s
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: A
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
%s
|
|
}
|
|
leafnodes {
|
|
reconnect: "10ms"
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
a1Conf := createConfFile(t, []byte(fmt.Sprintf(aConf, accs, "A1", _EMPTY_, sb1o.LeafNode.Port)))
|
|
sa1, sa1o := RunServerWithConfig(a1Conf)
|
|
defer sa1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sa1)
|
|
checkLeafNodeConnected(t, sb1)
|
|
|
|
a2Conf := createConfFile(t, []byte(fmt.Sprintf(aConf, accs, "A2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", sa1o.Cluster.Port), sb1o.LeafNode.Port)))
|
|
sa2, _ := RunServerWithConfig(a2Conf)
|
|
defer sa2.Shutdown()
|
|
|
|
checkClusterFormed(t, sa1, sa2)
|
|
checkLeafNodeConnected(t, sa2)
|
|
checkLeafNodeConnectedCount(t, sb1, 2)
|
|
|
|
otherLeafsConf := `
|
|
%s
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://user:[email protected]:%d"
|
|
account: USER
|
|
}
|
|
]
|
|
}
|
|
`
|
|
o1Conf := createConfFile(t, []byte(fmt.Sprintf(otherLeafsConf, accs, "OTHERLEAF1", sb2o.LeafNode.Port)))
|
|
so1, _ := RunServerWithConfig(o1Conf)
|
|
defer so1.Shutdown()
|
|
checkLeafNodeConnected(t, so1)
|
|
checkLeafNodeConnectedCount(t, sb2, 1)
|
|
|
|
o2Conf := createConfFile(t, []byte(fmt.Sprintf(otherLeafsConf, accs, "OTHERLEAF2", sb2o.LeafNode.Port)))
|
|
so2, _ := RunServerWithConfig(o2Conf)
|
|
defer so2.Shutdown()
|
|
checkLeafNodeConnected(t, so2)
|
|
checkLeafNodeConnectedCount(t, sb2, 2)
|
|
|
|
// Helper to check that the interest is propagated to all servers
|
|
checkInterest := func(t *testing.T, expected []int, expectedGW int32) {
|
|
t.Helper()
|
|
subj := "foo"
|
|
for i, s := range []*Server{sa1, sa2, so1, so2, sb1, sb2, sc, sd} {
|
|
if s == sc || !s.isRunning() {
|
|
continue
|
|
}
|
|
acc, err := s.LookupAccount("USER")
|
|
require_NoError(t, err)
|
|
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
|
|
n := acc.Interest(subj)
|
|
if n == expected[i] {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Expected interest count for server %q to be %v, got %v", s, expected[i], n)
|
|
})
|
|
}
|
|
// For server C, need to check in gateway's account.
|
|
checkForRegisteredQSubInterest(t, sc, "B", "USER", "foo", expected[6], time.Second)
|
|
|
|
// For server B1 and B2, check that we have the proper counts in the map.
|
|
for _, s := range []*Server{sb1, sb2} {
|
|
if !s.isRunning() {
|
|
continue
|
|
}
|
|
checkFor(t, 2*time.Second, 10*time.Millisecond, func() error {
|
|
s.gateway.pasi.Lock()
|
|
accMap := s.gateway.pasi.m
|
|
st := accMap["USER"]
|
|
var n int32
|
|
entry, ok := st["foo bar"]
|
|
if ok {
|
|
n = entry.n
|
|
}
|
|
s.gateway.pasi.Unlock()
|
|
if n == expectedGW {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Expected GW interest count for server %q to be %v, got %v", s, expectedGW, n)
|
|
})
|
|
}
|
|
}
|
|
|
|
ncA1 := natsConnect(t, sa1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA1.Close()
|
|
for i := 0; i < 3; i++ {
|
|
natsQueueSubSync(t, ncA1, "foo", "bar")
|
|
}
|
|
natsFlush(t, ncA1)
|
|
// With 3 queue subs on A1, we should have for servers (in order checked in checkInterest)
|
|
// for A1: 3 locals, for all others, 1 for the remote sub from A1.
|
|
// B1 and B2 GW map will be 3 (1 for each sub)
|
|
checkInterest(t, []int{3, 1, 1, 1, 1, 1, 1, 1}, 3)
|
|
|
|
ncA2 := natsConnect(t, sa2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA2.Close()
|
|
ncA2qsub1 := natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
ncA2qsub2 := natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
natsFlush(t, ncA2)
|
|
// A1 will have 1 more for remote sub, same for A2 (2 locals + 1 remote).
|
|
// B1 will have 2 interest (1 per leaf connection)
|
|
// B1 and B2 GW map goes to 5.
|
|
checkInterest(t, []int{4, 3, 1, 1, 2, 1, 1, 1}, 5)
|
|
|
|
ncOther1 := natsConnect(t, so1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncOther1.Close()
|
|
natsQueueSubSync(t, ncOther1, "foo", "bar")
|
|
natsQueueSubSync(t, ncOther1, "foo", "bar")
|
|
natsFlush(t, ncOther1)
|
|
// A1, A2 will have one more because of routed interest
|
|
// O1 will have 3 (2 locals + 1 for remote interest)
|
|
// O2 has still 1 for remote interest
|
|
// B1 has 1 more because of new leaf interest and B2 because of routed interest.
|
|
// B1 and B2 GW map goes to 7.
|
|
checkInterest(t, []int{5, 4, 3, 1, 3, 2, 1, 1}, 7)
|
|
|
|
ncOther2 := natsConnect(t, so2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncOther2.Close()
|
|
natsQueueSubSync(t, ncOther2, "foo", "bar")
|
|
natsFlush(t, ncOther2)
|
|
// O2 1 more for local interest
|
|
// B2 1 more for the new leaf interest
|
|
// B1 and B2 GW map goes to 8.
|
|
checkInterest(t, []int{5, 4, 3, 2, 3, 3, 1, 1}, 8)
|
|
|
|
// Stop the server so1.
|
|
so1.Shutdown()
|
|
so1.WaitForShutdown()
|
|
checkLeafNodeConnectedCount(t, sb2, 1)
|
|
// Now check interest still valid, but wait a little bit to make sure that
|
|
// even with the bug where we would send an RS- through the gateway, there
|
|
// would be enough time for it to propagate before we check for interest.
|
|
time.Sleep(250 * time.Millisecond)
|
|
// O1 is stopped, so expect 0
|
|
// B2 has 1 less because leaf connection went away.
|
|
// B1 and B2 GW map goes down to 6.
|
|
checkInterest(t, []int{5, 4, 0, 2, 3, 2, 1, 1}, 6)
|
|
|
|
// Store server sa1.
|
|
sa1.Shutdown()
|
|
sa1.WaitForShutdown()
|
|
checkLeafNodeConnectedCount(t, sb1, 1)
|
|
time.Sleep(250 * time.Millisecond)
|
|
// A1 and O1 are gone, so 0
|
|
// A2 has 1 less due to loss of routed interest
|
|
// B1 has 1 less because 1 leaf connection went away.
|
|
// B1 and B2 GW map goes down to 3.
|
|
checkInterest(t, []int{0, 3, 0, 2, 2, 2, 1, 1}, 3)
|
|
|
|
// Now remove the queue subs from A2
|
|
ncA2qsub1.Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
// A2 has 1 less
|
|
checkInterest(t, []int{0, 2, 0, 2, 2, 2, 1, 1}, 2)
|
|
|
|
ncA2qsub2.Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
// A2 has 1 (no more locals but still interest for O2).
|
|
// O2 has 1 (no more for remote interest, only local).
|
|
// B1, B2 has 1 less since no interest from any of its leaf connections.
|
|
checkInterest(t, []int{0, 1, 0, 1, 1, 1, 1, 1}, 1)
|
|
|
|
// Removing (closing connection) of the sub on O2 will remove
|
|
// interest globally.
|
|
ncOther2.Close()
|
|
checkInterest(t, []int{0, 0, 0, 0, 0, 0, 0, 0}, 0)
|
|
|
|
// Resubscribe now, and again, interest should be propagated.
|
|
natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
natsFlush(t, ncA2)
|
|
checkInterest(t, []int{0, 1, 0, 1, 1, 1, 1, 1}, 1)
|
|
|
|
natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
natsFlush(t, ncA2)
|
|
checkInterest(t, []int{0, 2, 0, 1, 1, 1, 1, 1}, 2)
|
|
|
|
// Close the client connection that has the 2 queue subs.
|
|
ncA2.Close()
|
|
checkInterest(t, []int{0, 0, 0, 0, 0, 0, 0, 0}, 0)
|
|
|
|
// Now we will test when a route is lost on a server that has gateway enabled
|
|
// that we update counts properly.
|
|
ncB2 := natsConnect(t, sb2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB2.Close()
|
|
natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
natsFlush(t, ncB2)
|
|
checkInterest(t, []int{0, 1, 0, 1, 1, 3, 1, 1}, 3)
|
|
|
|
ncB1 := natsConnect(t, sb1.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB1.Close()
|
|
natsQueueSubSync(t, ncB1, "foo", "bar")
|
|
natsQueueSubSync(t, ncB1, "foo", "bar")
|
|
checkInterest(t, []int{0, 1, 0, 1, 3, 4, 1, 1}, 5)
|
|
|
|
// Now shutdown B2
|
|
sb2.Shutdown()
|
|
sa1.WaitForShutdown()
|
|
time.Sleep(250 * time.Millisecond)
|
|
checkInterest(t, []int{0, 1, 0, 0, 2, 0, 1, 1}, 2)
|
|
|
|
ncB1.Close()
|
|
checkInterest(t, []int{0, 0, 0, 0, 0, 0, 0, 0}, 0)
|
|
|
|
// Now we will create 2 qsubs to sa2 that is still running.
|
|
ncA2 = natsConnect(t, sa2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncA2.Close()
|
|
qsub1 := natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
qsub2 := natsQueueSubSync(t, ncA2, "foo", "bar")
|
|
natsFlush(t, ncA2)
|
|
// sa1 is down, so 0, sa2 has 2 queue subs, so1 is down, so 0, so2 is up, so
|
|
// 1 for remote interest, same for sb1. Server sb2 is down, so 0, then 1 for
|
|
// remote interest for sc and sd. The expected tally for the gateway to "C"
|
|
// should be 2.
|
|
checkInterest(t, []int{0, 2, 0, 1, 1, 0, 1, 1}, 2)
|
|
|
|
// Close the leaf connection between sb1 and sa2
|
|
sa2.mu.Lock()
|
|
for _, l := range sa2.leafs {
|
|
l.mu.Lock()
|
|
l.nc.Close()
|
|
l.mu.Unlock()
|
|
}
|
|
sa2.mu.Unlock()
|
|
time.Sleep(50 * time.Millisecond)
|
|
checkLeafNodeConnected(t, sa2)
|
|
// Should be the same counts
|
|
checkInterest(t, []int{0, 2, 0, 1, 1, 0, 1, 1}, 2)
|
|
|
|
// Unsubscribe one of the queue sub.
|
|
qsub2.Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
time.Sleep(50 * time.Millisecond)
|
|
// One less for the local interest on sa2 and 1 less for the expected GW count.
|
|
checkInterest(t, []int{0, 1, 0, 1, 1, 0, 1, 1}, 1)
|
|
|
|
// Verify that interest works by publishing from server "C".
|
|
ncC := natsConnect(t, sc.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncC.Close()
|
|
natsPub(t, ncC, "foo", []byte("hello"))
|
|
|
|
// Message should be received by qsub1.
|
|
natsNexMsg(t, qsub1, time.Second)
|
|
|
|
// Now stop sa2 and instead start sb2.
|
|
ncA2.Close()
|
|
sa2.Shutdown()
|
|
sa2.WaitForShutdown()
|
|
|
|
// Wait for counts to go down to 0
|
|
checkInterest(t, []int{0, 0, 0, 0, 0, 0, 0, 0}, 0)
|
|
|
|
// Restart sb2 to form route to sb1.
|
|
sb2, _ = RunServerWithConfig(sb2Conf)
|
|
defer sb2.Shutdown()
|
|
|
|
checkClusterFormed(t, sb1, sb2)
|
|
waitForOutboundGateways(t, sb2, 1, time.Second)
|
|
|
|
// Create 2 queue subs on sb2.
|
|
ncB2 = natsConnect(t, sb2.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncB2.Close()
|
|
|
|
qsub1 = natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
qsub2 = natsQueueSubSync(t, ncB2, "foo", "bar")
|
|
natsFlush(t, ncB2)
|
|
// sa1 and sa2 are down, so 0, so1 is down, so 0, so2 is up, so 1 for remote interest,
|
|
// same for sb1. Server sb2 has 2 local queue subs, so 2. Servers sc and sd have 1 for
|
|
// remote interest. The expected tally for the gateway to "C" should be 2.
|
|
checkInterest(t, []int{0, 0, 0, 1, 1, 2, 1, 1}, 2)
|
|
|
|
// Now close the route(s) between sb1 and sb2.
|
|
sb2.mu.Lock()
|
|
sb2.forEachRoute(func(r *client) {
|
|
r.mu.Lock()
|
|
r.nc.Close()
|
|
r.mu.Unlock()
|
|
})
|
|
sb2.mu.Unlock()
|
|
time.Sleep(50 * time.Millisecond)
|
|
checkClusterFormed(t, sb1, sb2)
|
|
// Should be the same counts
|
|
checkInterest(t, []int{0, 0, 0, 1, 1, 2, 1, 1}, 2)
|
|
|
|
// Unsubscribe one of the queue sub.
|
|
qsub2.Unsubscribe()
|
|
natsFlush(t, ncB2)
|
|
time.Sleep(50 * time.Millisecond)
|
|
// One less for the local interest on sb2 and 1 less for the expected GW count.
|
|
checkInterest(t, []int{0, 0, 0, 1, 1, 1, 1, 1}, 1)
|
|
|
|
// Verify that interest works by publishing from server "C".
|
|
natsPub(t, ncC, "foo", []byte("hello"))
|
|
|
|
// Message should be received by qsub1.
|
|
natsNexMsg(t, qsub1, time.Second)
|
|
}
|
|
|
|
func TestLeafNodeQueueInterestAndWeightCorrectAfterServerRestartOrConnectionClose(t *testing.T) {
|
|
|
|
// Note that this is not what a normal configuration should be. Users should
|
|
// configure each leafnode to have the URLs of both B1 and B2 so that when
|
|
// a server fails, the leaf can reconnect to the other running server. But
|
|
// we force it to be this way to demonstrate what the issue was and see that
|
|
// it is now fixed.
|
|
//
|
|
// B1 <--- route ---> B2
|
|
// | |
|
|
// Leaf Leaf
|
|
// | |
|
|
// A1 <--- route ---> A2
|
|
//
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
pinnedAccount string
|
|
}{
|
|
{"without pinned account", _EMPTY_},
|
|
{"with pinned account", "accounts: [\"A\"]"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
leafBConf := `
|
|
accounts { A { users: [{user:a, password: pwd}] } }
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
}
|
|
`
|
|
b1Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B1", _EMPTY_, test.pinnedAccount)))
|
|
b1, b1Opts := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port), test.pinnedAccount)))
|
|
b2, b2Opts := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
leafAConf := `
|
|
accounts { A { users: [{user:a, password: pwd}] } }
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: LEAF
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
remotes: [
|
|
{
|
|
url: "nats://a:[email protected]:%d"
|
|
account: A
|
|
}
|
|
]
|
|
no_advertise: true
|
|
}
|
|
`
|
|
a1Conf := createConfFile(t, []byte(fmt.Sprintf(leafAConf, "A1", _EMPTY_, test.pinnedAccount, b1Opts.LeafNode.Port)))
|
|
a1, a1Opts := RunServerWithConfig(a1Conf)
|
|
defer a1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b1)
|
|
checkLeafNodeConnected(t, a1)
|
|
|
|
a2Conf := createConfFile(t, []byte(fmt.Sprintf(leafAConf, "A2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", a1Opts.Cluster.Port), test.pinnedAccount, b2Opts.LeafNode.Port)))
|
|
a2, _ := RunServerWithConfig(a2Conf)
|
|
defer a2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, a2)
|
|
checkClusterFormed(t, a1, a2)
|
|
|
|
// Create a client on A2 and 3 queue subs.
|
|
ncA2 := natsConnect(t, a2.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer ncA2.Close()
|
|
|
|
var qsubs []*nats.Subscription
|
|
for i := 0; i < 3; i++ {
|
|
qsubs = append(qsubs, natsQueueSub(t, ncA2, "foo", "queue", func(_ *nats.Msg) {}))
|
|
}
|
|
natsFlush(t, ncA2)
|
|
|
|
subj := "foo"
|
|
checkInterest := func(expected bool) {
|
|
t.Helper()
|
|
for _, s := range []*Server{a1, a2, b1, b2} {
|
|
acc, err := s.LookupAccount("A")
|
|
require_NoError(t, err)
|
|
checkFor(t, time.Second, 100*time.Millisecond, func() error {
|
|
i := acc.Interest(subj)
|
|
if expected && i == 0 {
|
|
return fmt.Errorf("Still no interest on %q in server %q", subj, s)
|
|
} else if !expected && i > 0 {
|
|
return fmt.Errorf("Still interest on %q in server %q", subj, s)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
checkInterest(true)
|
|
|
|
// Check that Leafz from A1 (which connects to B1) has the expected sub
|
|
// interest on "foo".
|
|
checkLeafA1 := func(expected bool) {
|
|
t.Helper()
|
|
// We will wait a bit before checking Leafz since with the bug, it would
|
|
// take a bit of time after the action to reproduce the issue for the
|
|
// LS+ to be sent to the wrong cluster, or the interest to not be removed.
|
|
time.Sleep(100 * time.Millisecond)
|
|
// Now check Leafz
|
|
leafsz, err := a1.Leafz(&LeafzOptions{Subscriptions: true})
|
|
require_NoError(t, err)
|
|
require_Equal(t, leafsz.NumLeafs, 1)
|
|
require_True(t, leafsz.Leafs[0] != nil)
|
|
lz := leafsz.Leafs[0]
|
|
require_Equal(t, lz.Name, "B1")
|
|
require_Equal(t, lz.NumSubs, uint32(len(lz.Subs)))
|
|
var ok bool
|
|
for _, sub := range lz.Subs {
|
|
if sub == "foo" {
|
|
if expected {
|
|
ok = true
|
|
break
|
|
}
|
|
t.Fatalf("Did not expect to have the %q subscription", sub)
|
|
}
|
|
}
|
|
if expected && !ok {
|
|
t.Fatalf("Expected to have the %q subscription", "foo")
|
|
}
|
|
}
|
|
checkLeafA1(false)
|
|
|
|
// Now restart server "B1". We need to create a conf file with the ports
|
|
// that it used.
|
|
restartBConf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
accounts { A { users: [{user:a, password: pwd}] } }
|
|
server_name: B1
|
|
listen: "127.0.0.1:%d"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:%d"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:%d"
|
|
no_advertise: true
|
|
}
|
|
`, b1Opts.Port, b1Opts.Cluster.Port, test.pinnedAccount, b1Opts.LeafNode.Port)))
|
|
b1.Shutdown()
|
|
b1, _ = RunServerWithConfig(restartBConf)
|
|
defer b1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b1)
|
|
checkLeafNodeConnected(t, a1)
|
|
|
|
// Stop one of the queue sub.
|
|
qsubs[0].Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
|
|
// Check that "foo" does not show up in the subscription list
|
|
// for the leaf from A1 to B1.
|
|
checkLeafA1(false)
|
|
|
|
// Now stop the other 2 and check again.
|
|
qsubs[1].Unsubscribe()
|
|
qsubs[2].Unsubscribe()
|
|
natsFlush(t, ncA2)
|
|
checkInterest(false)
|
|
|
|
checkLeafA1(false)
|
|
|
|
// Now recreate 3 queue subs.
|
|
for i := 0; i < 3; i++ {
|
|
natsQueueSub(t, ncA2, "foo", "queue", func(_ *nats.Msg) {})
|
|
}
|
|
// Check interest is present in all servers
|
|
checkInterest(true)
|
|
// But A1's leaf to B1 should still not have a sub interest for "foo".
|
|
checkLeafA1(false)
|
|
|
|
// Now stop the client connection instead of removing queue sub
|
|
// one at a time. This will ensure that we properly handle an LS-
|
|
// on B2 with an interest with a queue weight more than 1 still
|
|
// present at the time of processing.
|
|
ncA2.Close()
|
|
checkInterest(false)
|
|
|
|
checkLeafA1(false)
|
|
|
|
// We will now test that if the queue subs are created on B2,
|
|
// we have proper interest on A1, but when we close the connection,
|
|
// the interest disappears.
|
|
ncB2 := natsConnect(t, b2.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer ncB2.Close()
|
|
|
|
for i := 0; i < 3; i++ {
|
|
natsQueueSub(t, ncB2, "foo", "queue", func(_ *nats.Msg) {})
|
|
}
|
|
checkInterest(true)
|
|
checkLeafA1(true)
|
|
// Close the connection, so all queue subs should be removed at once.
|
|
ncB2.Close()
|
|
checkInterest(false)
|
|
checkLeafA1(false)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeQueueWeightCorrectOnRestart(t *testing.T) {
|
|
leafBConf := `
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
}
|
|
`
|
|
b1Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B1", _EMPTY_)))
|
|
b1, b1Opts := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port))))
|
|
b2, b2Opts := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
leafAConf := `
|
|
server_name: LEAF
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes: [{url: "nats://127.0.0.1:%d"}]
|
|
reconnect: "50ms"
|
|
}
|
|
`
|
|
aConf := createConfFile(t, []byte(fmt.Sprintf(leafAConf, b2Opts.LeafNode.Port)))
|
|
a, _ := RunServerWithConfig(aConf)
|
|
defer a.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, a)
|
|
|
|
nc := natsConnect(t, a.ClientURL())
|
|
defer nc.Close()
|
|
|
|
for i := 0; i < 2; i++ {
|
|
natsQueueSubSync(t, nc, "foo", "queue")
|
|
}
|
|
natsFlush(t, nc)
|
|
|
|
checkQueueWeight := func() {
|
|
for _, s := range []*Server{b1, b2} {
|
|
gacc := s.GlobalAccount()
|
|
gacc.mu.RLock()
|
|
sl := gacc.sl
|
|
gacc.mu.RUnlock()
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
// For remote queue interest, Match() will expand to queue weight.
|
|
// So we should have 1 group and 2 queue subs present.
|
|
res := sl.Match("foo")
|
|
for _, qsubs := range res.qsubs {
|
|
for _, sub := range qsubs {
|
|
if string(sub.subject) == "foo" && string(sub.queue) == "queue" && atomic.LoadInt32(&sub.qw) == 2 {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fmt.Errorf("Server %q does not have expected queue interest with expected weight", s)
|
|
})
|
|
}
|
|
}
|
|
checkQueueWeight()
|
|
|
|
// Now restart server "B2". We need to create a conf file with the ports
|
|
// that it used.
|
|
restartBConf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: B2
|
|
listen: "127.0.0.1:%d"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:%d"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:%d"
|
|
no_advertise: true
|
|
}
|
|
`, b2Opts.Port, b2Opts.Cluster.Port, fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port), b2Opts.LeafNode.Port)))
|
|
b2.Shutdown()
|
|
b2, _ = RunServerWithConfig(restartBConf)
|
|
defer b2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, a)
|
|
checkQueueWeight()
|
|
}
|
|
|
|
func TestLeafNodeRoutedSubKeyDifferentBetweenLeafSubAndRoutedSub(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
pinnedAccount string
|
|
lnocu bool
|
|
}{
|
|
{"without pinned account", _EMPTY_, true},
|
|
{"with pinned account", "accounts: [\"XYZ\"]", true},
|
|
{"old server without pinned account", _EMPTY_, false},
|
|
{"old server with pinned account", "accounts: [\"XYZ\"]", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
leafBConf := `
|
|
accounts: {XYZ {users:[{user:a, password:pwd}]}}
|
|
server_name: %s
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: HUB
|
|
listen: "127.0.0.1:-1"
|
|
%s
|
|
%s
|
|
}
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
no_advertise: true
|
|
}
|
|
`
|
|
b1Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B1", _EMPTY_, test.pinnedAccount)))
|
|
b1, b1Opts := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(leafBConf, "B2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", b1Opts.Cluster.Port), test.pinnedAccount)))
|
|
b2, b2Opts := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
// To make route connections behave like if the server was connected
|
|
// to an older server, change the routes' lnocu field.
|
|
if !test.lnocu {
|
|
for _, s := range []*Server{b1, b2} {
|
|
s.mu.RLock()
|
|
s.forEachRoute(func(r *client) {
|
|
r.mu.Lock()
|
|
r.route.lnocu = false
|
|
r.mu.Unlock()
|
|
})
|
|
s.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
// This leaf will have a cluster name that matches an account name.
|
|
// The idea is to make sure that hub servers are not using incorrect
|
|
// keys to differentiate a routed queue interest on subject "A" with
|
|
// queue name "foo" for account "A" in their cluster: "RS+ A A foo"
|
|
// with a leafnode plain subscription, which since there is an origin
|
|
// would be: "LS+ A A foo", that is, origin is "A", account is "A"
|
|
// and subject is "foo".
|
|
leafAConf := `
|
|
accounts: {XYZ {users:[{user:a, password:pwd}]}}
|
|
server_name: LEAF
|
|
listen: "127.0.0.1:-1"
|
|
cluster {
|
|
name: XYZ
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
leafnodes {
|
|
remotes: [
|
|
{
|
|
url: "nats://a:[email protected]:%d"
|
|
account: "XYZ"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
aConf := createConfFile(t, []byte(fmt.Sprintf(leafAConf, b2Opts.LeafNode.Port)))
|
|
a, _ := RunServerWithConfig(aConf)
|
|
defer a.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, b2)
|
|
checkLeafNodeConnected(t, a)
|
|
|
|
ncB2 := natsConnect(t, b2.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer ncB2.Close()
|
|
// Create a plain sub on "foo"
|
|
natsSubSync(t, ncB2, "foo")
|
|
// And a queue sub on "XYZ" with queue name "foo"
|
|
natsQueueSubSync(t, ncB2, "XYZ", "foo")
|
|
natsFlush(t, ncB2)
|
|
|
|
ncA := natsConnect(t, a.ClientURL(), nats.UserInfo("a", "pwd"))
|
|
defer ncA.Close()
|
|
// From the leafnode, create a plain sub on "foo"
|
|
natsSubSync(t, ncA, "foo")
|
|
// And a queue sub on "XYZ" with queue name "foo"
|
|
natsQueueSubSync(t, ncA, "XYZ", "foo")
|
|
natsFlush(t, ncA)
|
|
|
|
// Check the acc.rm on B2
|
|
b2Acc, err := b2.LookupAccount("XYZ")
|
|
require_NoError(t, err)
|
|
|
|
rsubKey := keyFromSubWithOrigin(&subscription{subject: []byte("foo")})
|
|
rqsubKey := keyFromSubWithOrigin(&subscription{subject: []byte("XYZ"), queue: []byte("foo")})
|
|
rlsubKey := keyFromSubWithOrigin(&subscription{origin: []byte("XYZ"), subject: []byte("foo")})
|
|
rlqsubKey := keyFromSubWithOrigin(&subscription{origin: []byte("XYZ"), subject: []byte("XYZ"), queue: []byte("foo")})
|
|
// Ensure all keys are different
|
|
require_True(t, rsubKey != rqsubKey && rqsubKey != rlsubKey && rlsubKey != rlqsubKey)
|
|
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
b2Acc.mu.RLock()
|
|
defer b2Acc.mu.RUnlock()
|
|
for _, key := range []string{rsubKey, rqsubKey, rlsubKey, rlqsubKey} {
|
|
v, ok := b2Acc.rm[key]
|
|
if !ok {
|
|
return fmt.Errorf("Did not find key %q for sub: %+v", key, sub)
|
|
}
|
|
if v != 1 {
|
|
return fmt.Errorf("Key %q v=%v for sub: %+v", key, v, sub)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Now check that on B1, we have 2 distinct subs for the route.
|
|
b1Acc, err := b1.LookupAccount("XYZ")
|
|
require_NoError(t, err)
|
|
|
|
var route *client
|
|
|
|
if test.pinnedAccount == _EMPTY_ {
|
|
b1Acc.mu.RLock()
|
|
rIdx := b1Acc.routePoolIdx
|
|
b1Acc.mu.RUnlock()
|
|
b1.mu.RLock()
|
|
b1.forEachRouteIdx(rIdx, func(r *client) bool {
|
|
route = r
|
|
return false
|
|
})
|
|
b1.mu.RUnlock()
|
|
} else {
|
|
b1.mu.RLock()
|
|
remotes := b1.accRoutes["XYZ"]
|
|
for _, r := range remotes {
|
|
route = r
|
|
break
|
|
}
|
|
b1.mu.RUnlock()
|
|
}
|
|
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
// Check that route.subs has 4 entries for the subs we
|
|
// created in this test.
|
|
var entries []string
|
|
route.mu.Lock()
|
|
for key := range route.subs {
|
|
if strings.Contains(key, "foo") {
|
|
entries = append(entries, key)
|
|
}
|
|
}
|
|
route.mu.Unlock()
|
|
// With new servers, we expect 4 entries, but with older servers,
|
|
// we have collisions and have only 2.
|
|
var expected int
|
|
if test.lnocu {
|
|
expected = 4
|
|
} else {
|
|
expected = 2
|
|
}
|
|
if len(entries) != expected {
|
|
return fmt.Errorf("Expected %d entries with %q, got this: %q", expected, "foo", entries)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Close the connections and expect all gone.
|
|
ncB2.Close()
|
|
ncA.Close()
|
|
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
b2Acc.mu.RLock()
|
|
defer b2Acc.mu.RUnlock()
|
|
for _, key := range []string{rsubKey, rqsubKey, rlsubKey, rlqsubKey} {
|
|
if _, ok := b2Acc.rm[key]; ok {
|
|
return fmt.Errorf("Key %q still present", key)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
var entries []string
|
|
route.mu.Lock()
|
|
for key := range route.subs {
|
|
if strings.Contains(key, "foo") {
|
|
entries = append(entries, key)
|
|
}
|
|
}
|
|
route.mu.Unlock()
|
|
if len(entries) != 0 {
|
|
return fmt.Errorf("Still routed subscriptions on %q: %q", "foo", entries)
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeQueueGroupWithLateLNJoin(t *testing.T) {
|
|
/*
|
|
|
|
Topology: A cluster of leafnodes LN2 and LN3, connect
|
|
to a cluster C1, C2.
|
|
|
|
sub(foo) sub(foo)
|
|
\ /
|
|
C1 <-> C2
|
|
^ ^
|
|
| |
|
|
LN2 <-> LN3
|
|
/ \
|
|
sub(foo) sub(foo)
|
|
|
|
Once the above is set, start LN1 that connects to C1.
|
|
|
|
sub(foo) sub(foo)
|
|
\ /
|
|
LN1 -> C1 <-> C2
|
|
^ ^
|
|
| |
|
|
LN2 <-> LN3
|
|
/ \
|
|
sub(foo) sub(foo)
|
|
|
|
Remove subs to LN3, C2 and C1.
|
|
|
|
LN1 -> C1 <-> C2
|
|
^ ^
|
|
| |
|
|
LN2 <-> LN3
|
|
/
|
|
sub(foo)
|
|
|
|
Publish from LN1 and verify message is received by sub on LN2.
|
|
|
|
pub(foo)
|
|
\
|
|
LN1 -> C1 <-> C2
|
|
^ ^
|
|
| |
|
|
LN2 <-> LN3
|
|
/
|
|
sub(foo)
|
|
*/
|
|
co1 := DefaultOptions()
|
|
co1.LeafNode.Host = "127.0.0.1"
|
|
co1.LeafNode.Port = -1
|
|
co1.Cluster.Name = "ngs"
|
|
co1.Cluster.Host = "127.0.0.1"
|
|
co1.Cluster.Port = -1
|
|
c1 := RunServer(co1)
|
|
defer c1.Shutdown()
|
|
|
|
co2 := DefaultOptions()
|
|
co2.LeafNode.Host = "127.0.0.1"
|
|
co2.LeafNode.Port = -1
|
|
co2.Cluster.Name = "ngs"
|
|
co2.Cluster.Host = "127.0.0.1"
|
|
co2.Cluster.Port = -1
|
|
co2.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", co1.Cluster.Port))
|
|
c2 := RunServer(co2)
|
|
defer c2.Shutdown()
|
|
|
|
checkClusterFormed(t, c1, c2)
|
|
|
|
lo2 := DefaultOptions()
|
|
lo2.Cluster.Name = "local"
|
|
lo2.Cluster.Host = "127.0.0.1"
|
|
lo2.Cluster.Port = -1
|
|
lo2.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
lo2.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", co1.LeafNode.Port)}}}}
|
|
ln2 := RunServer(lo2)
|
|
defer ln2.Shutdown()
|
|
|
|
lo3 := DefaultOptions()
|
|
lo3.Cluster.Name = "local"
|
|
lo3.Cluster.Host = "127.0.0.1"
|
|
lo3.Cluster.Port = -1
|
|
lo3.Routes = RoutesFromStr(fmt.Sprintf("nats://127.0.0.1:%d", lo2.Cluster.Port))
|
|
lo3.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
lo3.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", co2.LeafNode.Port)}}}}
|
|
ln3 := RunServer(lo3)
|
|
defer ln3.Shutdown()
|
|
|
|
checkClusterFormed(t, ln2, ln3)
|
|
checkLeafNodeConnected(t, ln2)
|
|
checkLeafNodeConnected(t, ln3)
|
|
|
|
cln2 := natsConnect(t, ln2.ClientURL())
|
|
defer cln2.Close()
|
|
sln2 := natsQueueSubSync(t, cln2, "foo", "qgroup")
|
|
natsFlush(t, cln2)
|
|
|
|
cln3 := natsConnect(t, ln3.ClientURL())
|
|
defer cln3.Close()
|
|
sln3 := natsQueueSubSync(t, cln3, "foo", "qgroup")
|
|
natsFlush(t, cln3)
|
|
|
|
cc1 := natsConnect(t, c1.ClientURL())
|
|
defer cc1.Close()
|
|
sc1 := natsQueueSubSync(t, cc1, "foo", "qgroup")
|
|
natsFlush(t, cc1)
|
|
|
|
cc2 := natsConnect(t, c2.ClientURL())
|
|
defer cc2.Close()
|
|
sc2 := natsQueueSubSync(t, cc2, "foo", "qgroup")
|
|
natsFlush(t, cc2)
|
|
|
|
checkSubInterest(t, c1, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, c2, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, ln2, globalAccountName, "foo", time.Second)
|
|
checkSubInterest(t, ln3, globalAccountName, "foo", time.Second)
|
|
|
|
lo1 := DefaultOptions()
|
|
lo1.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
lo1.LeafNode.Remotes = []*RemoteLeafOpts{{URLs: []*url.URL{{Scheme: "nats", Host: fmt.Sprintf("127.0.0.1:%d", co1.LeafNode.Port)}}}}
|
|
ln1 := RunServer(lo1)
|
|
defer ln1.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, ln1)
|
|
checkSubInterest(t, ln1, globalAccountName, "foo", time.Second)
|
|
|
|
sln3.Unsubscribe()
|
|
natsFlush(t, cln3)
|
|
sc2.Unsubscribe()
|
|
natsFlush(t, cc2)
|
|
sc1.Unsubscribe()
|
|
natsFlush(t, cc1)
|
|
|
|
cln1 := natsConnect(t, ln1.ClientURL())
|
|
defer cln1.Close()
|
|
|
|
natsPub(t, cln1, "foo", []byte("hello"))
|
|
natsNexMsg(t, sln2, time.Second)
|
|
}
|
|
|
|
func TestLeafNodeJetStreamDomainMapCrossTalk(t *testing.T) {
|
|
accs := `
|
|
accounts :{
|
|
A:{ jetstream: enable, users:[ {user:a1,password:a1}]},
|
|
SYS:{ users:[ {user:s1,password:s1}]},
|
|
}
|
|
system_account: SYS
|
|
`
|
|
|
|
sd1 := t.TempDir()
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
%s
|
|
jetstream: { domain: da, store_dir: '%s', max_mem: 50Mb, max_file: 50Mb }
|
|
leafnodes: {
|
|
listen: 127.0.0.1:-1
|
|
no_advertise: true
|
|
authorization: {
|
|
timeout: 0.5
|
|
}
|
|
}
|
|
`, accs, sd1)))
|
|
sA, _ := RunServerWithConfig(confA)
|
|
defer sA.Shutdown()
|
|
|
|
sd2 := t.TempDir()
|
|
confL := createConfFile(t, []byte(fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
%s
|
|
jetstream: { domain: dl, store_dir: '%s', max_mem: 50Mb, max_file: 50Mb }
|
|
leafnodes:{
|
|
no_advertise: true
|
|
remotes:[{url:nats://a1:[email protected]:%d, account: A},
|
|
{url:nats://s1:[email protected]:%d, account: SYS}]
|
|
}
|
|
`, accs, sd2, sA.opts.LeafNode.Port, sA.opts.LeafNode.Port)))
|
|
sL, _ := RunServerWithConfig(confL)
|
|
defer sL.Shutdown()
|
|
|
|
ncA := natsConnect(t, sA.ClientURL(), nats.UserInfo("a1", "a1"))
|
|
defer ncA.Close()
|
|
ncL := natsConnect(t, sL.ClientURL(), nats.UserInfo("a1", "a1"))
|
|
defer ncL.Close()
|
|
|
|
test := func(jsA, jsL nats.JetStreamContext) {
|
|
kvA, err := jsA.CreateKeyValue(&nats.KeyValueConfig{Bucket: "bucket"})
|
|
require_NoError(t, err)
|
|
kvL, err := jsL.CreateKeyValue(&nats.KeyValueConfig{Bucket: "bucket"})
|
|
require_NoError(t, err)
|
|
|
|
_, err = kvA.Put("A", nil)
|
|
require_NoError(t, err)
|
|
_, err = kvL.Put("L", nil)
|
|
require_NoError(t, err)
|
|
|
|
// check for unwanted cross talk
|
|
_, err = kvA.Get("A")
|
|
require_NoError(t, err)
|
|
_, err = kvA.Get("l")
|
|
require_Error(t, err)
|
|
require_True(t, err == nats.ErrKeyNotFound)
|
|
|
|
_, err = kvL.Get("A")
|
|
require_Error(t, err)
|
|
require_True(t, err == nats.ErrKeyNotFound)
|
|
_, err = kvL.Get("L")
|
|
require_NoError(t, err)
|
|
|
|
err = jsA.DeleteKeyValue("bucket")
|
|
require_NoError(t, err)
|
|
err = jsL.DeleteKeyValue("bucket")
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
jsA, err := ncA.JetStream()
|
|
require_NoError(t, err)
|
|
jsL, err := ncL.JetStream()
|
|
require_NoError(t, err)
|
|
test(jsA, jsL)
|
|
|
|
jsAL, err := ncA.JetStream(nats.Domain("dl"))
|
|
require_NoError(t, err)
|
|
jsLA, err := ncL.JetStream(nats.Domain("da"))
|
|
require_NoError(t, err)
|
|
test(jsAL, jsLA)
|
|
|
|
jsAA, err := ncA.JetStream(nats.Domain("da"))
|
|
require_NoError(t, err)
|
|
jsLL, err := ncL.JetStream(nats.Domain("dl"))
|
|
require_NoError(t, err)
|
|
test(jsAA, jsLL)
|
|
}
|
|
|
|
type checkLeafMinVersionLogger struct {
|
|
DummyLogger
|
|
errCh chan string
|
|
connCh chan string
|
|
}
|
|
|
|
func (l *checkLeafMinVersionLogger) Errorf(format string, args ...any) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
if strings.Contains(msg, "minimum version") {
|
|
select {
|
|
case l.errCh <- msg:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *checkLeafMinVersionLogger) Noticef(format string, args ...any) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
if strings.Contains(msg, "Leafnode connection created") {
|
|
select {
|
|
case l.connCh <- msg:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeMinVersion(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
min_version: 2.8.0
|
|
}
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
rconf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes {
|
|
remotes [
|
|
{url: "nats://127.0.0.1:%d" }
|
|
]
|
|
}
|
|
`, o.LeafNode.Port)))
|
|
ln, _ := RunServerWithConfig(rconf)
|
|
defer ln.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s)
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
ln.Shutdown()
|
|
s.Shutdown()
|
|
|
|
// Now makes sure we validate options, not just config file.
|
|
for _, test := range []struct {
|
|
name string
|
|
version string
|
|
err string
|
|
}{
|
|
{"invalid version", "abc", "semver"},
|
|
{"version too low", "2.7.9", "the minimum version should be at least 2.8.0"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o.Port = -1
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.MinVersion = test.version
|
|
if s, err := NewServer(o); err == nil || !strings.Contains(err.Error(), test.err) {
|
|
if s != nil {
|
|
s.Shutdown()
|
|
}
|
|
t.Fatalf("Expected error to contain %q, got %v", test.err, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Ok, so now to verify that a server rejects a leafnode connection
|
|
// we will set the min_version above our current VERSION. So first
|
|
// decompose the version:
|
|
major, minor, _, err := versionComponents(VERSION)
|
|
if err != nil {
|
|
t.Fatalf("The current server version %q is not valid: %v", VERSION, err)
|
|
}
|
|
// Let's make our minimum server an minor version above
|
|
mv := fmt.Sprintf("%d.%d.0", major, minor+1)
|
|
conf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
min_version: "%s"
|
|
}
|
|
`, mv)))
|
|
s, o = RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
l := &checkLeafMinVersionLogger{errCh: make(chan string, 1), connCh: make(chan string, 1)}
|
|
s.SetLogger(l, false, false)
|
|
|
|
rconf = createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes {
|
|
remotes [
|
|
{url: "nats://127.0.0.1:%d" }
|
|
]
|
|
}
|
|
`, o.LeafNode.Port)))
|
|
lo := LoadConfig(rconf)
|
|
lo.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
ln = RunServer(lo)
|
|
defer ln.Shutdown()
|
|
|
|
select {
|
|
case <-l.connCh:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Remote did not try to connect")
|
|
}
|
|
|
|
select {
|
|
case <-l.errCh:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Did not get the minimum version required error")
|
|
}
|
|
|
|
// Since we have a very small reconnect interval, if the connection was
|
|
// closed "right away", then we should have had a reconnect attempt with
|
|
// another failure. This should not be the case because the server will
|
|
// wait 5s before closing the connection.
|
|
select {
|
|
case <-l.connCh:
|
|
t.Fatal("Should not have tried to reconnect")
|
|
case <-time.After(250 * time.Millisecond):
|
|
// OK
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeStreamAndShadowSubs(t *testing.T) {
|
|
hubConf := createConfFile(t, []byte(`
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
authorization: {
|
|
user: leaf
|
|
password: leaf
|
|
account: B
|
|
}
|
|
}
|
|
accounts: {
|
|
A: {
|
|
users = [{user: usrA, password: usrA}]
|
|
exports: [{stream: foo.*.>}]
|
|
}
|
|
B: {
|
|
imports: [{stream: {account: A, subject: foo.*.>}}]
|
|
}
|
|
}
|
|
`))
|
|
hub, hubo := RunServerWithConfig(hubConf)
|
|
defer hub.Shutdown()
|
|
|
|
leafConfContet := fmt.Sprintf(`
|
|
port: -1
|
|
leafnodes {
|
|
remotes = [
|
|
{
|
|
url: "nats-leaf://leaf:[email protected]:%d"
|
|
account: B
|
|
}
|
|
]
|
|
}
|
|
accounts: {
|
|
B: {
|
|
exports: [{stream: foo.*.>}]
|
|
}
|
|
C: {
|
|
users: [{user: usrC, password: usrC}]
|
|
imports: [{stream: {account: B, subject: foo.bar.>}}]
|
|
}
|
|
}
|
|
`, hubo.LeafNode.Port)
|
|
leafConf := createConfFile(t, []byte(leafConfContet))
|
|
leafo := LoadConfig(leafConf)
|
|
leafo.LeafNode.ReconnectInterval = 50 * time.Millisecond
|
|
leaf := RunServer(leafo)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
subPubAndCheck := func() {
|
|
t.Helper()
|
|
|
|
ncl, err := nats.Connect(leaf.ClientURL(), nats.UserInfo("usrC", "usrC"))
|
|
if err != nil {
|
|
t.Fatalf("Error connecting: %v", err)
|
|
}
|
|
defer ncl.Close()
|
|
|
|
// This will send an LS+ to the "hub" server.
|
|
sub, err := ncl.SubscribeSync("foo.*.baz")
|
|
if err != nil {
|
|
t.Fatalf("Error subscribing: %v", err)
|
|
}
|
|
ncl.Flush()
|
|
|
|
ncm, err := nats.Connect(hub.ClientURL(), nats.UserInfo("usrA", "usrA"))
|
|
if err != nil {
|
|
t.Fatalf("Error connecting: %v", err)
|
|
}
|
|
defer ncm.Close()
|
|
|
|
// Try a few times in case subject interest has not propagated yet
|
|
for i := 0; i < 5; i++ {
|
|
ncm.Publish("foo.bar.baz", []byte("msg"))
|
|
if _, err := sub.NextMsg(time.Second); err == nil {
|
|
// OK, done!
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("Message was not received")
|
|
}
|
|
subPubAndCheck()
|
|
|
|
// Now cause a restart of the accepting side so that the leaf connection
|
|
// is recreated.
|
|
hub.Shutdown()
|
|
hub = RunServer(hubo)
|
|
defer hub.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
subPubAndCheck()
|
|
|
|
// Issue a config reload even though we make no modification. There was
|
|
// a defect that caused the interest propagation to break.
|
|
// Set the ReconnectInterval to the default value so that reload does not complain.
|
|
leaf.getOpts().LeafNode.ReconnectInterval = DEFAULT_LEAF_NODE_RECONNECT
|
|
reloadUpdateConfig(t, leaf, leafConf, leafConfContet)
|
|
|
|
// Check again
|
|
subPubAndCheck()
|
|
}
|
|
|
|
func TestLeafNodeAuthConfigReload(t *testing.T) {
|
|
template := `
|
|
listen: 127.0.0.1:-1
|
|
accounts { test: {} }
|
|
leaf {
|
|
listen: "127.0.0.1:7422"
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
}
|
|
authorization {
|
|
# These are only fields allowed atm.
|
|
users = [ { user: test, password: "s3cret1", account: "test" } ]
|
|
}
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(template))
|
|
|
|
s, _ := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
lg := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s.SetLogger(lg, false, false)
|
|
|
|
// Reload here should work ok.
|
|
reloadUpdateConfig(t, s, conf, template)
|
|
}
|
|
|
|
func TestLeafNodeSignatureCB(t *testing.T) {
|
|
content := `
|
|
port: -1
|
|
server_name: OP
|
|
operator = "../test/configs/nkeys/op.jwt"
|
|
resolver = MEMORY
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
`
|
|
conf := createConfFile(t, []byte(content))
|
|
s, opts := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
_, akp := createAccount(s)
|
|
kp, _ := nkeys.CreateUser()
|
|
pub, _ := kp.PublicKey()
|
|
nuc := jwt.NewUserClaims(pub)
|
|
ujwt, err := nuc.Encode(akp)
|
|
if err != nil {
|
|
t.Fatalf("Error generating user JWT: %v", err)
|
|
}
|
|
|
|
lopts := &DefaultTestOptions
|
|
u, err := url.Parse(fmt.Sprintf("nats://%s:%d", opts.LeafNode.Host, opts.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing url: %v", err)
|
|
}
|
|
remote := &RemoteLeafOpts{URLs: []*url.URL{u}}
|
|
remote.SignatureCB = func(nonce []byte) (string, []byte, error) {
|
|
return "", nil, fmt.Errorf("on purpose")
|
|
}
|
|
lopts.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
lopts.LeafNode.ReconnectInterval = 100 * time.Millisecond
|
|
sl := RunServer(lopts)
|
|
defer sl.Shutdown()
|
|
|
|
slog := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
sl.SetLogger(slog, false, false)
|
|
|
|
// Now check that the leafnode got the error that the callback returned.
|
|
select {
|
|
case err := <-slog.errCh:
|
|
if !strings.Contains(err, "on purpose") {
|
|
t.Fatalf("Expected error from cb, got %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Did not get expected error")
|
|
}
|
|
|
|
sl.Shutdown()
|
|
// Now check what happens if the connection is closed while in the callback.
|
|
blockCh := make(chan struct{})
|
|
remote.SignatureCB = func(nonce []byte) (string, []byte, error) {
|
|
<-blockCh
|
|
sig, err := kp.Sign(nonce)
|
|
return ujwt, sig, err
|
|
}
|
|
sl = RunServer(lopts)
|
|
defer sl.Shutdown()
|
|
|
|
// Recreate the logger so that we are sure not to have possible previous errors
|
|
slog = &captureErrorLogger{errCh: make(chan string, 10)}
|
|
sl.SetLogger(slog, false, false)
|
|
|
|
// Get the leaf connection from the temp clients map and close it.
|
|
checkFor(t, time.Second, 15*time.Millisecond, func() error {
|
|
var c *client
|
|
sl.grMu.Lock()
|
|
for _, cli := range sl.grTmpClients {
|
|
c = cli
|
|
}
|
|
sl.grMu.Unlock()
|
|
if c == nil {
|
|
return fmt.Errorf("Client still not found in temp map")
|
|
}
|
|
c.closeConnection(ClientClosed)
|
|
return nil
|
|
})
|
|
|
|
// Release the callback, and check we get the appropriate error.
|
|
close(blockCh)
|
|
select {
|
|
case err := <-slog.errCh:
|
|
if !strings.Contains(err, ErrConnectionClosed.Error()) {
|
|
t.Fatalf("Expected error that connection was closed, got %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Did not get expected error")
|
|
}
|
|
|
|
sl.Shutdown()
|
|
// Change to a good CB and now it should work
|
|
remote.SignatureCB = func(nonce []byte) (string, []byte, error) {
|
|
sig, err := kp.Sign(nonce)
|
|
return ujwt, sig, err
|
|
}
|
|
sl = RunServer(lopts)
|
|
defer sl.Shutdown()
|
|
checkLeafNodeConnected(t, sl)
|
|
}
|
|
|
|
type testLeafTraceLogger struct {
|
|
DummyLogger
|
|
ch chan string
|
|
}
|
|
|
|
func (l *testLeafTraceLogger) Tracef(format string, v ...any) {
|
|
msg := fmt.Sprintf(format, v...)
|
|
// We will sub to 'baz' and to 'bar', so filter on 'ba' prefix.
|
|
if strings.Contains(msg, "[LS+ ba") {
|
|
select {
|
|
case l.ch <- msg:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make sure permissioned denied subs do not make it to the leafnode even if existing.
|
|
func TestLeafNodePermsSuppressSubs(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
authorization {
|
|
PERMS = {
|
|
publish = "foo"
|
|
subscribe = ["_INBOX.>"]
|
|
}
|
|
users = [
|
|
{user: "user", password: "pass"}
|
|
{user: "ln", password: "pass" , permissions: $PERMS }
|
|
]
|
|
}
|
|
no_auth_user: user
|
|
|
|
leafnodes {
|
|
listen: 127.0.0.1:7422
|
|
}
|
|
`))
|
|
|
|
lconf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
leafnodes {
|
|
remotes = [ { url: "nats://ln:[email protected]" } ]
|
|
}
|
|
trace = true
|
|
`))
|
|
|
|
s, _ := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
// Connect client to the hub.
|
|
nc, err := nats.Connect(s.ClientURL())
|
|
require_NoError(t, err)
|
|
defer nc.Close()
|
|
|
|
// This should not be seen on leafnode side since we only allow pub to "foo"
|
|
_, err = nc.SubscribeSync("baz")
|
|
require_NoError(t, err)
|
|
|
|
ln, _ := RunServerWithConfig(lconf)
|
|
defer ln.Shutdown()
|
|
|
|
// Setup logger to capture trace events.
|
|
l := &testLeafTraceLogger{ch: make(chan string, 10)}
|
|
ln.SetLogger(l, true, true)
|
|
|
|
checkLeafNodeConnected(t, ln)
|
|
|
|
// Need to have ot reconnect to trigger since logger attaches too late.
|
|
ln.mu.Lock()
|
|
for _, c := range ln.leafs {
|
|
c.mu.Lock()
|
|
c.nc.Close()
|
|
c.mu.Unlock()
|
|
}
|
|
ln.mu.Unlock()
|
|
checkLeafNodeConnectedCount(t, ln, 0)
|
|
checkLeafNodeConnectedCount(t, ln, 1)
|
|
|
|
select {
|
|
case msg := <-l.ch:
|
|
t.Fatalf("Unexpected LS+ seen on leafnode: %s", msg)
|
|
case <-time.After(50 * time.Millisecond):
|
|
// OK
|
|
}
|
|
|
|
// Now double check that new subs also do not propagate.
|
|
// This behavior was working already.
|
|
_, err = nc.SubscribeSync("bar")
|
|
require_NoError(t, err)
|
|
|
|
select {
|
|
case msg := <-l.ch:
|
|
t.Fatalf("Unexpected LS+ seen on leafnode: %s", msg)
|
|
case <-time.After(50 * time.Millisecond):
|
|
// OK
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeDuplicateMsg(t *testing.T) {
|
|
// This involves 2 clusters with leafnodes to each other with a different
|
|
// account, and those accounts import/export a subject that caused
|
|
// duplicate messages. This test requires static ports since we need to
|
|
// have A->B and B->A.
|
|
a1Conf := createConfFile(t, []byte(`
|
|
cluster : {
|
|
name : A
|
|
port : -1
|
|
}
|
|
leafnodes : {
|
|
port : 14333
|
|
remotes : [{
|
|
account : A
|
|
urls : [nats://leafa:[email protected]:24333]
|
|
}]
|
|
}
|
|
port : -1
|
|
server_name : A_1
|
|
|
|
accounts:{
|
|
A:{
|
|
users:[
|
|
{user: leafa, password: pwd},
|
|
{user: usera, password: usera, permissions: {
|
|
publish:{ allow:["iot.b.topic"] }
|
|
subscribe:{ allow:["iot.a.topic"] }
|
|
}}
|
|
]
|
|
imports:[
|
|
{stream:{account:"B", subject:"iot.a.topic"}}
|
|
]
|
|
},
|
|
B:{
|
|
users:[
|
|
{user: leafb, password: pwd},
|
|
]
|
|
exports:[
|
|
{stream: "iot.a.topic", accounts: ["A"]}
|
|
]
|
|
}
|
|
}
|
|
`))
|
|
a1, oa1 := RunServerWithConfig(a1Conf)
|
|
defer a1.Shutdown()
|
|
|
|
a2Conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
cluster : {
|
|
name : A
|
|
port : -1
|
|
routes : [nats://127.0.0.1:%d]
|
|
}
|
|
leafnodes : {
|
|
port : 14334
|
|
remotes : [{
|
|
account : A
|
|
urls : [nats://leafa:[email protected]:24334]
|
|
}]
|
|
}
|
|
port : -1
|
|
server_name : A_2
|
|
|
|
accounts:{
|
|
A:{
|
|
users:[
|
|
{user: leafa, password: pwd},
|
|
{user: usera, password: usera, permissions: {
|
|
publish:{ allow:["iot.b.topic"] }
|
|
subscribe:{ allow:["iot.a.topic"] }
|
|
}}
|
|
]
|
|
imports:[
|
|
{stream:{account:"B", subject:"iot.a.topic"}}
|
|
]
|
|
},
|
|
B:{
|
|
users:[
|
|
{user: leafb, password: pwd},
|
|
]
|
|
exports:[
|
|
{stream: "iot.a.topic", accounts: ["A"]}
|
|
]
|
|
}
|
|
}`, oa1.Cluster.Port)))
|
|
a2, _ := RunServerWithConfig(a2Conf)
|
|
defer a2.Shutdown()
|
|
|
|
checkClusterFormed(t, a1, a2)
|
|
|
|
b1Conf := createConfFile(t, []byte(`
|
|
cluster : {
|
|
name : B
|
|
port : -1
|
|
}
|
|
leafnodes : {
|
|
port : 24333
|
|
remotes : [{
|
|
account : B
|
|
urls : [nats://leafb:[email protected]:14333]
|
|
}]
|
|
}
|
|
port : -1
|
|
server_name : B_1
|
|
|
|
accounts:{
|
|
A:{
|
|
users:[
|
|
{user: leafa, password: pwd},
|
|
]
|
|
exports:[
|
|
{stream: "iot.b.topic", accounts: ["B"]}
|
|
]
|
|
},
|
|
B:{
|
|
users:[
|
|
{user: leafb, password: pwd},
|
|
{user: userb, password: userb, permissions: {
|
|
publish:{ allow:["iot.a.topic"] },
|
|
subscribe:{ allow:["iot.b.topic"] }
|
|
}}
|
|
]
|
|
imports:[
|
|
{stream:{account:"A", subject:"iot.b.topic"}}
|
|
]
|
|
}
|
|
}`))
|
|
b1, ob1 := RunServerWithConfig(b1Conf)
|
|
defer b1.Shutdown()
|
|
|
|
b2Conf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
cluster : {
|
|
name : B
|
|
port : -1
|
|
routes : [nats://127.0.0.1:%d]
|
|
}
|
|
leafnodes : {
|
|
port : 24334
|
|
remotes : [{
|
|
account : B
|
|
urls : [nats://leafb:[email protected]:14334]
|
|
}]
|
|
}
|
|
port : -1
|
|
server_name : B_2
|
|
|
|
accounts:{
|
|
A:{
|
|
users:[
|
|
{user: leafa, password: pwd},
|
|
]
|
|
exports:[
|
|
{stream: "iot.b.topic", accounts: ["B"]}
|
|
]
|
|
},
|
|
B:{
|
|
users:[
|
|
{user: leafb, password: pwd},
|
|
{user: userb, password: userb, permissions: {
|
|
publish:{ allow:["iot.a.topic"] },
|
|
subscribe:{ allow:["iot.b.topic"] }
|
|
}}
|
|
]
|
|
imports:[
|
|
{stream:{account:"A", subject:"iot.b.topic"}}
|
|
]
|
|
}
|
|
}`, ob1.Cluster.Port)))
|
|
b2, _ := RunServerWithConfig(b2Conf)
|
|
defer b2.Shutdown()
|
|
|
|
checkClusterFormed(t, b1, b2)
|
|
|
|
checkLeafNodeConnectedCount(t, a1, 2)
|
|
checkLeafNodeConnectedCount(t, a2, 2)
|
|
checkLeafNodeConnectedCount(t, b1, 2)
|
|
checkLeafNodeConnectedCount(t, b2, 2)
|
|
|
|
check := func(t *testing.T, subSrv *Server, pubSrv *Server) {
|
|
|
|
sc := natsConnect(t, subSrv.ClientURL(), nats.UserInfo("userb", "userb"))
|
|
defer sc.Close()
|
|
|
|
subject := "iot.b.topic"
|
|
sub := natsSubSync(t, sc, subject)
|
|
|
|
// Wait for this to be available in A cluster
|
|
checkSubInterest(t, a1, "A", subject, time.Second)
|
|
checkSubInterest(t, a2, "A", subject, time.Second)
|
|
|
|
pb := natsConnect(t, pubSrv.ClientURL(), nats.UserInfo("usera", "usera"))
|
|
defer pb.Close()
|
|
|
|
natsPub(t, pb, subject, []byte("msg"))
|
|
natsNexMsg(t, sub, time.Second)
|
|
// Should be only 1
|
|
if msg, err := sub.NextMsg(100 * time.Millisecond); err == nil {
|
|
t.Fatalf("Received duplicate on %q: %s", msg.Subject, msg.Data)
|
|
}
|
|
}
|
|
t.Run("sub_b1_pub_a1", func(t *testing.T) { check(t, b1, a1) })
|
|
t.Run("sub_b1_pub_a2", func(t *testing.T) { check(t, b1, a2) })
|
|
t.Run("sub_b2_pub_a1", func(t *testing.T) { check(t, b2, a1) })
|
|
t.Run("sub_b2_pub_a2", func(t *testing.T) { check(t, b2, a2) })
|
|
}
|
|
|
|
func TestLeafNodeTLSHandshakeFirstVerifyNoInfoSent(t *testing.T) {
|
|
confHub := createConfFile(t, []byte(`
|
|
port : -1
|
|
leafnodes : {
|
|
port : -1
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
timeout: 2
|
|
handshake_first: true
|
|
}
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(confHub)
|
|
defer s1.Shutdown()
|
|
|
|
c, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", o1.LeafNode.Port), 2*time.Second)
|
|
require_NoError(t, err)
|
|
defer c.Close()
|
|
|
|
buf := make([]byte, 1024)
|
|
// We will wait for up to 500ms to see if the server is sending (incorrectly)
|
|
// the INFO.
|
|
c.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
|
n, err := c.Read(buf)
|
|
c.SetReadDeadline(time.Time{})
|
|
// If we did not get an error, this is an issue...
|
|
if err == nil {
|
|
t.Fatalf("Should not have received anything, got n=%v buf=%s", n, buf[:n])
|
|
}
|
|
// We expect a timeout error
|
|
if ne, ok := err.(net.Error); !ok || !ne.Timeout() {
|
|
t.Fatalf("Expected a timeout error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeTLSHandshakeFirst(t *testing.T) {
|
|
tmpl1 := `
|
|
port : -1
|
|
leafnodes : {
|
|
port : -1
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
timeout: 2
|
|
handshake_first: %s
|
|
}
|
|
}
|
|
`
|
|
confHub := createConfFile(t, []byte(fmt.Sprintf(tmpl1, "true")))
|
|
s1, o1 := RunServerWithConfig(confHub)
|
|
defer s1.Shutdown()
|
|
|
|
tmpl2 := `
|
|
port: -1
|
|
leafnodes : {
|
|
port : -1
|
|
remotes : [
|
|
{
|
|
urls : [tls://127.0.0.1:%d]
|
|
tls {
|
|
cert_file: "../test/configs/certs/client-cert.pem"
|
|
key_file: "../test/configs/certs/client-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
timeout: 2
|
|
first: %s
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`
|
|
confSpoke := createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, "true")))
|
|
s2, _ := RunServerWithConfig(confSpoke)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
s2.Shutdown()
|
|
|
|
// Now check that there will be a failure if the remote does not ask for
|
|
// handshake first since the hub is configured that way.
|
|
// Set a logger on s1 to capture errors
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s1.SetLogger(l, false, false)
|
|
|
|
confSpoke = createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, "false")))
|
|
s2, _ = RunServerWithConfig(confSpoke)
|
|
defer s2.Shutdown()
|
|
|
|
select {
|
|
case err := <-l.errCh:
|
|
if !strings.Contains(err, "handshake error") {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Did not get TLS handshake failure")
|
|
}
|
|
|
|
// Check configuration reload for this remote
|
|
reloadUpdateConfig(t, s2, confSpoke, fmt.Sprintf(tmpl2, o1.LeafNode.Port, "true"))
|
|
checkLeafNodeConnected(t, s2)
|
|
s2.Shutdown()
|
|
|
|
// Drain the logger error channel
|
|
for done := false; !done; {
|
|
select {
|
|
case <-l.errCh:
|
|
default:
|
|
done = true
|
|
}
|
|
}
|
|
|
|
// Now change the config on the hub
|
|
reloadUpdateConfig(t, s1, confHub, fmt.Sprintf(tmpl1, "false"))
|
|
// Restart s2
|
|
s2, _ = RunServerWithConfig(confSpoke)
|
|
defer s2.Shutdown()
|
|
|
|
select {
|
|
case err := <-l.errCh:
|
|
if !strings.Contains(err, "handshake error") {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Did not get TLS handshake failure")
|
|
}
|
|
|
|
// Reload again with "true"
|
|
reloadUpdateConfig(t, s1, confHub, fmt.Sprintf(tmpl1, "true"))
|
|
checkLeafNodeConnected(t, s2)
|
|
}
|
|
|
|
func TestLeafNodeTLSHandshakeEvenForRemoteWithNoTLSBlock(t *testing.T) {
|
|
confHub := createConfFile(t, []byte(`
|
|
port : -1
|
|
leafnodes : {
|
|
port : -1
|
|
tls {
|
|
cert_file: "../test/configs/certs/server-cert.pem"
|
|
key_file: "../test/configs/certs/server-key.pem"
|
|
ca_file: "../test/configs/certs/ca.pem"
|
|
timeout: 2
|
|
}
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(confHub)
|
|
defer s1.Shutdown()
|
|
|
|
tmpl2 := `
|
|
port: -1
|
|
leafnodes : {
|
|
port : -1
|
|
remotes : [
|
|
{
|
|
urls : [tls://127.0.0.1:%d]
|
|
}
|
|
]
|
|
}
|
|
`
|
|
confSpoke := createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port)))
|
|
s2, _ := RunServerWithConfig(confSpoke)
|
|
defer s2.Shutdown()
|
|
|
|
l := &captureDebugLogger{dbgCh: make(chan string, 100)}
|
|
s2.SetLogger(l, true, false)
|
|
|
|
tm := time.NewTimer(2 * time.Second)
|
|
defer tm.Stop()
|
|
for {
|
|
select {
|
|
case l := <-l.dbgCh:
|
|
if strings.Contains(l, "Starting TLS") {
|
|
// OK!
|
|
return
|
|
}
|
|
case <-tm.C:
|
|
t.Fatalf("Did not perform a TLS handshake")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeCompressionOptions(t *testing.T) {
|
|
org := testDefaultLeafNodeCompression
|
|
testDefaultLeafNodeCompression = _EMPTY_
|
|
defer func() { testDefaultLeafNodeCompression = org }()
|
|
|
|
tmpl := `
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
compression: %s
|
|
}
|
|
`
|
|
for _, test := range []struct {
|
|
name string
|
|
mode string
|
|
rttVals []int
|
|
expected string
|
|
rtts []time.Duration
|
|
}{
|
|
{"boolean enabled", "true", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string enabled", "enabled", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string EnaBled", "EnaBled", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string on", "on", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string ON", "ON", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string fast", "fast", nil, CompressionS2Fast, nil},
|
|
{"string Fast", "Fast", nil, CompressionS2Fast, nil},
|
|
{"string s2_fast", "s2_fast", nil, CompressionS2Fast, nil},
|
|
{"string s2_Fast", "s2_Fast", nil, CompressionS2Fast, nil},
|
|
{"boolean disabled", "false", nil, CompressionOff, nil},
|
|
{"string disabled", "disabled", nil, CompressionOff, nil},
|
|
{"string DisableD", "DisableD", nil, CompressionOff, nil},
|
|
{"string off", "off", nil, CompressionOff, nil},
|
|
{"string OFF", "OFF", nil, CompressionOff, nil},
|
|
{"better", "better", nil, CompressionS2Better, nil},
|
|
{"Better", "Better", nil, CompressionS2Better, nil},
|
|
{"s2_better", "s2_better", nil, CompressionS2Better, nil},
|
|
{"S2_BETTER", "S2_BETTER", nil, CompressionS2Better, nil},
|
|
{"best", "best", nil, CompressionS2Best, nil},
|
|
{"BEST", "BEST", nil, CompressionS2Best, nil},
|
|
{"s2_best", "s2_best", nil, CompressionS2Best, nil},
|
|
{"S2_BEST", "S2_BEST", nil, CompressionS2Best, nil},
|
|
{"auto no rtts", "auto", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"s2_auto no rtts", "s2_auto", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"auto", "{mode: auto, rtt_thresholds: [%s]}", []int{1}, CompressionS2Auto, []time.Duration{time.Millisecond}},
|
|
{"Auto", "{Mode: Auto, thresholds: [%s]}", []int{1, 2}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond}},
|
|
{"s2_auto", "{mode: s2_auto, thresholds: [%s]}", []int{1, 2, 3}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond}},
|
|
{"s2_AUTO", "{mode: s2_AUTO, thresholds: [%s]}", []int{1, 2, 3, 4}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond, 4 * time.Millisecond}},
|
|
{"s2_auto:-10,5,10", "{mode: s2_auto, thresholds: [%s]}", []int{-10, 5, 10}, CompressionS2Auto, []time.Duration{0, 5 * time.Millisecond, 10 * time.Millisecond}},
|
|
{"s2_auto:5,10,15", "{mode: s2_auto, thresholds: [%s]}", []int{5, 10, 15}, CompressionS2Auto, []time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 15 * time.Millisecond}},
|
|
{"s2_auto:0,5,10", "{mode: s2_auto, thresholds: [%s]}", []int{0, 5, 10}, CompressionS2Auto, []time.Duration{0, 5 * time.Millisecond, 10 * time.Millisecond}},
|
|
{"s2_auto:5,10,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{5, 10, 0, 20}, CompressionS2Auto, []time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 0, 20 * time.Millisecond}},
|
|
{"s2_auto:0,10,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{0, 10, 0, 20}, CompressionS2Auto, []time.Duration{0, 10 * time.Millisecond, 0, 20 * time.Millisecond}},
|
|
{"s2_auto:0,0,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{0, 0, 0, 20}, CompressionS2Auto, []time.Duration{0, 0, 0, 20 * time.Millisecond}},
|
|
{"s2_auto:0,10,0,0", "{mode: s2_auto, rtt_thresholds: [%s]}", []int{0, 10, 0, 0}, CompressionS2Auto, []time.Duration{0, 10 * time.Millisecond}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var val string
|
|
if len(test.rttVals) > 0 {
|
|
var rtts string
|
|
for i, v := range test.rttVals {
|
|
if i > 0 {
|
|
rtts += ", "
|
|
}
|
|
rtts += fmt.Sprintf("%dms", v)
|
|
}
|
|
val = fmt.Sprintf(test.mode, rtts)
|
|
} else {
|
|
val = test.mode
|
|
}
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, val)))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
if cm := o.LeafNode.Compression.Mode; cm != test.expected {
|
|
t.Fatalf("Expected compression value to be %q, got %q", test.expected, cm)
|
|
}
|
|
if !reflect.DeepEqual(test.rtts, o.LeafNode.Compression.RTTThresholds) {
|
|
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", test.rtts, o.LeafNode.Compression.RTTThresholds)
|
|
}
|
|
s.Shutdown()
|
|
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.Compression.Mode = test.mode
|
|
if len(test.rttVals) > 0 {
|
|
o.LeafNode.Compression.Mode = CompressionS2Auto
|
|
o.LeafNode.Compression.RTTThresholds = o.LeafNode.Compression.RTTThresholds[:0]
|
|
for _, v := range test.rttVals {
|
|
o.LeafNode.Compression.RTTThresholds = append(o.LeafNode.Compression.RTTThresholds, time.Duration(v)*time.Millisecond)
|
|
}
|
|
}
|
|
s = RunServer(o)
|
|
defer s.Shutdown()
|
|
if cm := o.LeafNode.Compression.Mode; cm != test.expected {
|
|
t.Fatalf("Expected compression value to be %q, got %q", test.expected, cm)
|
|
}
|
|
if !reflect.DeepEqual(test.rtts, o.LeafNode.Compression.RTTThresholds) {
|
|
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", test.rtts, o.LeafNode.Compression.RTTThresholds)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Same, but with remotes
|
|
tmpl = `
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
remotes [
|
|
{
|
|
url: "nats://127.0.0.1:1234"
|
|
compression: %s
|
|
}
|
|
]
|
|
}
|
|
`
|
|
for _, test := range []struct {
|
|
name string
|
|
mode string
|
|
rttVals []int
|
|
expected string
|
|
rtts []time.Duration
|
|
}{
|
|
{"boolean enabled", "true", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string enabled", "enabled", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string EnaBled", "EnaBled", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string on", "on", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string ON", "ON", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"string fast", "fast", nil, CompressionS2Fast, nil},
|
|
{"string Fast", "Fast", nil, CompressionS2Fast, nil},
|
|
{"string s2_fast", "s2_fast", nil, CompressionS2Fast, nil},
|
|
{"string s2_Fast", "s2_Fast", nil, CompressionS2Fast, nil},
|
|
{"boolean disabled", "false", nil, CompressionOff, nil},
|
|
{"string disabled", "disabled", nil, CompressionOff, nil},
|
|
{"string DisableD", "DisableD", nil, CompressionOff, nil},
|
|
{"string off", "off", nil, CompressionOff, nil},
|
|
{"string OFF", "OFF", nil, CompressionOff, nil},
|
|
{"better", "better", nil, CompressionS2Better, nil},
|
|
{"Better", "Better", nil, CompressionS2Better, nil},
|
|
{"s2_better", "s2_better", nil, CompressionS2Better, nil},
|
|
{"S2_BETTER", "S2_BETTER", nil, CompressionS2Better, nil},
|
|
{"best", "best", nil, CompressionS2Best, nil},
|
|
{"BEST", "BEST", nil, CompressionS2Best, nil},
|
|
{"s2_best", "s2_best", nil, CompressionS2Best, nil},
|
|
{"S2_BEST", "S2_BEST", nil, CompressionS2Best, nil},
|
|
{"auto no rtts", "auto", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"s2_auto no rtts", "s2_auto", nil, CompressionS2Auto, defaultCompressionS2AutoRTTThresholds},
|
|
{"auto", "{mode: auto, rtt_thresholds: [%s]}", []int{1}, CompressionS2Auto, []time.Duration{time.Millisecond}},
|
|
{"Auto", "{Mode: Auto, thresholds: [%s]}", []int{1, 2}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond}},
|
|
{"s2_auto", "{mode: s2_auto, thresholds: [%s]}", []int{1, 2, 3}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond}},
|
|
{"s2_AUTO", "{mode: s2_AUTO, thresholds: [%s]}", []int{1, 2, 3, 4}, CompressionS2Auto, []time.Duration{time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond, 4 * time.Millisecond}},
|
|
{"s2_auto:-10,5,10", "{mode: s2_auto, thresholds: [%s]}", []int{-10, 5, 10}, CompressionS2Auto, []time.Duration{0, 5 * time.Millisecond, 10 * time.Millisecond}},
|
|
{"s2_auto:5,10,15", "{mode: s2_auto, thresholds: [%s]}", []int{5, 10, 15}, CompressionS2Auto, []time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 15 * time.Millisecond}},
|
|
{"s2_auto:0,5,10", "{mode: s2_auto, thresholds: [%s]}", []int{0, 5, 10}, CompressionS2Auto, []time.Duration{0, 5 * time.Millisecond, 10 * time.Millisecond}},
|
|
{"s2_auto:5,10,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{5, 10, 0, 20}, CompressionS2Auto, []time.Duration{5 * time.Millisecond, 10 * time.Millisecond, 0, 20 * time.Millisecond}},
|
|
{"s2_auto:0,10,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{0, 10, 0, 20}, CompressionS2Auto, []time.Duration{0, 10 * time.Millisecond, 0, 20 * time.Millisecond}},
|
|
{"s2_auto:0,0,0,20", "{mode: s2_auto, thresholds: [%s]}", []int{0, 0, 0, 20}, CompressionS2Auto, []time.Duration{0, 0, 0, 20 * time.Millisecond}},
|
|
{"s2_auto:0,10,0,0", "{mode: s2_auto, rtt_thresholds: [%s]}", []int{0, 10, 0, 0}, CompressionS2Auto, []time.Duration{0, 10 * time.Millisecond}},
|
|
} {
|
|
t.Run("remote leaf "+test.name, func(t *testing.T) {
|
|
var val string
|
|
if len(test.rttVals) > 0 {
|
|
var rtts string
|
|
for i, v := range test.rttVals {
|
|
if i > 0 {
|
|
rtts += ", "
|
|
}
|
|
rtts += fmt.Sprintf("%dms", v)
|
|
}
|
|
val = fmt.Sprintf(test.mode, rtts)
|
|
} else {
|
|
val = test.mode
|
|
}
|
|
conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, val)))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
r := o.LeafNode.Remotes[0]
|
|
|
|
if cm := r.Compression.Mode; cm != test.expected {
|
|
t.Fatalf("Expected compression value to be %q, got %q", test.expected, cm)
|
|
}
|
|
if !reflect.DeepEqual(test.rtts, r.Compression.RTTThresholds) {
|
|
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", test.rtts, r.Compression.RTTThresholds)
|
|
}
|
|
s.Shutdown()
|
|
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.Remotes[0].Compression.Mode = test.mode
|
|
if len(test.rttVals) > 0 {
|
|
o.LeafNode.Remotes[0].Compression.Mode = CompressionS2Auto
|
|
o.LeafNode.Remotes[0].Compression.RTTThresholds = o.LeafNode.Remotes[0].Compression.RTTThresholds[:0]
|
|
for _, v := range test.rttVals {
|
|
o.LeafNode.Remotes[0].Compression.RTTThresholds = append(o.LeafNode.Remotes[0].Compression.RTTThresholds, time.Duration(v)*time.Millisecond)
|
|
}
|
|
}
|
|
s = RunServer(o)
|
|
defer s.Shutdown()
|
|
if cm := o.LeafNode.Remotes[0].Compression.Mode; cm != test.expected {
|
|
t.Fatalf("Expected compression value to be %q, got %q", test.expected, cm)
|
|
}
|
|
if !reflect.DeepEqual(test.rtts, o.LeafNode.Remotes[0].Compression.RTTThresholds) {
|
|
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", test.rtts, o.LeafNode.Remotes[0].Compression.RTTThresholds)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test that with no compression specified, we default to "s2_auto"
|
|
conf := createConfFile(t, []byte(`
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
if o.LeafNode.Compression.Mode != CompressionS2Auto {
|
|
t.Fatalf("Expected compression value to be %q, got %q", CompressionAccept, o.LeafNode.Compression.Mode)
|
|
}
|
|
if !reflect.DeepEqual(defaultCompressionS2AutoRTTThresholds, o.LeafNode.Compression.RTTThresholds) {
|
|
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", defaultCompressionS2AutoRTTThresholds, o.LeafNode.Compression.RTTThresholds)
|
|
}
|
|
// Same for remotes
|
|
conf = createConfFile(t, []byte(`
|
|
port: -1
|
|
leafnodes {
|
|
port: -1
|
|
remotes [ { url: "nats://127.0.0.1:1234" } ]
|
|
}
|
|
`))
|
|
s, o = RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
if cm := o.LeafNode.Remotes[0].Compression.Mode; cm != CompressionS2Auto {
|
|
t.Fatalf("Expected compression value to be %q, got %q", CompressionAccept, cm)
|
|
}
|
|
if !reflect.DeepEqual(defaultCompressionS2AutoRTTThresholds, o.LeafNode.Remotes[0].Compression.RTTThresholds) {
|
|
t.Fatalf("Expected RTT tresholds to be %+v, got %+v", defaultCompressionS2AutoRTTThresholds, o.LeafNode.Remotes[0].Compression.RTTThresholds)
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
mode string
|
|
rtts []time.Duration
|
|
err string
|
|
}{
|
|
{"unsupported mode", "gzip", nil, "unsupported"},
|
|
{"not ascending order", "s2_auto", []time.Duration{
|
|
5 * time.Millisecond,
|
|
10 * time.Millisecond,
|
|
2 * time.Millisecond,
|
|
}, "ascending"},
|
|
{"too many thresholds", "s2_auto", []time.Duration{
|
|
5 * time.Millisecond,
|
|
10 * time.Millisecond,
|
|
20 * time.Millisecond,
|
|
40 * time.Millisecond,
|
|
60 * time.Millisecond,
|
|
}, "more than 4"},
|
|
{"all 0", "s2_auto", []time.Duration{0, 0, 0, 0}, "at least one"},
|
|
{"single 0", "s2_auto", []time.Duration{0}, "at least one"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
o := DefaultOptions()
|
|
o.LeafNode.Port = -1
|
|
o.LeafNode.Compression = CompressionOpts{test.mode, test.rtts}
|
|
if _, err := NewServer(o); err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
// Same with remotes
|
|
o.LeafNode.Compression = CompressionOpts{}
|
|
o.LeafNode.Remotes = []*RemoteLeafOpts{{Compression: CompressionOpts{test.mode, test.rtts}}}
|
|
if _, err := NewServer(o); err == nil || !strings.Contains(err.Error(), test.err) {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeCompression(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: "Hub"
|
|
accounts {
|
|
A { users: [{user: a, password: pwd}] }
|
|
B { users: [{user: b, password: pwd}] }
|
|
C { users: [{user: c, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
compression: s2_fast
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
port := o1.LeafNode.Port
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "Spoke"
|
|
accounts {
|
|
A { users: [{user: a, password: pwd}] }
|
|
B { users: [{user: b, password: pwd}] }
|
|
C { users: [{user: c, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{ url: "nats://a:[email protected]:%d", account: "A", compression: s2_better }
|
|
{ url: "nats://b:[email protected]:%d", account: "B", compression: s2_best }
|
|
{ url: "nats://c:[email protected]:%d", account: "C", compression: off }
|
|
]
|
|
}
|
|
`, port, port, port)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(t, s1, 3)
|
|
checkLeafNodeConnectedCount(t, s2, 3)
|
|
|
|
s1.mu.RLock()
|
|
for _, l := range s1.leafs {
|
|
l.mu.Lock()
|
|
l.nc = &testConnSentBytes{Conn: l.nc}
|
|
l.mu.Unlock()
|
|
}
|
|
s1.mu.RUnlock()
|
|
|
|
var payloads [][]byte
|
|
totalPayloadSize := 0
|
|
count := 26
|
|
for i := 0; i < count; i++ {
|
|
n := rand.Intn(2048) + 1
|
|
p := make([]byte, n)
|
|
for j := 0; j < n; j++ {
|
|
p[j] = byte(i) + 'A'
|
|
}
|
|
totalPayloadSize += len(p)
|
|
payloads = append(payloads, p)
|
|
}
|
|
|
|
check := func(acc, user, subj string) {
|
|
t.Helper()
|
|
nc2 := natsConnect(t, s2.ClientURL(), nats.UserInfo(user, "pwd"))
|
|
defer nc2.Close()
|
|
sub := natsSubSync(t, nc2, subj)
|
|
natsFlush(t, nc2)
|
|
checkSubInterest(t, s1, acc, subj, time.Second)
|
|
|
|
nc1 := natsConnect(t, s1.ClientURL(), nats.UserInfo(user, "pwd"))
|
|
defer nc1.Close()
|
|
|
|
for i := 0; i < count; i++ {
|
|
natsPub(t, nc1, subj, payloads[i])
|
|
}
|
|
for i := 0; i < count; i++ {
|
|
m := natsNexMsg(t, sub, time.Second)
|
|
if !bytes.Equal(m.Data, payloads[i]) {
|
|
t.Fatalf("Expected payload %q - got %q", payloads[i], m.Data)
|
|
}
|
|
}
|
|
|
|
// Also check that the leafnode stats shows that compression likely occurred
|
|
var out int
|
|
s1.mu.RLock()
|
|
for _, l := range s1.leafs {
|
|
l.mu.Lock()
|
|
if l.acc.Name == acc && l.nc != nil {
|
|
nc := l.nc.(*testConnSentBytes)
|
|
nc.Lock()
|
|
out = nc.sent
|
|
nc.sent = 0
|
|
nc.Unlock()
|
|
}
|
|
l.mu.Unlock()
|
|
}
|
|
s1.mu.RUnlock()
|
|
// Except for account "C", where compression should be off,
|
|
// "out" should at least be smaller than totalPayloadSize, use 20%.
|
|
if acc == "C" {
|
|
if int(out) < totalPayloadSize {
|
|
t.Fatalf("Expected s1's sent bytes to be at least payload size (%v), got %v", totalPayloadSize, out)
|
|
}
|
|
} else {
|
|
limit := totalPayloadSize * 80 / 100
|
|
if int(out) > limit {
|
|
t.Fatalf("Expected s1's sent bytes to be less than %v, got %v (total payload was %v)", limit, out, totalPayloadSize)
|
|
}
|
|
}
|
|
}
|
|
check("A", "a", "foo")
|
|
check("B", "b", "bar")
|
|
check("C", "c", "baz")
|
|
|
|
// Check compression settings. S1 should always be s2_fast, except for account "C"
|
|
// since "C" wanted compression "off"
|
|
l, err := s1.Leafz(nil)
|
|
require_NoError(t, err)
|
|
for _, r := range l.Leafs {
|
|
switch r.Account {
|
|
case "C":
|
|
if r.Compression != CompressionOff {
|
|
t.Fatalf("Expected compression of remote for C account to be %q, got %q", CompressionOff, r.Compression)
|
|
}
|
|
default:
|
|
if r.Compression != CompressionS2Fast {
|
|
t.Fatalf("Expected compression of remote for %s account to be %q, got %q", r.Account, CompressionS2Fast, r.Compression)
|
|
}
|
|
}
|
|
}
|
|
|
|
l, err = s2.Leafz(nil)
|
|
require_NoError(t, err)
|
|
for _, r := range l.Leafs {
|
|
switch r.Account {
|
|
case "A":
|
|
if r.Compression != CompressionS2Better {
|
|
t.Fatalf("Expected compression for A account to be %q, got %q", CompressionS2Better, r.Compression)
|
|
}
|
|
case "B":
|
|
if r.Compression != CompressionS2Best {
|
|
t.Fatalf("Expected compression for B account to be %q, got %q", CompressionS2Best, r.Compression)
|
|
}
|
|
case "C":
|
|
if r.Compression != CompressionOff {
|
|
t.Fatalf("Expected compression for C account to be %q, got %q", CompressionOff, r.Compression)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkLeafNodeCompression(b *testing.B) {
|
|
conf1 := createConfFile(b, []byte(`
|
|
port: -1
|
|
server_name: "Hub"
|
|
accounts {
|
|
A { users: [{user: a, password: pwd}] }
|
|
B { users: [{user: b, password: pwd}] }
|
|
C { users: [{user: c, password: pwd}] }
|
|
D { users: [{user: d, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
port := o1.LeafNode.Port
|
|
conf2 := createConfFile(b, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "Spoke"
|
|
accounts {
|
|
A { users: [{user: a, password: pwd}] }
|
|
B { users: [{user: b, password: pwd}] }
|
|
C { users: [{user: c, password: pwd}] }
|
|
D { users: [{user: d, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{ url: "nats://a:[email protected]:%d", account: "A", compression: s2_better }
|
|
{ url: "nats://b:[email protected]:%d", account: "B", compression: s2_best }
|
|
{ url: "nats://c:[email protected]:%d", account: "C", compression: s2_fast }
|
|
{ url: "nats://d:[email protected]:%d", account: "D", compression: off }
|
|
]
|
|
}
|
|
`, port, port, port, port)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnectedCount(b, s1, 4)
|
|
checkLeafNodeConnectedCount(b, s2, 4)
|
|
|
|
l, err := s2.Leafz(nil)
|
|
require_NoError(b, err)
|
|
for _, r := range l.Leafs {
|
|
switch {
|
|
case r.Account == "A" && r.Compression == CompressionS2Better:
|
|
case r.Account == "B" && r.Compression == CompressionS2Best:
|
|
case r.Account == "C" && r.Compression == CompressionS2Fast:
|
|
case r.Account == "D" && r.Compression == CompressionOff:
|
|
default:
|
|
b.Fatalf("Account %q had incorrect compression mode %q on leaf connection", r.Account, r.Compression)
|
|
}
|
|
}
|
|
|
|
msg := make([]byte, 1024)
|
|
for _, p := range []struct {
|
|
algo string
|
|
user string
|
|
}{
|
|
{"Better", "a"},
|
|
{"Best", "b"},
|
|
{"Fast", "c"},
|
|
{"Off", "d"},
|
|
} {
|
|
nc1 := natsConnect(b, s1.ClientURL(), nats.UserInfo(p.user, "pwd"))
|
|
nc2 := natsConnect(b, s2.ClientURL(), nats.UserInfo(p.user, "pwd"))
|
|
|
|
sub, err := nc1.SubscribeSync("foo")
|
|
require_NoError(b, err)
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
b.Run(p.algo, func(b *testing.B) {
|
|
start := time.Now()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
err = nc2.Publish("foo", msg)
|
|
require_NoError(b, err)
|
|
|
|
_, err = sub.NextMsg(time.Second)
|
|
require_NoError(b, err)
|
|
}
|
|
|
|
b.ReportMetric(float64(len(msg)*b.N)/1024/1024, "MB")
|
|
b.ReportMetric(float64(len(msg)*b.N)/1024/1024/float64(time.Since(start).Seconds()), "MB/sec")
|
|
})
|
|
|
|
nc1.Close()
|
|
nc2.Close()
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeCompressionMatrixModes(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
s1 string
|
|
s2 string
|
|
s1Expected string
|
|
s2Expected string
|
|
}{
|
|
{"off off", "off", "off", CompressionOff, CompressionOff},
|
|
{"off accept", "off", "accept", CompressionOff, CompressionOff},
|
|
{"off on", "off", "on", CompressionOff, CompressionOff},
|
|
{"off better", "off", "better", CompressionOff, CompressionOff},
|
|
{"off best", "off", "best", CompressionOff, CompressionOff},
|
|
|
|
{"accept off", "accept", "off", CompressionOff, CompressionOff},
|
|
{"accept accept", "accept", "accept", CompressionOff, CompressionOff},
|
|
// Note: "on", means s2_auto, which will mean uncompressed since RTT is low.
|
|
{"accept on", "accept", "on", CompressionS2Fast, CompressionS2Uncompressed},
|
|
{"accept better", "accept", "better", CompressionS2Better, CompressionS2Better},
|
|
{"accept best", "accept", "best", CompressionS2Best, CompressionS2Best},
|
|
|
|
{"on off", "on", "off", CompressionOff, CompressionOff},
|
|
{"on accept", "on", "accept", CompressionS2Uncompressed, CompressionS2Fast},
|
|
{"on on", "on", "on", CompressionS2Uncompressed, CompressionS2Uncompressed},
|
|
{"on better", "on", "better", CompressionS2Uncompressed, CompressionS2Better},
|
|
{"on best", "on", "best", CompressionS2Uncompressed, CompressionS2Best},
|
|
|
|
{"better off", "better", "off", CompressionOff, CompressionOff},
|
|
{"better accept", "better", "accept", CompressionS2Better, CompressionS2Better},
|
|
{"better on", "better", "on", CompressionS2Better, CompressionS2Uncompressed},
|
|
{"better better", "better", "better", CompressionS2Better, CompressionS2Better},
|
|
{"better best", "better", "best", CompressionS2Better, CompressionS2Best},
|
|
|
|
{"best off", "best", "off", CompressionOff, CompressionOff},
|
|
{"best accept", "best", "accept", CompressionS2Best, CompressionS2Best},
|
|
{"best on", "best", "on", CompressionS2Best, CompressionS2Uncompressed},
|
|
{"best better", "best", "better", CompressionS2Best, CompressionS2Better},
|
|
{"best best", "best", "best", CompressionS2Best, CompressionS2Best},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "A"
|
|
leafnodes {
|
|
port: -1
|
|
compression: %s
|
|
}
|
|
`, test.s1)))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "B"
|
|
leafnodes {
|
|
remotes: [
|
|
{url: "nats://127.0.0.1:%d", compression: %s}
|
|
]
|
|
}
|
|
`, o1.LeafNode.Port, test.s2)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
nc1 := natsConnect(t, s1.ClientURL())
|
|
defer nc1.Close()
|
|
|
|
nc2 := natsConnect(t, s2.ClientURL())
|
|
defer nc2.Close()
|
|
|
|
payload := make([]byte, 128)
|
|
check := func(ncp, ncs *nats.Conn, subj string, s *Server) {
|
|
t.Helper()
|
|
sub := natsSubSync(t, ncs, subj)
|
|
checkSubInterest(t, s, globalAccountName, subj, time.Second)
|
|
natsPub(t, ncp, subj, payload)
|
|
natsNexMsg(t, sub, time.Second)
|
|
|
|
for _, srv := range []*Server{s1, s2} {
|
|
lz, err := srv.Leafz(nil)
|
|
require_NoError(t, err)
|
|
var expected string
|
|
if srv == s1 {
|
|
expected = test.s1Expected
|
|
} else {
|
|
expected = test.s2Expected
|
|
}
|
|
if cm := lz.Leafs[0].Compression; cm != expected {
|
|
t.Fatalf("Server %s - expected compression %q, got %q", srv, expected, cm)
|
|
}
|
|
}
|
|
}
|
|
check(nc1, nc2, "foo", s1)
|
|
check(nc2, nc1, "bar", s2)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeCompressionWithOlderServer(t *testing.T) {
|
|
tmpl1 := `
|
|
port: -1
|
|
server_name: "A"
|
|
leafnodes {
|
|
port: -1
|
|
compression: "%s"
|
|
}
|
|
`
|
|
conf1 := createConfFile(t, []byte(fmt.Sprintf(tmpl1, CompressionS2Fast)))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
tmpl2 := `
|
|
port: -1
|
|
server_name: "B"
|
|
leafnodes {
|
|
remotes [
|
|
{url: "nats://127.0.0.1:%d", compression: "%s"}
|
|
]
|
|
}
|
|
`
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, CompressionNotSupported)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
getLeafCompMode := func(s *Server) string {
|
|
var cm string
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
for _, l := range s1.leafs {
|
|
l.mu.Lock()
|
|
cm = l.leaf.compression
|
|
l.mu.Unlock()
|
|
return cm
|
|
}
|
|
return _EMPTY_
|
|
}
|
|
for _, s := range []*Server{s1, s2} {
|
|
if cm := getLeafCompMode(s); cm != CompressionNotSupported {
|
|
t.Fatalf("Expected compression not supported, got %q", cm)
|
|
}
|
|
}
|
|
|
|
s2.Shutdown()
|
|
s1.Shutdown()
|
|
|
|
conf1 = createConfFile(t, []byte(fmt.Sprintf(tmpl1, CompressionNotSupported)))
|
|
s1, o1 = RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
conf2 = createConfFile(t, []byte(fmt.Sprintf(tmpl2, o1.LeafNode.Port, CompressionS2Fast)))
|
|
s2, _ = RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
for _, s := range []*Server{s1, s2} {
|
|
if cm := getLeafCompMode(s); cm != CompressionNotSupported {
|
|
t.Fatalf("Expected compression not supported, got %q", cm)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeCompressionAuto(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
s1Ping string
|
|
s1Compression string
|
|
s2Ping string
|
|
s2Compression string
|
|
checkS1 bool
|
|
}{
|
|
{"remote side", "10s", CompressionS2Fast, "100ms", "{mode: s2_auto, rtt_thresholds: [10ms, 20ms, 30ms]}", false},
|
|
{"accept side", "100ms", "{mode: s2_auto, rtt_thresholds: [10ms, 20ms, 30ms]}", "10s", CompressionS2Fast, true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "A"
|
|
ping_interval: "%s"
|
|
leafnodes {
|
|
port: -1
|
|
compression: %s
|
|
}
|
|
`, test.s1Ping, test.s1Compression)))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
// Start with 0ms RTT
|
|
np := createNetProxy(0, 1024*1024*1024, 1024*1024*1024, fmt.Sprintf("nats://127.0.0.1:%d", o1.LeafNode.Port), true)
|
|
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "B"
|
|
ping_interval: "%s"
|
|
leafnodes {
|
|
remotes [
|
|
{url: %s, compression %s}
|
|
]
|
|
}
|
|
`, test.s2Ping, np.routeURL(), test.s2Compression)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
defer np.stop()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
checkComp := func(expected string) {
|
|
t.Helper()
|
|
var s *Server
|
|
if test.checkS1 {
|
|
s = s1
|
|
} else {
|
|
s = s2
|
|
}
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
for _, l := range s.leafs {
|
|
l.mu.Lock()
|
|
cm := l.leaf.compression
|
|
l.mu.Unlock()
|
|
if cm != expected {
|
|
return fmt.Errorf("Leaf %v compression mode expected to be %q, got %q", l, expected, cm)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
checkComp(CompressionS2Uncompressed)
|
|
|
|
// Change the proxy RTT and we should get compression "fast"
|
|
np.updateRTT(15 * time.Millisecond)
|
|
checkComp(CompressionS2Fast)
|
|
|
|
// Now 25ms, and get "better"
|
|
np.updateRTT(25 * time.Millisecond)
|
|
checkComp(CompressionS2Better)
|
|
|
|
// Above 35 and we should get "best"
|
|
np.updateRTT(35 * time.Millisecond)
|
|
checkComp(CompressionS2Best)
|
|
|
|
// Down to 1ms and again should get "uncompressed"
|
|
np.updateRTT(1 * time.Millisecond)
|
|
checkComp(CompressionS2Uncompressed)
|
|
})
|
|
}
|
|
|
|
// Make sure that if compression is off on one side, the update of RTT does
|
|
// not trigger a compression change.
|
|
conf1 := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: "A"
|
|
leafnodes {
|
|
port: -1
|
|
compression: off
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
// Start with 0ms RTT
|
|
np := createNetProxy(0, 1024*1024*1024, 1024*1024*1024, fmt.Sprintf("nats://127.0.0.1:%d", o1.LeafNode.Port), true)
|
|
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "B"
|
|
ping_interval: "50ms"
|
|
leafnodes {
|
|
remotes [
|
|
{url: %s, compression s2_auto}
|
|
]
|
|
}
|
|
`, np.routeURL())))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
defer np.stop()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
// Even with a bug of updating compression level while it should have been
|
|
// off, the check done below would almost always pass because after
|
|
// reconnecting, there could be a chance to get at first compression set
|
|
// to "off". So we will double check that the leaf node CID did not change
|
|
// at the end of the test.
|
|
getCID := func() uint64 {
|
|
s2.mu.RLock()
|
|
defer s2.mu.RUnlock()
|
|
for _, l := range s2.leafs {
|
|
l.mu.Lock()
|
|
cid := l.cid
|
|
l.mu.Unlock()
|
|
return cid
|
|
}
|
|
return 0
|
|
}
|
|
oldCID := getCID()
|
|
|
|
checkCompOff := func() {
|
|
t.Helper()
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
s2.mu.RLock()
|
|
defer s2.mu.RUnlock()
|
|
if len(s2.leafs) != 1 {
|
|
return fmt.Errorf("Leaf not currently connected")
|
|
}
|
|
for _, l := range s2.leafs {
|
|
l.mu.Lock()
|
|
cm := l.leaf.compression
|
|
l.mu.Unlock()
|
|
if cm != CompressionOff {
|
|
return fmt.Errorf("Leaf %v compression mode expected to be %q, got %q", l, CompressionOff, cm)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
checkCompOff()
|
|
|
|
// Now change RTT and again, make sure that it is still off
|
|
np.updateRTT(20 * time.Millisecond)
|
|
time.Sleep(100 * time.Millisecond)
|
|
checkCompOff()
|
|
if cid := getCID(); cid != oldCID {
|
|
t.Fatalf("Leafnode has reconnected, cid was %v, now %v", oldCID, cid)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeCompressionWithWSCompression(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: "A"
|
|
websocket {
|
|
port: -1
|
|
no_tls: true
|
|
compression: true
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
compression: s2_fast
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "B"
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "ws://127.0.0.1:%d"
|
|
ws_compression: true
|
|
compression: s2_fast
|
|
}
|
|
]
|
|
}
|
|
`, o1.Websocket.Port)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
nc1 := natsConnect(t, s1.ClientURL())
|
|
defer nc1.Close()
|
|
|
|
sub := natsSubSync(t, nc1, "foo")
|
|
checkSubInterest(t, s2, globalAccountName, "foo", time.Second)
|
|
|
|
nc2 := natsConnect(t, s2.ClientURL())
|
|
defer nc2.Close()
|
|
|
|
payload := make([]byte, 1024)
|
|
for i := 0; i < len(payload); i++ {
|
|
payload[i] = 'A'
|
|
}
|
|
natsPub(t, nc2, "foo", payload)
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
require_True(t, len(msg.Data) == 1024)
|
|
for i := 0; i < len(msg.Data); i++ {
|
|
if msg.Data[i] != 'A' {
|
|
t.Fatalf("Invalid msg: %s", msg.Data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeCompressionWithWSGetNeedsData(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: "A"
|
|
websocket {
|
|
port: -1
|
|
no_tls: true
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
compression: s2_fast
|
|
}
|
|
`))
|
|
srv1, o1 := RunServerWithConfig(conf1)
|
|
defer srv1.Shutdown()
|
|
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "B"
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "ws://127.0.0.1:%d"
|
|
ws_no_masking: true
|
|
compression: s2_fast
|
|
}
|
|
]
|
|
}
|
|
`, o1.Websocket.Port)))
|
|
srv2, _ := RunServerWithConfig(conf2)
|
|
defer srv2.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, srv2)
|
|
|
|
nc1 := natsConnect(t, srv1.ClientURL())
|
|
defer nc1.Close()
|
|
|
|
sub := natsSubSync(t, nc1, "foo")
|
|
checkSubInterest(t, srv2, globalAccountName, "foo", time.Second)
|
|
|
|
// We want to have the payload more than 126 bytes so that the websocket
|
|
// code need to read 2 bytes for the length. See below.
|
|
payload := "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
sentBytes := []byte("LMSG foo 156\r\n" + payload + "\r\n")
|
|
h, _ := wsCreateFrameHeader(false, false, wsBinaryMessage, len(sentBytes))
|
|
combined := &bytes.Buffer{}
|
|
combined.Write(h)
|
|
combined.Write(sentBytes)
|
|
toSend := combined.Bytes()
|
|
|
|
// We will make a compressed block that cuts the websocket header that
|
|
// makes the reader want to read bytes directly from the connection.
|
|
// We want to make sure that we are not going to get compressed data
|
|
// without going through the (de)compress library. So for that, compress
|
|
// the first 3 bytes.
|
|
b := &bytes.Buffer{}
|
|
w := s2.NewWriter(b)
|
|
w.Write(toSend[:3])
|
|
w.Close()
|
|
|
|
var nc net.Conn
|
|
srv2.mu.RLock()
|
|
for _, l := range srv2.leafs {
|
|
l.mu.Lock()
|
|
nc = l.nc
|
|
l.mu.Unlock()
|
|
}
|
|
srv2.mu.RUnlock()
|
|
|
|
nc.Write(b.Bytes())
|
|
|
|
// Pause to make sure other side just gets a partial of the whole WS frame.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
b.Reset()
|
|
w.Reset(b)
|
|
w.Write(toSend[3:])
|
|
w.Close()
|
|
|
|
nc.Write(b.Bytes())
|
|
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
require_True(t, len(msg.Data) == 156)
|
|
require_Equal(t, string(msg.Data), payload)
|
|
}
|
|
|
|
func TestLeafNodeCompressionAuthTimeout(t *testing.T) {
|
|
hconf := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: "hub"
|
|
leafnodes {
|
|
port: -1
|
|
authorization {
|
|
timeout: 0.75
|
|
}
|
|
}
|
|
`))
|
|
sh, oh := RunServerWithConfig(hconf)
|
|
defer sh.Shutdown()
|
|
|
|
sconfTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
cluster {
|
|
port: -1
|
|
name: "spoke"
|
|
%s
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
remotes [
|
|
{ url: "nats://127.0.0.1:%d" }
|
|
]
|
|
}
|
|
`
|
|
s1conf := createConfFile(t, []byte(fmt.Sprintf(sconfTmpl, "SP1", _EMPTY_, oh.LeafNode.Port)))
|
|
s1, o1 := RunServerWithConfig(s1conf)
|
|
defer s1.Shutdown()
|
|
|
|
s2conf := createConfFile(t, []byte(fmt.Sprintf(sconfTmpl, "SP2", fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", o1.Cluster.Port), oh.LeafNode.Port)))
|
|
s2, _ := RunServerWithConfig(s2conf)
|
|
defer s2.Shutdown()
|
|
|
|
checkClusterFormed(t, s1, s2)
|
|
|
|
checkLeafNodeConnected(t, s1)
|
|
checkLeafNodeConnected(t, s2)
|
|
|
|
getCID := func(s *Server) uint64 {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
var cid uint64
|
|
for _, l := range s.leafs {
|
|
l.mu.Lock()
|
|
cid = l.cid
|
|
l.mu.Unlock()
|
|
}
|
|
return cid
|
|
}
|
|
leaf1 := getCID(s1)
|
|
leaf2 := getCID(s2)
|
|
|
|
// Wait for more than auth timeout
|
|
time.Sleep(time.Second)
|
|
|
|
checkLeafNodeConnected(t, s1)
|
|
checkLeafNodeConnected(t, s2)
|
|
if l1 := getCID(s1); l1 != leaf1 {
|
|
t.Fatalf("Leaf connection first connection had CID %v, now %v", leaf1, l1)
|
|
}
|
|
if l2 := getCID(s2); l2 != leaf2 {
|
|
t.Fatalf("Leaf connection first connection had CID %v, now %v", leaf2, l2)
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeWithWeightedDQRequestsToSuperClusterWithSeparateAccounts(t *testing.T) {
|
|
sc := createJetStreamSuperClusterWithTemplate(t, jsClusterAccountsTempl, 3, 2)
|
|
defer sc.shutdown()
|
|
|
|
// Now create a leafnode cluster that has 2 LNs, one to each cluster but on separate accounts, ONE and TWO.
|
|
var lnTmpl = `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
{{leaf}}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }}
|
|
`
|
|
|
|
var leafFrag = `
|
|
leaf {
|
|
listen: 127.0.0.1:-1
|
|
remotes [
|
|
{ urls: [ %s ] }
|
|
{ urls: [ %s ] }
|
|
]
|
|
}`
|
|
|
|
// We want to have two leaf node connections that join to the same local account on the leafnode servers,
|
|
// but connect to different accounts in different clusters.
|
|
c1 := sc.clusters[0] // Will connect to account ONE
|
|
c2 := sc.clusters[1] // Will connect to account TWO
|
|
|
|
genLeafTmpl := func(tmpl string) string {
|
|
t.Helper()
|
|
|
|
var ln1, ln2 []string
|
|
for _, s := range c1.servers {
|
|
if s.ClusterName() != c1.name {
|
|
continue
|
|
}
|
|
ln := s.getOpts().LeafNode
|
|
ln1 = append(ln1, fmt.Sprintf("nats://one:p@%s:%d", ln.Host, ln.Port))
|
|
}
|
|
|
|
for _, s := range c2.servers {
|
|
if s.ClusterName() != c2.name {
|
|
continue
|
|
}
|
|
ln := s.getOpts().LeafNode
|
|
ln2 = append(ln2, fmt.Sprintf("nats://two:p@%s:%d", ln.Host, ln.Port))
|
|
}
|
|
return strings.Replace(tmpl, "{{leaf}}", fmt.Sprintf(leafFrag, strings.Join(ln1, ", "), strings.Join(ln2, ", ")), 1)
|
|
}
|
|
|
|
tmpl := strings.Replace(lnTmpl, "store_dir:", fmt.Sprintf(`domain: "%s", store_dir:`, "SA"), 1)
|
|
tmpl = genLeafTmpl(tmpl)
|
|
|
|
ln := createJetStreamCluster(t, tmpl, "SA", "SA-", 3, 22280, false)
|
|
ln.waitOnClusterReady()
|
|
defer ln.shutdown()
|
|
|
|
for _, s := range ln.servers {
|
|
checkLeafNodeConnectedCount(t, s, 2)
|
|
}
|
|
|
|
// Now connect DQ subscribers to each cluster and they separate accounts, and make sure we get the right behavior, balanced between
|
|
// them when requests originate from the leaf cluster.
|
|
|
|
// Create 5 clients for each cluster / account
|
|
var c1c, c2c []*nats.Conn
|
|
for i := 0; i < 5; i++ {
|
|
nc1, _ := jsClientConnect(t, c1.randomServer(), nats.UserInfo("one", "p"))
|
|
defer nc1.Close()
|
|
c1c = append(c1c, nc1)
|
|
nc2, _ := jsClientConnect(t, c2.randomServer(), nats.UserInfo("two", "p"))
|
|
defer nc2.Close()
|
|
c2c = append(c2c, nc2)
|
|
}
|
|
|
|
createSubs := func(num int, conns []*nats.Conn) (subs []*nats.Subscription) {
|
|
for i := 0; i < num; i++ {
|
|
nc := conns[rand.Intn(len(conns))]
|
|
sub, err := nc.QueueSubscribeSync("REQUEST", "MC")
|
|
require_NoError(t, err)
|
|
subs = append(subs, sub)
|
|
nc.Flush()
|
|
}
|
|
// Let subs propagate.
|
|
time.Sleep(100 * time.Millisecond)
|
|
return subs
|
|
}
|
|
closeSubs := func(subs []*nats.Subscription) {
|
|
for _, sub := range subs {
|
|
sub.Unsubscribe()
|
|
}
|
|
}
|
|
|
|
// Simple test first.
|
|
subs1 := createSubs(1, c1c)
|
|
defer closeSubs(subs1)
|
|
subs2 := createSubs(1, c2c)
|
|
defer closeSubs(subs2)
|
|
|
|
sendRequests := func(num int) {
|
|
t.Helper()
|
|
// Now connect to the leaf cluster and send some requests.
|
|
nc, _ := jsClientConnect(t, ln.randomServer())
|
|
defer nc.Close()
|
|
|
|
for i := 0; i < num; i++ {
|
|
require_NoError(t, nc.Publish("REQUEST", []byte("HELP")))
|
|
}
|
|
nc.Flush()
|
|
}
|
|
|
|
pending := func(subs []*nats.Subscription) (total int) {
|
|
t.Helper()
|
|
for _, sub := range subs {
|
|
n, _, err := sub.Pending()
|
|
require_NoError(t, err)
|
|
total += n
|
|
}
|
|
return total
|
|
}
|
|
|
|
num := 1000
|
|
checkAllReceived := func() error {
|
|
total := pending(subs1) + pending(subs2)
|
|
if total == num {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all received: %d vs %d", total, num)
|
|
}
|
|
|
|
checkBalanced := func(total, pc1, pc2 int) {
|
|
t.Helper()
|
|
tf := float64(total)
|
|
e1 := tf * (float64(pc1) / 100.00)
|
|
e2 := tf * (float64(pc2) / 100.00)
|
|
delta := tf / 10
|
|
p1 := float64(pending(subs1))
|
|
if p1 < e1-delta || p1 > e1+delta {
|
|
t.Fatalf("Value out of range for subs1, expected %v got %v", e1, p1)
|
|
}
|
|
p2 := float64(pending(subs2))
|
|
if p2 < e2-delta || p2 > e2+delta {
|
|
t.Fatalf("Value out of range for subs2, expected %v got %v", e2, p2)
|
|
}
|
|
}
|
|
|
|
// Now connect to the leaf cluster and send some requests.
|
|
|
|
// Simple 50/50
|
|
sendRequests(num)
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
|
|
checkBalanced(num, 50, 50)
|
|
|
|
closeSubs(subs1)
|
|
closeSubs(subs2)
|
|
|
|
// Now test unbalanced. 10/90
|
|
subs1 = createSubs(1, c1c)
|
|
defer closeSubs(subs1)
|
|
subs2 = createSubs(9, c2c)
|
|
defer closeSubs(subs2)
|
|
|
|
sendRequests(num)
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
|
|
checkBalanced(num, 10, 90)
|
|
|
|
// Now test draining the subs as we are sending from an initial balanced situation simulating a draining of a cluster.
|
|
|
|
closeSubs(subs1)
|
|
closeSubs(subs2)
|
|
subs1, subs2 = nil, nil
|
|
|
|
// These subs slightly different.
|
|
var r1, r2 atomic.Uint64
|
|
for i := 0; i < 20; i++ {
|
|
nc := c1c[rand.Intn(len(c1c))]
|
|
sub, err := nc.QueueSubscribe("REQUEST", "MC", func(m *nats.Msg) { r1.Add(1) })
|
|
require_NoError(t, err)
|
|
subs1 = append(subs1, sub)
|
|
nc.Flush()
|
|
|
|
nc = c2c[rand.Intn(len(c2c))]
|
|
sub, err = nc.QueueSubscribe("REQUEST", "MC", func(m *nats.Msg) { r2.Add(1) })
|
|
require_NoError(t, err)
|
|
subs2 = append(subs2, sub)
|
|
nc.Flush()
|
|
}
|
|
defer closeSubs(subs1)
|
|
defer closeSubs(subs2)
|
|
|
|
nc, _ := jsClientConnect(t, ln.randomServer())
|
|
defer nc.Close()
|
|
|
|
for i, dindex := 0, 1; i < num; i++ {
|
|
require_NoError(t, nc.Publish("REQUEST", []byte("HELP")))
|
|
// Check if we have more to simulate draining.
|
|
// Will drain within first ~100 requests using 20% rand test below.
|
|
// Will leave 1 behind.
|
|
if dindex < len(subs1)-1 && rand.Intn(6) > 4 {
|
|
sub := subs1[dindex]
|
|
dindex++
|
|
sub.Drain()
|
|
}
|
|
}
|
|
nc.Flush()
|
|
|
|
checkFor(t, time.Second, 200*time.Millisecond, func() error {
|
|
total := int(r1.Load() + r2.Load())
|
|
if total == num {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all received: %d vs %d", total, num)
|
|
})
|
|
require_True(t, r2.Load() > r1.Load())
|
|
}
|
|
|
|
func TestLeafNodeWithWeightedDQRequestsToSuperClusterWithStreamImportAccounts(t *testing.T) {
|
|
var tmpl = `
|
|
listen: 127.0.0.1:-1
|
|
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
accounts {
|
|
EFG {
|
|
users = [ { user: "efg", pass: "p" } ]
|
|
jetstream: enabled
|
|
imports [
|
|
{ stream: { account: STL, subject: "REQUEST"} }
|
|
{ stream: { account: KSC, subject: "REQUEST"} }
|
|
]
|
|
exports [ { stream: "RESPONSE" } ]
|
|
}
|
|
STL {
|
|
users = [ { user: "stl", pass: "p" } ]
|
|
exports [ { stream: "REQUEST" } ]
|
|
imports [ { stream: { account: EFG, subject: "RESPONSE"} } ]
|
|
}
|
|
KSC {
|
|
users = [ { user: "ksc", pass: "p" } ]
|
|
exports [ { stream: "REQUEST" } ]
|
|
imports [ { stream: { account: EFG, subject: "RESPONSE"} } ]
|
|
}
|
|
$SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }
|
|
}`
|
|
|
|
sc := createJetStreamSuperClusterWithTemplate(t, tmpl, 5, 2)
|
|
defer sc.shutdown()
|
|
|
|
// Now create a leafnode cluster that has 2 LNs, one to each cluster but on separate accounts, STL and KSC.
|
|
var lnTmpl = `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
{{leaf}}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }}
|
|
`
|
|
|
|
var leafFrag = `
|
|
leaf {
|
|
listen: 127.0.0.1:-1
|
|
remotes [
|
|
{ urls: [ %s ] }
|
|
{ urls: [ %s ] }
|
|
{ urls: [ %s ] ; deny_export: [REQUEST, RESPONSE], deny_import: RESPONSE }
|
|
]
|
|
}`
|
|
|
|
// We want to have two leaf node connections that join to the same local account on the leafnode servers,
|
|
// but connect to different accounts in different clusters.
|
|
c1 := sc.clusters[0] // Will connect to account KSC
|
|
c2 := sc.clusters[1] // Will connect to account STL
|
|
|
|
genLeafTmpl := func(tmpl string) string {
|
|
t.Helper()
|
|
|
|
var ln1, ln2, ln3 []string
|
|
for _, s := range c1.servers {
|
|
if s.ClusterName() != c1.name {
|
|
continue
|
|
}
|
|
ln := s.getOpts().LeafNode
|
|
ln1 = append(ln1, fmt.Sprintf("nats://ksc:p@%s:%d", ln.Host, ln.Port))
|
|
}
|
|
|
|
for _, s := range c2.servers {
|
|
if s.ClusterName() != c2.name {
|
|
continue
|
|
}
|
|
ln := s.getOpts().LeafNode
|
|
ln2 = append(ln2, fmt.Sprintf("nats://stl:p@%s:%d", ln.Host, ln.Port))
|
|
ln3 = append(ln3, fmt.Sprintf("nats://efg:p@%s:%d", ln.Host, ln.Port))
|
|
}
|
|
return strings.Replace(tmpl, "{{leaf}}", fmt.Sprintf(leafFrag, strings.Join(ln1, ", "), strings.Join(ln2, ", "), strings.Join(ln3, ", ")), 1)
|
|
}
|
|
|
|
tmpl = strings.Replace(lnTmpl, "store_dir:", fmt.Sprintf(`domain: "%s", store_dir:`, "SA"), 1)
|
|
tmpl = genLeafTmpl(tmpl)
|
|
|
|
ln := createJetStreamCluster(t, tmpl, "SA", "SA-", 3, 22280, false)
|
|
ln.waitOnClusterReady()
|
|
defer ln.shutdown()
|
|
|
|
for _, s := range ln.servers {
|
|
checkLeafNodeConnectedCount(t, s, 3)
|
|
}
|
|
|
|
// Now connect DQ subscribers to each cluster but to the global account.
|
|
|
|
// Create 5 clients for each cluster / account
|
|
var c1c, c2c []*nats.Conn
|
|
for i := 0; i < 5; i++ {
|
|
nc1, _ := jsClientConnect(t, c1.randomServer(), nats.UserInfo("efg", "p"))
|
|
defer nc1.Close()
|
|
c1c = append(c1c, nc1)
|
|
nc2, _ := jsClientConnect(t, c2.randomServer(), nats.UserInfo("efg", "p"))
|
|
defer nc2.Close()
|
|
c2c = append(c2c, nc2)
|
|
}
|
|
|
|
createSubs := func(num int, conns []*nats.Conn) (subs []*nats.Subscription) {
|
|
for i := 0; i < num; i++ {
|
|
nc := conns[rand.Intn(len(conns))]
|
|
sub, err := nc.QueueSubscribeSync("REQUEST", "MC")
|
|
require_NoError(t, err)
|
|
subs = append(subs, sub)
|
|
nc.Flush()
|
|
}
|
|
// Let subs propagate.
|
|
time.Sleep(100 * time.Millisecond)
|
|
return subs
|
|
}
|
|
closeSubs := func(subs []*nats.Subscription) {
|
|
for _, sub := range subs {
|
|
sub.Unsubscribe()
|
|
}
|
|
}
|
|
|
|
// Simple test first.
|
|
subs1 := createSubs(1, c1c)
|
|
defer closeSubs(subs1)
|
|
subs2 := createSubs(1, c2c)
|
|
defer closeSubs(subs2)
|
|
|
|
sendRequests := func(num int) {
|
|
t.Helper()
|
|
// Now connect to the leaf cluster and send some requests.
|
|
nc, _ := jsClientConnect(t, ln.randomServer())
|
|
defer nc.Close()
|
|
|
|
for i := 0; i < num; i++ {
|
|
require_NoError(t, nc.Publish("REQUEST", []byte("HELP")))
|
|
}
|
|
nc.Flush()
|
|
}
|
|
|
|
pending := func(subs []*nats.Subscription) (total int) {
|
|
t.Helper()
|
|
for _, sub := range subs {
|
|
n, _, err := sub.Pending()
|
|
require_NoError(t, err)
|
|
total += n
|
|
}
|
|
return total
|
|
}
|
|
|
|
num := 1000
|
|
checkAllReceived := func() error {
|
|
total := pending(subs1) + pending(subs2)
|
|
if total == num {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all received: %d vs %d", total, num)
|
|
}
|
|
|
|
checkBalanced := func(total, pc1, pc2 int) {
|
|
t.Helper()
|
|
tf := float64(total)
|
|
e1 := tf * (float64(pc1) / 100.00)
|
|
e2 := tf * (float64(pc2) / 100.00)
|
|
delta := tf / 10
|
|
p1 := float64(pending(subs1))
|
|
if p1 < e1-delta || p1 > e1+delta {
|
|
t.Fatalf("Value out of range for subs1, expected %v got %v", e1, p1)
|
|
}
|
|
p2 := float64(pending(subs2))
|
|
if p2 < e2-delta || p2 > e2+delta {
|
|
t.Fatalf("Value out of range for subs2, expected %v got %v", e2, p2)
|
|
}
|
|
}
|
|
|
|
// Now connect to the leaf cluster and send some requests.
|
|
|
|
// Simple 50/50
|
|
sendRequests(num)
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
|
|
checkBalanced(num, 50, 50)
|
|
|
|
closeSubs(subs1)
|
|
closeSubs(subs2)
|
|
|
|
// Now test unbalanced. 10/90
|
|
subs1 = createSubs(1, c1c)
|
|
defer closeSubs(subs1)
|
|
subs2 = createSubs(9, c2c)
|
|
defer closeSubs(subs2)
|
|
|
|
sendRequests(num)
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
|
|
checkBalanced(num, 10, 90)
|
|
|
|
closeSubs(subs1)
|
|
closeSubs(subs2)
|
|
|
|
// Now test unbalanced. 80/20
|
|
subs1 = createSubs(80, c1c)
|
|
defer closeSubs(subs1)
|
|
subs2 = createSubs(20, c2c)
|
|
defer closeSubs(subs2)
|
|
|
|
sendRequests(num)
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllReceived)
|
|
checkBalanced(num, 80, 20)
|
|
|
|
// Now test draining the subs as we are sending from an initial balanced situation simulating a draining of a cluster.
|
|
|
|
closeSubs(subs1)
|
|
closeSubs(subs2)
|
|
subs1, subs2 = nil, nil
|
|
|
|
// These subs slightly different.
|
|
var r1, r2 atomic.Uint64
|
|
for i := 0; i < 20; i++ {
|
|
nc := c1c[rand.Intn(len(c1c))]
|
|
sub, err := nc.QueueSubscribe("REQUEST", "MC", func(m *nats.Msg) { r1.Add(1) })
|
|
require_NoError(t, err)
|
|
subs1 = append(subs1, sub)
|
|
nc.Flush()
|
|
|
|
nc = c2c[rand.Intn(len(c2c))]
|
|
sub, err = nc.QueueSubscribe("REQUEST", "MC", func(m *nats.Msg) { r2.Add(1) })
|
|
require_NoError(t, err)
|
|
subs2 = append(subs2, sub)
|
|
nc.Flush()
|
|
}
|
|
// Let's them propagate
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer closeSubs(subs1)
|
|
defer closeSubs(subs2)
|
|
|
|
nc, _ := jsClientConnect(t, ln.randomServer())
|
|
defer nc.Close()
|
|
|
|
for i, dindex := 0, 1; i < num; i++ {
|
|
require_NoError(t, nc.Publish("REQUEST", []byte("HELP")))
|
|
// Check if we have more to simulate draining.
|
|
// Will drain within first ~100 requests using 20% rand test below.
|
|
// Will leave 1 behind.
|
|
if dindex < len(subs1)-1 && rand.Intn(6) > 4 {
|
|
sub := subs1[dindex]
|
|
dindex++
|
|
sub.Drain()
|
|
}
|
|
}
|
|
nc.Flush()
|
|
|
|
checkFor(t, time.Second, 200*time.Millisecond, func() error {
|
|
total := int(r1.Load() + r2.Load())
|
|
if total == num {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all received: %d vs %d", total, num)
|
|
})
|
|
require_True(t, r2.Load() > r1.Load())
|
|
|
|
// Now check opposite flow for responses.
|
|
|
|
// Create 10 subscribers.
|
|
var rsubs []*nats.Subscription
|
|
|
|
for i := 0; i < 10; i++ {
|
|
nc, _ := jsClientConnect(t, ln.randomServer())
|
|
defer nc.Close()
|
|
sub, err := nc.QueueSubscribeSync("RESPONSE", "SA")
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
rsubs = append(rsubs, sub)
|
|
}
|
|
// Let's them propagate
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
nc, _ = jsClientConnect(t, ln.randomServer())
|
|
defer nc.Close()
|
|
_, err := nc.SubscribeSync("RESPONSE")
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
|
|
// Now connect and send responses from EFG in cloud.
|
|
nc, _ = jsClientConnect(t, sc.randomServer(), nats.UserInfo("efg", "p"))
|
|
defer nc.Close()
|
|
|
|
for i := 0; i < 100; i++ {
|
|
require_NoError(t, nc.Publish("RESPONSE", []byte("OK")))
|
|
}
|
|
nc.Flush()
|
|
|
|
checkAllRespReceived := func() error {
|
|
p := pending(rsubs)
|
|
if p == 100 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all responses received: %d vs %d", p, 100)
|
|
}
|
|
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllRespReceived)
|
|
}
|
|
|
|
func TestLeafNodeWithWeightedDQResponsesWithStreamImportAccountsWithUnsub(t *testing.T) {
|
|
var tmpl = `
|
|
listen: 127.0.0.1:-1
|
|
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
accounts {
|
|
EFG {
|
|
users = [ { user: "efg", pass: "p" } ]
|
|
jetstream: enabled
|
|
exports [ { stream: "RESPONSE" } ]
|
|
}
|
|
STL {
|
|
users = [ { user: "stl", pass: "p" } ]
|
|
imports [ { stream: { account: EFG, subject: "RESPONSE"} } ]
|
|
}
|
|
KSC {
|
|
users = [ { user: "ksc", pass: "p" } ]
|
|
imports [ { stream: { account: EFG, subject: "RESPONSE"} } ]
|
|
}
|
|
$SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }
|
|
}`
|
|
|
|
c := createJetStreamClusterWithTemplate(t, tmpl, "US-CENTRAL", 3)
|
|
defer c.shutdown()
|
|
|
|
// Now create a leafnode cluster that has 2 LNs, one to each cluster but on separate accounts, STL and KSC.
|
|
var lnTmpl = `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {max_mem_store: 256MB, max_file_store: 2GB, store_dir: '%s'}
|
|
|
|
{{leaf}}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] }}
|
|
`
|
|
|
|
var leafFrag = `
|
|
leaf {
|
|
listen: 127.0.0.1:-1
|
|
remotes [ { urls: [ %s ] } ]
|
|
}`
|
|
|
|
genLeafTmpl := func(tmpl string) string {
|
|
t.Helper()
|
|
|
|
var ln []string
|
|
for _, s := range c.servers {
|
|
lno := s.getOpts().LeafNode
|
|
ln = append(ln, fmt.Sprintf("nats://ksc:p@%s:%d", lno.Host, lno.Port))
|
|
}
|
|
return strings.Replace(tmpl, "{{leaf}}", fmt.Sprintf(leafFrag, strings.Join(ln, ", ")), 1)
|
|
}
|
|
|
|
tmpl = strings.Replace(lnTmpl, "store_dir:", fmt.Sprintf(`domain: "%s", store_dir:`, "SA"), 1)
|
|
tmpl = genLeafTmpl(tmpl)
|
|
|
|
ln := createJetStreamCluster(t, tmpl, "SA", "SA-", 3, 22280, false)
|
|
ln.waitOnClusterReady()
|
|
defer ln.shutdown()
|
|
|
|
for _, s := range ln.servers {
|
|
checkLeafNodeConnectedCount(t, s, 1)
|
|
}
|
|
|
|
// Create 10 subscribers.
|
|
var rsubs []*nats.Subscription
|
|
|
|
closeSubs := func(subs []*nats.Subscription) {
|
|
for _, sub := range subs {
|
|
sub.Unsubscribe()
|
|
}
|
|
}
|
|
|
|
checkAllRespReceived := func() error {
|
|
t.Helper()
|
|
var total int
|
|
for _, sub := range rsubs {
|
|
n, _, err := sub.Pending()
|
|
require_NoError(t, err)
|
|
total += n
|
|
}
|
|
if total == 100 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not all responses received: %d vs %d", total, 100)
|
|
}
|
|
|
|
s := ln.randomServer()
|
|
for i := 0; i < 4; i++ {
|
|
nc, _ := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
sub, err := nc.QueueSubscribeSync("RESPONSE", "SA")
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
rsubs = append(rsubs, sub)
|
|
}
|
|
|
|
// Now connect and send responses from EFG in cloud.
|
|
nc, _ := jsClientConnect(t, c.randomServer(), nats.UserInfo("efg", "p"))
|
|
defer nc.Close()
|
|
for i := 0; i < 100; i++ {
|
|
require_NoError(t, nc.Publish("RESPONSE", []byte("OK")))
|
|
}
|
|
nc.Flush()
|
|
|
|
// Make sure all received.
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllRespReceived)
|
|
|
|
checkAccountInterest := func(s *Server, accName string) *SublistResult {
|
|
t.Helper()
|
|
acc, err := s.LookupAccount(accName)
|
|
require_NoError(t, err)
|
|
acc.mu.RLock()
|
|
r := acc.sl.Match("RESPONSE")
|
|
acc.mu.RUnlock()
|
|
return r
|
|
}
|
|
|
|
checkInterest := func() error {
|
|
t.Helper()
|
|
for _, s := range c.servers {
|
|
if r := checkAccountInterest(s, "KSC"); len(r.psubs)+len(r.qsubs) > 0 {
|
|
return fmt.Errorf("Subs still present for %q: %+v", "KSC", r)
|
|
}
|
|
if r := checkAccountInterest(s, "EFG"); len(r.psubs)+len(r.qsubs) > 0 {
|
|
return fmt.Errorf("Subs still present for %q: %+v", "EFG", r)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Now unsub them and create new ones on a different server.
|
|
closeSubs(rsubs)
|
|
rsubs = rsubs[:0]
|
|
|
|
// Also restart the server that we had all the rsubs on.
|
|
s.Shutdown()
|
|
s.WaitForShutdown()
|
|
s = ln.restartServer(s)
|
|
ln.waitOnClusterReady()
|
|
ln.waitOnServerCurrent(s)
|
|
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkInterest)
|
|
|
|
for i := 0; i < 4; i++ {
|
|
nc, _ := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
sub, err := nc.QueueSubscribeSync("RESPONSE", "SA")
|
|
require_NoError(t, err)
|
|
nc.Flush()
|
|
rsubs = append(rsubs, sub)
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
require_NoError(t, nc.Publish("RESPONSE", []byte("OK")))
|
|
}
|
|
nc.Flush()
|
|
|
|
// Make sure all received.
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkAllRespReceived)
|
|
|
|
closeSubs(rsubs)
|
|
checkFor(t, time.Second, 200*time.Millisecond, checkInterest)
|
|
}
|
|
|
|
func TestLeafNodeTwoRemotesToSameHubAccount(t *testing.T) {
|
|
conf1 := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: "hub"
|
|
accounts {
|
|
HA { users: [{user: ha, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`))
|
|
s1, o1 := RunServerWithConfig(conf1)
|
|
defer s1.Shutdown()
|
|
|
|
conf2 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: "spoke"
|
|
accounts {
|
|
A { users: [{user: A, password: pwd}] }
|
|
B { users: [{user: B, password: pwd}] }
|
|
C { users: [{user: C, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://ha:[email protected]:%d"
|
|
local: "A"
|
|
}
|
|
{
|
|
url: "nats://ha:[email protected]:%d"
|
|
local: "C"
|
|
}
|
|
]
|
|
}
|
|
`, o1.LeafNode.Port, o1.LeafNode.Port)))
|
|
s2, _ := RunServerWithConfig(conf2)
|
|
defer s2.Shutdown()
|
|
|
|
l := &captureErrorLogger{errCh: make(chan string, 10)}
|
|
s2.SetLogger(l, false, false)
|
|
|
|
checkLeafNodeConnectedCount(t, s2, 2)
|
|
|
|
// Make sure we don't get duplicate leafnode connection errors
|
|
deadline := time.NewTimer(1500 * time.Millisecond)
|
|
for done := false; !done; {
|
|
select {
|
|
case err := <-l.errCh:
|
|
if strings.Contains(err, DuplicateRemoteLeafnodeConnection.String()) {
|
|
t.Fatalf("Got error: %v", err)
|
|
}
|
|
case <-deadline.C:
|
|
done = true
|
|
}
|
|
}
|
|
|
|
nca := natsConnect(t, s2.ClientURL(), nats.UserInfo("A", "pwd"))
|
|
defer nca.Close()
|
|
suba := natsSubSync(t, nca, "A")
|
|
ncb := natsConnect(t, s2.ClientURL(), nats.UserInfo("B", "pwd"))
|
|
defer ncb.Close()
|
|
subb := natsSubSync(t, ncb, "B")
|
|
ncc := natsConnect(t, s2.ClientURL(), nats.UserInfo("C", "pwd"))
|
|
defer ncc.Close()
|
|
subc := natsSubSync(t, ncc, "C")
|
|
subs := map[string]*nats.Subscription{"A": suba, "B": subb, "C": subc}
|
|
|
|
for _, subj := range []string{"A", "C"} {
|
|
checkSubInterest(t, s1, "HA", subj, time.Second)
|
|
}
|
|
|
|
nc := natsConnect(t, s1.ClientURL(), nats.UserInfo("ha", "pwd"))
|
|
defer nc.Close()
|
|
|
|
for _, subj := range []string{"A", "B", "C"} {
|
|
natsPub(t, nc, subj, []byte("hello"))
|
|
}
|
|
|
|
for _, subj := range []string{"A", "B", "C"} {
|
|
var expected bool
|
|
if subj != "B" {
|
|
expected = true
|
|
}
|
|
sub := subs[subj]
|
|
if expected {
|
|
natsNexMsg(t, sub, time.Second)
|
|
} else {
|
|
if _, err := sub.NextMsg(50 * time.Millisecond); err != nats.ErrTimeout {
|
|
t.Fatalf("Expected timeout error, got %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeTwoRemotesToSameHubAccountWithClusters(t *testing.T) {
|
|
hubTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
accounts {
|
|
HA { users: [{user: HA, password: pwd}] }
|
|
}
|
|
cluster {
|
|
name: "hub"
|
|
port: -1
|
|
%s
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`
|
|
confH1 := createConfFile(t, []byte(fmt.Sprintf(hubTmpl, "H1", _EMPTY_)))
|
|
sh1, oh1 := RunServerWithConfig(confH1)
|
|
defer sh1.Shutdown()
|
|
|
|
confH2 := createConfFile(t, []byte(fmt.Sprintf(hubTmpl, "H2", fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", oh1.Cluster.Port))))
|
|
sh2, oh2 := RunServerWithConfig(confH2)
|
|
defer sh2.Shutdown()
|
|
|
|
checkClusterFormed(t, sh1, sh2)
|
|
|
|
spokeTmpl := `
|
|
port: -1
|
|
server_name: "%s"
|
|
accounts {
|
|
A { users: [{user: A, password: pwd}] }
|
|
B { users: [{user: B, password: pwd}] }
|
|
}
|
|
cluster {
|
|
name: "spoke"
|
|
port: -1
|
|
%s
|
|
}
|
|
leafnodes {
|
|
remotes [
|
|
{
|
|
url: "nats://HA:[email protected]:%d"
|
|
local: "A"
|
|
}
|
|
{
|
|
url: "nats://HA:[email protected]:%d"
|
|
local: "B"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
for _, test := range []struct {
|
|
name string
|
|
sp2Leafport int
|
|
}{
|
|
{"connect to different hub servers", oh2.LeafNode.Port},
|
|
{"connect to same hub server", oh1.LeafNode.Port},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
confSP1 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP1", _EMPTY_, oh1.LeafNode.Port, oh1.LeafNode.Port)))
|
|
sp1, osp1 := RunServerWithConfig(confSP1)
|
|
defer sp1.Shutdown()
|
|
|
|
confSP2 := createConfFile(t, []byte(fmt.Sprintf(spokeTmpl, "SP2",
|
|
fmt.Sprintf("routes: [\"nats://127.0.0.1:%d\"]", osp1.Cluster.Port), test.sp2Leafport, test.sp2Leafport)))
|
|
sp2, _ := RunServerWithConfig(confSP2)
|
|
defer sp2.Shutdown()
|
|
|
|
checkClusterFormed(t, sp1, sp2)
|
|
checkLeafNodeConnectedCount(t, sp1, 2)
|
|
checkLeafNodeConnectedCount(t, sp2, 2)
|
|
|
|
var conns []*nats.Conn
|
|
createConn := func(s *Server, user string) {
|
|
t.Helper()
|
|
nc := natsConnect(t, s.ClientURL(), nats.UserInfo(user, "pwd"))
|
|
conns = append(conns, nc)
|
|
}
|
|
createConn(sh1, "HA")
|
|
createConn(sh2, "HA")
|
|
createConn(sp1, "A")
|
|
createConn(sp2, "A")
|
|
createConn(sp1, "B")
|
|
createConn(sp2, "B")
|
|
for _, nc := range conns {
|
|
defer nc.Close()
|
|
}
|
|
|
|
check := func(subConn *nats.Conn, subj string, checkA, checkB bool) {
|
|
t.Helper()
|
|
sub := natsSubSync(t, subConn, subj)
|
|
defer sub.Unsubscribe()
|
|
|
|
checkSubInterest(t, sh1, "HA", subj, time.Second)
|
|
checkSubInterest(t, sh2, "HA", subj, time.Second)
|
|
if checkA {
|
|
checkSubInterest(t, sp1, "A", subj, time.Second)
|
|
checkSubInterest(t, sp2, "A", subj, time.Second)
|
|
}
|
|
if checkB {
|
|
checkSubInterest(t, sp1, "B", subj, time.Second)
|
|
checkSubInterest(t, sp2, "B", subj, time.Second)
|
|
}
|
|
|
|
for i, ncp := range conns {
|
|
// Don't publish from account "A" connections if we are
|
|
// dealing with account "B", and vice-versa.
|
|
if !checkA && i >= 2 && i <= 3 {
|
|
continue
|
|
}
|
|
if !checkB && i >= 4 {
|
|
continue
|
|
}
|
|
natsPub(t, ncp, subj, []byte("hello"))
|
|
natsNexMsg(t, sub, time.Second)
|
|
// Make sure we don't get a duplicate
|
|
if msg, err := sub.NextMsg(50 * time.Millisecond); err != nats.ErrTimeout {
|
|
t.Fatalf("Unexpected message or error: msg=%v - err=%v", msg, err)
|
|
}
|
|
}
|
|
}
|
|
check(conns[0], "HA.1", true, true)
|
|
check(conns[1], "HA.2", true, true)
|
|
check(conns[2], "SPA.1", true, false)
|
|
check(conns[3], "SPA.2", true, false)
|
|
check(conns[4], "SPB.1", false, true)
|
|
check(conns[5], "SPB.2", false, true)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeSameLocalAccountToMultipleHubs(t *testing.T) {
|
|
hub1Conf := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: hub1
|
|
accounts {
|
|
hub1 { users: [{user: hub1, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`))
|
|
sh1, oh1 := RunServerWithConfig(hub1Conf)
|
|
defer sh1.Shutdown()
|
|
|
|
hub2Conf := createConfFile(t, []byte(`
|
|
port: -1
|
|
server_name: hub2
|
|
accounts {
|
|
hub2 { users: [{user: hub2, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
}
|
|
`))
|
|
sh2, oh2 := RunServerWithConfig(hub2Conf)
|
|
defer sh2.Shutdown()
|
|
|
|
lconf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
port: -1
|
|
server_name: leaf
|
|
accounts {
|
|
A { users: [{user: A, password: pwd}] }
|
|
B { users: [{user: B, password: pwd}] }
|
|
C { users: [{user: C, password: pwd}] }
|
|
}
|
|
leafnodes {
|
|
port: -1
|
|
remotes [
|
|
{
|
|
url: nats://hub1:[email protected]:%[1]d
|
|
local: "A"
|
|
}
|
|
{
|
|
url: nats://hub1:[email protected]:%[1]d
|
|
local: "C"
|
|
}
|
|
{
|
|
url: nats://hub2:[email protected]:%[2]d
|
|
local: "A"
|
|
}
|
|
{
|
|
url: nats://hub2:[email protected]:%[2]d
|
|
local: "B"
|
|
}
|
|
]
|
|
}
|
|
`, oh1.LeafNode.Port, oh2.LeafNode.Port)))
|
|
s, _ := RunServerWithConfig(lconf)
|
|
defer s.Shutdown()
|
|
|
|
// The leafnode to hub1 should have 2 connections (A and C)
|
|
// while the one to hub2 should have 2 connections (A and B)
|
|
checkLeafNodeConnectedCount(t, sh1, 2)
|
|
checkLeafNodeConnectedCount(t, sh2, 2)
|
|
checkLeafNodeConnectedCount(t, s, 4)
|
|
|
|
nca := natsConnect(t, s.ClientURL(), nats.UserInfo("A", "pwd"))
|
|
defer nca.Close()
|
|
suba := natsSubSync(t, nca, "A")
|
|
ncb := natsConnect(t, s.ClientURL(), nats.UserInfo("B", "pwd"))
|
|
defer ncb.Close()
|
|
subb := natsSubSync(t, ncb, "B")
|
|
ncc := natsConnect(t, s.ClientURL(), nats.UserInfo("C", "pwd"))
|
|
defer ncc.Close()
|
|
subc := natsSubSync(t, ncc, "C")
|
|
|
|
checkSubInterest(t, sh1, "hub1", "A", time.Second)
|
|
checkSubNoInterest(t, sh1, "hub1", "B", time.Second)
|
|
checkSubInterest(t, sh1, "hub1", "C", time.Second)
|
|
|
|
checkSubInterest(t, sh2, "hub2", "A", time.Second)
|
|
checkSubInterest(t, sh2, "hub2", "B", time.Second)
|
|
checkSubNoInterest(t, sh2, "hub2", "C", time.Second)
|
|
|
|
nch1 := natsConnect(t, sh1.ClientURL(), nats.UserInfo("hub1", "pwd"))
|
|
defer nch1.Close()
|
|
nch2 := natsConnect(t, sh2.ClientURL(), nats.UserInfo("hub2", "pwd"))
|
|
defer nch2.Close()
|
|
|
|
checkNoMsg := func(sub *nats.Subscription) {
|
|
t.Helper()
|
|
if msg, err := sub.NextMsg(50 * time.Millisecond); err != nats.ErrTimeout {
|
|
t.Fatalf("Unexpected message: %s", msg.Data)
|
|
}
|
|
}
|
|
|
|
checkSub := func(sub *nats.Subscription, subj, payload string) {
|
|
t.Helper()
|
|
msg := natsNexMsg(t, sub, time.Second)
|
|
require_Equal(t, subj, msg.Subject)
|
|
require_Equal(t, payload, string(msg.Data))
|
|
// Make sure we don't get duplicates
|
|
checkNoMsg(sub)
|
|
}
|
|
|
|
natsPub(t, nch1, "A", []byte("msgA1"))
|
|
checkSub(suba, "A", "msgA1")
|
|
natsPub(t, nch1, "B", []byte("msgB1"))
|
|
checkNoMsg(subb)
|
|
natsPub(t, nch1, "C", []byte("msgC1"))
|
|
checkSub(subc, "C", "msgC1")
|
|
|
|
natsPub(t, nch2, "A", []byte("msgA2"))
|
|
checkSub(suba, "A", "msgA2")
|
|
natsPub(t, nch2, "B", []byte("msgB2"))
|
|
checkSub(subb, "B", "msgB2")
|
|
natsPub(t, nch2, "C", []byte("msgC2"))
|
|
checkNoMsg(subc)
|
|
}
|
|
|
|
func TestLeafNodeSlowConsumer(t *testing.T) {
|
|
ao := DefaultOptions()
|
|
ao.LeafNode.Host = "127.0.0.1"
|
|
ao.LeafNode.Port = -1
|
|
ao.WriteDeadline = 1 * time.Millisecond
|
|
a := RunServer(ao)
|
|
defer a.Shutdown()
|
|
|
|
c, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", ao.LeafNode.Port))
|
|
if err != nil {
|
|
t.Fatalf("Error connecting: %v", err)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
a.mu.Lock()
|
|
checkFor(t, 2*time.Second, 15*time.Millisecond, func() error {
|
|
a.grMu.Lock()
|
|
defer a.grMu.Unlock()
|
|
for _, cli := range a.grTmpClients {
|
|
cli.out.wdl = time.Nanosecond
|
|
return nil
|
|
}
|
|
return nil
|
|
})
|
|
a.mu.Unlock()
|
|
|
|
// Only leafnode slow consumers that made it past connect are tracked
|
|
// in the slow consumers counter.
|
|
if _, err := c.Write([]byte("CONNECT {}\r\n")); err != nil {
|
|
t.Fatalf("Error writing connect: %v", err)
|
|
}
|
|
// Read info
|
|
br := bufio.NewReader(c)
|
|
br.ReadLine()
|
|
for i := 0; i < 10; i++ {
|
|
if _, err := c.Write([]byte("PING\r\n")); err != nil {
|
|
t.Fatalf("Unexpected error writing PING: %v", err)
|
|
}
|
|
}
|
|
defer c.Close()
|
|
timeout := time.Now().Add(time.Second)
|
|
var (
|
|
got uint64
|
|
expected uint64 = 1
|
|
)
|
|
for time.Now().Before(timeout) {
|
|
got = a.NumSlowConsumersLeafs()
|
|
if got == expected {
|
|
return
|
|
}
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
t.Fatalf("Timed out waiting for slow consumer leafnodes, got: %v, expected: %v", got, expected)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/4367
|
|
func TestLeafNodeDQMultiAccountExportImport(t *testing.T) {
|
|
bConf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: cluster-b-0
|
|
accounts {
|
|
$SYS: { users: [ { user: admin, password: pwd } ] },
|
|
AGG: {
|
|
exports: [ { service: "PING.>" } ]
|
|
users: [ { user: agg, password: agg } ]
|
|
}
|
|
}
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`))
|
|
|
|
sb, ob := RunServerWithConfig(bConf)
|
|
defer sb.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: { store_dir: '%s' }
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
accounts {
|
|
$SYS: { users: [ { user: admin, password: pwd } ] },
|
|
A: {
|
|
mappings: { "A.>" : ">" }
|
|
exports: [ { service: A.> } ]
|
|
users: [ { user: a, password: a } ]
|
|
},
|
|
AGG: {
|
|
imports: [ { service: { subject: A.>, account: A } } ]
|
|
users: [ { user: agg, password: agg } ]
|
|
},
|
|
}
|
|
leaf {
|
|
remotes: [ {
|
|
urls: [ nats-leaf://agg:[email protected]:{LEAF_PORT} ]
|
|
account: AGG
|
|
} ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", ob.LeafNode.Port), 1)
|
|
c := createJetStreamCluster(t, tmpl, "cluster-a", "cluster-a-", 3, 22110, false)
|
|
defer c.shutdown()
|
|
|
|
// Make sure all servers are connected via leafnode to the hub, the b server.
|
|
for _, s := range c.servers {
|
|
checkLeafNodeConnectedCount(t, s, 1)
|
|
}
|
|
|
|
// Connect to a server in the cluster and create a DQ listener.
|
|
nc, _ := jsClientConnect(t, c.randomServer(), nats.UserInfo("a", "a"))
|
|
defer nc.Close()
|
|
|
|
var got atomic.Int32
|
|
|
|
natsQueueSub(t, nc, "PING", "Q", func(m *nats.Msg) {
|
|
got.Add(1)
|
|
m.Respond([]byte("REPLY"))
|
|
})
|
|
|
|
// Now connect to B and send the request.
|
|
ncb, _ := jsClientConnect(t, sb, nats.UserInfo("agg", "agg"))
|
|
defer ncb.Close()
|
|
|
|
_, err := ncb.Request("A.PING", []byte("REQUEST"), time.Second)
|
|
require_NoError(t, err)
|
|
require_Equal(t, got.Load(), 1)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/4934
|
|
func TestLeafNodeServerReloadSubjectMappings(t *testing.T) {
|
|
stmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
mappings = { "source1": "target" }
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`
|
|
conf := createConfFile(t, []byte(stmpl))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf {
|
|
remotes: [ {
|
|
urls: [ nats-leaf://127.0.0.1:{LEAF_PORT} ]
|
|
} ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
|
|
// Create our subscriber.
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
sub := natsSubSync(t, nc, "target")
|
|
natsFlush(t, nc)
|
|
|
|
// Create our publisher.
|
|
ncl := natsConnect(t, l.ClientURL())
|
|
defer ncl.Close()
|
|
// Publish our message.
|
|
ncl.Publish("source1", []byte("OK"))
|
|
|
|
// Make sure we receive it.
|
|
checkSubsPending(t, sub, 1)
|
|
|
|
// Now change mapping.
|
|
reloadUpdateConfig(t, s, conf, strings.Replace(stmpl, "source1", "source2", 1))
|
|
// Also make sure we do not have subscription interest for source1 on leaf anymore.
|
|
checkSubInterest(t, l, globalAccountName, "source2", 2*time.Second)
|
|
|
|
// Publish our new message.
|
|
ncl.Publish("source2", []byte("OK"))
|
|
|
|
// Make sure we receive it.
|
|
checkSubsPending(t, sub, 2)
|
|
|
|
// Also make sure we do not have subscription interest for source1 on leaf anymore.
|
|
checkSubNoInterest(t, l, globalAccountName, "source1", 2*time.Second)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/5099
|
|
func TestLeafNodeServerReloadSubjectMappingsWithSameSubject(t *testing.T) {
|
|
stmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
mappings = { "source": "target1" }
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`
|
|
conf := createConfFile(t, []byte(stmpl))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf {
|
|
remotes: [ { urls: [ nats-leaf://127.0.0.1:{LEAF_PORT} ] } ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
|
|
// Create our subscriber.
|
|
nc := natsConnect(t, s.ClientURL())
|
|
defer nc.Close()
|
|
sub1 := natsSubSync(t, nc, "target1")
|
|
sub2 := natsSubSync(t, nc, "target2")
|
|
natsFlush(t, nc)
|
|
|
|
// Create our publisher.
|
|
ncl := natsConnect(t, l.ClientURL())
|
|
defer ncl.Close()
|
|
// Publish our message.
|
|
ncl.Publish("source", []byte("OK"))
|
|
|
|
// Make sure we receive it.
|
|
checkSubsPending(t, sub1, 1)
|
|
// Make sure the other does not.
|
|
checkSubsPending(t, sub2, 0)
|
|
|
|
// Now change mapping, but only the "to" subject, keeping same "from"
|
|
reloadUpdateConfig(t, s, conf, strings.Replace(stmpl, "target1", "target2", 1))
|
|
checkLeafNodeConnected(t, l)
|
|
|
|
// Publish our new message.
|
|
ncl.Publish("source", []byte("OK"))
|
|
|
|
// Make sure we receive it.
|
|
checkSubsPending(t, sub2, 1)
|
|
// Make sure the other does not.
|
|
checkSubsPending(t, sub1, 1)
|
|
}
|
|
|
|
func TestLeafNodeNkeyAuth(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
leaf {
|
|
listen: 127.0.0.1:-1
|
|
authorization: { nkey: UCSTG5CRF5GEJERAFKUUYRODGABTBVWY2NPE4GGKRQVQOH74PIAKTVKO }
|
|
}
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf {
|
|
remotes: [ {
|
|
url: nats-leaf://127.0.0.1:{LEAF_PORT}
|
|
seed: SUACJN3OSKWWPQXME4JUNFJ3PARXPO657GGNWNU7PK7G3AUQQYHLW26XH4
|
|
} ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
}
|
|
|
|
func TestLeafNodeAccountNkeysAuth(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
leaf {
|
|
listen: 127.0.0.1:-1
|
|
}
|
|
accounts {
|
|
A { users [ {nkey: UCSTG5CRF5GEJERAFKUUYRODGABTBVWY2NPE4GGKRQVQOH74PIAKTVKO } ] }
|
|
}
|
|
`))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf {
|
|
remotes: [ {
|
|
url: nats-leaf://127.0.0.1:{LEAF_PORT}
|
|
seed: SUACJN3OSKWWPQXME4JUNFJ3PARXPO657GGNWNU7PK7G3AUQQYHLW26XH4
|
|
} ]
|
|
}
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/5117
|
|
func TestLeafNodeLoopDetectionOnActualLoop(t *testing.T) {
|
|
// Setup: B --[leaf]--> A C --[leaf]--> A C --[leaf] --> B
|
|
accConf := `
|
|
accounts: {
|
|
APP: {
|
|
users: [ { user:u, password: u,
|
|
permissions: { publish = "u.>", subscribe = "u.>" }} ]
|
|
}
|
|
$SYS: { users = [ {user: "s", password: "s"} ] }
|
|
}`
|
|
|
|
confA := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: a1
|
|
port: -1
|
|
cluster: { name: A }
|
|
leafnodes {
|
|
port: 17422
|
|
}
|
|
%s`, accConf)))
|
|
|
|
confB := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: b1
|
|
port: -1
|
|
cluster: { name: B }
|
|
leafnodes {
|
|
port: 17432
|
|
remotes [
|
|
{ urls: ["nats-leaf://u:u@localhost:17422"], account: "APP" }
|
|
]
|
|
reconnect: "2s"
|
|
}
|
|
%s`, accConf)))
|
|
|
|
confC := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: c1
|
|
port: -1
|
|
cluster: { name: C }
|
|
leafnodes {
|
|
port: 17442
|
|
remotes [
|
|
{ urls: ["nats-leaf://u:u@localhost:17422"], account: "APP" }
|
|
# This one creates the loop
|
|
{ urls: ["nats-leaf://u:u@localhost:17432"], account: "APP" }
|
|
]
|
|
reconnect: "0.5s"
|
|
}
|
|
%s`, accConf)))
|
|
|
|
// Start order will be B -> C -> A
|
|
// We will force C to connect to A first before B using different reconnect intervals.
|
|
// If B connects first we detect loops fine. If C connects first we do not.
|
|
|
|
srvB, _ := RunServerWithConfig(confB)
|
|
defer srvB.Shutdown()
|
|
lb := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
srvB.SetLogger(lb, false, false)
|
|
|
|
srvC, _ := RunServerWithConfig(confC)
|
|
defer srvC.Shutdown()
|
|
lc := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
srvC.SetLogger(lc, false, false)
|
|
|
|
// C should connect to B
|
|
checkLeafNodeConnectedCount(t, srvC, 1)
|
|
|
|
srvA, _ := RunServerWithConfig(confA)
|
|
defer srvA.Shutdown()
|
|
la := &loopDetectedLogger{ch: make(chan string, 1)}
|
|
srvA.SetLogger(la, false, false)
|
|
|
|
select {
|
|
case <-la.ch:
|
|
case <-lb.ch:
|
|
case <-lc.ch:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("Did not get any error regarding loop")
|
|
}
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/5473
|
|
func TestLeafNodeDupeDeliveryQueueSubAndPlainSub(t *testing.T) {
|
|
clusterCommonConf := `
|
|
accounts: {
|
|
tenant: {
|
|
users: [ { user:t, password: t } ]
|
|
exports: [{stream: system-a.events.>}]
|
|
}
|
|
system-a: {
|
|
users: [ { user:sa, password: sa } ]
|
|
imports: [
|
|
{stream: {subject: system-a.events.>, account: tenant}, prefix: tenant}
|
|
]
|
|
}
|
|
$SYS: { users = [ {user: "s", password: "s"} ] }
|
|
}
|
|
leafnodes {
|
|
remotes: [{
|
|
urls: [ "nats-leaf://sa:[email protected]:17422" ]
|
|
account: system-a
|
|
}]
|
|
}`
|
|
|
|
confCluster0 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: a-0
|
|
port: -1
|
|
cluster: {
|
|
name: cluster-a
|
|
listen: 127.0.0.1:16122
|
|
routes = [ nats://127.0.0.1:16123 ]
|
|
pool_size: -1
|
|
}
|
|
%s`, clusterCommonConf)))
|
|
|
|
confCluster1 := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: a-1
|
|
port: -1
|
|
cluster: {
|
|
name: cluster-a
|
|
listen: 127.0.0.1:16123
|
|
routes = [ nats://127.0.0.1:16122 ]
|
|
pool_size: -1
|
|
}
|
|
%s`, clusterCommonConf)))
|
|
|
|
serverB := createConfFile(t, []byte(`
|
|
server_name: b
|
|
port: -1
|
|
leafnodes: { port: 17422 }
|
|
accounts: {
|
|
system-a: {
|
|
users: [ { user: sa, password: sa } ]
|
|
exports: [{stream: *.system-a.>}]
|
|
}
|
|
system-b: {
|
|
users: [ { user: sb, password: sb } ]
|
|
imports: [ {stream: {subject: *.system-a.>, account: system-a }}]
|
|
}
|
|
$SYS: { users = [ {user: "s", password: "s"} ] }
|
|
}`))
|
|
|
|
// Start server B
|
|
srvB, _ := RunServerWithConfig(serverB)
|
|
defer srvB.Shutdown()
|
|
|
|
// Start the cluster servers.
|
|
srvA0, _ := RunServerWithConfig(confCluster0)
|
|
defer srvA0.Shutdown()
|
|
// Make sure this is connected first before starting the second server in cluster A.
|
|
checkLeafNodeConnectedCount(t, srvB, 1)
|
|
// Start second A server.
|
|
srvA1, _ := RunServerWithConfig(confCluster1)
|
|
defer srvA1.Shutdown()
|
|
// Make sure they are routed together.
|
|
checkNumRoutes(t, srvA0, 1)
|
|
checkNumRoutes(t, srvA1, 1)
|
|
// Make sure each cluster server is connected to server B.
|
|
checkLeafNodeConnectedCount(t, srvB, 2)
|
|
|
|
// Create plain subscriber on server B attached to system-b account.
|
|
ncB := natsConnect(t, srvB.ClientURL(), nats.UserInfo("sb", "sb"))
|
|
defer ncB.Close()
|
|
sub, err := ncB.SubscribeSync("*.system-a.events.>")
|
|
require_NoError(t, err)
|
|
// Create a new sub that has a queue group as well.
|
|
subq, err := ncB.QueueSubscribeSync("*.system-a.events.objectnotfound", "SYSB")
|
|
require_NoError(t, err)
|
|
ncB.Flush()
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
// Connect to cluster A
|
|
ncA := natsConnect(t, srvA0.ClientURL(), nats.UserInfo("t", "t"))
|
|
defer ncA.Close()
|
|
|
|
err = ncA.Publish("system-a.events.objectnotfound", []byte("EventA"))
|
|
require_NoError(t, err)
|
|
ncA.Flush()
|
|
// Wait for them to be received.
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
n, _, err := sub.Pending()
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 1)
|
|
n, _, err = subq.Pending()
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 1)
|
|
}
|
|
|
|
func TestLeafNodeServerKickClient(t *testing.T) {
|
|
stmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-server
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`
|
|
conf := createConfFile(t, []byte(stmpl))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
tmpl := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: test-leaf
|
|
leaf { remotes: [ { urls: [ nats-leaf://127.0.0.1:{LEAF_PORT} ] } ] }
|
|
`
|
|
tmpl = strings.Replace(tmpl, "{LEAF_PORT}", fmt.Sprintf("%d", o.LeafNode.Port), 1)
|
|
lConf := createConfFile(t, []byte(tmpl))
|
|
l, _ := RunServerWithConfig(lConf)
|
|
defer l.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, l)
|
|
|
|
// We want to make sure we can kick the leafnode connections as well as client connections.
|
|
conns, err := s.Connz(&ConnzOptions{Account: globalAccountName})
|
|
require_NoError(t, err)
|
|
require_Equal(t, len(conns.Conns), 1)
|
|
lid := conns.Conns[0].Cid
|
|
|
|
disconnectTime := time.Now()
|
|
err = s.DisconnectClientByID(lid)
|
|
require_NoError(t, err)
|
|
|
|
// Wait until we are reconnected.
|
|
checkLeafNodeConnected(t, s)
|
|
|
|
// Look back up again and make sure start time indicates a restart, meaning kick worked.
|
|
conns, err = s.Connz(&ConnzOptions{Account: globalAccountName})
|
|
require_NoError(t, err)
|
|
require_Equal(t, len(conns.Conns), 1)
|
|
ln := conns.Conns[0]
|
|
require_True(t, lid != ln.Cid)
|
|
require_True(t, ln.Start.After(disconnectTime))
|
|
}
|
|
|
|
func TestLeafCredFormatting(t *testing.T) {
|
|
//create the operator/sys/account tree
|
|
oKP, err := nkeys.CreateOperator()
|
|
require_NoError(t, err)
|
|
oPK, err := oKP.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
oc := jwt.NewOperatorClaims(oPK)
|
|
oc.Name = "O"
|
|
oJWT, err := oc.Encode(oKP)
|
|
require_NoError(t, err)
|
|
|
|
sysKP, err := nkeys.CreateAccount()
|
|
require_NoError(t, err)
|
|
sysPK, err := sysKP.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
sys := jwt.NewAccountClaims(sysPK)
|
|
sys.Name = "SYS"
|
|
sysJWT, err := sys.Encode(oKP)
|
|
require_NoError(t, err)
|
|
|
|
aKP, err := nkeys.CreateAccount()
|
|
require_NoError(t, err)
|
|
aPK, err := aKP.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
ac := jwt.NewAccountClaims(aPK)
|
|
ac.Name = "A"
|
|
aJWT, err := ac.Encode(oKP)
|
|
require_NoError(t, err)
|
|
|
|
uKP, err := nkeys.CreateUser()
|
|
require_NoError(t, err)
|
|
uSeed, err := uKP.Seed()
|
|
require_NoError(t, err)
|
|
uPK, err := uKP.PublicKey()
|
|
require_NoError(t, err)
|
|
|
|
// build the config
|
|
stmpl := fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
operator: %s
|
|
system_account: %s
|
|
resolver: MEM
|
|
resolver_preload: {
|
|
%s: %s
|
|
%s: %s
|
|
}
|
|
leaf { listen: 127.0.0.1:-1 }
|
|
`, oJWT, sysPK, sysPK, sysJWT, aPK, aJWT)
|
|
conf := createConfFile(t, []byte(stmpl))
|
|
s, o := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
// create the leaf node
|
|
// generate the user credentials
|
|
uc := jwt.NewUserClaims(uPK)
|
|
uc.Name = "U"
|
|
uc.Limits.Data = -1
|
|
uc.Limits.Payload = -1
|
|
uc.Permissions.Pub.Allow.Add(">")
|
|
uc.Permissions.Sub.Allow.Add(">")
|
|
uJWT, err := uc.Encode(aKP)
|
|
require_NoError(t, err)
|
|
|
|
runLeaf := func(t *testing.T, creds []byte) {
|
|
file, err := os.CreateTemp("", "tmp-*.creds")
|
|
require_NoError(t, err)
|
|
_, err = file.Write(creds)
|
|
require_NoError(t, err)
|
|
require_NoError(t, file.Close())
|
|
|
|
template := fmt.Sprintf(`
|
|
listen: 127.0.0.1:-1
|
|
leaf { remotes: [
|
|
{
|
|
urls: [ nats-leaf://127.0.0.1:%d ]
|
|
credentials: "%s"
|
|
}
|
|
] }`, o.LeafNode.Port, file.Name())
|
|
|
|
conf := createConfFile(t, []byte(template))
|
|
leaf, _ := RunServerWithConfig(conf)
|
|
defer leaf.Shutdown()
|
|
defer os.Remove(file.Name())
|
|
checkLeafNodeConnected(t, leaf)
|
|
}
|
|
|
|
creds, err := jwt.FormatUserConfig(uJWT, uSeed)
|
|
require_NoError(t, err)
|
|
|
|
runLeaf(t, creds)
|
|
runLeaf(t, bytes.ReplaceAll(creds, []byte{'\n'}, []byte{'\r', '\n'}))
|
|
}
|
|
|
|
func TestLeafNodePermissionWithLiteralSubjectAndQueueInterest(t *testing.T) {
|
|
hconf := createConfFile(t, []byte(`
|
|
server_name: "HUB"
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
listen: "127.0.0.1:-1"
|
|
}
|
|
accounts {
|
|
A {
|
|
users: [
|
|
{ user: "user", password: "pwd",
|
|
permissions: {
|
|
subscribe: { allow: ["_INBOX.>", "my.subject"] }
|
|
publish: {allow: [">"]}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
`))
|
|
hub, ohub := RunServerWithConfig(hconf)
|
|
defer hub.Shutdown()
|
|
|
|
lconf := createConfFile(t, []byte(fmt.Sprintf(`
|
|
server_name: "LEAF"
|
|
listen: "127.0.0.1:-1"
|
|
leafnodes {
|
|
remotes: [
|
|
{url: "nats://user:[email protected]:%d", account: A}
|
|
]
|
|
}
|
|
accounts {
|
|
A { users: [{user: user, password: pwd}] }
|
|
}
|
|
`, ohub.LeafNode.Port)))
|
|
leaf, _ := RunServerWithConfig(lconf)
|
|
defer leaf.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, hub)
|
|
checkLeafNodeConnected(t, leaf)
|
|
|
|
ncLeaf := natsConnect(t, leaf.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncLeaf.Close()
|
|
natsQueueSub(t, ncLeaf, "my.subject", "queue", func(m *nats.Msg) {
|
|
m.Respond([]byte("OK"))
|
|
})
|
|
natsFlush(t, ncLeaf)
|
|
|
|
ncHub := natsConnect(t, hub.ClientURL(), nats.UserInfo("user", "pwd"))
|
|
defer ncHub.Close()
|
|
|
|
resp, err := ncHub.Request("my.subject", []byte("hello"), time.Second)
|
|
require_NoError(t, err)
|
|
require_Equal(t, "OK", string(resp.Data))
|
|
}
|
|
|
|
func TestLeafNodeConnectionSucceedsEvenWithDelayedFirstINFO(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
websocket bool
|
|
}{
|
|
{"regular", false},
|
|
{"websocket", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ob := DefaultOptions()
|
|
ob.ServerName = "HUB"
|
|
ob.LeafNode.Host = "127.0.0.1"
|
|
ob.LeafNode.Port = -1
|
|
ob.LeafNode.AuthTimeout = 10
|
|
if test.websocket {
|
|
ob.Websocket.Host = "127.0.0.1"
|
|
ob.Websocket.Port = -1
|
|
ob.Websocket.HandshakeTimeout = 10 * time.Second
|
|
ob.Websocket.AuthTimeout = 10
|
|
ob.Websocket.NoTLS = true
|
|
}
|
|
sb := RunServer(ob)
|
|
defer sb.Shutdown()
|
|
|
|
var port int
|
|
var scheme string
|
|
if test.websocket {
|
|
port = ob.Websocket.Port
|
|
scheme = wsSchemePrefix
|
|
} else {
|
|
port = ob.LeafNode.Port
|
|
scheme = "nats"
|
|
}
|
|
|
|
urlStr := fmt.Sprintf("%s://127.0.0.1:%d", scheme, port)
|
|
proxy := createNetProxy(1100*time.Millisecond, 1024*1024*1024, 1024*1024*1024, urlStr, true)
|
|
defer proxy.stop()
|
|
proxyURL := proxy.clientURL()
|
|
_, proxyPort, err := net.SplitHostPort(proxyURL[len(scheme)+3:])
|
|
require_NoError(t, err)
|
|
|
|
lnBURL, err := url.Parse(fmt.Sprintf("%s://127.0.0.1:%s", scheme, proxyPort))
|
|
require_NoError(t, err)
|
|
|
|
oa := DefaultOptions()
|
|
oa.ServerName = "SPOKE"
|
|
oa.Cluster.Name = "xyz"
|
|
remote := &RemoteLeafOpts{
|
|
URLs: []*url.URL{lnBURL},
|
|
FirstInfoTimeout: 3 * time.Second,
|
|
}
|
|
oa.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
sa := RunServer(oa)
|
|
defer sa.Shutdown()
|
|
|
|
checkLeafNodeConnected(t, sa)
|
|
})
|
|
}
|
|
}
|
|
|
|
type captureLeafConnClosed struct {
|
|
DummyLogger
|
|
ch chan struct{}
|
|
}
|
|
|
|
func (l *captureLeafConnClosed) Noticef(format string, v ...any) {
|
|
msg := fmt.Sprintf(format, v...)
|
|
if strings.Contains(msg, "Leafnode connection closed: Read Error") {
|
|
select {
|
|
case l.ch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeafNodeDetectsStaleConnectionIfNoInfo(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
websocket bool
|
|
}{
|
|
{"regular", false},
|
|
{"websocket", true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require_NoError(t, err)
|
|
defer l.Close()
|
|
|
|
ch := make(chan struct{})
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
c, err := l.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close()
|
|
<-ch
|
|
}()
|
|
|
|
var scheme string
|
|
if test.websocket {
|
|
scheme = wsSchemePrefix
|
|
} else {
|
|
scheme = "nats"
|
|
}
|
|
urlStr := fmt.Sprintf("%s://%s", scheme, l.Addr())
|
|
lnBURL, err := url.Parse(urlStr)
|
|
require_NoError(t, err)
|
|
|
|
oa := DefaultOptions()
|
|
oa.ServerName = "SPOKE"
|
|
oa.Cluster.Name = "xyz"
|
|
remote := &RemoteLeafOpts{
|
|
URLs: []*url.URL{lnBURL},
|
|
FirstInfoTimeout: 250 * time.Millisecond,
|
|
}
|
|
oa.LeafNode.Remotes = []*RemoteLeafOpts{remote}
|
|
oa.DisableShortFirstPing = false
|
|
oa.NoLog = false
|
|
sa, err := NewServer(oa)
|
|
require_NoError(t, err)
|
|
defer sa.Shutdown()
|
|
|
|
log := &captureLeafConnClosed{ch: make(chan struct{}, 1)}
|
|
sa.SetLogger(log, false, false)
|
|
sa.Start()
|
|
|
|
select {
|
|
case <-log.ch:
|
|
// OK
|
|
case <-time.After(750 * time.Millisecond):
|
|
t.Fatalf("Connection was not closed")
|
|
}
|
|
|
|
sa.Shutdown()
|
|
close(ch)
|
|
wg.Wait()
|
|
sa.WaitForShutdown()
|
|
})
|
|
}
|
|
}
|