返回 DeepSeek-Reasonix
proxy.go
根目录 / internal / plugin / proxy.go
1 package plugin
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "sync"
8 "sync/atomic"
9 )
10
11 // serverProxy is a stable handle for one MCP server name. Consumers keep the
12 // same tool names while the live *Client rolls underneath.
13 type serverProxy struct {
14 name string
15 mu sync.RWMutex
16 active *Client
17 generation uint64
18 closed atomic.Bool
19 }
20
21 func newServerProxy(name string) *serverProxy {
22 return &serverProxy{name: name}
23 }
24
25 func (p *serverProxy) replace(ctx context.Context, next *Client, generation uint64) error {
26 prev, err := p.swap(next, generation)
27 if err != nil {
28 if next != nil {
29 next.close()
30 }
31 return err
32 }
33 if prev != nil && prev != next && prev.t != nil {
34 prev.close()
35 }
36 _ = ctx
37 return nil
38 }
39
40 func (p *serverProxy) swap(next *Client, generation uint64) (*Client, error) {
41 if p == nil {
42 return nil, fmt.Errorf("plugin: nil server proxy")
43 }
44 if p.closed.Load() {
45 return nil, fmt.Errorf("plugin: server proxy %q closed", p.name)
46 }
47 p.mu.Lock()
48 prev := p.active
49 p.active = next
50 p.generation = generation
51 p.mu.Unlock()
52 return prev, nil
53 }
54
55 func (p *serverProxy) client() *Client {
56 if p == nil {
57 return nil
58 }
59 p.mu.RLock()
60 defer p.mu.RUnlock()
61 return p.active
62 }
63
64 // detachIf clears the active backend only when it is the exact instance being
65 // removed. The caller closes the client after releasing Host.mu.
66 func (p *serverProxy) detachIf(client *Client) bool {
67 if p == nil || client == nil {
68 return false
69 }
70 p.mu.Lock()
71 defer p.mu.Unlock()
72 if p.active != client {
73 return false
74 }
75 p.active = nil
76 return true
77 }
78
79 func (p *serverProxy) close() {
80 if p == nil || !p.closed.CompareAndSwap(false, true) {
81 return
82 }
83 p.mu.Lock()
84 c := p.active
85 p.active = nil
86 p.mu.Unlock()
87 if c != nil && c.t != nil {
88 c.close()
89 }
90 }
91
92 func closeServerProxies(proxies map[string]*serverProxy) {
93 for _, p := range proxies {
94 p.close()
95 }
96 }
97
98 // CancelInFlightMCP is a best-effort drain hook: closes active proxied
99 // backends for every server so generation drain can abort mid-call work.
100 // Ordinary tool calls do not yet track per-call context cancel on Host;
101 // ReplaceServerBackend still closes the previous client.
102 func (h *Host) CancelInFlightMCP() {
103 if h == nil {
104 return
105 }
106 h.mu.Lock()
107 proxies := make([]*serverProxy, 0, len(h.proxies))
108 for _, p := range h.proxies {
109 proxies = append(proxies, p)
110 }
111 h.mu.Unlock()
112 for _, p := range proxies {
113 // Close active client to abort stdio/HTTP transport reads.
114 if c := p.client(); c != nil && c.t != nil {
115 c.close()
116 }
117 }
118 }
119
120 // ReplaceServerBackend swaps the live client for name behind a stable proxy.
121 // Tool schemas and names stay owned by the registry; only the connection moves.
122 // generation is the runtime generation performing the replace.
123 func (h *Host) ReplaceServerBackend(ctx context.Context, name string, next *Client, generation uint64) error {
124 if h == nil {
125 return fmt.Errorf("plugin: nil Host")
126 }
127 name = strings.TrimSpace(name)
128 if name == "" {
129 return fmt.Errorf("plugin: empty server name")
130 }
131 h.mu.Lock()
132 if h.closed {
133 h.mu.Unlock()
134 if next != nil {
135 next.close()
136 }
137 return fmt.Errorf("plugin: host closed")
138 }
139 if h.proxies == nil {
140 h.proxies = make(map[string]*serverProxy)
141 }
142 p := h.proxies[name]
143 if p == nil {
144 p = newServerProxy(name)
145 h.proxies[name] = p
146 }
147 // Publish the proxy and clients slice under the same Host lock. Otherwise a
148 // scope rollback can remove next after h.mu is released but before p.replace
149 // publishes it, leaving the proxy pointed at a closed client.
150 prev := p.client()
151 if prev == next {
152 _, err := p.swap(next, generation)
153 h.mu.Unlock()
154 return err
155 }
156 if next != nil {
157 if err := h.noteClientFromContext(ctx, next); err != nil {
158 h.mu.Unlock()
159 next.close()
160 return err
161 }
162 }
163 replaced, err := p.swap(next, generation)
164 if err != nil {
165 if next != nil {
166 for i, client := range h.clients {
167 if client == next {
168 h.clients = append(h.clients[:i], h.clients[i+1:]...)
169 break
170 }
171 }
172 }
173 h.mu.Unlock()
174 if next != nil {
175 next.close()
176 }
177 return err
178 }
179 if prev != nil {
180 for i, client := range h.clients {
181 if client == prev {
182 h.clients = append(h.clients[:i], h.clients[i+1:]...)
183 break
184 }
185 }
186 }
187 h.mu.Unlock()
188 if replaced != nil && replaced != next && replaced.t != nil {
189 replaced.close()
190 }
191 return nil
192 }
193
194 func (h *Host) lookupClient(name string) *Client {
195 if h == nil {
196 return nil
197 }
198 h.mu.RLock()
199 defer h.mu.RUnlock()
200 return h.lookupClientLocked(name)
201 }
202
203 // lookupClientLocked returns the active exact client. Caller holds h.mu for
204 // read or write; proxy.client has its own leaf lock.
205 func (h *Host) lookupClientLocked(name string) *Client {
206 if h.closed {
207 return nil
208 }
209 if h.proxies != nil {
210 if p := h.proxies[name]; p != nil {
211 if c := p.client(); c != nil {
212 return c
213 }
214 }
215 }
216 for _, c := range h.clients {
217 if c.name == name {
218 return c
219 }
220 }
221 return nil
222 }
223
223 lines GO