Skip to content

Commit b4146f7

Browse files
committed
fix: fixed connection bug
1 parent 3e23e3b commit b4146f7

18 files changed

Lines changed: 1040 additions & 108 deletions

File tree

config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ type Config struct {
1010
// PollInterval is how often the delivery engine checks for pending deliveries.
1111
PollInterval time.Duration
1212

13+
// MaxPollInterval caps the delivery engine's idle backoff. When polls
14+
// come back empty the interval doubles from PollInterval up to this
15+
// value, so an idle relay does not hit the store every PollInterval.
16+
MaxPollInterval time.Duration
17+
1318
// BatchSize is the maximum number of deliveries dequeued per poll cycle.
1419
BatchSize int
1520

@@ -44,6 +49,7 @@ func DefaultConfig() Config {
4449
return Config{
4550
Concurrency: 10,
4651
PollInterval: 1 * time.Second,
52+
MaxPollInterval: 30 * time.Second,
4753
BatchSize: 50,
4854
RequestTimeout: 30 * time.Second,
4955
MaxRetries: 5,

delivery/engine.go

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ type DLQPusher interface {
3030

3131
// EngineConfig holds engine configuration.
3232
type 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.
90115
func (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

delivery/engine_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,81 @@ func TestEngineNilDLQ(t *testing.T) {
342342

343343
engine.Stop(ctx)
344344
}
345+
346+
// countingStore wraps an EngineStore and counts Dequeue calls.
347+
type countingStore struct {
348+
delivery.EngineStore
349+
dequeues atomic.Int32
350+
}
351+
352+
func (c *countingStore) Dequeue(ctx context.Context, limit int) ([]*delivery.Delivery, error) {
353+
c.dequeues.Add(1)
354+
return c.EngineStore.Dequeue(ctx, limit)
355+
}
356+
357+
func TestEngineIdleBackoff(t *testing.T) {
358+
cs := &countingStore{EngineStore: memory.New()}
359+
cfg := delivery.EngineConfig{
360+
Concurrency: 2,
361+
PollInterval: 10 * time.Millisecond,
362+
MaxPollInterval: 160 * time.Millisecond,
363+
BatchSize: 10,
364+
RequestTimeout: time.Second,
365+
}
366+
engine := delivery.NewEngine(cs, &stubDLQ{}, cfg, nil)
367+
368+
engine.Start(context.Background())
369+
time.Sleep(1 * time.Second)
370+
engine.Stop(context.Background())
371+
372+
// Fixed-rate polling at 10ms would issue ~100 dequeues in 1s. With
373+
// doubling backoff capped at 160ms the loop polls at 10, 20, 40, 80,
374+
// then every 160ms: roughly 10 calls. Allow generous slack.
375+
if n := cs.dequeues.Load(); n > 20 {
376+
t.Fatalf("expected backoff to limit idle dequeues to ~10, got %d", n)
377+
}
378+
}
379+
380+
func TestEngineWakeDeliversPromptly(t *testing.T) {
381+
var delivered atomic.Int32
382+
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
383+
delivered.Add(1)
384+
w.WriteHeader(http.StatusOK)
385+
})
386+
srv := httptest.NewServer(handler)
387+
defer srv.Close()
388+
389+
store := memory.New()
390+
cfg := delivery.EngineConfig{
391+
Concurrency: 2,
392+
PollInterval: 10 * time.Millisecond,
393+
MaxPollInterval: 5 * time.Second,
394+
BatchSize: 10,
395+
RequestTimeout: time.Second,
396+
RetrySchedule: []time.Duration{10 * time.Millisecond},
397+
}
398+
engine := delivery.NewEngine(store, &stubDLQ{}, cfg, nil)
399+
400+
ctx := context.Background()
401+
engine.Start(ctx)
402+
defer engine.Stop(ctx)
403+
404+
// Let the idle loop back off to a multi-second interval: polls land at
405+
// ~10, 30, 70, 150, 310, 630, 1270ms; after that the next poll is
406+
// seconds away.
407+
time.Sleep(1300 * time.Millisecond)
408+
409+
// New work arrives; Wake must reset the backoff and trigger an
410+
// immediate poll instead of waiting out the inflated interval.
411+
createTestData(t, store, srv.URL)
412+
engine.Wake()
413+
414+
deadline := time.After(700 * time.Millisecond)
415+
for delivered.Load() == 0 {
416+
select {
417+
case <-deadline:
418+
t.Fatal("delivery not processed promptly after Wake")
419+
case <-time.After(10 * time.Millisecond):
420+
}
421+
}
422+
}

extension/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ func (c Config) ToRelayOptions() []relay.Option {
5757
if c.PollInterval > 0 {
5858
opts = append(opts, relay.WithPollInterval(c.PollInterval))
5959
}
60+
if c.MaxPollInterval > 0 {
61+
opts = append(opts, relay.WithMaxPollInterval(c.MaxPollInterval))
62+
}
6063
if c.BatchSize > 0 {
6164
opts = append(opts, relay.WithBatchSize(c.BatchSize))
6265
}

extension/extension.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,9 @@ func (e *Extension) mergeWithDefaults(cfg Config) Config {
321321
if cfg.PollInterval == 0 {
322322
cfg.PollInterval = defaults.PollInterval
323323
}
324+
if cfg.MaxPollInterval == 0 {
325+
cfg.MaxPollInterval = defaults.MaxPollInterval
326+
}
324327
if cfg.BatchSize == 0 {
325328
cfg.BatchSize = defaults.BatchSize
326329
}
@@ -371,6 +374,9 @@ func (e *Extension) mergeConfigurations(yamlConfig, programmaticConfig Config) C
371374
if yamlConfig.PollInterval == 0 && programmaticConfig.PollInterval != 0 {
372375
yamlConfig.PollInterval = programmaticConfig.PollInterval
373376
}
377+
if yamlConfig.MaxPollInterval == 0 && programmaticConfig.MaxPollInterval != 0 {
378+
yamlConfig.MaxPollInterval = programmaticConfig.MaxPollInterval
379+
}
374380
if yamlConfig.BatchSize == 0 && programmaticConfig.BatchSize != 0 {
375381
yamlConfig.BatchSize = programmaticConfig.BatchSize
376382
}

0 commit comments

Comments
 (0)