返回 DeepSeek-Reasonix
registration_scope.go
根目录 / internal / plugin / registration_scope.go
1 package plugin
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8 "sync"
9 )
10
11 // HostClientRef identifies one live Client instance on a Host. Desktop
12 // generation-rollback uses RemoveIfInstance so a lost build cannot tear down
13 // sibling or newer-generation connections that only share a server name.
14 type HostClientRef struct {
15 Name string
16 ID uint64
17 }
18
19 // ErrRegistrationScopeAborted is returned when a connection completes after
20 // its owning build scope was aborted (generation loss / superseded build).
21 var ErrRegistrationScopeAborted = errors.New("plugin: registration scope aborted")
22
23 type registrationScopeKey struct{}
24
25 type registrationScopeState uint8
26
27 const (
28 registrationScopeActive registrationScopeState = iota
29 registrationScopeCommitted
30 registrationScopeAborted
31 )
32
33 type registrationRecordState uint8
34
35 const (
36 registrationRecordRejected registrationRecordState = iota
37 registrationRecordActive
38 registrationRecordCommitted
39 )
40
41 // RegistrationScope is a per-build ownership token for Host client
42 // registrations. Only connections that carry this scope via context are
43 // attributed to the build; sibling hot-adds omit it. Scopes do not serialize
44 // Host mutations. Abort rejects late LazyToolset registrations.
45 type RegistrationScope struct {
46 host *Host
47 id uint64
48
49 mu sync.Mutex
50 refs []HostClientRef
51 state registrationScopeState
52 }
53
54 // BeginRegistrationScope creates an independent ownership token for one
55 // controller build. Callers must propagate it with ContextWithRegistrationScope
56 // on both synchronous and asynchronous MCP connection paths.
57 func (h *Host) BeginRegistrationScope() *RegistrationScope {
58 if h == nil {
59 return &RegistrationScope{}
60 }
61 return &RegistrationScope{
62 host: h,
63 id: h.nextScopeID.Add(1),
64 }
65 }
66
67 // ContextWithRegistrationScope attaches scope to ctx for EnsureConnected /
68 // LazyToolset / ReplaceServerBackend ownership attribution.
69 func ContextWithRegistrationScope(ctx context.Context, scope *RegistrationScope) context.Context {
70 if ctx == nil {
71 ctx = context.Background()
72 }
73 if scope == nil {
74 return ctx
75 }
76 return context.WithValue(ctx, registrationScopeKey{}, scope)
77 }
78
79 // RegistrationScopeFromContext returns the build scope on ctx, if any.
80 func RegistrationScopeFromContext(ctx context.Context) *RegistrationScope {
81 if ctx == nil {
82 return nil
83 }
84 scope, _ := ctx.Value(registrationScopeKey{}).(*RegistrationScope)
85 return scope
86 }
87
88 // ID returns the Host-local scope identifier (0 when Host was nil).
89 func (s *RegistrationScope) ID() uint64 {
90 if s == nil {
91 return 0
92 }
93 return s.id
94 }
95
96 // Aborted reports whether AbortAndRollback has been called.
97 func (s *RegistrationScope) Aborted() bool {
98 if s == nil {
99 return false
100 }
101 s.mu.Lock()
102 defer s.mu.Unlock()
103 return s.state == registrationScopeAborted
104 }
105
106 // Committed reports whether the owning controller build was published. Late
107 // LazyToolset connections are accepted after commit and become Host-owned
108 // immediately; abort is terminal only for scopes that never published.
109 func (s *RegistrationScope) Committed() bool {
110 if s == nil {
111 return false
112 }
113 s.mu.Lock()
114 defer s.mu.Unlock()
115 return s.state == registrationScopeCommitted
116 }
117
118 // Snapshot returns the client instances attributed to this scope.
119 func (s *RegistrationScope) Snapshot() []HostClientRef {
120 if s == nil {
121 return nil
122 }
123 s.mu.Lock()
124 defer s.mu.Unlock()
125 return append([]HostClientRef(nil), s.refs...)
126 }
127
128 // record appends an active claim, reports that a published scope should commit
129 // the instance immediately, or rejects a late registration after abort.
130 func (s *RegistrationScope) record(ref HostClientRef) registrationRecordState {
131 if s == nil {
132 return registrationRecordCommitted
133 }
134 s.mu.Lock()
135 defer s.mu.Unlock()
136 switch s.state {
137 case registrationScopeAborted:
138 return registrationRecordRejected
139 case registrationScopeCommitted:
140 return registrationRecordCommitted
141 }
142 for _, existing := range s.refs {
143 if existing.ID == ref.ID {
144 return registrationRecordActive
145 }
146 }
147 s.refs = append(s.refs, ref)
148 return registrationRecordActive
149 }
150
151 // Commit publishes every instance used by this build into Host ownership.
152 // It returns false only when the scope was already aborted. Commit is
153 // idempotent, and late registrations on a committed scope are committed by
154 // noteClientLocked/claimClientFromContext as they arrive.
155 func (s *RegistrationScope) Commit() bool {
156 if s == nil {
157 return true
158 }
159 s.mu.Lock()
160 switch s.state {
161 case registrationScopeAborted:
162 s.mu.Unlock()
163 return false
164 case registrationScopeCommitted:
165 s.mu.Unlock()
166 return true
167 }
168 s.state = registrationScopeCommitted
169 refs := append([]HostClientRef(nil), s.refs...)
170 s.refs = nil
171 s.mu.Unlock()
172 if s.host != nil {
173 s.host.commitRegistration(s.id, refs)
174 }
175 return true
176 }
177
178 // AbortAndRollback marks the scope aborted (rejecting late registrations) and
179 // removes every instance previously recorded under this scope.
180 func (s *RegistrationScope) AbortAndRollback() {
181 if s == nil {
182 return
183 }
184 s.mu.Lock()
185 if s.state != registrationScopeActive {
186 s.mu.Unlock()
187 return
188 }
189 s.state = registrationScopeAborted
190 refs := append([]HostClientRef(nil), s.refs...)
191 s.refs = nil
192 s.mu.Unlock()
193 if s.host != nil {
194 s.host.rollbackRegistration(s.id, refs)
195 }
196 }
197
198 // noteClientLocked assigns an instance ID and records ownership on scope.
199 // Caller holds h.mu. Aborted scopes return ErrRegistrationScopeAborted and do
200 // not leave c in h.clients (caller closes c).
201 func (h *Host) noteClientLocked(c *Client, scope *RegistrationScope) error {
202 if c == nil {
203 return nil
204 }
205 if c.instanceID == 0 {
206 c.instanceID = h.nextInstanceID.Add(1)
207 }
208 // Append first, then record. Aborted scopes unpublish immediately. Clients
209 // registered outside a build scope are already Host-owned.
210 h.clients = append(h.clients, c)
211 if scope == nil {
212 c.registrationCommitted = true
213 return nil
214 }
215 switch scope.record(HostClientRef{Name: c.name, ID: c.instanceID}) {
216 case registrationRecordRejected:
217 h.clients = h.clients[:len(h.clients)-1]
218 return ErrRegistrationScopeAborted
219 case registrationRecordCommitted:
220 c.registrationCommitted = true
221 case registrationRecordActive:
222 if c.registrationClaims == nil {
223 c.registrationClaims = make(map[uint64]struct{})
224 }
225 c.registrationClaims[scope.id] = struct{}{}
226 }
227 return nil
228 }
229
230 // noteClientFromContext is noteClientLocked using the scope on ctx.
231 func (h *Host) noteClientFromContext(ctx context.Context, c *Client) error {
232 return h.noteClientLocked(c, RegistrationScopeFromContext(ctx))
233 }
234
235 // claimClientFromContext attributes reuse of an existing exact instance to the
236 // current build. The instance is revalidated under Host.mu so a concurrent
237 // replace/remove cannot turn a pre-check into a stale claim.
238 func (h *Host) claimClientFromContext(ctx context.Context, c *Client) error {
239 if h == nil || c == nil {
240 return errors.New("plugin: client is unavailable")
241 }
242 h.mu.Lock()
243 defer h.mu.Unlock()
244 if h.closed {
245 return errors.New("plugin host is closed")
246 }
247 if live := h.lookupClientLocked(c.name); live != c {
248 return fmt.Errorf("client %q changed while being claimed", c.name)
249 }
250 scope := RegistrationScopeFromContext(ctx)
251 if scope == nil {
252 return nil
253 }
254 switch scope.record(HostClientRef{Name: c.name, ID: c.instanceID}) {
255 case registrationRecordRejected:
256 return ErrRegistrationScopeAborted
257 case registrationRecordCommitted:
258 c.registrationCommitted = true
259 case registrationRecordActive:
260 if c.registrationClaims == nil {
261 c.registrationClaims = make(map[uint64]struct{})
262 }
263 c.registrationClaims[scope.id] = struct{}{}
264 }
265 return nil
266 }
267
268 // RemoveIfInstance disconnects name only when the live client instance ID still
269 // matches. Returns whether a matching client was removed.
270 func (h *Host) RemoveIfInstance(name string, instanceID uint64) bool {
271 if h == nil || instanceID == 0 {
272 return false
273 }
274 name = strings.TrimSpace(name)
275 if name == "" {
276 return false
277 }
278 h.mu.Lock()
279 idx := -1
280 var removed *Client
281 for i, c := range h.clients {
282 if c != nil && c.name == name && c.instanceID == instanceID {
283 idx = i
284 removed = c
285 break
286 }
287 }
288 if idx < 0 || removed == nil {
289 h.mu.Unlock()
290 return false
291 }
292 removed = h.removeClientAtLocked(idx)
293 h.mu.Unlock()
294 removed.close()
295 return true
296 }
297
298 func (h *Host) commitRegistration(scopeID uint64, refs []HostClientRef) {
299 if h == nil || scopeID == 0 {
300 return
301 }
302 h.mu.Lock()
303 defer h.mu.Unlock()
304 for _, ref := range refs {
305 _, client := h.findClientInstanceLocked(ref.Name, ref.ID)
306 if client == nil {
307 continue
308 }
309 delete(client.registrationClaims, scopeID)
310 client.registrationCommitted = true
311 }
312 }
313
314 func (h *Host) rollbackRegistration(scopeID uint64, refs []HostClientRef) {
315 if h == nil || scopeID == 0 {
316 return
317 }
318 h.mu.Lock()
319 removed := make([]*Client, 0, len(refs))
320 for _, ref := range refs {
321 idx, client := h.findClientInstanceLocked(ref.Name, ref.ID)
322 if client == nil {
323 continue
324 }
325 delete(client.registrationClaims, scopeID)
326 if client.registrationCommitted || len(client.registrationClaims) > 0 {
327 continue
328 }
329 if removedClient := h.removeClientAtLocked(idx); removedClient != nil {
330 removed = append(removed, removedClient)
331 }
332 }
333 h.mu.Unlock()
334 for _, client := range removed {
335 client.close()
336 }
337 }
338
339 func (h *Host) findClientInstanceLocked(name string, instanceID uint64) (int, *Client) {
340 for i, client := range h.clients {
341 if client != nil && client.name == name && client.instanceID == instanceID {
342 return i, client
343 }
344 }
345 return -1, nil
346 }
347
348 // removeClientAtLocked removes one exact live instance without touching
349 // name-wide deferred startup generations. The owning build context cancels its
350 // own lazy work; canceling every entry for the name would affect newer builds.
351 func (h *Host) removeClientAtLocked(idx int) *Client {
352 if idx < 0 || idx >= len(h.clients) {
353 return nil
354 }
355 removed := h.clients[idx]
356 h.clients = append(h.clients[:idx], h.clients[idx+1:]...)
357 if h.proxies != nil {
358 if proxy := h.proxies[removed.name]; proxy != nil {
359 proxy.detachIf(removed)
360 }
361 }
362 keptPrompts := h.prompts[:0]
363 for _, prompt := range h.prompts {
364 if prompt.Server != removed.name {
365 keptPrompts = append(keptPrompts, prompt)
366 }
367 }
368 h.prompts = keptPrompts
369 keptResources := h.resources[:0]
370 for _, resource := range h.resources {
371 if resource.Server != removed.name {
372 keptResources = append(keptResources, resource)
373 }
374 }
375 h.resources = keptResources
376 h.clearFailure(removed.name)
377 return removed
378 }
379
380 // RollbackRegistration removes every journaled client instance. Safe when a
381 // ref is already gone (RemoveIfInstance is a no-op).
382 func (h *Host) RollbackRegistration(refs []HostClientRef) {
383 if h == nil {
384 return
385 }
386 for _, ref := range refs {
387 h.RemoveIfInstance(ref.Name, ref.ID)
388 }
389 }
390
390 lines GO