@@ -2,7 +2,6 @@ package discovery
22
33import (
44 "context"
5- "encoding/json"
65 "fmt"
76 "sync"
87 "time"
@@ -69,11 +68,19 @@ type Integration struct {
6968 proxyTimeout time.Duration
7069 logger forge.Logger
7170
71+ // localServiceIDs holds service IDs that belong to the host process. Any
72+ // instance whose ID matches is filtered out before registration so the
73+ // dashboard never tries to fetch a contributor manifest from itself
74+ // (memory-backed discovery surfaces the host's own service registration
75+ // alongside any peers it learns about).
76+ localServiceIDs map [string ]struct {}
77+
7278 // tracked keeps track of remote contributors we've registered
7379 tracked map [string ]string // service ID → contributor name
7480
75- stopCh chan struct {}
76- wg sync.WaitGroup
81+ stopOnce sync.Once
82+ stopCh chan struct {}
83+ wg sync.WaitGroup
7784}
7885
7986// NewIntegration creates a new discovery integration.
@@ -97,8 +104,30 @@ func NewIntegration(
97104 }
98105}
99106
107+ // LocalServiceIDProvider is the optional interface a discovery service
108+ // implements when it can report the host process's own service ID. Used so
109+ // memory-backed discovery doesn't make the dashboard try to fetch a
110+ // contributor manifest from itself.
111+ type LocalServiceIDProvider interface {
112+ LocalServiceID () string
113+ }
114+
100115// Start begins polling the discovery service for dashboard contributors.
101116func (i * Integration ) Start (ctx context.Context ) {
117+ // Auto-filter the host's own service ID when the discovery service can
118+ // report it. Memory-backed discovery surfaces the local registration in
119+ // every poll; without this guard the dashboard would attempt to fetch a
120+ // contributor manifest from itself.
121+ if provider , ok := i .discovery .(LocalServiceIDProvider ); ok {
122+ if id := provider .LocalServiceID (); id != "" {
123+ i .IgnoreLocalService (id )
124+
125+ i .logger .Debug ("discovery integration: filtering local service ID" ,
126+ forge .F ("local_service_id" , id ),
127+ )
128+ }
129+ }
130+
102131 i .wg .Add (1 )
103132
104133 go i .pollLoop (ctx )
@@ -109,12 +138,34 @@ func (i *Integration) Start(ctx context.Context) {
109138 )
110139}
111140
112- // Stop stops the discovery integration.
141+ // Stop stops the discovery integration. Safe to call multiple times — the
142+ // dashboard extension's Stop() can fire more than once (forge calls Stop on
143+ // the extension and the DI container Stop walks the same instance again).
113144func (i * Integration ) Stop () {
114- close (i .stopCh )
115- i .wg .Wait ()
145+ i .stopOnce .Do (func () {
146+ close (i .stopCh )
147+ i .wg .Wait ()
148+
149+ i .logger .Info ("discovery integration stopped" )
150+ })
151+ }
152+
153+ // IgnoreLocalService instructs the integration to skip a service ID when
154+ // scanning discovery. This is used to filter the host's own self-registration
155+ // out of dashboard contributor candidates. Must be called before Start.
156+ func (i * Integration ) IgnoreLocalService (serviceID string ) {
157+ if serviceID == "" {
158+ return
159+ }
160+
161+ i .mu .Lock ()
162+ defer i .mu .Unlock ()
163+
164+ if i .localServiceIDs == nil {
165+ i .localServiceIDs = make (map [string ]struct {})
166+ }
116167
117- i .logger . Info ( "discovery integration stopped" )
168+ i .localServiceIDs [ serviceID ] = struct {}{}
118169}
119170
120171// pollLoop periodically checks discovery for services with the dashboard tag.
@@ -165,6 +216,14 @@ func (i *Integration) reconcile(ctx context.Context) {
165216 continue
166217 }
167218
219+ i .mu .Lock ()
220+ _ , isLocal := i .localServiceIDs [inst .ID ]
221+ i .mu .Unlock ()
222+
223+ if isLocal {
224+ continue
225+ }
226+
168227 foundIDs [inst .ID ] = true
169228
170229 i .mu .Lock ()
@@ -269,7 +328,7 @@ func (i *Integration) refreshRemoteService(ctx context.Context, inst *ServiceIns
269328 }
270329
271330 current , ok := i .registry .GetManifest (contribName )
272- if ok && manifestsEqual (current , manifest ) {
331+ if ok && contributor . ManifestsEqual (current , manifest ) {
273332 return
274333 }
275334
@@ -303,26 +362,6 @@ func (i *Integration) refreshRemoteService(ctx context.Context, inst *ServiceIns
303362 )
304363}
305364
306- // manifestsEqual compares two manifests via JSON serialisation. This avoids a
307- // deep reflect equality check over templ.Component fields (which are nil on
308- // remote-fetched manifests anyway).
309- func manifestsEqual (a , b * contributor.Manifest ) bool {
310- if a == nil || b == nil {
311- return a == b
312- }
313-
314- ab , err := json .Marshal (a )
315- if err != nil {
316- return false
317- }
318-
319- bb , err := json .Marshal (b )
320- if err != nil {
321- return false
322- }
323-
324- return string (ab ) == string (bb )
325- }
326365
327366// TrackedCount returns the number of tracked remote contributors.
328367func (i * Integration ) TrackedCount () int {
0 commit comments