Skip to content

Commit 6c60759

Browse files
committed
switch to event based hedging queue handling
1 parent 1c4716c commit 6c60759

4 files changed

Lines changed: 316 additions & 120 deletions

File tree

pubsub/v2/GEMINI.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
DO NOT RUN any mutating git commands without permission
2+
This includes
3+
4+
- git add
5+
- git commit
6+
- git push
7+
- git pull

pubsub/v2/publisher.go

Lines changed: 133 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
108115
type 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

522536
func (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

Comments
 (0)