@@ -30,13 +30,17 @@ type DLQPusher interface {
3030
3131// EngineConfig holds engine configuration.
3232type EngineConfig struct {
33- Concurrency int
34- PollInterval time.Duration
35- BatchSize int
36- RequestTimeout time.Duration
37- RetrySchedule []time.Duration
38- Metrics * observability.Metrics
39- Tracer * observability.Tracer
33+ Concurrency int
34+ PollInterval time.Duration
35+ // MaxPollInterval caps the idle backoff. When polls come back empty the
36+ // poll interval doubles from PollInterval up to this value, so an idle
37+ // engine stops hammering the store every PollInterval. Defaults to 30s.
38+ MaxPollInterval time.Duration
39+ BatchSize int
40+ RequestTimeout time.Duration
41+ RetrySchedule []time.Duration
42+ Metrics * observability.Metrics
43+ Tracer * observability.Tracer
4044}
4145
4246// Engine is the delivery worker pool that dequeues and processes deliveries.
@@ -48,6 +52,7 @@ type Engine struct {
4852 config EngineConfig
4953 logger log.Logger
5054
55+ wakeCh chan struct {}
5156 cancel context.CancelFunc
5257 wg sync.WaitGroup
5358}
@@ -57,13 +62,20 @@ func NewEngine(store EngineStore, dlq DLQPusher, cfg EngineConfig, logger log.Lo
5762 if logger == nil {
5863 logger = log .NewNoopLogger ()
5964 }
65+ if cfg .MaxPollInterval <= 0 {
66+ cfg .MaxPollInterval = 30 * time .Second
67+ }
68+ if cfg .MaxPollInterval < cfg .PollInterval {
69+ cfg .MaxPollInterval = cfg .PollInterval
70+ }
6071 return & Engine {
6172 store : store ,
6273 sender : NewSender (cfg .RequestTimeout ),
6374 retrier : NewRetrier (cfg .RetrySchedule ),
6475 dlq : dlq ,
6576 config : cfg ,
6677 logger : logger ,
78+ wakeCh : make (chan struct {}, 1 ),
6779 }
6880}
6981
@@ -86,39 +98,72 @@ func (e *Engine) Stop(_ context.Context) {
8698 e .wg .Wait ()
8799}
88100
89- // pollLoop periodically dequeues pending deliveries and dispatches them to workers.
101+ // Wake nudges the poll loop to check for pending deliveries immediately,
102+ // resetting any idle backoff. It is non-blocking and safe to call from any
103+ // goroutine, including before Start.
104+ func (e * Engine ) Wake () {
105+ select {
106+ case e .wakeCh <- struct {}{}:
107+ default :
108+ }
109+ }
110+
111+ // pollLoop dequeues pending deliveries and dispatches them to workers. Empty
112+ // polls double the wait up to MaxPollInterval so an idle engine doesn't issue
113+ // a dequeue (an UPDATE/findAndModify against the store) every PollInterval;
114+ // any dequeued work or a Wake call resets the cadence to PollInterval.
90115func (e * Engine ) pollLoop (ctx context.Context ) {
91- ticker := time .NewTicker (e .config .PollInterval )
92- defer ticker .Stop ()
116+ interval := e .config .PollInterval
117+ timer := time .NewTimer (interval )
118+ defer timer .Stop ()
93119
94120 sem := make (chan struct {}, e .config .Concurrency )
95121
96122 for {
97123 select {
98124 case <- ctx .Done ():
99125 return
100- case <- ticker .C :
101- batch , err := e .store .Dequeue (ctx , e .config .BatchSize )
102- if err != nil {
103- e .logger .Error ("dequeue failed" , log .Any ("error" , err ))
104- continue
105- }
106-
107- for _ , d := range batch {
126+ case <- e .wakeCh :
127+ interval = e .config .PollInterval
128+ if ! timer .Stop () {
108129 select {
109- case <- ctx .Done ():
110- return
111- case sem <- struct {}{}:
130+ case <- timer .C :
131+ default :
112132 }
133+ }
134+ case <- timer .C :
135+ }
136+
137+ batch , err := e .store .Dequeue (ctx , e .config .BatchSize )
138+ if err != nil {
139+ e .logger .Error ("dequeue failed" , log .Any ("error" , err ))
140+ }
113141
114- e .wg .Add (1 )
115- go func (del * Delivery ) {
116- defer e .wg .Done ()
117- defer func () { <- sem }()
118- e .process (ctx , del )
119- }(d )
142+ if len (batch ) > 0 {
143+ interval = e .config .PollInterval
144+ } else {
145+ // Back off on empty polls and on errors alike; errors hot-
146+ // spinning at PollInterval would only pile onto a struggling
147+ // store.
148+ interval = min (interval * 2 , e .config .MaxPollInterval )
149+ }
150+
151+ for _ , d := range batch {
152+ select {
153+ case <- ctx .Done ():
154+ return
155+ case sem <- struct {}{}:
120156 }
157+
158+ e .wg .Add (1 )
159+ go func (del * Delivery ) {
160+ defer e .wg .Done ()
161+ defer func () { <- sem }()
162+ e .process (ctx , del )
163+ }(d )
121164 }
165+
166+ timer .Reset (interval )
122167 }
123168}
124169
0 commit comments