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:
43
internal/tracker/tracker.go
Normal file
43
internal/tracker/tracker.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Package tracker prevents duplicate processing of IMAP messages by
|
||||
// maintaining a transient set of active, in-flight UIDs.
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
// Tracker ensures unique concurrent processing of messages.
|
||||
// This prevents race conditions where a message might be fetched
|
||||
// again before its initial processing completes.
|
||||
type Tracker struct {
|
||||
mu sync.Mutex
|
||||
ids map[imap.UID]struct{}
|
||||
}
|
||||
|
||||
// New returns a ready-to-use Tracker.
|
||||
func New() *Tracker {
|
||||
return &Tracker{
|
||||
ids: make(map[imap.UID]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// TryAcquire attempts to claim exclusive processing rights for a UID.
|
||||
// It returns true only if the UID is not currently being tracked.
|
||||
func (t *Tracker) TryAcquire(uid imap.UID) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if _, exists := t.ids[uid]; exists {
|
||||
return false
|
||||
}
|
||||
t.ids[uid] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
// Release relinquishes processing rights for a UID, allowing it to be acquired again.
|
||||
func (t *Tracker) Release(uid imap.UID) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
delete(t.ids, uid)
|
||||
}
|
||||
94
internal/tracker/tracker_test.go
Normal file
94
internal/tracker/tracker_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tr := New()
|
||||
if tr == nil {
|
||||
t.Fatal("New() returned nil")
|
||||
}
|
||||
if tr.ids == nil {
|
||||
t.Fatal("New() initialized with nil map")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLifecycle(t *testing.T) {
|
||||
tr := New()
|
||||
uid := imap.UID(123)
|
||||
|
||||
// 1. First acquire should succeed.
|
||||
if !tr.TryAcquire(uid) {
|
||||
t.Fatalf(
|
||||
"expected TryAcquire(%d) to return true, got false",
|
||||
uid,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Second acquire on same UID should fail.
|
||||
if tr.TryAcquire(uid) {
|
||||
t.Fatalf(
|
||||
"expected TryAcquire(%d) to return false when already held, got true",
|
||||
uid,
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Different UID should succeed.
|
||||
otherUID := imap.UID(456)
|
||||
if !tr.TryAcquire(otherUID) {
|
||||
t.Fatalf(
|
||||
"expected TryAcquire(%d) to return true, got false",
|
||||
otherUID,
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Release first UID.
|
||||
tr.Release(uid)
|
||||
|
||||
// 5. First UID should be acquirable again.
|
||||
if !tr.TryAcquire(uid) {
|
||||
t.Fatalf(
|
||||
"expected TryAcquire(%d) to return true after Release, got false",
|
||||
uid,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrency(t *testing.T) {
|
||||
tr := New()
|
||||
uid := imap.UID(1)
|
||||
routines := 16
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Counter for successful acquisitions.
|
||||
successCount := 0
|
||||
var mu sync.Mutex
|
||||
|
||||
for range routines {
|
||||
wg.Go(func() {
|
||||
if tr.TryAcquire(uid) {
|
||||
mu.Lock()
|
||||
successCount++
|
||||
mu.Unlock()
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if successCount != 1 {
|
||||
t.Errorf(
|
||||
"expected exactly 1 successful acquisition for concurrent access to same UID, got %d",
|
||||
successCount,
|
||||
)
|
||||
}
|
||||
|
||||
// Verify we can release safely after concurrent hammer.
|
||||
tr.Release(uid)
|
||||
if !tr.TryAcquire(uid) {
|
||||
t.Error("failed to acquire UID after concurrent test release")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user