github.com/anycable/anycable-go@v1.5.1/enats/enats.go (about)

     1  //go:build !freebsd || amd64
     2  // +build !freebsd amd64
     3  
     4  package enats
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"log"
    10  	"log/slog"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/joomcode/errorx"
    20  	gonanoid "github.com/matoous/go-nanoid"
    21  	"github.com/nats-io/nats-server/v2/server"
    22  	"github.com/nats-io/nats.go"
    23  )
    24  
    25  // NewConfig returns defaults for NATSServiceConfig
    26  func NewConfig() Config {
    27  	return Config{
    28  		ServiceAddr:           nats.DefaultURL,
    29  		ClusterName:           "anycable-cluster",
    30  		JetStreamReadyTimeout: 30, // seconds
    31  	}
    32  }
    33  
    34  const (
    35  	serverStartTimeout = 5 * time.Second
    36  )
    37  
    38  // Service represents NATS service
    39  type Service struct {
    40  	config *Config
    41  	server *server.Server
    42  	name   string
    43  
    44  	log *slog.Logger
    45  	mu  sync.Mutex
    46  }
    47  
    48  // LogEntry represents LoggerV2 decorator for nats server logger
    49  type LogEntry struct {
    50  	*slog.Logger
    51  }
    52  
    53  // Debugf is an alias for Debug
    54  func (e *LogEntry) Debugf(format string, v ...interface{}) {
    55  	e.Debug(fmt.Sprintf(format, v...))
    56  }
    57  
    58  // Warnf is an alias for Warn
    59  func (e *LogEntry) Warnf(format string, v ...interface{}) {
    60  	e.Warn(fmt.Sprintf(format, v...))
    61  }
    62  
    63  // Errorf is an alias for Error
    64  func (e *LogEntry) Errorf(format string, v ...interface{}) {
    65  	e.Error(fmt.Sprintf(format, v...))
    66  }
    67  
    68  // Infof is an alias for Info
    69  func (e *LogEntry) Infof(format string, v ...interface{}) {
    70  	e.Info(fmt.Sprintf(format, v...))
    71  }
    72  
    73  // Fatalf is an alias for Fatal
    74  func (e *LogEntry) Fatalf(format string, v ...interface{}) {
    75  	log.Fatalf(format, v...)
    76  }
    77  
    78  // Noticef is an alias for Infof
    79  func (e *LogEntry) Noticef(format string, v ...interface{}) {
    80  	e.Info(fmt.Sprintf(format, v...))
    81  }
    82  
    83  // Tracef is an alias for Debugf
    84  func (e *LogEntry) Tracef(format string, v ...interface{}) {
    85  	e.Debug(fmt.Sprintf(format, v...))
    86  }
    87  
    88  // NewService returns an instance of NATS service
    89  func NewService(c *Config, l *slog.Logger) *Service {
    90  	return &Service{config: c, log: l.With("context", "enats")}
    91  }
    92  
    93  // Start starts the service
    94  func (s *Service) Start() error {
    95  	var err error
    96  
    97  	u, err := url.Parse(s.config.ServiceAddr)
    98  	if err != nil {
    99  		return errorx.Decorate(err, "error parsing NATS service addr")
   100  	}
   101  	if u.Port() == "" {
   102  		return errorx.IllegalArgument.New("failed to parse NATS server URL, can not fetch port")
   103  	}
   104  
   105  	port, err := strconv.ParseInt(u.Port(), 10, 32)
   106  	if err != nil {
   107  		return errorx.Decorate(err, "failed to parse NATS service port")
   108  	}
   109  
   110  	clusterOpts, err := s.getCluster(s.config.ClusterAddr, s.config.ClusterName)
   111  	if err != nil {
   112  		return errorx.Decorate(err, "failed to configure NATS cluster")
   113  	}
   114  
   115  	routes, err := s.getRoutes()
   116  	if err != nil {
   117  		return errorx.Decorate(err, "failed to parse routes")
   118  	}
   119  
   120  	gatewayOpts, err := s.getGateway(s.config.GatewayAddr, s.config.GatewayAdvertise, s.config.ClusterName, s.config.Gateways)
   121  	if err != nil {
   122  		return errorx.Decorate(err, "failed to configure NATS gateway")
   123  	}
   124  
   125  	opts := &server.Options{
   126  		Host:       u.Hostname(),
   127  		Port:       int(port),
   128  		Debug:      s.config.Debug,
   129  		Trace:      s.config.Trace,
   130  		ServerName: s.serverName(),
   131  		Cluster:    clusterOpts,
   132  		Gateway:    gatewayOpts,
   133  		Routes:     routes,
   134  		NoSigs:     true,
   135  		JetStream:  s.config.JetStream,
   136  	}
   137  
   138  	if s.config.StoreDir != "" {
   139  		opts.StoreDir = s.config.StoreDir
   140  	} else {
   141  		opts.StoreDir = filepath.Join(os.TempDir(), "nats-data", s.serverName())
   142  	}
   143  
   144  	s.server, err = server.NewServer(opts)
   145  	if err != nil {
   146  		return errorx.Decorate(err, "failed to start NATS server")
   147  	}
   148  
   149  	if s.config.Debug {
   150  		e := &LogEntry{s.log.With("service", "nats")}
   151  		s.server.SetLogger(e, s.config.Debug, s.config.Trace)
   152  	}
   153  
   154  	go s.server.Start()
   155  
   156  	return s.WaitReady()
   157  }
   158  
   159  // WaitReady waits while NATS server is starting
   160  func (s *Service) WaitReady() error {
   161  	if s.server.ReadyForConnections(serverStartTimeout) {
   162  		// We don't want to block the bootstrap process while waiting for JetStream.
   163  		// JetStream requires a cluster to be formed before it can be enabled, but when we
   164  		// perform a rolling update, the newly created instance may have no network connectivity,
   165  		// thus, it won't be able to join the cluster and enable JetStream.
   166  		go s.WaitJetStreamReady(s.config.JetStreamReadyTimeout) // nolint:errcheck
   167  		return nil
   168  	}
   169  
   170  	return errorx.TimeoutElapsed.New(
   171  		"failed to start NATS server within %d seconds", serverStartTimeout,
   172  	)
   173  }
   174  
   175  func (s *Service) Description() string {
   176  	var builder strings.Builder
   177  
   178  	builder.WriteString(fmt.Sprintf("server_name: %s", s.serverName()))
   179  
   180  	if s.config.ClusterAddr != "" {
   181  		builder.WriteString(fmt.Sprintf(", cluster: %s, cluster_name: %s", s.config.ClusterAddr, s.config.ClusterName))
   182  	}
   183  
   184  	if s.config.Routes != nil {
   185  		builder.WriteString(fmt.Sprintf(", routes: %s", strings.Join(s.config.Routes, ",")))
   186  	}
   187  
   188  	if s.config.GatewayAddr != "" {
   189  		builder.WriteString(fmt.Sprintf(", gateway: %s, gateways: %s", s.config.GatewayAddr, s.config.Gateways))
   190  
   191  		if s.config.GatewayAdvertise != "" {
   192  			builder.WriteString(fmt.Sprintf(", gateway_advertise: %s", s.config.GatewayAdvertise))
   193  		}
   194  	}
   195  
   196  	return builder.String()
   197  }
   198  
   199  // Shutdown shuts the NATS server down
   200  func (s *Service) Shutdown(ctx context.Context) error {
   201  	s.server.DisableJetStream() // nolint:errcheck
   202  	s.server.Shutdown()
   203  	s.server.WaitForShutdown()
   204  	return nil
   205  }
   206  
   207  // getRoutes transforms []string routes to []*url.URL routes
   208  func (s *Service) getRoutes() ([]*url.URL, error) {
   209  	if len(s.config.Routes) == 0 {
   210  		return nil, nil
   211  	}
   212  
   213  	routes := make([]*url.URL, len(s.config.Routes))
   214  	for i, r := range s.config.Routes {
   215  		u, err := url.Parse(r)
   216  		if err != nil {
   217  			return nil, errorx.Decorate(err, "error parsing route URL")
   218  		}
   219  		routes[i] = u
   220  	}
   221  	return routes, nil
   222  }
   223  
   224  func (s *Service) getCluster(addr string, name string) (opts server.ClusterOpts, err error) {
   225  	if addr == "" || name == "" {
   226  		return
   227  	}
   228  
   229  	host, port, err := parseAddress(addr)
   230  
   231  	if err != nil {
   232  		err = errorx.Decorate(err, "failed to parse cluster URL")
   233  		return
   234  	}
   235  
   236  	opts = server.ClusterOpts{
   237  		Name: name,
   238  		Host: host,
   239  		Port: port,
   240  	}
   241  
   242  	return
   243  }
   244  
   245  func (s *Service) getGateway(addr string, advertise string, name string, gateways []string) (opts server.GatewayOpts, err error) {
   246  	if addr == "" || name == "" {
   247  		return
   248  	}
   249  
   250  	host, port, err := parseAddress(addr)
   251  
   252  	if err != nil {
   253  		err = errorx.Decorate(err, "failed to parse gateway URL")
   254  		return
   255  	}
   256  
   257  	opts = server.GatewayOpts{
   258  		Name:      s.config.ClusterName,
   259  		Host:      host,
   260  		Port:      port,
   261  		Advertise: advertise,
   262  	}
   263  
   264  	if len(gateways) != 0 {
   265  		gateOpts := make([]*server.RemoteGatewayOpts, len(gateways))
   266  		for i, g := range gateways {
   267  			parts := strings.SplitN(g, ":", 2)
   268  
   269  			if len(parts) != 2 {
   270  				err = errorx.Decorate(err, "gateway has unknown format: %s", g)
   271  				return
   272  			}
   273  
   274  			name := parts[0]
   275  			addrs := strings.Split(parts[1], ",")
   276  
   277  			nameAddrs := make([]*url.URL, len(addrs))
   278  
   279  			for j, addr := range addrs {
   280  				u, gateErr := url.Parse(addr)
   281  				if gateErr != nil {
   282  					err = errorx.Decorate(gateErr, "error parsing gateway URL")
   283  					return
   284  				}
   285  
   286  				nameAddrs[j] = u
   287  			}
   288  
   289  			gateOpts[i] = &server.RemoteGatewayOpts{URLs: nameAddrs, Name: name}
   290  		}
   291  
   292  		opts.Gateways = gateOpts
   293  	}
   294  
   295  	return
   296  }
   297  
   298  func parseAddress(addr string) (string, int, error) {
   299  	var uri *url.URL
   300  
   301  	uri, err := url.Parse(addr)
   302  	if err != nil {
   303  		return "", 0, errorx.Decorate(err, "failed to parse URL")
   304  	}
   305  
   306  	if uri.Port() == "" {
   307  		return "", 0, errorx.IllegalArgument.New("port cannot be empty")
   308  	}
   309  
   310  	port, err := strconv.ParseInt(uri.Port(), 10, 32)
   311  	if err != nil {
   312  		return "", 0, errorx.Decorate(err, "port is not valid")
   313  	}
   314  
   315  	return uri.Hostname(), int(port), nil
   316  }
   317  
   318  func (s *Service) serverName() string {
   319  	s.mu.Lock()
   320  	defer s.mu.Unlock()
   321  
   322  	if s.name != "" {
   323  		return s.name
   324  	}
   325  
   326  	if s.config.Name != "" {
   327  		s.name = s.config.Name
   328  		return s.name
   329  	}
   330  
   331  	suf, _ := gonanoid.Nanoid(3) // nolint: errcheck
   332  
   333  	s.name = strings.ReplaceAll(strings.ReplaceAll(s.config.ServiceAddr, ":", "-"), "/", "") + "-" + suf
   334  	return s.name
   335  }
   336  
   337  func (s *Service) WaitJetStreamReady(maxSeconds int) error {
   338  	if !s.config.JetStream {
   339  		return nil
   340  	}
   341  
   342  	start := time.Now()
   343  	for {
   344  		if time.Since(start) > time.Duration(maxSeconds)*time.Second {
   345  			return fmt.Errorf("JetStream is not ready after %d seconds", maxSeconds)
   346  		}
   347  
   348  		c, err := nats.Connect("", nats.InProcessServer(s.server))
   349  		if err != nil {
   350  			s.log.Debug("NATS server not accepting connections", "error", err)
   351  			continue
   352  		}
   353  
   354  		j, err := c.JetStream()
   355  		if err != nil {
   356  			return err
   357  		}
   358  
   359  		st, err := j.StreamInfo("__anycable__ready__", nats.MaxWait(1*time.Second))
   360  		if err == nats.ErrStreamNotFound || st != nil {
   361  			leader := s.server.JetStreamIsLeader()
   362  			s.log.Debug("JetStream cluster is ready", "leader", leader)
   363  			return nil
   364  		}
   365  
   366  		c.Close()
   367  
   368  		s.log.Debug("JetStream cluster is not ready yet, waiting for 1 second...")
   369  
   370  		time.Sleep(1 * time.Second)
   371  	}
   372  }