@@ -23,6 +23,7 @@ import (
2323 "runtime"
2424 "runtime/debug"
2525 "strings"
26+ "sync"
2627 "sync/atomic"
2728 "testing"
2829 "time"
@@ -152,6 +153,186 @@ func TestCall(t *testing.T) {
152153 test .Assert (t , err == nil , err )
153154}
154155
156+ type asyncRPCInfoReadTracker struct {
157+ mu sync.Mutex
158+ stops []chan struct {}
159+ dones []chan any
160+ }
161+
162+ func (r * asyncRPCInfoReadTracker ) middleware (next endpoint.Endpoint ) endpoint.Endpoint {
163+ return func (ctx context.Context , req , resp interface {}) error {
164+ stop := make (chan struct {})
165+ started := make (chan struct {})
166+ done := make (chan any , 1 )
167+
168+ r .mu .Lock ()
169+ r .stops = append (r .stops , stop )
170+ r .dones = append (r .dones , done )
171+ r .mu .Unlock ()
172+
173+ go readRPCInfoUntilStopped (ctx , stop , started , done )
174+ <- started
175+ return next (ctx , req , resp )
176+ }
177+ }
178+
179+ func (r * asyncRPCInfoReadTracker ) stopAndAssert (t * testing.T ) {
180+ t .Helper ()
181+
182+ r .mu .Lock ()
183+ stops := append ([]chan struct {}(nil ), r .stops ... )
184+ dones := append ([]chan any (nil ), r .dones ... )
185+ r .mu .Unlock ()
186+
187+ for _ , stop := range stops {
188+ close (stop )
189+ }
190+ for _ , done := range dones {
191+ if panicInfo := <- done ; panicInfo != nil {
192+ t .Fatalf ("async RPCInfo read panicked: %v" , panicInfo )
193+ }
194+ }
195+ }
196+
197+ func TestCallDisablePoolKeepsRPCInfoReadableAcrossLifecycle (t * testing.T ) {
198+ mockErr := errors .New ("mock" )
199+ testcases := []struct {
200+ name string
201+ options func (* asyncRPCInfoReadTracker ) []Option
202+ assertErr func (error )
203+ }{
204+ {
205+ name : "no retry success recycles" ,
206+ options : func (reader * asyncRPCInfoReadTracker ) []Option {
207+ return []Option {WithMiddleware (reader .middleware )}
208+ },
209+ assertErr : func (err error ) {
210+ test .Assert (t , err == nil , err )
211+ },
212+ },
213+ {
214+ name : "no retry error skips recycle" ,
215+ options : func (reader * asyncRPCInfoReadTracker ) []Option {
216+ errMW := func (next endpoint.Endpoint ) endpoint.Endpoint {
217+ return func (ctx context.Context , req , resp interface {}) error {
218+ return mockErr
219+ }
220+ }
221+ return []Option {WithMiddleware (reader .middleware ), WithMiddleware (errMW )}
222+ },
223+ assertErr : func (err error ) {
224+ test .Assert (t , errors .Is (err , mockErr ), err )
225+ },
226+ },
227+ {
228+ name : "failure retry configured without retry recycles" ,
229+ options : func (reader * asyncRPCInfoReadTracker ) []Option {
230+ return []Option {
231+ WithMiddleware (reader .middleware ),
232+ WithFailureRetry (& retry.FailurePolicy {
233+ StopPolicy : retry.StopPolicy {
234+ MaxRetryTimes : 1 ,
235+ CBPolicy : retry.CBPolicy {ErrorRate : 0.1 },
236+ },
237+ }),
238+ }
239+ },
240+ assertErr : func (err error ) {
241+ test .Assert (t , err == nil , err )
242+ },
243+ },
244+ {
245+ name : "failure retry actual retry skips recycle" ,
246+ options : func (reader * asyncRPCInfoReadTracker ) []Option {
247+ var callTimes int32
248+ errMW := func (next endpoint.Endpoint ) endpoint.Endpoint {
249+ return func (ctx context.Context , req , resp interface {}) error {
250+ if atomic .AddInt32 (& callTimes , 1 ) == 1 {
251+ return mockErr
252+ }
253+ return next (ctx , req , resp )
254+ }
255+ }
256+ return []Option {
257+ WithMiddleware (reader .middleware ),
258+ WithMiddleware (errMW ),
259+ WithFailureRetry (& retry.FailurePolicy {
260+ StopPolicy : retry.StopPolicy {
261+ MaxRetryTimes : 1 ,
262+ CBPolicy : retry.CBPolicy {ErrorRate : 0.1 },
263+ },
264+ ShouldResultRetry : & retry.ShouldResultRetry {
265+ ErrorRetry : func (err error , ri rpcinfo.RPCInfo ) bool {
266+ return errors .Is (err , mockErr )
267+ },
268+ },
269+ }),
270+ }
271+ },
272+ assertErr : func (err error ) {
273+ test .Assert (t , err == nil , err )
274+ },
275+ },
276+ {
277+ name : "backup retry skips recycle" ,
278+ options : func (reader * asyncRPCInfoReadTracker ) []Option {
279+ var callTimes int32
280+ slowFirstCallMW := func (next endpoint.Endpoint ) endpoint.Endpoint {
281+ return func (ctx context.Context , req , resp interface {}) error {
282+ if atomic .AddInt32 (& callTimes , 1 ) == 1 {
283+ time .Sleep (20 * time .Millisecond )
284+ }
285+ return next (ctx , req , resp )
286+ }
287+ }
288+ return []Option {
289+ WithMiddleware (reader .middleware ),
290+ WithMiddleware (slowFirstCallMW ),
291+ WithBackupRequest (retry .NewBackupPolicy (1 )),
292+ }
293+ },
294+ assertErr : func (err error ) {
295+ test .Assert (t , err == nil , err )
296+ },
297+ },
298+ {
299+ name : "mixed retry skips recycle" ,
300+ options : func (reader * asyncRPCInfoReadTracker ) []Option {
301+ var callTimes int32
302+ slowFirstCallMW := func (next endpoint.Endpoint ) endpoint.Endpoint {
303+ return func (ctx context.Context , req , resp interface {}) error {
304+ if atomic .AddInt32 (& callTimes , 1 ) == 1 {
305+ time .Sleep (20 * time .Millisecond )
306+ }
307+ return next (ctx , req , resp )
308+ }
309+ }
310+ return []Option {
311+ WithMiddleware (reader .middleware ),
312+ WithMiddleware (slowFirstCallMW ),
313+ WithMixedRetry (retry .NewMixedPolicy (1 )),
314+ }
315+ },
316+ assertErr : func (err error ) {
317+ test .Assert (t , err == nil , err )
318+ },
319+ },
320+ }
321+
322+ for _ , tc := range testcases {
323+ t .Run (tc .name , func (t * testing.T ) {
324+ ctrl := gomock .NewController (t )
325+ defer ctrl .Finish ()
326+
327+ reader := & asyncRPCInfoReadTracker {}
328+ cli := newMockClient (t , ctrl , tc .options (reader )... )
329+ err := cli .Call (context .Background (), mocks .MockMethod , mocks .NewMockArgs (), mocks .NewMockResult ())
330+ tc .assertErr (err )
331+ reader .stopAndAssert (t )
332+ })
333+ }
334+ }
335+
155336func TestCallWithContextBackup (t * testing.T ) {
156337 localsession .InitDefaultManager (localsession .DefaultManagerOptions ())
157338 d , dd := "d" , "dd"
0 commit comments