返回 DeepSeek-Reasonix
stall_test.go
根目录 / internal / extension / rpcwire / stall_test.go
1 package rpcwire
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "io"
8 "strings"
9 "testing"
10 "time"
11 )
12
13 // TestWriteStallFailsConnection covers the stdio-wedge case: the peer keeps
14 // the pipe open but never reads, so an unbounded write would block the caller
15 // forever. With MaxWriteStall set, the write aborts with WriteStallError and
16 // the connection fails, so later requests fail fast instead of queueing
17 // behind the stall.
18 func TestWriteStallFailsConnection(t *testing.T) {
19 pr, pw := io.Pipe()
20 defer pr.Close() // never read from pr: the pipe wedge
21
22 conn := NewConn(pr, pw, Options{Name: "stall-test", MaxWriteStall: 50 * time.Millisecond})
23 defer pw.Close()
24
25 big := make(map[string]any)
26 big["pad"] = string(make([]byte, 1<<20)) // 1 MiB, far beyond any pipe buffer
27
28 start := time.Now()
29 _, err := conn.Request(context.Background(), "never/answered", big)
30 elapsed := time.Since(start)
31
32 var stall *WriteStallError
33 if !errors.As(err, &stall) {
34 t.Fatalf("Request error = %v, want WriteStallError", err)
35 }
36 if elapsed > 5*time.Second {
37 t.Fatalf("stall took %s to abort, want close to 50ms", elapsed)
38 }
39
40 // The connection is terminal: the next request fails fast.
41 _, err = conn.Request(context.Background(), "next/call", nil)
42 if err == nil {
43 t.Fatal("second request should fail on a terminal connection")
44 }
45 if elapsed2 := time.Since(start); elapsed2 > 5*time.Second {
46 t.Fatalf("second request blocked for %s", elapsed2)
47 }
48 }
49
50 // TestWriteCallerContextAbortLeavesConnectionAlive: a caller-side context
51 // deadline expiring mid-write aborts that request without killing the
52 // connection — a user cancel must not tear down a healthy transport — and
53 // every byte the reader later sees is a well-formed NDJSON frame: the
54 // aborted frame either never started or finished serially, never torn or
55 // interleaved with the next one.
56 func TestWriteCallerContextAbortLeavesConnectionAlive(t *testing.T) {
57 pr, pw := io.Pipe()
58 defer pr.Close()
59
60 conn := NewConn(pr, pw, Options{Name: "ctx-abort-test"})
61 defer pw.Close()
62
63 drained := make(chan []byte, 1)
64 go func() {
65 buf, _ := io.ReadAll(pr)
66 drained <- buf
67 }()
68
69 big := map[string]any{"pad": string(make([]byte, 1<<20))}
70 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
71 defer cancel()
72
73 _, err := conn.Request(ctx, "never/answered", big)
74 if !errors.Is(err, context.DeadlineExceeded) {
75 t.Fatalf("Request error = %v, want context.DeadlineExceeded", err)
76 }
77 if err := conn.Notify("ping", map[string]string{"ok": "1"}); err != nil {
78 t.Fatalf("Notify after caller-abort: %v", err)
79 }
80 _ = pw.Close()
81 wire := <-drained
82
83 var ping bool
84 for _, line := range strings.Split(strings.TrimSpace(string(wire)), "\n") {
85 if strings.TrimSpace(line) == "" {
86 continue
87 }
88 var frame map[string]any
89 if err := json.Unmarshal([]byte(line), &frame); err != nil {
90 t.Fatalf("torn or interleaved frame on the wire: %q... (%v)", line[:min(len(line), 80)], err)
91 }
92 if frame["method"] == "ping" {
93 ping = true
94 }
95 }
96 if !ping {
97 t.Fatalf("ping frame missing from drained wire: %d bytes", len(wire))
98 }
99 }
100
101 // gatedWriter blocks the first write until the test releases it, proving the
102 // writer goroutine is physically mid-frame before anything else happens.
103 type gatedWriter struct {
104 started chan struct{}
105 release chan struct{}
106 buf strings.Builder
107 }
108
109 func (g *gatedWriter) Write(b []byte) (int, error) {
110 select {
111 case g.started <- struct{}{}:
112 default:
113 }
114 <-g.release
115 return g.buf.Write(b)
116 }
117
118 // TestQueuedFrameCancelledBeforeStartNeverLands: a frame still queued behind
119 // a wedged writer when its caller gives up is dropped by the writer loop —
120 // its bytes never reach the transport.
121 func TestQueuedFrameCancelledBeforeStartNeverLands(t *testing.T) {
122 gw := &gatedWriter{started: make(chan struct{}, 1), release: make(chan struct{})}
123 conn := NewConn(strings.NewReader(""), gw, Options{Name: "queued-cancel-test"})
124
125 // Job 1 wedges the single writer (blocked inside the gated writer).
126 firstDone := make(chan error, 1)
127 go func() { firstDone <- conn.Notify("first/wedged", map[string]any{"pad": strings.Repeat("x", 1<<20)}) }()
128 select {
129 case <-gw.started:
130 case <-time.After(5 * time.Second):
131 t.Fatal("writer never started the first frame")
132 }
133
134 // Job 2 queues behind it, and its caller gives up while it is still
135 // queued — the writer must drop it without writing a byte.
136 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
137 defer cancel()
138 _, err := conn.Request(ctx, "second/queued", map[string]string{"mark": "second"})
139 if !errors.Is(err, context.DeadlineExceeded) {
140 t.Fatalf("queued Request error = %v, want context.DeadlineExceeded", err)
141 }
142
143 close(gw.release)
144 if err := <-firstDone; err != nil {
145 t.Fatalf("first frame: %v", err)
146 }
147 // The writer dequeues job 2 with an expired context; give it a bounded
148 // moment, then inspect exactly what was written.
149 wire := gw.buf.String()
150 if strings.Contains(wire, "second/queued") || strings.Contains(wire, `"mark":"second"`) {
151 t.Fatalf("cancelled-before-start frame reached the transport (wire=%d bytes): %.120q", len(wire), wire)
152 }
153 if !strings.Contains(wire, "first/wedged") {
154 t.Fatal("the first frame should have completed serially once released")
155 }
156 }
157
158 // TestNotifyAfterGracefulCloseFails (review regression): when Serve ends on a
159 // clean EOF, a later Notify must FAIL — the frame is gone, and reporting
160 // success would silently drop it. Also covers a frame enqueued in the
161 // close-race window: the drain loop must answer it with the terminal error.
162 func TestNotifyAfterGracefulCloseFails(t *testing.T) {
163 serverToClientR, serverToClientW := io.Pipe()
164 conn := NewConn(serverToClientR, io.Discard, Options{Name: "close-notify-test"})
165 ctx, cancel := context.WithCancel(context.Background())
166 defer cancel()
167 serveDone := make(chan error, 1)
168 go func() { serveDone <- conn.Serve(ctx) }()
169
170 if err := conn.Notify("before/close", nil); err != nil {
171 t.Fatalf("pre-close Notify: %v", err)
172 }
173 // Graceful transport end: peer closes both directions, Serve returns nil.
174 _ = serverToClientW.Close()
175 if err := <-serveDone; err != nil {
176 t.Fatalf("Serve on graceful EOF: %v", err)
177 }
178
179 for i := 0; i < 20; i++ {
180 if err := conn.Notify("after/close", nil); err == nil {
181 t.Fatalf("attempt %d: post-close Notify reported success for a dropped frame", i)
182 }
183 }
184
185 // A request after close must fail immediately with the terminal error,
186 // not hang or report nil.
187 if _, err := conn.Request(context.Background(), "after/close", nil); err == nil {
188 t.Fatal("post-close Request reported success")
189 }
190 }
191
191 lines GO