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