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