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 }