Add intake worker
Subsystem to monitor IMAP mailbox for new messages. Introduces: - intake: worker that uses IDLE or polling to detect new emails. - imap: client wrapper for connection management and IMAP commands. - filter: logic for IMAP search and sender allow-list. - tracker: concurrency control to prevent processing the same UID twice. - backoff: for handling connection retries with jitter.
This commit is contained in:
94
internal/backoff/backoff.go
Normal file
94
internal/backoff/backoff.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Package backoff implements exponential backoff with jitter for retry logic.
|
||||
package backoff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config holds the configuration for a Backoff instance.
|
||||
type Config struct {
|
||||
// Initial is the starting upper bound for the backoff duration.
|
||||
// Must be > 0.
|
||||
Initial time.Duration `yaml:"initial"`
|
||||
// Max is the absolute maximum upper bound for the backoff duration.
|
||||
// Must be >= Initial.
|
||||
Max time.Duration `yaml:"max"`
|
||||
// RNG is an optional source of randomness.
|
||||
// If nil, the global math/rand/v2 source is used.
|
||||
RNG *rand.Rand `yaml:"-"`
|
||||
}
|
||||
|
||||
// Validate checks that the configuration values are sensible.
|
||||
func (cfg Config) Validate() error {
|
||||
if cfg.Initial <= 0 {
|
||||
return fmt.Errorf("invalid initial backoff: %v", cfg.Initial)
|
||||
}
|
||||
if cfg.Max < cfg.Initial {
|
||||
return fmt.Errorf("invalid max duration: %v", cfg.Max)
|
||||
}
|
||||
// MaxInt64 will overflow when calculating jitter.
|
||||
if cfg.Max == math.MaxInt64 {
|
||||
return fmt.Errorf("max duration cannot be MaxInt64")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Backoff implements exponential backoff with full jitter.
|
||||
// It is NOT safe for concurrent use.
|
||||
type Backoff struct {
|
||||
initial time.Duration
|
||||
max time.Duration
|
||||
current time.Duration
|
||||
rng *rand.Rand
|
||||
}
|
||||
|
||||
// New creates a new Backoff instance with the provided configuration.
|
||||
func New(cfg Config) (*Backoff, error) {
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %v", err)
|
||||
}
|
||||
|
||||
return &Backoff{
|
||||
initial: cfg.Initial,
|
||||
max: cfg.Max,
|
||||
current: cfg.Initial,
|
||||
rng: cfg.RNG,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Next returns the next backoff delay.
|
||||
func (b *Backoff) Next() time.Duration {
|
||||
limit := b.current
|
||||
|
||||
// Update state for the next call.
|
||||
if b.current >= b.max {
|
||||
b.current = b.max
|
||||
} else if b.current > b.max/2 {
|
||||
// If doubling would exceed max, just clamp to max.
|
||||
b.current = b.max
|
||||
} else {
|
||||
b.current *= 2
|
||||
}
|
||||
|
||||
// Calculate jitter; return random in [0, limit].
|
||||
// Int64N(n) returns values in [0, n).
|
||||
// For [0, limit], we use limit + 1.
|
||||
var jitter int64
|
||||
if b.rng != nil {
|
||||
jitter = b.rng.Int64N(int64(limit) + 1)
|
||||
} else {
|
||||
jitter = rand.Int64N(int64(limit) + 1)
|
||||
}
|
||||
|
||||
return time.Duration(jitter)
|
||||
}
|
||||
|
||||
// Reset resets the current backoff cap to the initial value.
|
||||
// This should be called after a successful operation to restart
|
||||
// the backoff sequence for future retries.
|
||||
func (b *Backoff) Reset() {
|
||||
b.current = b.initial
|
||||
}
|
||||
Reference in New Issue
Block a user