separate known and active relays

rabbitprincess committed Mar 21, 2026 at 14:39 UTC 4f29e48e761ba20cd172a916ee3e22cf04c39ab2
4 files changed +102 -30
portal/api_server.go
+1 -1
@@ -518,7 +518,7 @@ func (s *Server) registerLease(req types.RegisterRequest, clientIP string) (type
518 return types.RegisterResponse{}, err
519 }
520 if s.DiscoveryEnabled() {
521 - if err := s.mergeDiscoveryBootstraps(bootstraps); err != nil {
521 + if _, err := s.mergeDiscoveryBootstraps(bootstraps); err != nil {
522 record.Close()
523 _, _ = s.registry.Unregister(record.ID, record.ReverseToken)
524 return types.RegisterResponse{}, err
portal/server.go
+32 -7
@@ -544,18 +544,34 @@ func (s *Server) discoveryBootstrapsSnapshot() []string {
544 return append([]string(nil), s.discoveryBootstraps...)
545 }
546
547 -func (s *Server) mergeDiscoveryBootstraps(inputs []string) error {
547 +func (s *Server) mergeDiscoveryBootstraps(inputs []string) ([]string, error) {
548 if s == nil || !s.DiscoveryEnabled() || len(inputs) == 0 {
549 - return nil
549 + return nil, nil
550 }
551
552 s.discoveryMu.Lock()
553 + defer s.discoveryMu.Unlock()
554 +
555 next, err := utils.MergeRelayURLs(s.discoveryBootstraps, []string{s.cfg.PortalURL}, inputs)
554 - if err == nil {
555 - s.discoveryBootstraps = next
556 + if err != nil {
557 + return nil, err
558 + }
559 +
560 + existing := make(map[string]struct{}, len(s.discoveryBootstraps))
561 + for _, bootstrap := range s.discoveryBootstraps {
562 + existing[bootstrap] = struct{}{}
563 }
557 - s.discoveryMu.Unlock()
558 - return err
564 +
565 + added := make([]string, 0, len(next))
566 + for _, bootstrap := range next {
567 + if _, ok := existing[bootstrap]; ok {
568 + continue
569 + }
570 + added = append(added, bootstrap)
571 + }
572 +
573 + s.discoveryBootstraps = next
574 + return added, nil
575 }
576
577 func (s *Server) runDiscoveryLoop(ctx context.Context) error {
@@ -568,11 +584,20 @@ func (s *Server) runDiscoveryLoop(ctx context.Context) error {
584 bootstraps, err := discovery.DiscoverBootstraps(ctx, peers, types.DiscoverRequest{}, nil)
585 switch {
586 case err == nil:
571 - if err := s.mergeDiscoveryBootstraps(bootstraps); err != nil {
587 + added, err := s.mergeDiscoveryBootstraps(bootstraps)
588 + if err != nil {
589 log.Warn().
590 Err(err).
591 Int("bootstrap_count", len(peers)).
592 Msg("merge discovered bootstraps failed")
593 + } else if len(added) > 0 {
594 + log.Info().
595 + Str("component", "relay-server").
596 + Int("peer_count", len(peers)).
597 + Int("added_count", len(added)).
598 + Int("total_bootstrap_count", len(s.discoveryBootstrapsSnapshot())).
599 + Strs("added_bootstraps", added).
600 + Msg("discovery bootstraps updated")
601 }
602 case ctx.Err() != nil:
603 return nil
sdk/expose.go
+68 -21
@@ -37,10 +37,11 @@ type Exposure struct {
37 accepted chan net.Conn
38 datagrams chan types.DatagramFrame
39
40 - mu sync.RWMutex
41 - relayURLs []string
42 - listeners map[string]*Listener
43 - starting map[string]struct{}
40 + mu sync.RWMutex
41 + knownRelayURLs []string
42 + activeRelayURLs []string
43 + listeners map[string]*Listener
44 + starting map[string]struct{}
45
46 closeOnce sync.Once
47 connSeq atomic.Uint64
@@ -102,7 +103,7 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
103 starting: make(map[string]struct{}, len(relayURLs)),
104 }
105
105 - if err := exposure.applyRelayURLs(relayURLs, true); err != nil {
106 + if _, err := exposure.applyRelayURLs(relayURLs, true); err != nil {
107 _ = exposure.Close()
108 return nil, err
109 }
@@ -118,8 +119,8 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
119
120 log.Info().
121 Str("release_version", types.ReleaseVersion).
121 - Int("relay_count", len(exposure.RelayURLs())).
122 - Strs("relays", exposure.RelayURLs()).
122 + Int("relay_count", len(exposure.ActiveRelayURLs())).
123 + Strs("relays", exposure.ActiveRelayURLs()).
124 Msg("exposure relay started")
125
126 return exposure, nil
@@ -170,6 +171,10 @@ func ResolveRelayURLs(ctx context.Context, explicit []string, includeDefaults bo
171 }
172
173 func (e *Exposure) RelayURLs() []string {
174 + return e.ActiveRelayURLs()
175 +}
176 +
177 +func (e *Exposure) KnownRelayURLs() []string {
178 if e == nil {
179 return nil
180 }
@@ -177,11 +182,26 @@ func (e *Exposure) RelayURLs() []string {
182 e.mu.RLock()
183 defer e.mu.RUnlock()
184
180 - if len(e.relayURLs) == 0 {
185 + if len(e.knownRelayURLs) == 0 {
186 return nil
187 }
188
184 - return append([]string(nil), e.relayURLs...)
189 + return append([]string(nil), e.knownRelayURLs...)
190 +}
191 +
192 +func (e *Exposure) ActiveRelayURLs() []string {
193 + if e == nil {
194 + return nil
195 + }
196 +
197 + e.mu.RLock()
198 + defer e.mu.RUnlock()
199 +
200 + if len(e.activeRelayURLs) == 0 {
201 + return nil
202 + }
203 +
204 + return append([]string(nil), e.activeRelayURLs...)
205 }
206
207 func (e *Exposure) OwnerIdentity() discovery.Identity {
@@ -381,31 +401,50 @@ func (e *Exposure) Close() error {
401
402 event := log.Info().
403 Int("relay_count", len(listeners)).
384 - Strs("relays", e.RelayURLs())
404 + Strs("relays", e.ActiveRelayURLs())
405 if closeErr != nil {
406 event = log.Warn().
407 Err(closeErr).
408 Int("relay_count", len(listeners)).
389 - Strs("relays", e.RelayURLs())
409 + Strs("relays", e.ActiveRelayURLs())
410 }
411 event.Msg("exposure closed")
412 })
413 return closeErr
414 }
415
396 -func (e *Exposure) applyRelayURLs(relayURLs []string, failOnError bool) error {
416 +func (e *Exposure) applyRelayURLs(relayURLs []string, failOnError bool) ([]string, error) {
417 if e == nil || len(relayURLs) == 0 {
398 - return nil
418 + return nil, nil
419 }
420
421 snapshot := append([]string(nil), relayURLs...)
422
423 e.mu.Lock()
404 - if strings.Join(e.relayURLs, "\x00") != strings.Join(snapshot, "\x00") {
405 - e.relayURLs = snapshot
424 + existing := make(map[string]struct{}, len(e.knownRelayURLs))
425 + for _, relayURL := range e.knownRelayURLs {
426 + existing[relayURL] = struct{}{}
427 + }
428 + if strings.Join(e.knownRelayURLs, "\x00") != strings.Join(snapshot, "\x00") {
429 + e.knownRelayURLs = snapshot
430 + }
431 + if strings.Join(e.activeRelayURLs, "\x00") != strings.Join(snapshot, "\x00") {
432 + e.activeRelayURLs = snapshot
433 }
434 e.mu.Unlock()
408 - return e.syncListeners(failOnError)
435 +
436 + added := make([]string, 0, len(snapshot))
437 + for _, relayURL := range snapshot {
438 + if _, ok := existing[relayURL]; ok {
439 + continue
440 + }
441 + added = append(added, relayURL)
442 + }
443 +
444 + if err := e.syncListeners(failOnError); err != nil {
445 + return nil, err
446 + }
447 + return added, nil
448 }
449
450 func (e *Exposure) syncListeners(failOnError bool) error {
@@ -436,7 +475,7 @@ func (e *Exposure) reserveMissingRelayURLs() []string {
475 defer e.mu.Unlock()
476
477 missing := make([]string, 0)
439 - for _, relayURL := range e.relayURLs {
478 + for _, relayURL := range e.activeRelayURLs {
479 if _, ok := e.listeners[relayURL]; ok {
480 continue
481 }
@@ -452,7 +491,7 @@ func (e *Exposure) reserveMissingRelayURLs() []string {
491 func (e *Exposure) newListener(relayURL string) (*Listener, error) {
492 bootstraps := []string(nil)
493 if e.discoveryEnabled {
455 - bootstraps = e.RelayURLs()
494 + bootstraps = e.KnownRelayURLs()
495 }
496
497 cfg := ListenerConfig{
@@ -505,7 +544,7 @@ func (e *Exposure) listenersOrdered() []*Listener {
544 defer e.mu.RUnlock()
545
546 out := make([]*Listener, 0, len(e.listeners))
508 - for _, relayURL := range e.relayURLs {
547 + for _, relayURL := range e.activeRelayURLs {
548 if listener, ok := e.listeners[relayURL]; ok {
549 out = append(out, listener)
550 }
@@ -861,16 +900,24 @@ func (e *Exposure) runDiscoveryLoop(ctx context.Context) {
900 defer ticker.Stop()
901
902 for {
864 - peers := e.RelayURLs()
903 + peers := e.KnownRelayURLs()
904 if len(peers) > 0 {
905 relayURLs, err := discovery.DiscoverBootstraps(ctx, peers, types.DiscoverRequest{}, e.rootCAPEM)
906 switch {
907 case err == nil:
869 - if err := e.applyRelayURLs(relayURLs, false); err != nil {
908 + added, err := e.applyRelayURLs(relayURLs, false)
909 + if err != nil {
910 log.Warn().
911 Err(err).
912 Int("relay_count", len(peers)).
913 Msg("apply discovered relay urls failed")
914 + } else if len(added) > 0 {
915 + log.Info().
916 + Int("peer_count", len(peers)).
917 + Int("added_count", len(added)).
918 + Int("total_known_relay_count", len(e.KnownRelayURLs())).
919 + Strs("added_relays", added).
920 + Msg("discovery relays updated")
921 }
922 case ctx.Err() != nil:
923 return
sdk/sdk_test.go
+1 -1
@@ -535,7 +535,7 @@ func TestExposeRemovesClosedListenersSoRelaysCanRestart(t *testing.T) {
535 return registerCount.Load() >= 1 && len(exposure.listeners) == 0
536 })
537
538 - if err := exposure.applyRelayURLs(exposure.RelayURLs(), false); err != nil {
538 + if _, err := exposure.applyRelayURLs(exposure.RelayURLs(), false); err != nil {
539 t.Fatalf("applyRelayURLs() error = %v", err)
540 }
541