mirror of
https://github.com/hanzoai/pubsub-go.git
synced 2026-10-30 18:23:30 +00:00
* Release v1.49.0 (#2025) Signed-off-by: Piotr Piotrowski <[email protected]> * [FIXED] Remove conn status listener on Consume and Messages cleanup (#2027) Signed-off-by: txuna <[email protected]> * Add Claude Code reviews (#2035) Signed-off-by: Tomasz Pietrek <[email protected]> * [IMPROVED] Bump CI Go versions to 1.25/1.26 and simplify version management (#2039) Signed-off-by: Piotr Piotrowski <[email protected]> * [IMPROVED] Reject control characters in stream and consumer names (#2038) Signed-off-by: Piotr Piotrowski <[email protected]> * [IMPROVED] Deduplicate keys in KeyValue.Keys() and document ListKeys behavior (#2029) Un-deprecate Keys() and add deduplication using slices.Sort + slices.Compact to handle cases where the server returns duplicate keys during concurrent writes. Add documentation notes to ListKeys and ListKeysFiltered warning about possible duplicates on large buckets with frequent writes. Signed-off-by: Piotr Piotrowski <[email protected]> * Add missing AccountLimits fields in jetstream package (#2041) Signed-off-by: Piotr Piotrowski <[email protected]> * [IMPROVED] Bump dependencies and go version (#2044) Signed-off-by: Piotr Piotrowski <[email protected]> * [FIXED] Fix race condition in orderedSubscription.Drain() (#2030) Signed-off-by: Piotr Piotrowski <[email protected]> * [FIXED] Fix flaky TestConsumerPrioritized/messages test (#2033) Signed-off-by: Piotr Piotrowski <[email protected]> * [IMPROVED] De-flake TestAlwaysReconnectOnAccountMaxConnectionsExceededErr (#2042) Signed-off-by: Piotr Piotrowski <[email protected]> * [FIXED] Fix flaky TestKeyValueWithSources test (#2036) * [FIXED] Fix flaky TestKeyValueWithSources test Signed-off-by: Piotr Piotrowski <[email protected]> * Adjust timeout in test Signed-off-by: Piotr Piotrowski <[email protected]> * trigger ci Signed-off-by: Piotr Piotrowski <[email protected]> --------- Signed-off-by: Piotr Piotrowski <[email protected]> * [FIXED] Fixed OrderedConsumer.Consume race in handler (#2043) Signed-off-by: Piotr Piotrowski <[email protected]> * [FIXED] WebSocket close frame discarding buffered data frames (#2032) When a NATS server rejects a WebSocket connection (e.g., due to an expired JWT), it sends a -ERR protocol message followed by a WebSocket close frame. If both frames arrive in the same TCP read, the websocketReader.Read() method would process the close frame and immediately return io.EOF, discarding the -ERR data that had already been buffered in the pending list. This caused nats.Connect to return a generic EOF error instead of the proper authentication error (ErrAuthorization, ErrAuthExpired, etc.), making it difficult for users to diagnose connection failures. The fix defers the close frame's io.EOF when there is already pending data, storing it in a new closeErr field. The pending data is returned first, and the deferred error is returned on the subsequent Read() call. Signed-off-by: Waldemar Quevedo <[email protected]> * [IMPROVED] Wrap EOF/connection reset errors with TLS context after handshake (#2031) * Wrap EOF/connection reset errors with TLS context after handshake When a TLS proxy (e.g. nginx with mTLS) completes the TLS handshake but then closes the connection due to an untrusted client certificate, the client would receive a bare "EOF" or "connection reset by peer" with no indication that TLS was involved. This wraps such errors with context: "nats: connection closed by remote after TLS handshake: EOF" The wrapped error preserves the original via %w for backwards compatibility (errors.Is(err, io.EOF) still returns true). Signed-off-by: Waldemar Quevedo <[email protected]> * Wrap TLS errors in ErrTLS Signed-off-by: Piotr Piotrowski <[email protected]> --------- Signed-off-by: Waldemar Quevedo <[email protected]> Signed-off-by: Piotr Piotrowski <[email protected]> Co-authored-by: Piotr Piotrowski <[email protected]> * Release v1.50.0 (#2045) Signed-off-by: Piotr Piotrowski <[email protected]> --------- Signed-off-by: Piotr Piotrowski <[email protected]> Signed-off-by: txuna <[email protected]> Signed-off-by: Tomasz Pietrek <[email protected]> Signed-off-by: Waldemar Quevedo <[email protected]> Co-authored-by: Piotr Piotrowski <[email protected]> Co-authored-by: tuuna <[email protected]> Co-authored-by: Tomasz Pietrek <[email protected]> Co-authored-by: Waldemar Quevedo <[email protected]>
1946 lines
50 KiB
Go
1946 lines
50 KiB
Go
// Copyright 2013-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 test
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/jwt/v2"
|
|
"github.com/nats-io/nats-server/v2/server"
|
|
"github.com/hanzoai/pubsub-go"
|
|
"github.com/nats-io/nkeys"
|
|
)
|
|
|
|
func startReconnectServer(t *testing.T) *server.Server {
|
|
return RunServerOnPort(TEST_PORT)
|
|
}
|
|
|
|
func TestReconnectTotalTime(t *testing.T) {
|
|
opts := nats.GetDefaultOptions()
|
|
totalReconnectTime := time.Duration(opts.MaxReconnect) * opts.ReconnectWait
|
|
if totalReconnectTime < (2 * time.Minute) {
|
|
t.Fatalf("Total reconnect time should be at least 2 mins: Currently %v\n",
|
|
totalReconnectTime)
|
|
}
|
|
}
|
|
|
|
func TestDefaultReconnectJitter(t *testing.T) {
|
|
opts := nats.GetDefaultOptions()
|
|
if opts.ReconnectJitter != nats.DefaultReconnectJitter {
|
|
t.Fatalf("Expected default jitter for non TLS to be %v, got %v", nats.DefaultReconnectJitter, opts.ReconnectJitter)
|
|
}
|
|
if opts.ReconnectJitterTLS != nats.DefaultReconnectJitterTLS {
|
|
t.Fatalf("Expected default jitter for TLS to be %v, got %v", nats.DefaultReconnectJitterTLS, opts.ReconnectJitterTLS)
|
|
}
|
|
}
|
|
|
|
func TestReconnectDisallowedFlags(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
ch := make(chan bool)
|
|
opts := nats.GetDefaultOptions()
|
|
opts.Url = fmt.Sprintf("nats://127.0.0.1:%d", TEST_PORT)
|
|
opts.AllowReconnect = false
|
|
opts.ClosedCB = func(_ *nats.Conn) {
|
|
ch <- true
|
|
}
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
ts.Shutdown()
|
|
|
|
if e := Wait(ch); e != nil {
|
|
t.Fatal("Did not trigger ClosedCB correctly")
|
|
}
|
|
}
|
|
|
|
func TestReconnectAllowedFlags(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
ch := make(chan bool)
|
|
dch := make(chan bool)
|
|
opts := nats.GetDefaultOptions()
|
|
opts.Url = fmt.Sprintf("nats://127.0.0.1:%d", TEST_PORT)
|
|
opts.AllowReconnect = true
|
|
opts.MaxReconnect = 2
|
|
opts.ReconnectWait = 1 * time.Second
|
|
nats.ReconnectJitter(0, 0)(&opts)
|
|
|
|
opts.ClosedCB = func(_ *nats.Conn) {
|
|
ch <- true
|
|
}
|
|
opts.DisconnectedErrCB = func(_ *nats.Conn, _ error) {
|
|
dch <- true
|
|
}
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
ts.Shutdown()
|
|
|
|
// We want wait to timeout here, and the connection
|
|
// should not trigger the Close CB.
|
|
if e := WaitTime(ch, 500*time.Millisecond); e == nil {
|
|
t.Fatal("Triggered ClosedCB incorrectly")
|
|
}
|
|
|
|
// We should wait to get the disconnected callback to ensure
|
|
// that we are in the process of reconnecting.
|
|
if e := Wait(dch); e != nil {
|
|
t.Fatal("DisconnectedErrCB should have been triggered")
|
|
}
|
|
|
|
if !nc.IsReconnecting() {
|
|
t.Fatal("Expected to be in a reconnecting state")
|
|
}
|
|
|
|
// clear the CloseCB since ch will block
|
|
nc.Opts.ClosedCB = nil
|
|
}
|
|
|
|
func TestConnCloseBreaksReconnectLoop(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
cch := make(chan bool)
|
|
|
|
opts := reconnectOpts
|
|
// Bump the max reconnect attempts
|
|
opts.MaxReconnect = 100
|
|
opts.ClosedCB = func(_ *nats.Conn) {
|
|
cch <- true
|
|
}
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
nc.Flush()
|
|
|
|
// Shutdown the server
|
|
ts.Shutdown()
|
|
|
|
// Wait a second, then close the connection
|
|
time.Sleep(time.Second)
|
|
|
|
// Close the connection, this should break the reconnect loop.
|
|
// Do this in a go routine since the issue was that Close()
|
|
// would block until the reconnect loop is done.
|
|
go nc.Close()
|
|
|
|
// Even on Windows (where a createConn takes more than a second)
|
|
// we should be able to break the reconnect loop with the following
|
|
// timeout.
|
|
if err := WaitTime(cch, 3*time.Second); err != nil {
|
|
t.Fatal("Did not get a closed callback")
|
|
}
|
|
}
|
|
|
|
func TestBasicReconnectFunctionality(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
ch := make(chan bool)
|
|
dch := make(chan bool, 2)
|
|
|
|
opts := reconnectOpts
|
|
|
|
opts.DisconnectedErrCB = func(_ *nats.Conn, _ error) {
|
|
dch <- true
|
|
}
|
|
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v\n", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
testString := "bar"
|
|
nc.Subscribe("foo", func(m *nats.Msg) {
|
|
if string(m.Data) != testString {
|
|
t.Fatal("String doesn't match")
|
|
}
|
|
ch <- true
|
|
})
|
|
nc.Flush()
|
|
|
|
ts.Shutdown()
|
|
// server is stopped here...
|
|
|
|
if err := Wait(dch); err != nil {
|
|
t.Fatalf("Did not get the disconnected callback on time\n")
|
|
}
|
|
|
|
if err := nc.Publish("foo", []byte("bar")); err != nil {
|
|
t.Fatalf("Failed to publish message: %v\n", err)
|
|
}
|
|
|
|
ts = startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
if err := nc.FlushTimeout(5 * time.Second); err != nil {
|
|
t.Fatalf("Error on Flush: %v", err)
|
|
}
|
|
|
|
if e := Wait(ch); e != nil {
|
|
t.Fatal("Did not receive our message")
|
|
}
|
|
|
|
expectedReconnectCount := uint64(1)
|
|
reconnectCount := nc.Stats().Reconnects
|
|
|
|
if reconnectCount != expectedReconnectCount {
|
|
t.Fatalf("Reconnect count incorrect: %d vs %d\n",
|
|
reconnectCount, expectedReconnectCount)
|
|
}
|
|
}
|
|
|
|
func TestExtendedReconnectFunctionality(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
opts := reconnectOpts
|
|
dch := make(chan bool, 2)
|
|
opts.DisconnectedErrCB = func(_ *nats.Conn, _ error) {
|
|
dch <- true
|
|
}
|
|
rch := make(chan bool, 1)
|
|
opts.ReconnectedCB = func(_ *nats.Conn) {
|
|
rch <- true
|
|
}
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
testString := "bar"
|
|
received := int32(0)
|
|
|
|
nc.Subscribe("foo", func(*nats.Msg) {
|
|
atomic.AddInt32(&received, 1)
|
|
})
|
|
|
|
sub, _ := nc.Subscribe("foobar", func(*nats.Msg) {
|
|
atomic.AddInt32(&received, 1)
|
|
})
|
|
|
|
nc.Publish("foo", []byte(testString))
|
|
nc.Flush()
|
|
|
|
ts.Shutdown()
|
|
// server is stopped here..
|
|
|
|
// wait for disconnect
|
|
if e := WaitTime(dch, 2*time.Second); e != nil {
|
|
t.Fatal("Did not receive a disconnect callback message")
|
|
}
|
|
|
|
// Sub while disconnected
|
|
nc.Subscribe("bar", func(*nats.Msg) {
|
|
atomic.AddInt32(&received, 1)
|
|
})
|
|
|
|
// Unsub foobar while disconnected
|
|
sub.Unsubscribe()
|
|
|
|
if err = nc.Publish("foo", []byte(testString)); err != nil {
|
|
t.Fatalf("Received an error after disconnect: %v\n", err)
|
|
}
|
|
|
|
if err = nc.Publish("bar", []byte(testString)); err != nil {
|
|
t.Fatalf("Received an error after disconnect: %v\n", err)
|
|
}
|
|
|
|
ts = startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
// server is restarted here..
|
|
// wait for reconnect
|
|
if e := WaitTime(rch, 2*time.Second); e != nil {
|
|
t.Fatal("Did not receive a reconnect callback message")
|
|
}
|
|
|
|
if err = nc.Publish("foobar", []byte(testString)); err != nil {
|
|
t.Fatalf("Received an error after server restarted: %v\n", err)
|
|
}
|
|
|
|
if err = nc.Publish("foo", []byte(testString)); err != nil {
|
|
t.Fatalf("Received an error after server restarted: %v\n", err)
|
|
}
|
|
|
|
ch := make(chan bool)
|
|
nc.Subscribe("done", func(*nats.Msg) {
|
|
ch <- true
|
|
})
|
|
nc.Publish("done", nil)
|
|
|
|
if e := Wait(ch); e != nil {
|
|
t.Fatal("Did not receive our message")
|
|
}
|
|
|
|
// Sleep a bit to guarantee scheduler runs and process all subs.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
if atomic.LoadInt32(&received) != 4 {
|
|
t.Fatalf("Received != %d, equals %d\n", 4, received)
|
|
}
|
|
}
|
|
|
|
func TestQueueSubsOnReconnect(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
opts := reconnectOpts
|
|
|
|
// Allow us to block on reconnect complete.
|
|
reconnectsDone := make(chan bool)
|
|
opts.ReconnectedCB = func(nc *nats.Conn) {
|
|
reconnectsDone <- true
|
|
}
|
|
|
|
// Create connection
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v\n", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
// To hold results.
|
|
results := make(map[int]int)
|
|
var mu sync.Mutex
|
|
|
|
// Make sure we got what we needed, 1 msg only and all seqnos accounted for..
|
|
checkResults := func(numSent int) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
for i := 0; i < numSent; i++ {
|
|
if results[i] != 1 {
|
|
t.Fatalf("Received incorrect number of messages, [%d] for seq: %d\n", results[i], i)
|
|
}
|
|
}
|
|
|
|
// Auto reset results map
|
|
results = make(map[int]int)
|
|
}
|
|
|
|
subj := "foo.bar"
|
|
qgroup := "workers"
|
|
|
|
cb := func(m *nats.Msg) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
seqno, err := strconv.Atoi(string(m.Data))
|
|
if err != nil {
|
|
t.Fatalf("Received an invalid sequence number: %v\n", err)
|
|
}
|
|
results[seqno] = results[seqno] + 1
|
|
}
|
|
|
|
// Create Queue Subscribers
|
|
nc.QueueSubscribe(subj, qgroup, cb)
|
|
nc.QueueSubscribe(subj, qgroup, cb)
|
|
|
|
nc.Flush()
|
|
|
|
// Helper function to send messages and check results.
|
|
sendAndCheckMsgs := func(numToSend int) {
|
|
for i := 0; i < numToSend; i++ {
|
|
nc.Publish(subj, []byte(fmt.Sprint(i)))
|
|
}
|
|
// Wait for processing.
|
|
nc.Flush()
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Check Results
|
|
checkResults(numToSend)
|
|
}
|
|
|
|
// Base Test
|
|
sendAndCheckMsgs(10)
|
|
|
|
// Stop and restart server
|
|
ts.Shutdown()
|
|
ts = startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
if err := Wait(reconnectsDone); err != nil {
|
|
t.Fatal("Did not get the ReconnectedCB!")
|
|
}
|
|
|
|
// Reconnect Base Test
|
|
sendAndCheckMsgs(10)
|
|
}
|
|
|
|
func TestIsClosed(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
nc := NewConnection(t, TEST_PORT)
|
|
defer nc.Close()
|
|
|
|
if nc.IsClosed() {
|
|
t.Fatalf("IsClosed returned true when the connection is still open.")
|
|
}
|
|
ts.Shutdown()
|
|
if nc.IsClosed() {
|
|
t.Fatalf("IsClosed returned true when the connection is still open.")
|
|
}
|
|
ts = startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
if nc.IsClosed() {
|
|
t.Fatalf("IsClosed returned true when the connection is still open.")
|
|
}
|
|
nc.Close()
|
|
if !nc.IsClosed() {
|
|
t.Fatalf("IsClosed returned false after Close() was called.")
|
|
}
|
|
}
|
|
|
|
func TestIsReconnectingAndStatus(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
disconnectedch := make(chan bool, 3)
|
|
reconnectch := make(chan bool, 2)
|
|
opts := nats.GetDefaultOptions()
|
|
opts.Url = fmt.Sprintf("nats://127.0.0.1:%d", TEST_PORT)
|
|
opts.AllowReconnect = true
|
|
opts.MaxReconnect = 10000
|
|
opts.ReconnectWait = 100 * time.Millisecond
|
|
nats.ReconnectJitter(0, 0)(&opts)
|
|
|
|
opts.DisconnectedErrCB = func(_ *nats.Conn, _ error) {
|
|
disconnectedch <- true
|
|
}
|
|
opts.ReconnectedCB = func(_ *nats.Conn) {
|
|
reconnectch <- true
|
|
}
|
|
|
|
// Connect, verify initial reconnecting state check, then stop the server
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
if nc.IsReconnecting() {
|
|
t.Fatalf("IsReconnecting returned true when the connection is still open.")
|
|
}
|
|
if status := nc.Status(); status != nats.CONNECTED {
|
|
t.Fatalf("Status returned %d when connected instead of CONNECTED", status)
|
|
}
|
|
ts.Shutdown()
|
|
|
|
// Wait until we get the disconnected callback
|
|
if e := Wait(disconnectedch); e != nil {
|
|
t.Fatalf("Disconnect callback wasn't triggered: %v", e)
|
|
}
|
|
if !nc.IsReconnecting() {
|
|
t.Fatalf("IsReconnecting returned false when the client is reconnecting.")
|
|
}
|
|
if status := nc.Status(); status != nats.RECONNECTING {
|
|
t.Fatalf("Status returned %d when reconnecting instead of CONNECTED", status)
|
|
}
|
|
|
|
ts = startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
// Wait until we get the reconnect callback
|
|
if e := Wait(reconnectch); e != nil {
|
|
t.Fatalf("Reconnect callback wasn't triggered: %v", e)
|
|
}
|
|
if nc.IsReconnecting() {
|
|
t.Fatalf("IsReconnecting returned true after the connection was reconnected.")
|
|
}
|
|
if status := nc.Status(); status != nats.CONNECTED {
|
|
t.Fatalf("Status returned %d when reconnected instead of CONNECTED", status)
|
|
}
|
|
|
|
// Close the connection, reconnecting should still be false
|
|
nc.Close()
|
|
if nc.IsReconnecting() {
|
|
t.Fatalf("IsReconnecting returned true after Close() was called.")
|
|
}
|
|
if status := nc.Status(); status != nats.CLOSED {
|
|
t.Fatalf("Status returned %d after Close() was called instead of CLOSED", status)
|
|
}
|
|
}
|
|
|
|
func TestFullFlushChanDuringReconnect(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
reconnectch := make(chan bool, 2)
|
|
|
|
opts := nats.GetDefaultOptions()
|
|
opts.Url = fmt.Sprintf("nats://127.0.0.1:%d", TEST_PORT)
|
|
opts.AllowReconnect = true
|
|
opts.MaxReconnect = 10000
|
|
opts.ReconnectWait = 100 * time.Millisecond
|
|
nats.ReconnectJitter(0, 0)(&opts)
|
|
|
|
opts.ReconnectedCB = func(_ *nats.Conn) {
|
|
reconnectch <- true
|
|
}
|
|
|
|
// Connect
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
// Channel used to make the go routine sending messages to stop.
|
|
stop := make(chan bool)
|
|
|
|
// While connected, publish as fast as we can
|
|
go func() {
|
|
for i := 0; ; i++ {
|
|
_ = nc.Publish("foo", []byte("hello"))
|
|
|
|
// Make sure we are sending at least flushChanSize (1024) messages
|
|
// before potentially pausing.
|
|
if i%2000 == 0 {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Send a bit...
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Shut down the server
|
|
ts.Shutdown()
|
|
|
|
// Continue sending while we are disconnected
|
|
time.Sleep(time.Second)
|
|
|
|
// Restart the server
|
|
ts = startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
// Wait for the reconnect CB to be invoked (but not for too long)
|
|
if e := WaitTime(reconnectch, 5*time.Second); e != nil {
|
|
t.Fatalf("Reconnect callback wasn't triggered: %v", e)
|
|
}
|
|
close(stop)
|
|
}
|
|
|
|
func TestReconnectVerbose(t *testing.T) {
|
|
s := RunDefaultServer()
|
|
defer s.Shutdown()
|
|
|
|
o := nats.GetDefaultOptions()
|
|
o.ReconnectWait = 50 * time.Millisecond
|
|
o.Verbose = true
|
|
rch := make(chan bool)
|
|
o.ReconnectedCB = func(_ *nats.Conn) {
|
|
rch <- true
|
|
}
|
|
|
|
nc, err := o.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
err = nc.Flush()
|
|
if err != nil {
|
|
t.Fatalf("Error during flush: %v", err)
|
|
}
|
|
|
|
s.Shutdown()
|
|
s = RunDefaultServer()
|
|
defer s.Shutdown()
|
|
|
|
if e := Wait(rch); e != nil {
|
|
t.Fatal("Should have reconnected ok")
|
|
}
|
|
|
|
err = nc.Flush()
|
|
if err != nil {
|
|
t.Fatalf("Error during flush: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReconnectBufSizeOption(t *testing.T) {
|
|
s := RunDefaultServer()
|
|
defer s.Shutdown()
|
|
|
|
nc, err := nats.Connect("nats://127.0.0.1:4222", nats.ReconnectBufSize(32))
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
if nc.Opts.ReconnectBufSize != 32 {
|
|
t.Fatalf("ReconnectBufSize should be 32 but it is %d", nc.Opts.ReconnectBufSize)
|
|
}
|
|
}
|
|
|
|
func TestReconnectBufSize(t *testing.T) {
|
|
s := RunDefaultServer()
|
|
defer s.Shutdown()
|
|
|
|
o := nats.GetDefaultOptions()
|
|
o.ReconnectBufSize = 32 // 32 bytes
|
|
|
|
dch := make(chan bool)
|
|
o.DisconnectedErrCB = func(_ *nats.Conn, _ error) {
|
|
dch <- true
|
|
}
|
|
|
|
nc, err := o.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
err = nc.Flush()
|
|
if err != nil {
|
|
t.Fatalf("Error during flush: %v", err)
|
|
}
|
|
|
|
// Force disconnected state.
|
|
s.Shutdown()
|
|
|
|
if e := Wait(dch); e != nil {
|
|
t.Fatal("DisconnectedErrCB should have been triggered")
|
|
}
|
|
|
|
msg := []byte("food") // 4 bytes paylaod, total proto is 16 bytes
|
|
// These should work, 2X16 = 32
|
|
if err := nc.Publish("foo", msg); err != nil {
|
|
t.Fatalf("Failed to publish message: %v\n", err)
|
|
}
|
|
if err := nc.Publish("foo", msg); err != nil {
|
|
t.Fatalf("Failed to publish message: %v\n", err)
|
|
}
|
|
|
|
// This should fail since we have exhausted the backing buffer.
|
|
if err := nc.Publish("foo", msg); err == nil {
|
|
t.Fatalf("Expected to fail to publish message: got no error\n")
|
|
}
|
|
nc.Buffered()
|
|
}
|
|
|
|
// When a cluster is fronted by a single DNS name (desired) but communicates IPs to clients (also desired),
|
|
// and we use TLS, we want to make sure we do the right thing connecting to an IP directly for TLS to work.
|
|
// The reason this may happen is that the cluster has a single DNS name and a single certificate, but the cluster
|
|
// wants to vend out IPs and not wait on DNS for topology changes and failover.
|
|
func TestReconnectTLSHostNoIP(t *testing.T) {
|
|
sa, optsA := RunServerWithConfig("./configs/tls_noip_a.conf")
|
|
defer sa.Shutdown()
|
|
sb, optsB := RunServerWithConfig("./configs/tls_noip_b.conf")
|
|
defer sb.Shutdown()
|
|
|
|
// Wait for cluster to form.
|
|
wait := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(wait) {
|
|
sanr := sa.NumRoutes()
|
|
sbnr := sb.NumRoutes()
|
|
if sanr == 1 && sbnr == 1 {
|
|
break
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
|
|
endpoint := fmt.Sprintf("%s:%d", optsA.Host, optsA.Port)
|
|
secureURL := fmt.Sprintf("tls://%s:%s@%s/", optsA.Username, optsA.Password, endpoint)
|
|
|
|
dch := make(chan bool, 2)
|
|
dcb := func(_ *nats.Conn, _ error) { dch <- true }
|
|
rch := make(chan bool)
|
|
rcb := func(_ *nats.Conn) { rch <- true }
|
|
|
|
nc, err := nats.Connect(secureURL,
|
|
nats.RootCAs("./configs/certs/ca.pem"),
|
|
nats.DisconnectErrHandler(dcb),
|
|
nats.ReconnectHandler(rcb))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create secure (TLS) connection: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
// Wait for DiscoveredServers() to be 1.
|
|
wait = time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(wait) {
|
|
if len(nc.DiscoveredServers()) == 1 {
|
|
break
|
|
}
|
|
}
|
|
// Make sure this is the server B info, and that it is an IP.
|
|
expectedDiscoverURL := fmt.Sprintf("tls://%s:%d", optsB.Host, optsB.Port)
|
|
eurl, err := url.Parse(expectedDiscoverURL)
|
|
if err != nil {
|
|
t.Fatalf("Expected to parse discovered server URL: %v", err)
|
|
}
|
|
if addr := net.ParseIP(eurl.Hostname()); addr == nil {
|
|
t.Fatalf("Expected the discovered server to be an IP, got %v", eurl.Hostname())
|
|
}
|
|
ds := nc.DiscoveredServers()
|
|
if ds[0] != expectedDiscoverURL {
|
|
t.Fatalf("Expected %q, got %q", expectedDiscoverURL, ds[0])
|
|
}
|
|
|
|
// Force us to switch servers.
|
|
sa.Shutdown()
|
|
|
|
if e := Wait(dch); e != nil {
|
|
t.Fatal("DisconnectedErrCB should have been triggered")
|
|
}
|
|
if e := WaitTime(rch, time.Second); e != nil {
|
|
t.Fatalf("ReconnectedCB should have been triggered: %v", nc.LastError())
|
|
}
|
|
}
|
|
|
|
var reconnectOpts = nats.Options{
|
|
Url: fmt.Sprintf("nats://127.0.0.1:%d", TEST_PORT),
|
|
AllowReconnect: true,
|
|
MaxReconnect: 10,
|
|
ReconnectWait: 100 * time.Millisecond,
|
|
Timeout: nats.DefaultTimeout,
|
|
}
|
|
|
|
func TestConnCloseNoCallback(t *testing.T) {
|
|
ts := startReconnectServer(t)
|
|
defer ts.Shutdown()
|
|
|
|
// create a connection that manually sets the options
|
|
var conns []*nats.Conn
|
|
cch := make(chan string, 2)
|
|
opts := reconnectOpts
|
|
opts.ClosedCB = func(_ *nats.Conn) {
|
|
cch <- "manual"
|
|
}
|
|
opts.NoCallbacksAfterClientClose = true
|
|
nc, err := opts.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
conns = append(conns, nc)
|
|
|
|
// and another connection that uses the option
|
|
nc2, err := nats.Connect(reconnectOpts.Url, nats.NoCallbacksAfterClientClose(),
|
|
nats.ClosedHandler(func(_ *nats.Conn) {
|
|
cch <- "opts"
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
conns = append(conns, nc2)
|
|
|
|
// defer close() for safety, flush() and close()
|
|
for _, c := range conns {
|
|
defer c.Close()
|
|
c.Flush()
|
|
|
|
// Close the connection, we don't expect to get a notification
|
|
c.Close()
|
|
}
|
|
|
|
// if the timeout happens we didn't get data from the channel
|
|
// if we get a value from the channel that connection type failed.
|
|
select {
|
|
case <-time.After(500 * time.Millisecond):
|
|
// test passed - we timed so no callback was called
|
|
case what := <-cch:
|
|
t.Fatalf("%s issued a callback and it shouldn't have", what)
|
|
}
|
|
}
|
|
|
|
func TestReconnectBufSizeDisable(t *testing.T) {
|
|
s := RunDefaultServer()
|
|
defer s.Shutdown()
|
|
|
|
o := nats.GetDefaultOptions()
|
|
|
|
// Disable buffering to always get a synchronous error when publish fails.
|
|
o.ReconnectBufSize = -1
|
|
|
|
dch := make(chan bool)
|
|
o.DisconnectedErrCB = func(_ *nats.Conn, _ error) {
|
|
dch <- true
|
|
}
|
|
|
|
nc, err := o.Connect()
|
|
if err != nil {
|
|
t.Fatalf("Should have connected ok: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
err = nc.Flush()
|
|
if err != nil {
|
|
t.Fatalf("Error during flush: %v", err)
|
|
}
|
|
|
|
// Force disconnected state.
|
|
s.Shutdown()
|
|
|
|
if e := Wait(dch); e != nil {
|
|
t.Fatal("DisconnectedErrCB should have been triggered")
|
|
}
|
|
|
|
msg := []byte("food")
|
|
if err := nc.Publish("foo", msg); err != nats.ErrReconnectBufExceeded {
|
|
t.Fatalf("Unexpected error: %v\n", err)
|
|
}
|
|
got, _ := nc.Buffered()
|
|
if got != 0 {
|
|
t.Errorf("Unexpected buffered bytes: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAuthExpiredReconnect(t *testing.T) {
|
|
ts := runTrustServer()
|
|
defer ts.Shutdown()
|
|
|
|
_, err := nats.Connect(ts.ClientURL())
|
|
if err == nil {
|
|
t.Fatalf("Expecting an error on connect")
|
|
}
|
|
ukp, err := nkeys.FromSeed(uSeed)
|
|
if err != nil {
|
|
t.Fatalf("Error creating user key pair: %v", err)
|
|
}
|
|
upub, err := ukp.PublicKey()
|
|
if err != nil {
|
|
t.Fatalf("Error getting user public key: %v", err)
|
|
}
|
|
akp, err := nkeys.FromSeed(aSeed)
|
|
if err != nil {
|
|
t.Fatalf("Error creating account key pair: %v", err)
|
|
}
|
|
|
|
jwtCB := func() (string, error) {
|
|
claims := jwt.NewUserClaims("test")
|
|
claims.Expires = time.Now().Add(time.Second).Unix()
|
|
claims.Subject = upub
|
|
jwt, err := claims.Encode(akp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return jwt, nil
|
|
}
|
|
sigCB := func(nonce []byte) ([]byte, error) {
|
|
kp, _ := nkeys.FromSeed(uSeed)
|
|
sig, _ := kp.Sign(nonce)
|
|
return sig, nil
|
|
}
|
|
|
|
errCh := make(chan error, 1)
|
|
nc, err := nats.Connect(ts.ClientURL(), nats.UserJWT(jwtCB, sigCB), nats.ReconnectWait(100*time.Millisecond),
|
|
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
|
|
errCh <- err
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("Expected to connect, got %v", err)
|
|
}
|
|
stasusCh := nc.StatusChanged(nats.RECONNECTING, nats.CONNECTED)
|
|
select {
|
|
case err := <-errCh:
|
|
if !errors.Is(err, nats.ErrAuthExpired) {
|
|
t.Fatalf("Expected auth expired error, got %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Did not get the auth expired error")
|
|
}
|
|
WaitOnChannel(t, stasusCh, nats.RECONNECTING)
|
|
WaitOnChannel(t, stasusCh, nats.CONNECTED)
|
|
nc.Close()
|
|
}
|
|
|
|
func TestForceReconnect(t *testing.T) {
|
|
s := RunDefaultServer()
|
|
|
|
nc, err := nats.Connect(s.ClientURL(), nats.ReconnectWait(10*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error on connect: %v", err)
|
|
}
|
|
|
|
statusCh := nc.StatusChanged(nats.RECONNECTING, nats.CONNECTED)
|
|
defer close(statusCh)
|
|
newStatus := make(chan nats.Status, 10)
|
|
// non-blocking channel, so we need to be constantly listening
|
|
go func() {
|
|
for {
|
|
s, ok := <-statusCh
|
|
if !ok {
|
|
return
|
|
}
|
|
newStatus <- s
|
|
}
|
|
}()
|
|
|
|
sub, err := nc.SubscribeSync("foo")
|
|
if err != nil {
|
|
t.Fatalf("Error on subscribe: %v", err)
|
|
}
|
|
if err := nc.Publish("foo", []byte("msg")); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
_, err = sub.NextMsg(time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Error getting message: %v", err)
|
|
}
|
|
|
|
// Force a reconnect
|
|
err = nc.ForceReconnect()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error on reconnect: %v", err)
|
|
}
|
|
|
|
WaitOnChannel(t, newStatus, nats.RECONNECTING)
|
|
WaitOnChannel(t, newStatus, nats.CONNECTED)
|
|
|
|
if err := nc.Publish("foo", []byte("msg")); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
_, err = sub.NextMsg(time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Error getting message: %v", err)
|
|
}
|
|
|
|
// shutdown server and then force a reconnect
|
|
s.Shutdown()
|
|
WaitOnChannel(t, newStatus, nats.RECONNECTING)
|
|
_, err = sub.NextMsg(100 * time.Millisecond)
|
|
if err == nil {
|
|
t.Fatal("Expected error getting message")
|
|
}
|
|
|
|
// restart server
|
|
s = RunDefaultServer()
|
|
defer s.Shutdown()
|
|
|
|
if err := nc.ForceReconnect(); err != nil {
|
|
t.Fatalf("Unexpected error on reconnect: %v", err)
|
|
}
|
|
// wait for the reconnect
|
|
// because the connection has long ReconnectWait,
|
|
// if force reconnect does not work, the test will timeout
|
|
WaitOnChannel(t, newStatus, nats.CONNECTED)
|
|
|
|
if err := nc.Publish("foo", []byte("msg")); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
_, err = sub.NextMsg(time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Error getting message: %v", err)
|
|
}
|
|
nc.Close()
|
|
}
|
|
|
|
func TestForceReconnectDisallowReconnect(t *testing.T) {
|
|
s := RunDefaultServer()
|
|
defer s.Shutdown()
|
|
|
|
nc, err := nats.Connect(s.ClientURL(), nats.NoReconnect())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error on connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
statusCh := nc.StatusChanged(nats.RECONNECTING, nats.CONNECTED)
|
|
defer close(statusCh)
|
|
newStatus := make(chan nats.Status, 10)
|
|
// non-blocking channel, so we need to be constantly listening
|
|
go func() {
|
|
for {
|
|
s, ok := <-statusCh
|
|
if !ok {
|
|
return
|
|
}
|
|
newStatus <- s
|
|
}
|
|
}()
|
|
|
|
sub, err := nc.SubscribeSync("foo")
|
|
if err != nil {
|
|
t.Fatalf("Error on subscribe: %v", err)
|
|
}
|
|
if err := nc.Publish("foo", []byte("msg")); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
_, err = sub.NextMsg(time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Error getting message: %v", err)
|
|
}
|
|
|
|
// Force a reconnect
|
|
err = nc.ForceReconnect()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error on reconnect: %v", err)
|
|
}
|
|
|
|
WaitOnChannel(t, newStatus, nats.RECONNECTING)
|
|
WaitOnChannel(t, newStatus, nats.CONNECTED)
|
|
|
|
if err := nc.Publish("foo", []byte("msg")); err != nil {
|
|
t.Fatalf("Error on publish: %v", err)
|
|
}
|
|
_, err = sub.NextMsg(time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Error getting message: %v", err)
|
|
}
|
|
|
|
}
|
|
|
|
func TestForceReconnectSubsequentCalls(t *testing.T) {
|
|
s := RunDefaultServer()
|
|
defer s.Shutdown()
|
|
|
|
nc, err := nats.Connect(s.ClientURL(), nats.ReconnectWait(10*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error on connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
statusCh := nc.StatusChanged(nats.RECONNECTING, nats.CONNECTED)
|
|
defer close(statusCh)
|
|
newStatus := make(chan nats.Status, 10)
|
|
// non-blocking channel, so we need to be constantly listening
|
|
go func() {
|
|
for {
|
|
s, ok := <-statusCh
|
|
if !ok {
|
|
return
|
|
}
|
|
newStatus <- s
|
|
}
|
|
}()
|
|
|
|
for range 10 {
|
|
err = nc.ForceReconnect()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error on reconnect: %v", err)
|
|
}
|
|
}
|
|
|
|
WaitOnChannel(t, newStatus, nats.RECONNECTING)
|
|
WaitOnChannel(t, newStatus, nats.CONNECTED)
|
|
|
|
// check that we did not try to reconnect again
|
|
select {
|
|
case <-newStatus:
|
|
t.Fatal("Should not have received a new status")
|
|
case <-time.After(200 * time.Millisecond):
|
|
}
|
|
|
|
// now force a reconnect again
|
|
if err := nc.ForceReconnect(); err != nil {
|
|
t.Fatalf("Unexpected error on reconnect: %v", err)
|
|
}
|
|
WaitOnChannel(t, newStatus, nats.RECONNECTING)
|
|
WaitOnChannel(t, newStatus, nats.CONNECTED)
|
|
}
|
|
|
|
func TestAuthExpiredForceReconnect(t *testing.T) {
|
|
ts := runTrustServer()
|
|
defer ts.Shutdown()
|
|
|
|
_, err := nats.Connect(ts.ClientURL())
|
|
if err == nil {
|
|
t.Fatalf("Expecting an error on connect")
|
|
}
|
|
ukp, err := nkeys.FromSeed(uSeed)
|
|
if err != nil {
|
|
t.Fatalf("Error creating user key pair: %v", err)
|
|
}
|
|
upub, err := ukp.PublicKey()
|
|
if err != nil {
|
|
t.Fatalf("Error getting user public key: %v", err)
|
|
}
|
|
akp, err := nkeys.FromSeed(aSeed)
|
|
if err != nil {
|
|
t.Fatalf("Error creating account key pair: %v", err)
|
|
}
|
|
|
|
jwtCB := func() (string, error) {
|
|
claims := jwt.NewUserClaims("test")
|
|
claims.Expires = time.Now().Add(time.Second).Unix()
|
|
claims.Subject = upub
|
|
jwt, err := claims.Encode(akp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return jwt, nil
|
|
}
|
|
sigCB := func(nonce []byte) ([]byte, error) {
|
|
kp, _ := nkeys.FromSeed(uSeed)
|
|
sig, _ := kp.Sign(nonce)
|
|
return sig, nil
|
|
}
|
|
|
|
errCh := make(chan error, 1)
|
|
nc, err := nats.Connect(ts.ClientURL(), nats.UserJWT(jwtCB, sigCB), nats.ReconnectWait(10*time.Second),
|
|
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
|
|
errCh <- err
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("Expected to connect, got %v", err)
|
|
}
|
|
defer nc.Close()
|
|
statusCh := nc.StatusChanged(nats.RECONNECTING, nats.CONNECTED)
|
|
defer close(statusCh)
|
|
newStatus := make(chan nats.Status, 10)
|
|
// non-blocking channel, so we need to be constantly listening
|
|
go func() {
|
|
for {
|
|
s, ok := <-statusCh
|
|
if !ok {
|
|
return
|
|
}
|
|
newStatus <- s
|
|
}
|
|
}()
|
|
time.Sleep(100 * time.Millisecond)
|
|
select {
|
|
case err := <-errCh:
|
|
if !errors.Is(err, nats.ErrAuthExpired) {
|
|
t.Fatalf("Expected auth expired error, got %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Did not get the auth expired error")
|
|
}
|
|
if err := nc.ForceReconnect(); err != nil {
|
|
t.Fatalf("Unexpected error on reconnect: %v", err)
|
|
}
|
|
WaitOnChannel(t, newStatus, nats.RECONNECTING)
|
|
WaitOnChannel(t, newStatus, nats.CONNECTED)
|
|
}
|
|
|
|
func TestAlwaysReconnectOnMaxConnectionsExceededErr(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
server_name: "A"
|
|
listen: 127.0.0.1:-1
|
|
max_connections: 2
|
|
`))
|
|
defer os.Remove(conf)
|
|
|
|
s, _ := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
clientOpts := func(name string) ([]nats.Option, chan error, chan struct{}, chan struct{}) {
|
|
disconnectCh := make(chan error, 10)
|
|
reconnectCh := make(chan struct{}, 10)
|
|
closedCh := make(chan struct{}, 10)
|
|
return []nats.Option{
|
|
nats.MaxReconnects(-1),
|
|
nats.ReconnectWait(10 * time.Millisecond),
|
|
nats.Timeout(200 * time.Millisecond),
|
|
nats.DisconnectErrHandler(func(c *nats.Conn, err error) {
|
|
clientID, _ := c.GetClientID()
|
|
t.Logf("[%v] Disconnected: %v: %v", name, clientID, err)
|
|
if err != nil {
|
|
disconnectCh <- err
|
|
}
|
|
}),
|
|
nats.ReconnectHandler(func(c *nats.Conn) {
|
|
clientID, _ := c.GetClientID()
|
|
t.Logf("[%v] Reconnected: %v", name, clientID)
|
|
reconnectCh <- struct{}{}
|
|
}),
|
|
nats.ClosedHandler(func(c *nats.Conn) {
|
|
clientID, _ := c.GetClientID()
|
|
t.Logf("[%v] Closed: %v", name, clientID)
|
|
closedCh <- struct{}{}
|
|
}),
|
|
nats.Name(name),
|
|
}, disconnectCh, reconnectCh, closedCh
|
|
}
|
|
|
|
opts1, errCh1, reconnectCh1, _ := clientOpts("A")
|
|
nc1, err := nats.Connect(s.ClientURL(), opts1...)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect first client: %v", err)
|
|
}
|
|
defer nc1.Close()
|
|
|
|
opts2, errCh2, reconnectCh2, _ := clientOpts("B")
|
|
nc2, err := nats.Connect(s.ClientURL(), opts2...)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect second client: %v", err)
|
|
}
|
|
defer nc2.Close()
|
|
|
|
if s.NumClients() != 2 {
|
|
t.Fatalf("Expected 2 client connections to server. Got %d", s.NumClients())
|
|
}
|
|
if err := nc1.Flush(); err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
if err := nc2.Flush(); err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// Reload to reduce number of connections.
|
|
err = os.WriteFile(conf, []byte(`
|
|
server_name: "A"
|
|
listen: 127.0.0.1:-1
|
|
max_connections: 1
|
|
`), 0666)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update config file: %v", err)
|
|
}
|
|
err = s.Reload()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error reloading config: %v", err)
|
|
}
|
|
|
|
select {
|
|
case err := <-errCh1:
|
|
if err != nats.ErrMaxConnectionsExceeded {
|
|
t.Fatalf("Expected ErrMaxConnectionsExceeded, got %v", err)
|
|
}
|
|
case err := <-errCh2:
|
|
if err != nats.ErrMaxConnectionsExceeded {
|
|
t.Fatalf("Expected ErrMaxConnectionsExceeded, got %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for disconnect event")
|
|
}
|
|
if nc1.IsConnected() && nc2.IsConnected() {
|
|
t.Fatalf("Expected a client to be disconnected")
|
|
}
|
|
|
|
// Increase number of connections.
|
|
err = os.WriteFile(conf, []byte(`
|
|
server_name: "A"
|
|
listen: 127.0.0.1:-1
|
|
max_connections: 10
|
|
`), 0666)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update config file: %v", err)
|
|
}
|
|
err = s.Reload()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error reloading config: %v", err)
|
|
}
|
|
|
|
// Wait for reconnect.
|
|
select {
|
|
case <-reconnectCh1:
|
|
case <-reconnectCh2:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("Timed out waiting for reconnect event")
|
|
}
|
|
if !(nc1.IsConnected() || nc2.IsConnected()) {
|
|
t.Fatal("At least one client should have reconnected successfully")
|
|
}
|
|
if s.NumClients() < 1 {
|
|
t.Fatalf("Expected at least 1 client reconnected to server. Got %d", s.NumClients())
|
|
}
|
|
}
|
|
|
|
func TestAlwaysReconnectOnAccountMaxConnectionsExceededErr(t *testing.T) {
|
|
conf := createConfFile(t, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: A
|
|
accounts: {
|
|
ACCT1: {
|
|
users: [ {user: "user1", password: "pass1"} ]
|
|
limits: {
|
|
max_conn: 1
|
|
}
|
|
},
|
|
ACCT2: {
|
|
users: [ {user: "user2", password: "pass2"} ]
|
|
limits: {
|
|
max_conn: 2
|
|
}
|
|
}
|
|
}
|
|
`))
|
|
defer os.Remove(conf)
|
|
|
|
s, _ := RunServerWithConfig(conf)
|
|
defer s.Shutdown()
|
|
|
|
clientOpts := func(name string) ([]nats.Option, chan error, chan struct{}, chan struct{}) {
|
|
disconnectCh := make(chan error, 10)
|
|
reconnectCh := make(chan struct{}, 10)
|
|
closedCh := make(chan struct{}, 10)
|
|
return []nats.Option{
|
|
nats.MaxReconnects(-1),
|
|
nats.ReconnectWait(200 * time.Millisecond),
|
|
nats.Timeout(200 * time.Millisecond),
|
|
nats.DisconnectErrHandler(func(c *nats.Conn, err error) {
|
|
clientID, _ := c.GetClientID()
|
|
t.Logf("[%v] Disconnected: %v: %v", name, clientID, err)
|
|
if err != nil {
|
|
disconnectCh <- err
|
|
}
|
|
}),
|
|
nats.ReconnectHandler(func(c *nats.Conn) {
|
|
clientID, _ := c.GetClientID()
|
|
t.Logf("[%v] Reconnected: %v", name, clientID)
|
|
reconnectCh <- struct{}{}
|
|
}),
|
|
nats.ClosedHandler(func(c *nats.Conn) {
|
|
clientID, _ := c.GetClientID()
|
|
t.Logf("[%v] Closed: %v", name, clientID)
|
|
closedCh <- struct{}{}
|
|
}),
|
|
nats.Name(name),
|
|
}, disconnectCh, reconnectCh, closedCh
|
|
}
|
|
|
|
opts1, _, _, _ := clientOpts("ACCT1:C:1")
|
|
opts1 = append(opts1, nats.UserInfo("user1", "pass1"))
|
|
nc1, err := nats.Connect(s.ClientURL(), opts1...)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect first client: %v", err)
|
|
}
|
|
defer nc1.Close()
|
|
|
|
if s.NumClients() != 1 {
|
|
t.Fatalf("Expected 1 client connection to server. Got %d", s.NumClients())
|
|
}
|
|
|
|
// New connection fails due to limit.
|
|
opts2, _, _, _ := clientOpts("ACCT1:C:2")
|
|
opts2 = append(opts2, nats.UserInfo("user1", "pass1"))
|
|
_, err = nats.Connect(s.ClientURL(), opts2...)
|
|
if err == nil {
|
|
t.Fatal("Expected connection to fail due to account max connections limit")
|
|
}
|
|
if !strings.Contains(err.Error(), `maximum account active connections exceeded`) {
|
|
t.Fatalf("Expected second connection to be properly rejected: %v", err)
|
|
}
|
|
|
|
// Update account limits.
|
|
err = os.WriteFile(conf, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: A
|
|
accounts: {
|
|
ACCT1: {
|
|
users: [ {user: "user1", password: "pass1"} ]
|
|
limits: {
|
|
max_conn: 10
|
|
}
|
|
},
|
|
ACCT2: {
|
|
users: [ {user: "user2", password: "pass2"} ]
|
|
limits: {
|
|
max_conn: 2
|
|
}
|
|
}
|
|
}
|
|
`), 0666)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update config file: %v", err)
|
|
}
|
|
|
|
err = s.Reload()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error reloading config: %v", err)
|
|
}
|
|
|
|
// Ensure the first connection is fully established before creating the second
|
|
if err := nc1.Flush(); err != nil {
|
|
t.Fatalf("Failed to flush first connection: %v", err)
|
|
}
|
|
|
|
opts3, _, _, _ := clientOpts("ACCT1:C:3")
|
|
opts3 = append(opts3, nats.UserInfo("user1", "pass1"))
|
|
nc3, err := nats.Connect(s.ClientURL(), opts3...)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect client after limit increase: %v", err)
|
|
}
|
|
defer nc3.Close()
|
|
|
|
if s.NumClients() != 2 {
|
|
t.Fatalf("Expected 2 client connections to server. Got %d", s.NumClients())
|
|
}
|
|
if err = nc3.Flush(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// Both nc1 and nc3 should be connected at this point.
|
|
if !(nc1.IsConnected() && nc3.IsConnected()) {
|
|
t.Errorf("Expected both clients to be connected")
|
|
}
|
|
|
|
// Decrease connections...
|
|
err = os.WriteFile(conf, []byte(`
|
|
listen: 127.0.0.1:-1
|
|
server_name: A
|
|
accounts: {
|
|
ACCT1: {
|
|
users: [ {user: "user1", password: "pass1"} ]
|
|
limits: {
|
|
max_conn: 1
|
|
}
|
|
},
|
|
ACCT2: {
|
|
users: [ {user: "user2", password: "pass2"} ]
|
|
limits: {
|
|
max_conn: 2
|
|
}
|
|
}
|
|
}
|
|
`), 0666)
|
|
if err != nil {
|
|
t.Fatalf("Failed to update config file: %v", err)
|
|
}
|
|
err = s.Reload()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error reloading config: %v", err)
|
|
}
|
|
|
|
_, err = nats.Connect(s.ClientURL(), nats.UserInfo("user1", "pass1"))
|
|
if err == nil {
|
|
t.Error("Expected connection to fail due to account max connections limit after reload")
|
|
}
|
|
checkFor(t, 2*time.Second, 50*time.Millisecond, func() error {
|
|
if got := s.NumClients(); got != 1 {
|
|
return fmt.Errorf("unexpected number of connections: %v", got)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Now test is reconnecting against JWT based policy changes.
|
|
// Add a two connections to the other account.
|
|
opts4, errCh4, reconnectCh4, _ := clientOpts("ACCT2:C:4")
|
|
opts4 = append(opts4, nats.UserInfo("user2", "pass2"))
|
|
nc4, err := nats.Connect(s.ClientURL(), opts4...)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to ACCT2: %v", err)
|
|
}
|
|
defer nc4.Close()
|
|
if !nc4.IsConnected() {
|
|
t.Fatal("Client connected to ACCT2 should be connected")
|
|
}
|
|
opts5, errCh5, reconnectCh5, _ := clientOpts("ACCT2:C:5")
|
|
opts5 = append(opts5, nats.UserInfo("user2", "pass2"))
|
|
nc5, err := nats.Connect(s.ClientURL(), opts5...)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to ACCT2: %v", err)
|
|
}
|
|
defer nc5.Close()
|
|
if !nc5.IsConnected() {
|
|
t.Fatal("Client connected to ACCT2 should be connected")
|
|
}
|
|
|
|
// Now via JWT update.
|
|
acc, err := s.LookupAccount("ACCT2")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
accClaims := jwt.NewAccountClaims(acc.Name)
|
|
accClaims.Limits.Conn = 1
|
|
s.UpdateAccountClaims(acc, accClaims)
|
|
|
|
select {
|
|
case err := <-errCh4:
|
|
if err != nats.ErrMaxAccountConnectionsExceeded {
|
|
t.Fatalf("Expected ErrMaxConnectionsExceeded, got %v", err)
|
|
}
|
|
case err := <-errCh5:
|
|
if err != nats.ErrMaxAccountConnectionsExceeded {
|
|
t.Fatalf("Expected ErrMaxConnectionsExceeded, got %v", err)
|
|
}
|
|
if nc5.IsConnected() {
|
|
t.Fatalf("Expected client to be disconnected")
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for disconnect event")
|
|
}
|
|
if !nc4.IsConnected() && !nc5.IsConnected() {
|
|
t.Fatalf("Expected at least one client to be connected")
|
|
}
|
|
|
|
acc, err = s.LookupAccount("ACCT2")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
accClaims = jwt.NewAccountClaims(acc.Name)
|
|
accClaims.Limits.Conn = 10
|
|
s.UpdateAccountClaims(acc, accClaims)
|
|
select {
|
|
case <-reconnectCh4:
|
|
case <-reconnectCh5:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("Timed out waiting for reconnect event")
|
|
}
|
|
if !nc4.IsConnected() || !nc5.IsConnected() {
|
|
t.Fatal("Expected both clients to be reconnected successfully")
|
|
}
|
|
if got := s.NumClients(); got != 3 {
|
|
t.Errorf("Unexpected number of connections: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestReconnectToServerCallback(t *testing.T) {
|
|
t.Run("select custom server", func(t *testing.T) {
|
|
srv1 := RunServerOnPort(-1)
|
|
defer srv1.Shutdown()
|
|
srv2 := RunServerOnPort(-1)
|
|
defer srv2.Shutdown()
|
|
|
|
url1 := srv1.ClientURL()
|
|
url2 := srv2.ClientURL()
|
|
|
|
var callbackCalled atomic.Int32
|
|
reconnectCh := make(chan bool, 1)
|
|
|
|
nc, err := nats.Connect(
|
|
url1+","+url2,
|
|
nats.ReconnectWait(100*time.Millisecond),
|
|
nats.MaxReconnects(10),
|
|
nats.DontRandomize(),
|
|
nats.ReconnectToServer(func(servers []nats.Server, info nats.ServerInfo) (*nats.Server, time.Duration) {
|
|
callbackCalled.Add(1)
|
|
for i := range servers {
|
|
if servers[i].URL.String() == url2 {
|
|
return &servers[i], 0
|
|
}
|
|
}
|
|
return &servers[0], 0
|
|
}),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
select {
|
|
case reconnectCh <- true:
|
|
default:
|
|
}
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
if nc.ConnectedUrl() != url1 {
|
|
t.Fatalf("Expected initial connection to %s, got %s", url1, nc.ConnectedUrl())
|
|
}
|
|
|
|
// try reconnecting a few times, we should end up on url2 each time
|
|
for i := range 5 {
|
|
if err := nc.ForceReconnect(); err != nil {
|
|
t.Fatalf("Failed to force reconnect: %v", err)
|
|
}
|
|
|
|
select {
|
|
case <-reconnectCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for reconnect")
|
|
}
|
|
|
|
if callbackCalled.Load() != int32(i+1) {
|
|
t.Fatal("Expected ReconnectToServer callback to be called on each reconnect")
|
|
}
|
|
|
|
if nc.ConnectedUrl() != url2 {
|
|
t.Fatalf("Expected connection to %s after callback selection, got %s", url2, nc.ConnectedUrl())
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("wait before reconnect", func(t *testing.T) {
|
|
srv1 := RunServerOnPort(-1)
|
|
defer srv1.Shutdown()
|
|
srv2 := RunServerOnPort(-1)
|
|
defer srv2.Shutdown()
|
|
|
|
url1 := srv1.ClientURL()
|
|
url2 := srv2.ClientURL()
|
|
|
|
reconnectCh := make(chan bool, 1)
|
|
|
|
nc, err := nats.Connect(
|
|
url1+","+url2,
|
|
nats.ReconnectWait(2*time.Second),
|
|
nats.MaxReconnects(10),
|
|
nats.DontRandomize(),
|
|
nats.ReconnectToServer(func(servers []nats.Server, info nats.ServerInfo) (*nats.Server, time.Duration) {
|
|
for i := range servers {
|
|
if servers[i].URL.String() == url2 {
|
|
return &servers[i], 500 * time.Millisecond
|
|
}
|
|
}
|
|
return &servers[0], 500 * time.Millisecond
|
|
}),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
select {
|
|
case reconnectCh <- true:
|
|
default:
|
|
}
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
if nc.ConnectedUrl() != url1 {
|
|
t.Fatalf("Expected initial connection to %s, got %s", url1, nc.ConnectedUrl())
|
|
}
|
|
now := time.Now()
|
|
if err := nc.ForceReconnect(); err != nil {
|
|
t.Fatalf("Failed to force reconnect: %v", err)
|
|
}
|
|
|
|
select {
|
|
case <-reconnectCh:
|
|
elapsed := time.Since(now)
|
|
if elapsed < 500*time.Millisecond {
|
|
t.Fatalf("Reconnect occurred too quickly, expected ~500ms, got %v", elapsed)
|
|
}
|
|
if elapsed > 1500*time.Millisecond {
|
|
t.Fatalf("Reconnect took too long (used ReconnectWait instead of callback delay?), got %v", elapsed)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Timed out waiting for reconnect")
|
|
}
|
|
})
|
|
|
|
t.Run("server not in pool fires error callback", func(t *testing.T) {
|
|
srv1 := RunServerOnPort(-1)
|
|
defer srv1.Shutdown()
|
|
srv2 := RunServerOnPort(-1)
|
|
defer srv2.Shutdown()
|
|
|
|
url1 := srv1.ClientURL()
|
|
url2 := srv2.ClientURL()
|
|
|
|
reconnectCh := make(chan bool, 1)
|
|
var reconnectErr atomic.Value
|
|
|
|
nc, err := nats.Connect(
|
|
url1+","+url2,
|
|
nats.ReconnectWait(50*time.Millisecond),
|
|
nats.MaxReconnects(10),
|
|
nats.DontRandomize(),
|
|
nats.ReconnectToServer(func(servers []nats.Server, info nats.ServerInfo) (*nats.Server, time.Duration) {
|
|
return &nats.Server{URL: &url.URL{Scheme: "nats", Host: "127.0.0.1:9999"}}, 0
|
|
}),
|
|
nats.ReconnectErrHandler(func(_ *nats.Conn, err error) {
|
|
reconnectErr.Store(err)
|
|
}),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
select {
|
|
case reconnectCh <- true:
|
|
default:
|
|
}
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
srv1.Shutdown()
|
|
|
|
select {
|
|
case <-reconnectCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for reconnect")
|
|
}
|
|
|
|
if nc.ConnectedUrl() != url2 {
|
|
t.Fatalf("Expected connection to %s via default fallback, got %s", url2, nc.ConnectedUrl())
|
|
}
|
|
|
|
if storedErr, ok := reconnectErr.Load().(error); !ok {
|
|
t.Fatalf("Expected an error in ReconnectErrHandler, got %v", storedErr)
|
|
} else if !errors.Is(storedErr, nats.ErrServerNotInPool) {
|
|
t.Fatalf("Expected ErrServerNotInPool, got %v", storedErr)
|
|
}
|
|
})
|
|
|
|
t.Run("nil server falls back to default selection", func(t *testing.T) {
|
|
srv1 := RunServerOnPort(-1)
|
|
defer srv1.Shutdown()
|
|
srv2 := RunServerOnPort(-1)
|
|
defer srv2.Shutdown()
|
|
|
|
url1 := srv1.ClientURL()
|
|
url2 := srv2.ClientURL()
|
|
|
|
reconnectCh := make(chan bool, 1)
|
|
|
|
nc, err := nats.Connect(
|
|
url1+","+url2,
|
|
nats.ReconnectWait(50*time.Millisecond),
|
|
nats.MaxReconnects(10),
|
|
nats.DontRandomize(),
|
|
nats.ReconnectToServer(func(servers []nats.Server, info nats.ServerInfo) (*nats.Server, time.Duration) {
|
|
return nil, 0
|
|
}),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
select {
|
|
case reconnectCh <- true:
|
|
default:
|
|
}
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
srv1.Shutdown()
|
|
|
|
select {
|
|
case <-reconnectCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for reconnect")
|
|
}
|
|
|
|
if nc.ConnectedUrl() != url2 {
|
|
t.Fatalf("Expected connection to %s via default fallback, got %s", url2, nc.ConnectedUrl())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSetServerPool(t *testing.T) {
|
|
t.Run("reconnect to server from new pool", func(t *testing.T) {
|
|
srv1 := RunServerOnPort(-1)
|
|
defer srv1.Shutdown()
|
|
srv2 := RunServerOnPort(-1)
|
|
defer srv2.Shutdown()
|
|
|
|
url1 := srv1.ClientURL()
|
|
url2 := srv2.ClientURL()
|
|
|
|
reconnectCh := make(chan bool, 1)
|
|
disconnectCh := make(chan bool, 1)
|
|
|
|
nc, err := nats.Connect(
|
|
url1,
|
|
nats.ReconnectWait(50*time.Millisecond),
|
|
nats.MaxReconnects(10),
|
|
nats.DisconnectErrHandler(func(_ *nats.Conn, _ error) {
|
|
select {
|
|
case disconnectCh <- true:
|
|
default:
|
|
}
|
|
}),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
select {
|
|
case reconnectCh <- true:
|
|
default:
|
|
}
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
if nc.ConnectedUrl() != url1 {
|
|
t.Fatalf("Expected initial connection to %s, got %s", url1, nc.ConnectedUrl())
|
|
}
|
|
|
|
srv3 := RunServerOnPort(-1)
|
|
defer srv3.Shutdown()
|
|
url3 := srv3.ClientURL()
|
|
|
|
if err := nc.SetServerPool([]string{url2, url3}); err != nil {
|
|
t.Fatalf("Failed to set server pool: %v", err)
|
|
}
|
|
|
|
srv1.Shutdown()
|
|
|
|
select {
|
|
case <-disconnectCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for disconnect")
|
|
}
|
|
|
|
select {
|
|
case <-reconnectCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for reconnect")
|
|
}
|
|
|
|
connectedURL := nc.ConnectedUrl()
|
|
if connectedURL != url2 && connectedURL != url3 {
|
|
t.Fatalf("Expected connection to %s or %s, got %s", url2, url3, connectedURL)
|
|
}
|
|
})
|
|
|
|
t.Run("atomic operation - partial failure doesn't modify pool", func(t *testing.T) {
|
|
srv := RunServerOnPort(-1)
|
|
defer srv.Shutdown()
|
|
|
|
nc, err := nats.Connect(srv.ClientURL())
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
originalPool := nc.ServerPool()
|
|
if len(originalPool) != 1 {
|
|
t.Fatalf("Expected 1 server in original pool, got %d", len(originalPool))
|
|
}
|
|
|
|
err = nc.SetServerPool([]string{
|
|
"nats://localhost:4222",
|
|
"invalid://bad url with spaces",
|
|
"nats://localhost:4223",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Expected error from invalid URL, got nil")
|
|
}
|
|
|
|
currentPool := nc.ServerPool()
|
|
if len(currentPool) != len(originalPool) {
|
|
t.Fatalf("Pool was modified on error: expected %d servers, got %d", len(originalPool), len(currentPool))
|
|
}
|
|
if currentPool[0].URL.String() != originalPool[0].URL.String() {
|
|
t.Fatalf("Pool was modified on error: expected %s, got %s", originalPool[0].URL.String(), currentPool[0].URL.String())
|
|
}
|
|
})
|
|
|
|
t.Run("websocket URLs rejected for non-websocket connection", func(t *testing.T) {
|
|
srv := RunServerOnPort(-1)
|
|
defer srv.Shutdown()
|
|
|
|
nc, err := nats.Connect(srv.ClientURL())
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
originalPool := nc.ServerPool()
|
|
|
|
err = nc.SetServerPool([]string{"ws://localhost:8080"})
|
|
if !errors.Is(err, nats.ErrMixingWebsocketSchemes) {
|
|
t.Fatal("Expected error from websocket URL on non-websocket connection")
|
|
}
|
|
|
|
currentPool := nc.ServerPool()
|
|
if len(currentPool) != len(originalPool) {
|
|
t.Fatalf("Pool was modified on error: expected %d servers, got %d", len(originalPool), len(currentPool))
|
|
}
|
|
})
|
|
|
|
t.Run("preserves reconnect count for existing servers", func(t *testing.T) {
|
|
srv1 := RunServerOnPort(-1)
|
|
defer srv1.Shutdown()
|
|
srv2 := RunServerOnPort(-1)
|
|
|
|
url1 := srv1.ClientURL()
|
|
url2 := srv2.ClientURL()
|
|
|
|
reconnectCh := make(chan struct{}, 1)
|
|
|
|
nc, err := nats.Connect(
|
|
url1+","+url2,
|
|
nats.ReconnectWait(50*time.Millisecond),
|
|
nats.MaxReconnects(10),
|
|
nats.DontRandomize(),
|
|
nats.ReconnectHandler(func(_ *nats.Conn) {
|
|
reconnectCh <- struct{}{}
|
|
}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
// Shut down srv2 so it fails when attempted during reconnect.
|
|
// Then ForceReconnect: client tries url2 (fails, Reconnects++),
|
|
// then url1 (succeeds, Reconnects=0). So url2 has non-zero count.
|
|
srv2.Shutdown()
|
|
|
|
if err := nc.ForceReconnect(); err != nil {
|
|
t.Fatalf("Failed to force reconnect: %v", err)
|
|
}
|
|
|
|
select {
|
|
case <-reconnectCh:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Timed out waiting for reconnect")
|
|
}
|
|
|
|
pool := nc.ServerPool()
|
|
var srv2Reconnects int
|
|
for _, s := range pool {
|
|
if s.URL.String() == url2 {
|
|
srv2Reconnects = s.Reconnects
|
|
break
|
|
}
|
|
}
|
|
if srv2Reconnects != 1 {
|
|
t.Fatal("Expected one reconnect for srv2")
|
|
}
|
|
|
|
srv3 := RunServerOnPort(-1)
|
|
defer srv3.Shutdown()
|
|
url3 := srv3.ClientURL()
|
|
|
|
if err := nc.SetServerPool([]string{url1, url2, url3}); err != nil {
|
|
t.Fatalf("Failed to set server pool: %v", err)
|
|
}
|
|
|
|
pool = nc.ServerPool()
|
|
for _, s := range pool {
|
|
if s.URL.String() == url2 {
|
|
if s.Reconnects != srv2Reconnects {
|
|
t.Fatalf("Expected reconnect count %d to be preserved, got %d", srv2Reconnects, s.Reconnects)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("srv2 not found in pool after SetServerPool")
|
|
})
|
|
|
|
t.Run("server pool returns deep copy", func(t *testing.T) {
|
|
srv := RunServerOnPort(-1)
|
|
defer srv.Shutdown()
|
|
|
|
nc, err := nats.Connect(srv.ClientURL())
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect: %v", err)
|
|
}
|
|
defer nc.Close()
|
|
|
|
pool := nc.ServerPool()
|
|
originalURL := pool[0].URL.String()
|
|
|
|
pool[0].URL.Host = "modified:9999"
|
|
|
|
pool2 := nc.ServerPool()
|
|
if pool2[0].URL.String() != originalURL {
|
|
t.Fatalf("Internal pool was modified: expected %s, got %s", originalURL, pool2[0].URL.String())
|
|
}
|
|
})
|
|
}
|