@@ -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,99 @@ 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+ // todo: using options in request
519+ if c .ClientOptions .SenseContextCancel && ctx .Done () != nil {
520+ doChan := make (chan doResult )
521+ go func () {
522+ canRetry , dErr := c .readResp (cc , inPool , resetConnection , req , resp , deadline )
523+ doChan <- doResult {
524+ canIdempotentRetry : canRetry ,
525+ err : dErr ,
526+ }
527+ }()
528+ select {
529+ case <- ctx .Done ():
530+ // Close connection directly and wait for readResp releasing connection resource
531+ cc .c .Close ()
532+ // wait for goroutine exited
533+ // since the lifecycle of context has finished,
534+ // there is no need to retry
535+ <- doChan
536+ return false , ctx .Err ()
537+ case res := <- doChan :
538+ return res .canIdempotentRetry , res .err
539+ }
540+ }
541+
542+ canIdempotentRetry , err = c .readResp (cc , inPool , resetConnection , req , resp , deadline )
543+
472544 return canIdempotentRetry , err
473545}
474546
547+ type doResult struct {
548+ canIdempotentRetry bool
549+ err error
550+ }
551+
475552func minTimeout (d0 , d1 time.Duration ) time.Duration {
476553 if d0 <= 0 {
477554 if d1 <= 0 {
@@ -516,7 +593,7 @@ func calcTimeout(deadline time.Time, timeout time.Duration) time.Duration {
516593 return timeout
517594}
518595
519- func (c * HostClient ) doNonNilReqResp (req * protocol.Request , resp * protocol.Response ) ( bool , error ) {
596+ func (c * HostClient ) checkAndConfigureReqResp (req * protocol.Request , resp * protocol.Response ) {
520597 if req == nil {
521598 panic ("BUG: req cannot be nil" )
522599 }
@@ -536,46 +613,16 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
536613 if c .DisablePathNormalizing {
537614 req .URI ().DisablePathNormalizing = true
538615 }
616+ }
539617
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- }
618+ func (c * HostClient ) writeReq (cc * clientConn , connInPool bool , req * protocol.Request , resp * protocol.Response , deadline time.Time ) (
619+ canIdempotentRetry bool ,
620+ resetConnection bool ,
621+ shouldFinish bool ,
622+ err error ,
623+ ) {
556624 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- }
625+ o := req .Options ()
579626
580627 usingProxy := false
581628 if c .ProxyURI != nil && bytes .Equal (req .Scheme (), bytestr .StrHTTP ) {
@@ -584,18 +631,16 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
584631 }
585632
586633 // write starts
587-
588- timeout = calcTimeout (deadline , minTimeout (c .WriteTimeout , o .WriteTimeout ()))
634+ timeout := calcTimeout (deadline , minTimeout (c .WriteTimeout , o .WriteTimeout ()))
589635 if timeout < 0 {
590636 c .closeConn (cc )
591- return false , errTimeout
637+ return false , resetConnection , true , errTimeout
592638 }
593639 if err = conn .SetWriteTimeout (timeout ); err != nil {
594640 c .closeConn (cc )
595- return true , err
641+ return true , resetConnection , true , err
596642 }
597643
598- resetConnection := false
599644 if c .MaxConnDuration > 0 && time .Since (cc .createdTime ) > c .MaxConnDuration && ! req .ConnectionClose () {
600645 req .SetConnectionClose ()
601646 resetConnection = true
@@ -625,7 +670,7 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
625670 err = errNorm .ToHertzError (err )
626671 }
627672 if ! errors .Is (err , errs .ErrConnectionClosed ) {
628- return true , err
673+ return true , resetConnection , true , err
629674 }
630675
631676 // introduced by https://github.com/cloudwego/hertz/pull/412
@@ -635,27 +680,34 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
635680 // NOTE: can't use deadline since it likely already exceeded deadline when write
636681 timeout = minTimeout (50 * time .Millisecond , minTimeout (c .ReadTimeout , o .ReadTimeout ()))
637682 if conn .SetReadTimeout (timeout ) != nil {
638- return true , err
683+ return true , resetConnection , true , err
639684 }
640685 zr := c .acquireReader (conn )
641686 defer zr .Release ()
642687 if respI .ReadHeaderAndLimitBody (resp , zr , c .MaxResponseBodySize ) == nil {
643688 if code := resp .StatusCode (); code >= 400 && code < 600 {
644689 // strictly for 4xx only, but 5xx is also acceptable.
645690 // both can be considered better response rather than write err
646- return false , nil
691+ return false , resetConnection , true , nil
647692 }
648693 }
649694
650- if inPool {
695+ if connInPool {
651696 err = errs .ErrBadPoolConn
652697 }
653- return true , err
698+ return true , resetConnection , true , err
654699 }
700+ return false , resetConnection , false , nil
701+ }
655702
656- // read starts
703+ func (c * HostClient ) readResp (cc * clientConn , connInPool , resetConnection bool , req * protocol.Request , resp * protocol.Response , deadline time.Time ) (bool , error ) {
704+ var err error
705+ customSkipBody := resp .SkipBody
706+ conn := cc .c
707+ o := req .Options ()
657708
658- timeout = calcTimeout (deadline , minTimeout (c .ReadTimeout , o .ReadTimeout ()))
709+ // read starts
710+ timeout := calcTimeout (deadline , minTimeout (c .ReadTimeout , o .ReadTimeout ()))
659711 if timeout < 0 {
660712 c .closeConn (cc )
661713 return false , errTimeout
@@ -683,7 +735,7 @@ func (c *HostClient) doNonNilReqResp(req *protocol.Request, resp *protocol.Respo
683735 if err != nil {
684736 zr .Release () //nolint:errcheck
685737 c .closeConn (cc )
686- if inPool && (err == io .EOF || err == syscall .ECONNRESET ) {
738+ if connInPool && (err == io .EOF || err == syscall .ECONNRESET ) {
687739 return true , errs .ErrBadPoolConn
688740 }
689741 // if this is not a pooled connection,
@@ -1454,4 +1506,8 @@ type ClientOptions struct {
14541506
14551507 // StateObserve execution interval
14561508 ObservationInterval time.Duration
1509+
1510+ // Whether to let the ctx passed into Do control the lifecycle of the request,
1511+ // so that when the ctx is canceled, the corresponding request will also be finished
1512+ SenseContextCancel bool
14571513}
0 commit comments