chrislu
7 months ago
2 changed files with 205 additions and 0 deletions
-
120weed/mq/sub_coordinator/inflight_message_tracker.go
-
85weed/mq/sub_coordinator/inflight_message_tracker_test.go
@ -0,0 +1,120 @@ |
|||||
|
package sub_coordinator |
||||
|
|
||||
|
import ( |
||||
|
"sort" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
type InflightMessageTracker struct { |
||||
|
messages map[string]int64 |
||||
|
mu sync.Mutex |
||||
|
timestamps *RingBuffer |
||||
|
} |
||||
|
|
||||
|
func NewInflightMessageTracker(capacity int) *InflightMessageTracker { |
||||
|
return &InflightMessageTracker{ |
||||
|
messages: make(map[string]int64), |
||||
|
timestamps: NewRingBuffer(capacity), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// InflightMessage tracks the message with the key and timestamp.
|
||||
|
// These messages are sent to the consumer group instances and waiting for ack.
|
||||
|
func (imt *InflightMessageTracker) InflightMessage(key []byte, tsNs int64) { |
||||
|
imt.mu.Lock() |
||||
|
defer imt.mu.Unlock() |
||||
|
imt.messages[string(key)] = tsNs |
||||
|
imt.timestamps.Add(tsNs) |
||||
|
} |
||||
|
// IsMessageAcknowledged returns true if the message has been acknowledged.
|
||||
|
// If the message is older than the oldest inflight messages, returns false.
|
||||
|
// returns false if the message is inflight.
|
||||
|
// Otherwise, returns false if the message is old and can be ignored.
|
||||
|
func (imt *InflightMessageTracker) IsMessageAcknowledged(key []byte, tsNs int64) bool { |
||||
|
imt.mu.Lock() |
||||
|
defer imt.mu.Unlock() |
||||
|
|
||||
|
if tsNs < imt.timestamps.Oldest() { |
||||
|
return true |
||||
|
} |
||||
|
if tsNs > imt.timestamps.Latest() { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if _, found := imt.messages[string(key)]; found { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
// AcknowledgeMessage acknowledges the message with the key and timestamp.
|
||||
|
func (imt *InflightMessageTracker) AcknowledgeMessage(key []byte, tsNs int64) bool { |
||||
|
imt.mu.Lock() |
||||
|
defer imt.mu.Unlock() |
||||
|
timestamp, exists := imt.messages[string(key)] |
||||
|
if !exists || timestamp != tsNs { |
||||
|
return false |
||||
|
} |
||||
|
delete(imt.messages, string(key)) |
||||
|
// Remove the specific timestamp from the ring buffer.
|
||||
|
imt.timestamps.Remove(tsNs) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// RingBuffer represents a circular buffer to hold timestamps.
|
||||
|
type RingBuffer struct { |
||||
|
buffer []int64 |
||||
|
head int |
||||
|
size int |
||||
|
} |
||||
|
// NewRingBuffer creates a new RingBuffer of the given capacity.
|
||||
|
func NewRingBuffer(capacity int) *RingBuffer { |
||||
|
return &RingBuffer{ |
||||
|
buffer: make([]int64, capacity), |
||||
|
} |
||||
|
} |
||||
|
// Add adds a new timestamp to the ring buffer.
|
||||
|
func (rb *RingBuffer) Add(timestamp int64) { |
||||
|
rb.buffer[rb.head] = timestamp |
||||
|
rb.head = (rb.head + 1) % len(rb.buffer) |
||||
|
if rb.size < len(rb.buffer) { |
||||
|
rb.size++ |
||||
|
} |
||||
|
} |
||||
|
// Remove removes the specified timestamp from the ring buffer.
|
||||
|
func (rb *RingBuffer) Remove(timestamp int64) { |
||||
|
// Perform binary search
|
||||
|
index := sort.Search(rb.size, func(i int) bool { |
||||
|
return rb.buffer[(rb.head+len(rb.buffer)-rb.size+i)%len(rb.buffer)] >= timestamp |
||||
|
}) |
||||
|
actualIndex := (rb.head + len(rb.buffer) - rb.size + index) % len(rb.buffer) |
||||
|
|
||||
|
if index < rb.size && rb.buffer[actualIndex] == timestamp { |
||||
|
// Shift elements to maintain the buffer order
|
||||
|
for i := index; i < rb.size-1; i++ { |
||||
|
fromIndex := (rb.head + len(rb.buffer) - rb.size + i + 1) % len(rb.buffer) |
||||
|
toIndex := (rb.head + len(rb.buffer) - rb.size + i) % len(rb.buffer) |
||||
|
rb.buffer[toIndex] = rb.buffer[fromIndex] |
||||
|
} |
||||
|
rb.size-- |
||||
|
rb.buffer[(rb.head+len(rb.buffer)-1)%len(rb.buffer)] = 0 // Clear the last element
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Oldest returns the oldest timestamp in the ring buffer.
|
||||
|
func (rb *RingBuffer) Oldest() int64 { |
||||
|
if rb.size == 0 { |
||||
|
return 0 |
||||
|
} |
||||
|
oldestIndex := (rb.head + len(rb.buffer) - rb.size) % len(rb.buffer) |
||||
|
return rb.buffer[oldestIndex] |
||||
|
} |
||||
|
|
||||
|
// Latest returns the most recently added timestamp in the ring buffer.
|
||||
|
func (rb *RingBuffer) Latest() int64 { |
||||
|
if rb.size == 0 { |
||||
|
return 0 |
||||
|
} |
||||
|
latestIndex := (rb.head + len(rb.buffer) - 1) % len(rb.buffer) |
||||
|
return rb.buffer[latestIndex] |
||||
|
} |
@ -0,0 +1,85 @@ |
|||||
|
package sub_coordinator |
||||
|
|
||||
|
import ( |
||||
|
"sort" |
||||
|
"testing" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
func TestRingBuffer(t *testing.T) { |
||||
|
// Initialize a RingBuffer with capacity 5
|
||||
|
rb := NewRingBuffer(5) |
||||
|
|
||||
|
// Add timestamps to the buffer
|
||||
|
timestamps := []int64{100, 200, 300, 400, 500} |
||||
|
for _, ts := range timestamps { |
||||
|
rb.Add(ts) |
||||
|
} |
||||
|
|
||||
|
// Test Add method and buffer size
|
||||
|
expectedSize := 5 |
||||
|
if rb.size != expectedSize { |
||||
|
t.Errorf("Expected buffer size %d, got %d", expectedSize, rb.size) |
||||
|
} |
||||
|
|
||||
|
// Test Oldest and Latest methods
|
||||
|
expectedOldest := int64(100) |
||||
|
if oldest := rb.Oldest(); oldest != expectedOldest { |
||||
|
t.Errorf("Expected oldest timestamp %d, got %d", expectedOldest, oldest) |
||||
|
} |
||||
|
expectedLatest := int64(500) |
||||
|
if latest := rb.Latest(); latest != expectedLatest { |
||||
|
t.Errorf("Expected latest timestamp %d, got %d", expectedLatest, latest) |
||||
|
} |
||||
|
|
||||
|
// Test Remove method
|
||||
|
rb.Remove(200) |
||||
|
expectedSize-- |
||||
|
if rb.size != expectedSize { |
||||
|
t.Errorf("Expected buffer size %d after removal, got %d", expectedSize, rb.size) |
||||
|
} |
||||
|
|
||||
|
// Test removal of non-existent element
|
||||
|
rb.Remove(600) |
||||
|
if rb.size != expectedSize { |
||||
|
t.Errorf("Expected buffer size %d after attempting removal of non-existent element, got %d", expectedSize, rb.size) |
||||
|
} |
||||
|
|
||||
|
// Test binary search correctness
|
||||
|
target := int64(300) |
||||
|
index := sort.Search(rb.size, func(i int) bool { |
||||
|
return rb.buffer[(rb.head+len(rb.buffer)-rb.size+i)%len(rb.buffer)] >= target |
||||
|
}) |
||||
|
actualIndex := (rb.head + len(rb.buffer) - rb.size + index) % len(rb.buffer) |
||||
|
if rb.buffer[actualIndex] != target { |
||||
|
t.Errorf("Binary search failed to find the correct index for timestamp %d", target) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestInflightMessageTracker(t *testing.T) { |
||||
|
// Initialize an InflightMessageTracker with capacity 5
|
||||
|
tracker := NewInflightMessageTracker(5) |
||||
|
|
||||
|
// Add inflight messages
|
||||
|
key := []byte("exampleKey") |
||||
|
timestamp := time.Now().UnixNano() |
||||
|
tracker.InflightMessage(key, timestamp) |
||||
|
|
||||
|
// Test IsMessageAcknowledged method
|
||||
|
isOld := tracker.IsMessageAcknowledged(key, timestamp-10) |
||||
|
if !isOld { |
||||
|
t.Error("Expected message to be old") |
||||
|
} |
||||
|
|
||||
|
// Test AcknowledgeMessage method
|
||||
|
acked := tracker.AcknowledgeMessage(key, timestamp) |
||||
|
if !acked { |
||||
|
t.Error("Expected message to be acked") |
||||
|
} |
||||
|
if _, exists := tracker.messages[string(key)]; exists { |
||||
|
t.Error("Expected message to be deleted after ack") |
||||
|
} |
||||
|
if tracker.timestamps.size != 0 { |
||||
|
t.Error("Expected buffer size to be 0 after ack") |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue