@@ -398,7 +398,7 @@ func (c *HostClient) Do(ctx context.Context, req *protocol.Request, resp *protoc
398398 default :
399399 }
400400
401- canIdempotentRetry , err = c .do (req , resp )
401+ canIdempotentRetry , err = c .do (ctx , req , resp )
402402 // If there is no custom retry and err is equal to nil, the loop simply exits.
403403 if err == nil && isDefaultRetryFunc {
404404 if connAttempts != 0 {
@@ -456,22 +456,98 @@ func (c *HostClient) PendingRequests() int {
456456 return int (atomic .LoadInt32 (& c .pendingRequests ))
457457}
458458
459- func (c * HostClient ) do (req * protocol.Request , resp * protocol.Response ) (bool , error ) {
459+ func (c * HostClient ) do (ctx context. Context , req * protocol.Request , resp * protocol.Response ) (bool , error ) {
460460 nilResp := false
461461 if resp == nil {
462462 nilResp = true
463463 resp = protocol .AcquireResponse ()
464464 }
465+ defer func () {
466+ if nilResp {
467+ protocol .ReleaseResponse (resp )
468+ }
469+ }()
465470
466- canIdempotentRetry , err := c . doNonNilReqResp (req , resp )
471+ c . checkAndConfigureReqResp (req , resp )
467472
468- if nilResp {
469- protocol .ReleaseResponse (resp )
473+ // whole request timeout
474+ o := req .Options ()
475+ deadline := time.Time {}
476+ if v := o .RequestTimeout (); v > 0 {
477+ deadline = o .StartTime ().Add (v )
470478 }
471479
480+ // dial starts
481+ timeout := calcTimeout (deadline , minTimeout (c .DialTimeout , o .DialTimeout ()))
482+ if timeout < 0 {
483+ return false , errTimeout
484+ }
485+ cc , inPool , err := c .acquireConn (timeout )
486+ if err != nil {
487+ return false , err
488+ }
489+ conn := cc .c
490+ resp .ParseNetAddr (conn )
491+ // force handshake using dial timeout
492+ if c .IsTLS && timeout > 0 {
493+ // NOTE: Handshake() here is optional as Write would tirigger handshake
494+ // but for tls handshake, it writes and reads, and we need to set deadline for that.
495+ tlsconn , ok := conn .(network.ConnTLSer )
496+ if ok {
497+ // currently netpoll doesn't support conn.SetDeadline nor tls, but crypto/tls.Conn does.
498+ // in case netpoll supports tls in the future, may need to change this to
499+ // call both conn.SetReadTimeout, and conn.SetWriteTimeout
500+ err = conn .SetDeadline (time .Now ().Add (timeout ))
501+ if err == nil {
502+ err = tlsconn .Handshake ()
503+ // NOTE: no need conn.SetDeadline(time.Time{})?
504+ // we always reset before Write and Read
505+ }
506+ if err != nil {
507+ c .closeConn (cc )
508+ return true , err
509+ }
510+ }
511+ }
512+
513+ // write req
514+ canIdempotentRetry , resetConnection , shouldFinish , err := c .writeReq (cc , inPool , req , resp , deadline )
515+ if shouldFinish {
516+ return canIdempotentRetry , err
517+ }
518+ if (c .ClientOptions .SenseContextCancel || o .SenseContextCancel ()) && ctx .Done () != nil {
519+ doChan := make (chan doResult )
520+ go func () {
521+ canRetry , dErr := c .readResp (cc , inPool , resetConnection , req , resp , deadline )
522+ doChan <- doResult {
523+ canIdempotentRetry : canRetry ,
524+ err : dErr ,
525+ }
526+ }()
527+ select {
528+ case <- ctx .Done ():
529+ // Close connection directly and wait for readResp releasing connection resource
530+ cc .c .Close ()
531+ // wait for goroutine exited
532+ // since the lifecycle of context has finished,
533+ // there is no need to retry
534+ <- doChan
535+ return false , ctx .Err ()
536+ case res := <- doChan :
537+ return res .canIdempotentRetry , res .err
538+ }
539+ }
540+
541+ canIdempotentRetry , err = c .readResp (cc , inPool , resetConnection , req , resp , deadline )
542+
472543 return canIdempotentRetry , err
473544}
474545
546+ type doResult struct {
547+ canIdempotentRetry bool
548+ err error
549+ }
550+
475551func minTimeout (d0 , d1 time.Duration ) time.Duration {
476552 if d0 <= 0 {
477553 if d1 <= 0 {
@@ -516,7 +592,7 @@ func calcTimeout(deadline time.Time, timeout time.Duration) time.Duration {
516592 return timeout
517593}
518594
519- func (c * HostClient ) doNonNilReqResp (req * protocol.Request , resp * protocol.Response ) ( bool , error ) {
595+ func (c * HostClient ) checkAndConfigureReqResp (req * protocol.Request , resp * protocol.Response ) {
520596 if req == nil {
521597 panic ("BUG: req cannot be nil" )
522598 }
@@ -536,46 +612,16 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
536612 if c .DisablePathNormalizing {
537613 req .URI ().DisablePathNormalizing = true
538614 }
615+ }
539616
540- o := req .Options ()
541- deadline := time.Time {}
542- if v := o .RequestTimeout (); v > 0 {
543- deadline = o .StartTime ().Add (v )
544- }
545-
546- // dial starts
547-
548- timeout := calcTimeout (deadline , minTimeout (c .DialTimeout , o .DialTimeout ()))
549- if timeout < 0 {
550- return false , errTimeout
551- }
552- cc , inPool , err := c .acquireConn (timeout )
553- if err != nil {
554- return false , err
555- }
617+ func (c * HostClient ) writeReq (cc * clientConn , connInPool bool , req * protocol.Request , resp * protocol.Response , deadline time.Time ) (
618+ canIdempotentRetry bool ,
619+ resetConnection bool ,
620+ shouldFinish bool ,
621+ err error ,
622+ ) {
556623 conn := cc .c
557- resp .ParseNetAddr (conn )
558-
559- if c .IsTLS && timeout > 0 { // force handshake using dial timeout
560- // NOTE: Handshake() here is optional as Write would tirigger handshake
561- // but for tls handshake, it writes and reads, and we need to set deadline for that.
562- tlsconn , ok := conn .(network.ConnTLSer )
563- if ok {
564- // currently netpoll doesn't support conn.SetDeadline nor tls, but crypto/tls.Conn does.
565- // in case netpoll supports tls in the future, may need to change this to
566- // call both conn.SetReadTimeout, and conn.SetWriteTimeout
567- err := conn .SetDeadline (time .Now ().Add (timeout ))
568- if err == nil {
569- err = tlsconn .Handshake ()
570- // NOTE: no need conn.SetDeadline(time.Time{})?
571- // we always reset before Write and Read
572- }
573- if err != nil {
574- c .closeConn (cc )
575- return true , err
576- }
577- }
578- }
624+ o := req .Options ()
579625
580626 usingProxy := false
581627 if c .ProxyURI != nil && bytes .Equal (req .Scheme (), bytestr .StrHTTP ) {
@@ -584,18 +630,16 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
584630 }
585631
586632 // write starts
587-
588- timeout = calcTimeout (deadline , minTimeout (c .WriteTimeout , o .WriteTimeout ()))
633+ timeout := calcTimeout (deadline , minTimeout (c .WriteTimeout , o .WriteTimeout ()))
589634 if timeout < 0 {
590635 c .closeConn (cc )
591- return false , errTimeout
636+ return false , resetConnection , true , errTimeout
592637 }
593638 if err = conn .SetWriteTimeout (timeout ); err != nil {
594639 c .closeConn (cc )
595- return true , err
640+ return true , resetConnection , true , err
596641 }
597642
598- resetConnection := false
599643 if c .MaxConnDuration > 0 && time .Since (cc .createdTime ) > c .MaxConnDuration && ! req .ConnectionClose () {
600644 req .SetConnectionClose ()
601645 resetConnection = true
@@ -625,7 +669,7 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
625669 err = errNorm .ToHertzError (err )
626670 }
627671 if ! errors .Is (err , errs .ErrConnectionClosed ) {
628- return true , err
672+ return true , resetConnection , true , err
629673 }
630674
631675 // introduced by https://github.com/cloudwego/hertz/pull/412
@@ -635,27 +679,34 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
635679 // NOTE: can't use deadline since it likely already exceeded deadline when write
636680 timeout = minTimeout (50 * time .Millisecond , minTimeout (c .ReadTimeout , o .ReadTimeout ()))
637681 if conn .SetReadTimeout (timeout ) != nil {
638- return true , err
682+ return true , resetConnection , true , err
639683 }
640684 zr := c .acquireReader (conn )
641685 defer zr .Release ()
642686 if respI .ReadHeaderAndLimitBody (resp , zr , c .MaxResponseBodySize ) == nil {
643687 if code := resp .StatusCode (); code >= 400 && code < 600 {
644688 // strictly for 4xx only, but 5xx is also acceptable.
645689 // both can be considered better response rather than write err
646- return false , nil
690+ return false , resetConnection , true , nil
647691 }
648692 }
649693
650- if inPool {
694+ if connInPool {
651695 err = errs .ErrBadPoolConn
652696 }
653- return true , err
697+ return true , resetConnection , true , err
654698 }
699+ return false , resetConnection , false , nil
700+ }
655701
656- // read starts
702+ func (c * HostClient ) readResp (cc * clientConn , connInPool , resetConnection bool , req * protocol.Request , resp * protocol.Response , deadline time.Time ) (bool , error ) {
703+ var err error
704+ customSkipBody := resp .SkipBody
705+ conn := cc .c
706+ o := req .Options ()
657707
658- timeout = calcTimeout (deadline , minTimeout (c .ReadTimeout , o .ReadTimeout ()))
708+ // read starts
709+ timeout := calcTimeout (deadline , minTimeout (c .ReadTimeout , o .ReadTimeout ()))
659710 if timeout < 0 {
660711 c .closeConn (cc )
661712 return false , errTimeout
@@ -683,7 +734,7 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
683734 if err != nil {
684735 zr .Release () //nolint:errcheck
685736 c .closeConn (cc )
686- if inPool && (err == io .EOF || err == syscall .ECONNRESET ) {
737+ if connInPool && (err == io .EOF || err == syscall .ECONNRESET ) {
687738 return true , errs .ErrBadPoolConn
688739 }
689740 // if this is not a pooled connection,
@@ -1454,4 +1505,8 @@ type ClientOptions struct {
14541505
14551506 // StateObserve execution interval
14561507 ObservationInterval time.Duration
1508+
1509+ // Whether to let the ctx passed into Do control the lifecycle of the request,
1510+ // so that when the ctx is canceled, the corresponding request will also be finished
1511+ SenseContextCancel bool
14571512}
0 commit comments