The previous dedupe map rotation logic completely cleared the map when it reached max size, causing an 'amnesia cliff' where immediately arriving duplicates of just-forgotten messages would be processed. This change replaces that with a MessageDeduplicator struct that uses a circular queue (ring buffer) to track insertions. When the limit is reached, it only evicts the absolute oldest message from the map, completely resolving the cliff issue. This also cleans up the WeCom Bot and App webhook handlers by encapsulating the mutex and map state.
83 lines
1.9 KiB
Go
83 lines
1.9 KiB
Go
package channels
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestMessageDeduplicator_DuplicateDetection(t *testing.T) {
|
|
d := NewMessageDeduplicator(wecomMaxProcessedMessages)
|
|
|
|
if ok := d.MarkMessageProcessed("msg-1"); !ok {
|
|
t.Fatalf("first message should be accepted")
|
|
}
|
|
|
|
if ok := d.MarkMessageProcessed("msg-1"); ok {
|
|
t.Fatalf("duplicate message should be rejected")
|
|
}
|
|
}
|
|
|
|
func TestMessageDeduplicator_ConcurrentSameMessage(t *testing.T) {
|
|
d := NewMessageDeduplicator(wecomMaxProcessedMessages)
|
|
|
|
const goroutines = 64
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
|
|
results := make(chan bool, goroutines)
|
|
for i := 0; i < goroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
results <- d.MarkMessageProcessed("msg-concurrent")
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
close(results)
|
|
|
|
successes := 0
|
|
for ok := range results {
|
|
if ok {
|
|
successes++
|
|
}
|
|
}
|
|
|
|
if successes != 1 {
|
|
t.Fatalf("expected exactly 1 successful mark, got %d", successes)
|
|
}
|
|
}
|
|
|
|
func TestMessageDeduplicator_CircularQueueEviction(t *testing.T) {
|
|
// Create a deduplicator with a very small capacity to test eviction easily
|
|
capacity := 3
|
|
d := NewMessageDeduplicator(capacity)
|
|
|
|
// Fill the queue
|
|
d.MarkMessageProcessed("msg-1")
|
|
d.MarkMessageProcessed("msg-2")
|
|
d.MarkMessageProcessed("msg-3")
|
|
|
|
// At this point, the queue is full. msg-1 is the oldest.
|
|
if len(d.msgs) != 3 {
|
|
t.Fatalf("expected map size to be 3, got %d", len(d.msgs))
|
|
}
|
|
|
|
// This should evict msg-1 and add msg-4
|
|
if ok := d.MarkMessageProcessed("msg-4"); !ok {
|
|
t.Fatalf("msg-4 should be accepted")
|
|
}
|
|
|
|
if len(d.msgs) != 3 {
|
|
t.Fatalf("expected map size to remain at max capacity (3), got %d", len(d.msgs))
|
|
}
|
|
|
|
// msg-1 should now be forgotten (evicted)
|
|
if ok := d.MarkMessageProcessed("msg-1"); !ok {
|
|
t.Fatalf("msg-1 should be accepted again because it was evicted")
|
|
}
|
|
|
|
// msg-2 should have been evicted when we added msg-1 back
|
|
if ok := d.MarkMessageProcessed("msg-2"); !ok {
|
|
t.Fatalf("msg-2 should be accepted again because it was evicted")
|
|
}
|
|
}
|