@@ -102,13 +102,28 @@ type Publisher struct {
102102
103103 hedgingMu sync.Mutex
104104 hedgingQueue []* hedgedRequest
105+ hedgingTimer * time.Timer
105106 hedgingStopped bool
106107}
107108
109+ type attemptResult struct {
110+ res * pb.PublishResponse
111+ err error
112+ id int
113+ }
114+
108115type hedgedRequest struct {
116+ attemptID int
109117 sendAfter time.Time
110- exec func ()
111- isDone func () bool
118+ resCh chan attemptResult
119+ cs * cancellationSharer
120+ ctx context.Context
121+ pbMsgs []* pb.PubsubMessage
122+ gaxOpts []gax.CallOption
123+ }
124+
125+ func (req * hedgedRequest ) isDone () bool {
126+ return req .cs .isDone ()
112127}
113128
114129// cancellationSharer coordinates cancellation between all publish attempts.
@@ -515,13 +530,16 @@ func (t *Publisher) initBundler() {
515530 if t .hedgingDelay == 0 {
516531 t .hedgingDelay = defaultHedgingDelay
517532 }
518- go t .runHedgingQueue ()
519533 }
520534}
521535
522536func (t * Publisher ) stopHedging () {
523537 t .hedgingMu .Lock ()
524538 t .hedgingStopped = true
539+ if t .hedgingTimer != nil {
540+ t .hedgingTimer .Stop ()
541+ t .hedgingTimer = nil
542+ }
525543 t .hedgingQueue = nil
526544 t .hedgingMu .Unlock ()
527545}
@@ -533,48 +551,95 @@ func (t *Publisher) enqueueHedgedRequest(req *hedgedRequest) {
533551 return
534552 }
535553 t .hedgingQueue = append (t .hedgingQueue , req )
554+ if len (t .hedgingQueue ) == 1 {
555+ delay := time .Until (req .sendAfter )
556+ if delay < 0 {
557+ delay = 0
558+ }
559+ t .hedgingTimer = time .AfterFunc (delay , t .processHedgingQueue )
560+ }
536561}
537562
538- func (t * Publisher ) runHedgingQueue () {
539- ticker := time .NewTicker (5 * time .Millisecond )
540- defer ticker .Stop ()
541- for range ticker .C {
542- t .hedgingMu .Lock ()
543- if t .hedgingStopped {
544- t .hedgingMu .Unlock ()
545- return
563+ func (t * Publisher ) processHedgingQueue () {
564+ t .hedgingMu .Lock ()
565+ if t .hedgingStopped {
566+ t .hedgingMu .Unlock ()
567+ return
568+ }
569+
570+ now := time .Now ()
571+ var ready []* hedgedRequest
572+ for len (t .hedgingQueue ) > 0 {
573+ head := t .hedgingQueue [0 ]
574+ if head .sendAfter .After (now ) {
575+ break
546576 }
547- now := time .Now ()
548- var ready []* hedgedRequest
549- var remaining []* hedgedRequest
550- for _ , req := range t .hedgingQueue {
551- if req .isDone != nil && req .isDone () {
552- // Request already completed or cancelled; discard without token usage.
553- continue
554- }
555- if ! now .Before (req .sendAfter ) {
556- ready = append (ready , req )
557- } else {
558- remaining = append (remaining , req )
559- }
577+ ready = append (ready , head )
578+ t .hedgingQueue = t .hedgingQueue [1 :]
579+ }
580+
581+ if len (t .hedgingQueue ) > 0 {
582+ nextDelay := time .Until (t .hedgingQueue [0 ].sendAfter )
583+ if nextDelay < 0 {
584+ nextDelay = 0
585+ }
586+ t .hedgingTimer = time .AfterFunc (nextDelay , t .processHedgingQueue )
587+ } else {
588+ t .hedgingTimer = nil
589+ }
590+ t .hedgingMu .Unlock ()
591+
592+ for _ , req := range ready {
593+ if req .isDone () {
594+ continue
595+ }
596+ t .hedgingMu .Lock ()
597+ hasToken := t .hedgingTokenBucket >= 1.0
598+ if hasToken {
599+ t .hedgingTokenBucket -= 1.0
560600 }
561- t .hedgingQueue = remaining
562601 t .hedgingMu .Unlock ()
563602
564- for _ , req := range ready {
565- if req .isDone != nil && req .isDone () {
566- continue
567- }
568- t .hedgingMu .Lock ()
569- hasToken := t .hedgingTokenBucket >= 1.0
570- if hasToken {
571- t .hedgingTokenBucket -= 1.0
572- }
573- t .hedgingMu .Unlock ()
603+ if hasToken {
604+ go t .fireHedgedAttempt (req )
605+ }
606+ }
607+ }
574608
575- if hasToken {
576- go req .exec ()
577- }
609+ func (t * Publisher ) fireHedgedAttempt (req * hedgedRequest ) {
610+ if req .isDone () || req .ctx .Err () != nil {
611+ return
612+ }
613+
614+ maxHedged := t .PublishSettings .HedgingSettings .MaxHedgedAttempts
615+ if maxHedged == 0 || req .attemptID < maxHedged {
616+ t .enqueueHedgedRequest (& hedgedRequest {
617+ attemptID : req .attemptID + 1 ,
618+ sendAfter : time .Now ().Add (t .hedgingDelay ),
619+ resCh : req .resCh ,
620+ cs : req .cs ,
621+ ctx : req .ctx ,
622+ pbMsgs : req .pbMsgs ,
623+ gaxOpts : req .gaxOpts ,
624+ })
625+ }
626+
627+ hedgedCtx , hedgedCancel := context .WithCancel (req .ctx )
628+ id := req .cs .add (hedgedCancel )
629+ if id == - 1 {
630+ return
631+ }
632+
633+ r , e := t .c .TopicAdminClient .Publish (hedgedCtx , & pb.PublishRequest {
634+ Topic : t .name ,
635+ Messages : req .pbMsgs ,
636+ }, req .gaxOpts ... )
637+
638+ if e == nil {
639+ select {
640+ case req .resCh <- attemptResult {res : r , err : e , id : id }:
641+ req .cs .win (id )
642+ default :
578643 }
579644 }
580645}
@@ -695,52 +760,21 @@ func (t *Publisher) publishMessageBundle(ctx context.Context, bms []*bundledMess
695760 }
696761
697762 if t .hedgingDelay > 0 && orderingKey == "" {
698- type attemptResult struct {
699- res * pb.PublishResponse
700- err error
701- isMain bool
702- id int
703- }
704763 cs := newCancellationSharer ()
705764 defer cs .cancelAll ()
706765
707766 resCh := make (chan attemptResult , 1 )
708767
709- var scheduleNextHedge func (int )
710- scheduleNextHedge = func (attemptNum int ) {
711- maxHedged := t .PublishSettings .HedgingSettings .MaxHedgedAttempts
712- if maxHedged > 0 && attemptNum > maxHedged {
713- return
714- }
715- t .enqueueHedgedRequest (& hedgedRequest {
716- sendAfter : time .Now ().Add (t .hedgingDelay ),
717- isDone : cs .isDone ,
718- exec : func () {
719- if cs .isDone () || ctx .Err () != nil {
720- return
721- }
722- // Schedule the next hedged attempt dynamically
723- scheduleNextHedge (attemptNum + 1 )
724-
725- hedgedCtx , hedgedCancel := context .WithCancel (ctx )
726- id := cs .add (hedgedCancel )
727- if id == - 1 {
728- return
729- }
730- r , e := t .c .TopicAdminClient .Publish (hedgedCtx , & pb.PublishRequest {
731- Topic : t .name ,
732- Messages : pbMsgs ,
733- }, gaxOpts ... )
734- select {
735- case resCh <- attemptResult {res : r , err : e , isMain : false , id : id }:
736- cs .win (id )
737- default :
738- }
739- },
740- })
768+ initialHedge := & hedgedRequest {
769+ attemptID : 1 ,
770+ sendAfter : time .Now ().Add (t .hedgingDelay ),
771+ resCh : resCh ,
772+ cs : cs ,
773+ ctx : ctx ,
774+ pbMsgs : pbMsgs ,
775+ gaxOpts : gaxOpts ,
741776 }
742-
743- scheduleNextHedge (1 )
777+ t .enqueueHedgedRequest (initialHedge )
744778
745779 mainCtx , mainCancel := context .WithCancel (ctx )
746780 mainID := cs .add (mainCancel )
@@ -749,17 +783,34 @@ func (t *Publisher) publishMessageBundle(ctx context.Context, bms []*bundledMess
749783 Messages : pbMsgs ,
750784 }, gaxOpts ... )
751785
786+ if e == nil {
787+ select {
788+ case resCh <- attemptResult {res : r , err : e , id : mainID }:
789+ cs .win (mainID )
790+ default :
791+ }
792+ }
793+
752794 select {
753795 case winner := <- resCh :
754796 res = winner .res
755797 err = winner .err
756- default :
757- cs .win (mainID )
758- res = r
759- err = e
760798 if err == nil {
761799 t .replenishHedgingTokens ()
762800 }
801+ default :
802+ // Main attempt failed or delayed; wait for a winning hedged attempt if one succeeds
803+ select {
804+ case winner := <- resCh :
805+ res = winner .res
806+ err = winner .err
807+ if err == nil {
808+ t .replenishHedgingTokens ()
809+ }
810+ default :
811+ res = r
812+ err = e
813+ }
763814 }
764815 } else {
765816 // regular publish without hedging
0 commit comments