返回 DeepSeek-Reasonix
stream.go
1 package providerext
2
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "errors"
7 "fmt"
8 "log/slog"
9 "sync"
10 "time"
11
12 "reasonix/internal/extension/protocol"
13 "reasonix/internal/extension/providerconv"
14 "reasonix/internal/provider"
15 "reasonix/internal/secrets"
16 )
17
18 // extensionStream is one in-flight sidecar provider stream. Its fields are
19 // guarded by Resolver.mu and mirror the broker's hostStream: chunks arrive
20 // with 1-based seqs, buffer out of order, and flush contiguously; stream/end
21 // freezes the terminal boundary (LastSeq) and a gap timer converts a missing
22 // tail chunk into an interruption instead of a hang.
23 type extensionStream struct {
24 client ProviderClient
25 out chan provider.Chunk
26 done chan struct{}
27 abortDelivery chan struct{}
28 nextSeq int64
29 pending map[int64]provider.Chunk
30 ended bool
31 endSeq int64
32 endError string
33 interrupted bool
34 gapTimer bool
35 closeOnce sync.Once
36 delivery []provider.Chunk
37 deliveryWake chan struct{}
38 deliveryFinal bool
39 activity chan struct{}
40 unregisterDrainCancel func()
41 }
42
43 // deliveryQueueLimit bounds the per-stream delivery queue without applying
44 // backpressure while Resolver.mu is held. One slot is reserved for the
45 // terminal chunk; mirroring the broker's hostDeliveryQueueLimit.
46 const deliveryQueueLimit = 256
47
48 // pendingWindowLimit bounds out-of-order buffering: chunks with a sequence at
49 // or beyond nextSeq+pendingWindowLimit never enter pending. A sidecar with a
50 // sequencing bug (emitting ever-higher seqs without the missing one or an
51 // end) must not grow host memory without limit — the stream is failed
52 // interrupted instead.
53 const pendingWindowLimit = 256
54
55 // RouteStreamChunk implements sidecar.StreamRouter. Unknown stream IDs are
56 // dropped with a debug log — a sidecar can legitimately race a late chunk
57 // against the host's cancel or its own crash teardown. Stale-generation
58 // chunks (after publish of a newer runtime) are also dropped.
59 func (r *Resolver) RouteStreamChunk(p protocol.StreamChunkParams) {
60 gen := p.Generation
61 if gen == 0 {
62 gen = p.Chunk.Generation
63 }
64 if r.owner.Gate.DropStale(gen, "provider_chunk") {
65 slog.Debug("providerext: dropping stale-generation chunk", "stream", p.StreamID, "seq", p.Seq, "generation", gen)
66 return
67 }
68 r.mu.Lock()
69 defer r.mu.Unlock()
70 stream := r.streams[p.StreamID]
71 if stream == nil {
72 slog.Debug("providerext: dropping chunk for unknown stream", "stream", p.StreamID, "seq", p.Seq)
73 return
74 }
75 signalStreamActivity(stream)
76 if p.Seq < stream.nextSeq {
77 return // duplicate or already delivered
78 }
79 if stream.ended && p.Seq > stream.endSeq {
80 r.finishLocked(p.StreamID, stream, provider.Chunk{Type: provider.ChunkError, Err: &provider.StreamInterruptedError{
81 Err: fmt.Errorf("extension stream %s: chunk seq %d exceeds frozen LastSeq %d", p.StreamID, p.Seq, stream.endSeq),
82 }})
83 return
84 }
85 if p.Seq >= stream.nextSeq+pendingWindowLimit {
86 r.finishLocked(p.StreamID, stream, provider.Chunk{Type: provider.ChunkError, Err: &provider.StreamInterruptedError{
87 Err: fmt.Errorf("extension stream %s: chunk seq %d exceeds the pending window of stream seq %d", p.StreamID, p.Seq, stream.nextSeq),
88 }})
89 return
90 }
91 stream.pending[p.Seq] = providerconv.ChunkFromProtocol(p.Chunk)
92 r.flushLocked(p.StreamID, stream)
93 }
94
95 // RouteStreamEnd implements sidecar.StreamRouter. LastSeq freezes the
96 // terminal boundary: the stream completes only after chunks 1..LastSeq have
97 // been delivered, and a missing chunk trips the gap timer.
98 func (r *Resolver) RouteStreamEnd(p protocol.StreamEndParams) {
99 r.mu.Lock()
100 stream := r.streams[p.StreamID]
101 if stream == nil {
102 r.mu.Unlock()
103 slog.Debug("providerext: dropping stream end for unknown stream", "stream", p.StreamID)
104 return
105 }
106 signalStreamActivity(stream)
107 if stream.ended {
108 if stream.endSeq != p.LastSeq || stream.endError != p.Error || stream.interrupted != p.Interrupted {
109 r.finishLocked(p.StreamID, stream, provider.Chunk{Type: provider.ChunkError, Err: &provider.StreamInterruptedError{
110 Err: fmt.Errorf("extension stream %s: conflicting duplicate end changed frozen LastSeq or terminal state", p.StreamID),
111 }})
112 r.mu.Unlock()
113 return
114 }
115 r.flushLocked(p.StreamID, stream)
116 r.mu.Unlock()
117 return
118 }
119 stream.ended = true
120 stream.endSeq = p.LastSeq
121 stream.endError = p.Error
122 stream.interrupted = p.Interrupted
123 for seq := range stream.pending {
124 if seq > stream.endSeq {
125 r.finishLocked(p.StreamID, stream, provider.Chunk{Type: provider.ChunkError, Err: &provider.StreamInterruptedError{
126 Err: fmt.Errorf("extension stream %s: buffered chunk seq %d exceeds frozen LastSeq %d", p.StreamID, seq, stream.endSeq),
127 }})
128 r.mu.Unlock()
129 return
130 }
131 }
132 r.flushLocked(p.StreamID, stream)
133 if r.streams[p.StreamID] == stream && stream.nextSeq <= stream.endSeq && !stream.gapTimer {
134 stream.gapTimer = true
135 go r.expireGap(p.StreamID, stream)
136 }
137 r.mu.Unlock()
138 }
139
140 func signalStreamActivity(stream *extensionStream) {
141 if stream == nil || stream.activity == nil {
142 return
143 }
144 select {
145 case stream.activity <- struct{}{}:
146 default:
147 }
148 }
149
150 // flushLocked delivers every contiguous pending chunk, then completes the
151 // stream once the end boundary is fully delivered. The terminal chunk mirrors
152 // the broker: a reported error is defensively credential-redacted even though
153 // the protocol also requires producer-side redaction, an interruption becomes
154 // StreamInterruptedError, and a clean end closes the channel.
155 func (r *Resolver) flushLocked(id string, stream *extensionStream) {
156 for !stream.ended || stream.nextSeq <= stream.endSeq {
157 chunk, ok := stream.pending[stream.nextSeq]
158 if !ok {
159 break
160 }
161 delete(stream.pending, stream.nextSeq)
162 stream.nextSeq++
163 if !r.enqueueDeliveryLocked(stream, chunk) {
164 r.finishLocked(id, stream, provider.Chunk{Type: provider.ChunkError, Err: &provider.StreamInterruptedError{
165 Err: errors.New("extension provider stream output overflow"),
166 }})
167 return
168 }
169 }
170 if !stream.ended || stream.nextSeq <= stream.endSeq {
171 return
172 }
173 if stream.endError != "" || stream.interrupted {
174 redactedEndError := secrets.RedactCredentials(stream.endError)
175 err := errors.New(redactedEndError)
176 if stream.endError == "" {
177 err = errors.New("extension provider stream failed")
178 }
179 if stream.interrupted {
180 message := redactedEndError
181 if message == "" {
182 message = "extension provider stream was interrupted"
183 }
184 err = &provider.StreamInterruptedError{Err: errors.New(message)}
185 }
186 r.finishLocked(id, stream, provider.Chunk{Type: provider.ChunkError, Err: err})
187 return
188 }
189 r.finishLocked(id, stream, provider.Chunk{})
190 }
191
192 // finishLocked appends the terminal chunk (when non-zero) and marks delivery
193 // final. The first finish wins; later finishes for a replaced or completed
194 // stream are no-ops.
195 func (r *Resolver) finishLocked(id string, stream *extensionStream, terminal provider.Chunk) {
196 if r.streams[id] != stream {
197 return
198 }
199 delete(r.streams, id)
200 stream.closeOnce.Do(func() {
201 if stream.unregisterDrainCancel != nil {
202 stream.unregisterDrainCancel()
203 stream.unregisterDrainCancel = nil
204 }
205 if terminal.Err != nil || terminal.Type != 0 {
206 stream.delivery = append(stream.delivery, terminal)
207 }
208 stream.deliveryFinal = true
209 close(stream.done)
210 r.signalDeliveryLocked(stream)
211 })
212 }
213
214 // removeStream tears down a stream whose open never completed, aborting the
215 // delivery loop without a terminal chunk.
216 func (r *Resolver) removeStream(id string, stream *extensionStream) {
217 r.mu.Lock()
218 defer r.mu.Unlock()
219 if r.streams[id] == stream {
220 r.abortDeliveryLocked(stream)
221 r.finishLocked(id, stream, provider.Chunk{})
222 }
223 }
224
225 func (r *Resolver) abortDeliveryLocked(stream *extensionStream) {
226 if stream.abortDelivery == nil {
227 return
228 }
229 select {
230 case <-stream.abortDelivery:
231 default:
232 close(stream.abortDelivery)
233 }
234 }
235
236 func (r *Resolver) enqueueDeliveryLocked(stream *extensionStream, chunk provider.Chunk) bool {
237 if stream.deliveryFinal || len(stream.delivery) >= deliveryQueueLimit-1 {
238 return false
239 }
240 stream.delivery = append(stream.delivery, chunk)
241 r.signalDeliveryLocked(stream)
242 return true
243 }
244
245 func (r *Resolver) signalDeliveryLocked(stream *extensionStream) {
246 select {
247 case stream.deliveryWake <- struct{}{}:
248 default:
249 }
250 }
251
252 // deliverStream is the sole sender and closer of stream.out. It may wait for
253 // a slow consumer, but never while holding Resolver.mu, so routing,
254 // cancellation, disconnects, gap expiry, and unrelated streams keep moving.
255 func (r *Resolver) deliverStream(stream *extensionStream) {
256 defer close(stream.out)
257 for {
258 r.mu.Lock()
259 if len(stream.delivery) > 0 {
260 chunk := stream.delivery[0]
261 stream.delivery[0] = provider.Chunk{}
262 stream.delivery = stream.delivery[1:]
263 r.mu.Unlock()
264 select {
265 case stream.out <- chunk:
266 case <-stream.abortDelivery:
267 return
268 }
269 continue
270 }
271 if stream.deliveryFinal {
272 r.mu.Unlock()
273 return
274 }
275 wake := stream.deliveryWake
276 r.mu.Unlock()
277 select {
278 case <-wake:
279 case <-stream.abortDelivery:
280 return
281 }
282 }
283 }
284
285 // expireGap converts a missing tail chunk into an interruption one second
286 // after stream/end: the LastSeq boundary froze, so the absent seq will never
287 // legitimately arrive.
288 func (r *Resolver) expireGap(id string, stream *extensionStream) {
289 timer := time.NewTimer(time.Second)
290 defer timer.Stop()
291 select {
292 case <-timer.C:
293 case <-stream.done:
294 return
295 }
296 r.mu.Lock()
297 defer r.mu.Unlock()
298 if r.streams[id] != stream || !stream.ended || stream.nextSeq > stream.endSeq {
299 return
300 }
301 r.finishLocked(id, stream, provider.Chunk{Type: provider.ChunkError, Err: &provider.StreamInterruptedError{
302 Err: fmt.Errorf("extension provider stream missing chunk %d of %d", stream.nextSeq, stream.endSeq),
303 }})
304 }
305
306 func randomID(n int) string {
307 b := make([]byte, n)
308 if _, err := rand.Read(b); err != nil {
309 return fmt.Sprintf("%d", time.Now().UnixNano())
310 }
311 return hex.EncodeToString(b)
312 }
313
313 lines GO