mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
241 lines
5.8 KiB
Go
241 lines
5.8 KiB
Go
package nats
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"net/url"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog/v3"
|
|
)
|
|
|
|
const defaultClusterTokenUsername = "coder"
|
|
|
|
// PeerFetcher fetches NATS peer route addresses.
|
|
type PeerFetcher interface {
|
|
PrimaryPeerAddresses() []string
|
|
}
|
|
|
|
type NopPeerFetcher struct{}
|
|
|
|
func (NopPeerFetcher) PrimaryPeerAddresses() []string {
|
|
return nil
|
|
}
|
|
|
|
// SetPeerFetcher replaces the peer fetcher used by RefreshPeers and triggers
|
|
// an immediate peer refresh. Passing nil disables peering.
|
|
func (p *Pubsub) SetPeerFetcher(fetcher PeerFetcher) {
|
|
p.mu.Lock()
|
|
if fetcher == nil {
|
|
fetcher = NopPeerFetcher{}
|
|
}
|
|
p.peerFetcher = fetcher
|
|
p.mu.Unlock()
|
|
p.RefreshPeers()
|
|
}
|
|
|
|
// RefreshPeers signals the peer refresh worker to fetch and apply the latest
|
|
// peer route addresses. Multiple pending refreshes are coalesced.
|
|
func (p *Pubsub) RefreshPeers() {
|
|
select {
|
|
case p.peerRefresh <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (p *Pubsub) runPeerRefresh() {
|
|
for {
|
|
p.mu.Lock()
|
|
fetcher := p.peerFetcher
|
|
p.mu.Unlock()
|
|
|
|
addrs := fetcher.PrimaryPeerAddresses()
|
|
if err := p.setPeerAddresses(addrs); err != nil {
|
|
if errors.Is(err, errClosed) && p.ctx.Err() != nil {
|
|
return
|
|
}
|
|
p.logger.Error(p.ctx, "refresh nats peers", slog.Error(err))
|
|
}
|
|
|
|
select {
|
|
case <-p.ctx.Done():
|
|
return
|
|
case <-p.peerRefresh:
|
|
}
|
|
}
|
|
}
|
|
|
|
// setPeerAddresses replaces the configured NATS cluster peer routes.
|
|
func (p *Pubsub) setPeerAddresses(addresses []string) error {
|
|
p.clusterMu.Lock()
|
|
defer p.clusterMu.Unlock()
|
|
|
|
if p.ctx.Err() != nil {
|
|
return errClosed
|
|
}
|
|
if !p.clustered {
|
|
return xerrors.New("nats pubsub was not started with clustering enabled")
|
|
}
|
|
|
|
routes, err := p.parsePeerAddresses(addresses)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
self := &url.URL{Scheme: "nats", Host: p.Server.ClusterAddr().String()}
|
|
routes = filterSelfRoutes(routes, self)
|
|
|
|
if p.opts.ClusterAuthToken != "" {
|
|
routes = routesWithAuth(routes, p.opts.ClusterAuthToken)
|
|
}
|
|
|
|
routes = sortRouteURLs(routes)
|
|
|
|
if sortedURLsEqual(p.currentRoutes, routes) {
|
|
return nil
|
|
}
|
|
|
|
newOpts := p.serverOpts.Clone()
|
|
newOpts.Routes = cloneRouteURLs(routes)
|
|
if err := p.Server.ReloadOptions(newOpts); err != nil {
|
|
return xerrors.Errorf("reload nats peer addresses: %w", err)
|
|
}
|
|
p.serverOpts = newOpts.Clone()
|
|
p.currentRoutes = cloneRouteURLs(routes)
|
|
return nil
|
|
}
|
|
|
|
func (p *Pubsub) parsePeerAddresses(addresses []string) ([]*url.URL, error) {
|
|
routesByAddress := make(map[string]*url.URL, len(addresses))
|
|
for i, address := range addresses {
|
|
trimmed := strings.TrimSpace(address)
|
|
if trimmed == "" {
|
|
return nil, xerrors.Errorf("peer address %d is empty", i)
|
|
}
|
|
|
|
host, port, err := normalizeHostPort(trimmed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// This is a hack to enable testing with an arbitrary port. The logic here
|
|
// is to presume if the default port is being used then we are running in prod
|
|
// and all peers are using the same port. If the port is not the default then
|
|
// we are running a test in which case we should pass through the custom port.
|
|
// This hack will be removed when https://github.com/coder/scaletest/issues/149
|
|
// is resolved.
|
|
if p.opts.ClusterPort == defaultClusterPort {
|
|
port = defaultClusterPort
|
|
}
|
|
|
|
hostPort := net.JoinHostPort(host, strconv.Itoa(port))
|
|
routesByAddress[hostPort] = &url.URL{
|
|
Scheme: "nats",
|
|
Host: hostPort,
|
|
}
|
|
}
|
|
|
|
routes := make([]*url.URL, 0, len(routesByAddress))
|
|
for _, route := range routesByAddress {
|
|
routes = append(routes, route)
|
|
}
|
|
return routes, nil
|
|
}
|
|
|
|
func filterSelfRoutes(routes []*url.URL, self *url.URL) []*url.URL {
|
|
filtered := make([]*url.URL, 0, len(routes))
|
|
for _, route := range routes {
|
|
if route.String() == self.String() {
|
|
continue
|
|
}
|
|
filtered = append(filtered, route)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
func normalizeHostPort(address string) (string, int, error) {
|
|
route, err := url.Parse(address)
|
|
if err != nil {
|
|
return "", 0, xerrors.Errorf("parse peer address %q: %w", address, err)
|
|
}
|
|
if route.User != nil {
|
|
return "", 0, xerrors.Errorf("peer address %q must not include userinfo", address)
|
|
}
|
|
if route.Path != "" || route.RawQuery != "" || route.Fragment != "" {
|
|
return "", 0, xerrors.Errorf("peer address %q must not include path, query, or fragment", address)
|
|
}
|
|
|
|
host, port, err := net.SplitHostPort(route.Host)
|
|
if err != nil {
|
|
return "", 0, xerrors.Errorf("split %q host port: %w", address, err)
|
|
}
|
|
if host == "" || port == "" {
|
|
return "", 0, xerrors.Errorf("%q must include host and port", address)
|
|
}
|
|
|
|
portNumber, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return "", 0, xerrors.Errorf("parse %q port: %w", address, err)
|
|
}
|
|
if portNumber <= 0 || portNumber > 65535 {
|
|
return "", 0, xerrors.Errorf("peer address %q must include a valid port", address)
|
|
}
|
|
return host, portNumber, nil
|
|
}
|
|
|
|
func sortRouteURLs(routes []*url.URL) []*url.URL {
|
|
slices.SortFunc(routes, func(a, b *url.URL) int {
|
|
return strings.Compare(a.String(), b.String())
|
|
})
|
|
return routes
|
|
}
|
|
|
|
func routesWithAuth(routes []*url.URL, token string) []*url.URL {
|
|
if token == "" {
|
|
return routes
|
|
}
|
|
withAuth := make([]*url.URL, 0, len(routes))
|
|
for _, route := range routes {
|
|
if route == nil {
|
|
withAuth = append(withAuth, nil)
|
|
continue
|
|
}
|
|
clone := *route
|
|
clone.User = url.UserPassword(defaultClusterTokenUsername, token)
|
|
withAuth = append(withAuth, &clone)
|
|
}
|
|
return withAuth
|
|
}
|
|
|
|
// sortedURLsEqual assumes sorted slices.
|
|
func sortedURLsEqual(a, b []*url.URL) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i].String() != b[i].String() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func cloneRouteURLs(routes []*url.URL) []*url.URL {
|
|
if routes == nil {
|
|
return nil
|
|
}
|
|
clones := make([]*url.URL, len(routes))
|
|
for i, route := range routes {
|
|
if route == nil {
|
|
continue
|
|
}
|
|
clone := *route
|
|
clones[i] = &clone
|
|
}
|
|
return clones
|
|
}
|