Repository: nats-io/nats-streaming-server
Pull request #1075 (2020-07-28 06:19:11)

[ADDED] ClientURL() API and expose an OS-assigned server port via the NATS server options 
If a random port is asked for in a call to `RunServerWithOpts`, return this port
in the `*server.Options argument`. Replaces the Port field with the OS assigned
port.

This PR also adds `ClientURL()` to `StanServer` as an alternate method of
fetching connection information.

ripienaar: We could return a *net.URL which serves most needs including the easy access to the port. 
kozlovic: > We could return a *net.URL which serves most needs including the easy access to the port.

Actually `*url.URL.Port()` returns a string, so would still have to convert to `int` if `int` is what is needed by the user.
That being said, we could certainly do both (update `natsOpts.Port` and provide `ClientURL()` that returns an `*url.URL`).

Patch:
diff --git a/server/server.go b/server/server.go
index bdcf140..962c7e7 100644
--- a/server/server.go
+++ b/server/server.go
@@ -756,6 +756,9 @@ type StanServer struct {
 	// cause races when running more than 1 server in a program or test.
 	pingResponseOKBytes            []byte
 	pingResponseInvalidClientBytes []byte
+
+	// If using an external server, capture the URL that was given for return in ClientURL().
+	providedServerURL string
 }
 
 type subsSentAndAckReplication struct {
@@ -1442,6 +1445,9 @@ func (s *StanServer) buildServerURLs() ([]string, error) {
 		}
 		// Use net.Join to support IPV6 addresses.
 		hostport = net.JoinHostPort(host, port)
+
+		// Capture for ClientURL()
+		s.providedServerURL = natsURL
 	} else {
 		// We embed the server, so it is local. If host is "any",
 		// use 127.0.0.1 or ::1 for host address (important for
@@ -1763,6 +1769,9 @@ func RunServerWithOpts(stanOpts *Options, natsOpts *server.Options) (newServer *
 		if err := s.startNATSServer(); err != nil {
 			return nil, err
 		}
+		if natsOpts != nil && natsOpts.Port == server.RANDOM_PORT {
+			natsOpts.Port = nOpts.Port
+		}
 	}
 	// Check for monitoring
 	if nOpts.HTTPPort != 0 || nOpts.HTTPSPort != 0 {
@@ -1805,6 +1814,19 @@ func RunServerWithOpts(stanOpts *Options, natsOpts *server.Options) (newServer *
 	return &s, nil
 }
 
+// ClientURL returns the basic URL string representation suitable for a client to use to connect
+func (s *StanServer) ClientURL() string {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.providedServerURL != "" {
+		return s.providedServerURL
+	} else if s.natsServer != nil {
+		return s.natsServer.ClientURL()
+	} else {
+		return ""
+	}
+}
+
 // Logging in STAN
 //
 // The STAN logger is an instance of a NATS logger, (basically duplicated


Test patch:
diff --git a/server/server_test.go b/server/server_test.go
index 55ec2ee..3770a26 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -20,7 +20,9 @@ import (
 	"io/ioutil"
 	"os"
 	"reflect"
+	"regexp"
 	"runtime"
+	"strconv"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -28,6 +30,7 @@ import (
 	"time"
 
 	natsd "github.com/nats-io/nats-server/v2/server"
+	natsdTest "github.com/nats-io/nats-server/v2/test"
 	"github.com/nats-io/nats-streaming-server/logger"
 	"github.com/nats-io/nats-streaming-server/stores"
 	"github.com/nats-io/nats-streaming-server/test"
@@ -1370,3 +1373,73 @@ func TestAckProcessedBeforeClose(t *testing.T) {
 		dur.Unsubscribe()
 	}
 }
+
+func TestServerRandomPort(t *testing.T) {
+	natsOpts := natsdTest.DefaultTestOptions
+	natsOpts.Port = natsd.RANDOM_PORT
+	opts := GetDefaultOptions()
+	s := runServerWithOpts(t, opts, &natsOpts)
+	defer s.Shutdown()
+
+	if natsOpts.Port == natsd.RANDOM_PORT {
+		t.Fatal("port was not updated")
+	}
+	s.Shutdown()
+
+	// Try with no nats options provided to make sure we don't
+	// access natsOpts without checking that it is not nil
+	s = runServerWithOpts(t, opts, nil)
+	s.Shutdown()
+}
+
+func TestServerRandomClientURL(t *testing.T) {
+	// Try with an embedded server.
+	natsOpts := natsdTest.DefaultTestOptions
+	natsOpts.Port = natsd.RANDOM_PORT
+	opts := GetDefaultOptions()
+	s := runServerWithOpts(t, opts, &natsOpts)
+	clientURL := s.ClientURL()
+	defer s.Shutdown()
+
+	re := regexp.MustCompile(":[0-9]+$")
+	portString := re.FindString(clientURL)
+	if len(portString) == 3 {
+		t.Fatal("could not locate port in clientURL")
+	}
+
+	urlPort, err := strconv.Atoi(portString[1:])
+	if err != nil {
+		t.Fatal("failed to convert clientURL to integer")
+	}
+
+	if natsOpts.Port != urlPort {
+		t.Fatal("options port did not match clientURL port")
+	}
+	s.Shutdown()
+
+	// Try with remote server
+	natsOpts = natsdTest.DefaultTestOptions
+	natsOpts.Port = natsd.RANDOM_PORT
+	ns := natsdTest.RunServer(&natsOpts)
+	defer ns.Shutdown()
+	print(ns.ClientURL())
+
+	opts.NATSServerURL = fmt.Sprintf("nats://%s:%d", natsOpts.Host, natsOpts.Port)
+	s = runServerWithOpts(t, opts, nil)
+	defer s.Shutdown()
+
+	clientURL = s.ClientURL()
+	portString = re.FindString(clientURL)
+	if len(portString) == 3 {
+		t.Fatal("could not locate port in clientURL")
+	}
+
+	urlPort, err = strconv.Atoi(portString[1:])
+	if err != nil {
+		t.Fatal("failed to convert clientURL to integer")
+	}
+
+	if natsOpts.Port != urlPort {
+		t.Fatal("options port did not match clientURL port")
+	}
+}