返回 DeepSeek-Reasonix
proxy.go
根目录 / internal / extension / proxy.go
1 package extension
2
3 import (
4 "context"
5 "fmt"
6 "sync"
7 "sync/atomic"
8 )
9
10 // Backend is a replaceable provider or MCP backend behind a StableProxy.
11 type Backend interface {
12 ID() string
13 // Close drains in-flight work owned by this backend.
14 Close(context.Context) error
15 }
16
17 // StableProxy presents a stable consumer-facing handle while backends roll.
18 // It does not alter provider-visible prompt/tool prefixes; cache identity
19 // remains owned by RuntimeSnapshot.CacheHash.
20 type StableProxy struct {
21 mu sync.RWMutex
22 active Backend
23 draining []Backend
24 closed atomic.Bool
25 // generation of the currently active backend registration.
26 generation uint64
27 // inFlight tracks CallCtx cancel funcs so Replace/Close/drain can abort
28 // mid-call work instead of hanging across a backend roll.
29 inFlight map[uint64]context.CancelFunc
30 nextCall atomic.Uint64
31 }
32
33 // NewStableProxy returns an empty proxy.
34 func NewStableProxy() *StableProxy {
35 return &StableProxy{inFlight: make(map[uint64]context.CancelFunc)}
36 }
37
38 // Active returns the current backend, or nil.
39 func (p *StableProxy) Active() Backend {
40 if p == nil {
41 return nil
42 }
43 p.mu.RLock()
44 defer p.mu.RUnlock()
45 return p.active
46 }
47
48 // Generation returns the active backend generation.
49 func (p *StableProxy) Generation() uint64 {
50 if p == nil {
51 return 0
52 }
53 p.mu.RLock()
54 defer p.mu.RUnlock()
55 return p.generation
56 }
57
58 // Replace swaps in a new backend and begins draining the previous one.
59 // Rolling replacement keeps the consumer pointer stable. In-flight CallCtx
60 // work is cancelled before the previous backend is closed.
61 func (p *StableProxy) Replace(ctx context.Context, next Backend, generation uint64) error {
62 if p == nil {
63 return fmt.Errorf("extension: nil StableProxy")
64 }
65 p.mu.Lock()
66 if p.closed.Load() {
67 p.mu.Unlock()
68 if next != nil {
69 _ = next.Close(ctx)
70 }
71 return fmt.Errorf("extension: proxy closed")
72 }
73 prev := p.active
74 p.active = next
75 p.generation = generation
76 if prev != nil {
77 p.draining = append(p.draining, prev)
78 }
79 cancels := p.takeInFlightLocked()
80 p.mu.Unlock()
81 for _, c := range cancels {
82 c()
83 }
84 if prev != nil {
85 if err := prev.Close(ctx); err != nil {
86 return fmt.Errorf("drain previous backend: %w", err)
87 }
88 p.mu.Lock()
89 out := p.draining[:0]
90 for _, b := range p.draining {
91 if b != prev {
92 out = append(out, b)
93 }
94 }
95 p.draining = out
96 p.mu.Unlock()
97 }
98 return nil
99 }
100
101 // Close drains the active and any remaining backends after cancelling in-flight calls.
102 func (p *StableProxy) Close(ctx context.Context) error {
103 if p == nil {
104 return nil
105 }
106 if !p.closed.CompareAndSwap(false, true) {
107 return nil
108 }
109 p.mu.Lock()
110 active := p.active
111 p.active = nil
112 draining := append([]Backend(nil), p.draining...)
113 p.draining = nil
114 cancels := p.takeInFlightLocked()
115 p.mu.Unlock()
116 for _, c := range cancels {
117 c()
118 }
119 var first error
120 if active != nil {
121 first = active.Close(ctx)
122 }
123 for _, b := range draining {
124 if err := b.Close(ctx); err != nil && first == nil {
125 first = err
126 }
127 }
128 return first
129 }
130
131 // Call invokes fn with the active backend. If no backend is registered the
132 // call fails fast so consumers do not hang across a crash/replace window.
133 func (p *StableProxy) Call(fn func(Backend) error) error {
134 return p.CallCtx(context.Background(), func(_ context.Context, b Backend) error {
135 return fn(b)
136 })
137 }
138
139 // CallCtx is Call with a parent context. The call is cancelled when the proxy
140 // is Replaced or Closed (drain of in-flight work).
141 func (p *StableProxy) CallCtx(ctx context.Context, fn func(context.Context, Backend) error) error {
142 if p == nil || p.closed.Load() {
143 return fmt.Errorf("extension: proxy unavailable")
144 }
145 if ctx == nil {
146 ctx = context.Background()
147 }
148 ctx, cancel := context.WithCancel(ctx)
149 id := p.nextCall.Add(1)
150 p.mu.Lock()
151 if p.inFlight == nil {
152 p.inFlight = make(map[uint64]context.CancelFunc)
153 }
154 p.inFlight[id] = cancel
155 b := p.active
156 p.mu.Unlock()
157 defer func() {
158 cancel()
159 p.mu.Lock()
160 delete(p.inFlight, id)
161 p.mu.Unlock()
162 }()
163 if b == nil {
164 return fmt.Errorf("extension: no active backend")
165 }
166 return fn(ctx, b)
167 }
168
169 // CancelInFlight aborts every outstanding CallCtx. Used by generation drain.
170 func (p *StableProxy) CancelInFlight() {
171 if p == nil {
172 return
173 }
174 p.mu.Lock()
175 cancels := p.takeInFlightLocked()
176 p.mu.Unlock()
177 for _, c := range cancels {
178 c()
179 }
180 }
181
182 func (p *StableProxy) takeInFlightLocked() []context.CancelFunc {
183 if p == nil || len(p.inFlight) == 0 {
184 return nil
185 }
186 out := make([]context.CancelFunc, 0, len(p.inFlight))
187 for id, c := range p.inFlight {
188 out = append(out, c)
189 delete(p.inFlight, id)
190 }
191 return out
192 }
193
193 lines GO