mirror of https://github.com/grafana/loki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
779 lines
20 KiB
779 lines
20 KiB
package limits
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/loki/v3/pkg/logproto"
|
|
)
|
|
|
|
func TestStreamMetadata_All(t *testing.T) {
|
|
now := time.Now()
|
|
m := NewStreamMetadata(10)
|
|
|
|
for i := range 10 {
|
|
m.Store("tenant1", int32(i), uint64(i), 1000, now.UnixNano(), now.Truncate(time.Minute).UnixNano(), now.Add(-time.Hour).UnixNano())
|
|
}
|
|
|
|
expected := []uint64{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9}
|
|
|
|
actual := make([]uint64, 0, len(expected))
|
|
m.All(func(_ string, _ int32, stream Stream) {
|
|
actual = append(actual, stream.Hash)
|
|
})
|
|
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
|
|
func TestStreamMetadata_All_Concurrent(t *testing.T) {
|
|
now := time.Now()
|
|
m := NewStreamMetadata(10)
|
|
|
|
for i := range 10 {
|
|
tenant := fmt.Sprintf("tenant%d", i)
|
|
m.Store(tenant, 0, uint64(i), 1000, now.UnixNano(), now.Truncate(time.Minute).UnixNano(), now.Add(-time.Hour).UnixNano())
|
|
}
|
|
|
|
expected := []uint64{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9}
|
|
|
|
actual := make([]uint64, 10)
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(10)
|
|
for i := range 10 {
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
m.All(func(tenant string, _ int32, stream Stream) {
|
|
if tenant == fmt.Sprintf("tenant%d", i) {
|
|
actual[i] = stream.Hash
|
|
}
|
|
})
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
|
|
func TestStreamMetadata_Usage(t *testing.T) {
|
|
now := time.Now()
|
|
m := NewStreamMetadata(10)
|
|
|
|
for i := range 10 {
|
|
if i%2 == 0 {
|
|
m.Store("tenant1", int32(i), uint64(i), 1000, now.UnixNano(), now.Truncate(time.Minute).UnixNano(), now.Add(-time.Hour).UnixNano())
|
|
} else {
|
|
m.Store("tenant2", int32(i), uint64(i), 1000, now.UnixNano(), now.Truncate(time.Minute).UnixNano(), now.Add(-time.Hour).UnixNano())
|
|
}
|
|
}
|
|
|
|
expected := []uint64{0x0, 0x2, 0x4, 0x6, 0x8}
|
|
|
|
actual := make([]uint64, 0, 5)
|
|
m.Usage("tenant1", func(_ int32, stream Stream) {
|
|
actual = append(actual, stream.Hash)
|
|
})
|
|
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
|
|
func TestStreamMetadata_Usage_Concurrent(t *testing.T) {
|
|
now := time.Now()
|
|
m := NewStreamMetadata(10)
|
|
|
|
for i := range 10 {
|
|
if i%2 == 0 {
|
|
m.Store("tenant1", int32(i), uint64(i), 1000, now.UnixNano(), now.Truncate(time.Minute).UnixNano(), now.Add(-time.Hour).UnixNano())
|
|
} else {
|
|
m.Store("tenant2", int32(i), uint64(i), 1000, now.UnixNano(), now.Truncate(time.Minute).UnixNano(), now.Add(-time.Hour).UnixNano())
|
|
}
|
|
}
|
|
|
|
expected := []int{5, 5, 5, 5, 5, 5, 5, 5, 5, 5}
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(10)
|
|
|
|
actual := make([]int, 10)
|
|
for i := range 10 {
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
m.Usage("tenant1", func(_ int32, stream Stream) {
|
|
if stream.Hash%2 == 0 {
|
|
actual[i]++
|
|
}
|
|
})
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
|
|
func TestStreamMetadata_Store(t *testing.T) {
|
|
var (
|
|
bucketDuration = time.Minute
|
|
rateWindow = 5 * time.Minute
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
|
|
// Setup data.
|
|
metadata StreamMetadata
|
|
|
|
// The test case.
|
|
tenantID string
|
|
partitionID int32
|
|
lastSeenAt time.Time
|
|
record *logproto.StreamMetadata
|
|
|
|
// Expectations.
|
|
expected map[string]map[int32][]Stream
|
|
}{
|
|
{
|
|
name: "insert new tenant and new partition",
|
|
metadata: NewStreamMetadata(1),
|
|
tenantID: "tenant1",
|
|
partitionID: 0,
|
|
lastSeenAt: time.Unix(100, 0),
|
|
record: &logproto.StreamMetadata{
|
|
StreamHash: 123,
|
|
EntriesSize: 1000,
|
|
StructuredMetadataSize: 500,
|
|
},
|
|
expected: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 123,
|
|
LastSeenAt: time.Unix(100, 0).UnixNano(),
|
|
TotalSize: 1500,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(100, 0).Truncate(time.Minute).UnixNano(), Size: 1500},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "insert existing tenant and new partition",
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 123,
|
|
LastSeenAt: time.Unix(100, 0).UnixNano(),
|
|
TotalSize: 1000,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(100, 0).Truncate(time.Minute).UnixNano(), Size: 1000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{},
|
|
},
|
|
locks: make([]stripeLock, 2),
|
|
},
|
|
tenantID: "tenant1",
|
|
partitionID: 1,
|
|
record: &logproto.StreamMetadata{
|
|
StreamHash: 456,
|
|
EntriesSize: 2000,
|
|
StructuredMetadataSize: 1000,
|
|
},
|
|
lastSeenAt: time.Unix(200, 0),
|
|
expected: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 123,
|
|
LastSeenAt: time.Unix(100, 0).UnixNano(),
|
|
TotalSize: 1000,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(100, 0).Truncate(time.Minute).UnixNano(), Size: 1000},
|
|
},
|
|
},
|
|
},
|
|
1: {
|
|
{
|
|
Hash: 456,
|
|
LastSeenAt: time.Unix(200, 0).UnixNano(),
|
|
TotalSize: 3000,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(200, 0).Truncate(time.Minute).UnixNano(), Size: 3000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update existing stream",
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 123,
|
|
LastSeenAt: time.Unix(100, 0).UnixNano(),
|
|
TotalSize: 1000,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(100, 0).Truncate(time.Minute).UnixNano(), Size: 1000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 1),
|
|
},
|
|
tenantID: "tenant1",
|
|
partitionID: 0,
|
|
record: &logproto.StreamMetadata{
|
|
StreamHash: 123,
|
|
EntriesSize: 3000,
|
|
StructuredMetadataSize: 1500,
|
|
},
|
|
lastSeenAt: time.Unix(300, 0),
|
|
expected: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 123,
|
|
LastSeenAt: time.Unix(300, 0).UnixNano(),
|
|
TotalSize: 5500,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(100, 0).Truncate(time.Minute).UnixNano(), Size: 1000},
|
|
{Timestamp: time.Unix(300, 0).Truncate(time.Minute).UnixNano(), Size: 4500},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update existing bucket",
|
|
tenantID: "tenant1",
|
|
record: &logproto.StreamMetadata{
|
|
StreamHash: 888,
|
|
EntriesSize: 1000,
|
|
StructuredMetadataSize: 500,
|
|
},
|
|
lastSeenAt: time.Unix(852, 0),
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 888,
|
|
LastSeenAt: time.Unix(850, 0).UnixNano(),
|
|
TotalSize: 1500,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(850, 0).Truncate(time.Minute).UnixNano(), Size: 1500},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 1),
|
|
},
|
|
expected: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 888,
|
|
LastSeenAt: time.Unix(852, 0).UnixNano(),
|
|
TotalSize: 3000,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(850, 0).Truncate(time.Minute).UnixNano(), Size: 3000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "clean up buckets outside rate window",
|
|
tenantID: "tenant1",
|
|
record: &logproto.StreamMetadata{
|
|
StreamHash: 999,
|
|
EntriesSize: 2000,
|
|
StructuredMetadataSize: 1000,
|
|
},
|
|
lastSeenAt: time.Unix(1000, 0), // Current time reference
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 999,
|
|
LastSeenAt: time.Unix(950, 0).UnixNano(),
|
|
TotalSize: 5000,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(1000, 0).Add(-5 * time.Minute).Truncate(time.Minute).UnixNano(), Size: 1000}, // Old, outside window
|
|
{Timestamp: time.Unix(1000, 0).Add(-10 * time.Minute).Truncate(time.Minute).UnixNano(), Size: 1500}, // Outside rate window (>5 min old from 1000)
|
|
{Timestamp: time.Unix(950, 0).Truncate(time.Minute).UnixNano(), Size: 2500}, // Recent, within window
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 1),
|
|
},
|
|
expected: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 999,
|
|
LastSeenAt: time.Unix(1000, 0).UnixNano(),
|
|
TotalSize: 8000, // Old total + new 3000
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(950, 0).Truncate(time.Minute).UnixNano(), Size: 2500},
|
|
{Timestamp: time.Unix(1000, 0).Truncate(time.Minute).UnixNano(), Size: 3000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "update same minute bucket",
|
|
tenantID: "tenant1",
|
|
record: &logproto.StreamMetadata{
|
|
StreamHash: 555,
|
|
EntriesSize: 1000,
|
|
StructuredMetadataSize: 500,
|
|
},
|
|
lastSeenAt: time.Unix(1100, 0),
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 555,
|
|
LastSeenAt: time.Unix(1080, 0).UnixNano(), // Same minute as new data
|
|
TotalSize: 2000,
|
|
RateBuckets: []RateBucket{
|
|
{Timestamp: time.Unix(1080, 0).Truncate(time.Minute).UnixNano(), Size: 2000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 1),
|
|
},
|
|
expected: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: {
|
|
{
|
|
Hash: 555,
|
|
LastSeenAt: time.Unix(1100, 0).UnixNano(),
|
|
TotalSize: 3500, // 2000 + 1500
|
|
RateBuckets: []RateBucket{
|
|
// Same bucket as before but updated with new size
|
|
{Timestamp: time.Unix(1100, 0).Truncate(time.Minute).UnixNano(), Size: 3500},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
totalSize := tt.record.EntriesSize + tt.record.StructuredMetadataSize
|
|
bucketStart := tt.lastSeenAt.Truncate(bucketDuration).UnixNano()
|
|
bucketCutOff := tt.lastSeenAt.Add(-rateWindow).UnixNano()
|
|
|
|
tt.metadata.Store(tt.tenantID, tt.partitionID, tt.record.StreamHash, totalSize, tt.lastSeenAt.UnixNano(), bucketStart, bucketCutOff)
|
|
|
|
tt.metadata.All(func(tenant string, partitionID int32, stream Stream) {
|
|
require.Contains(t, tt.expected, tenant)
|
|
require.Contains(t, tt.expected[tenant], partitionID)
|
|
require.Contains(t, tt.expected[tenant][partitionID], stream)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStreamMetadata_Store_Concurrent(t *testing.T) {
|
|
var (
|
|
numTenants = 6
|
|
bucketDuration = time.Minute
|
|
rateWindow = 5 * time.Minute
|
|
|
|
lastSeenAt = time.Unix(100, 0)
|
|
bucketStart = lastSeenAt.Truncate(bucketDuration).UnixNano()
|
|
bucketCutOff = lastSeenAt.Add(-rateWindow).UnixNano()
|
|
)
|
|
|
|
m := NewStreamMetadata(numTenants)
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(numTenants)
|
|
|
|
for i := range numTenants {
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
|
|
tenantID := fmt.Sprintf("tenant%d", i)
|
|
partitionID := int32(0)
|
|
if i%2 == 0 {
|
|
partitionID = 1
|
|
}
|
|
|
|
record := &logproto.StreamMetadata{
|
|
StreamHash: uint64(i),
|
|
EntriesSize: 1000,
|
|
StructuredMetadataSize: 500,
|
|
}
|
|
|
|
totalSize := record.EntriesSize + record.StructuredMetadataSize
|
|
m.Store(tenantID, partitionID, record.StreamHash, totalSize, lastSeenAt.UnixNano(), bucketStart, bucketCutOff)
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
expected := map[string]map[int32][]Stream{
|
|
"tenant0": {
|
|
1: []Stream{
|
|
{Hash: 0x0, LastSeenAt: lastSeenAt.UnixNano(), TotalSize: 1500, RateBuckets: []RateBucket{{Timestamp: bucketStart, Size: 1500}}},
|
|
},
|
|
},
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 0x1, LastSeenAt: lastSeenAt.UnixNano(), TotalSize: 1500, RateBuckets: []RateBucket{{Timestamp: bucketStart, Size: 1500}}},
|
|
},
|
|
},
|
|
"tenant2": {
|
|
1: []Stream{
|
|
{Hash: 0x2, LastSeenAt: lastSeenAt.UnixNano(), TotalSize: 1500, RateBuckets: []RateBucket{{Timestamp: bucketStart, Size: 1500}}},
|
|
},
|
|
},
|
|
"tenant3": {
|
|
0: []Stream{
|
|
{Hash: 0x3, LastSeenAt: lastSeenAt.UnixNano(), TotalSize: 1500, RateBuckets: []RateBucket{{Timestamp: bucketStart, Size: 1500}}},
|
|
},
|
|
},
|
|
"tenant4": {
|
|
1: []Stream{
|
|
{Hash: 0x4, LastSeenAt: lastSeenAt.UnixNano(), TotalSize: 1500, RateBuckets: []RateBucket{{Timestamp: bucketStart, Size: 1500}}},
|
|
},
|
|
},
|
|
"tenant5": {
|
|
0: []Stream{
|
|
{Hash: 0x5, LastSeenAt: lastSeenAt.UnixNano(), TotalSize: 1500, RateBuckets: []RateBucket{{Timestamp: bucketStart, Size: 1500}}},
|
|
},
|
|
},
|
|
}
|
|
|
|
actual := make(map[string]map[int32][]Stream)
|
|
m.All(func(tenant string, partitionID int32, stream Stream) {
|
|
if _, ok := actual[tenant]; !ok {
|
|
actual[tenant] = make(map[int32][]Stream)
|
|
}
|
|
actual[tenant][partitionID] = append(actual[tenant][partitionID], stream)
|
|
})
|
|
|
|
require.Equal(t, expected, actual)
|
|
}
|
|
|
|
func TestStreamMetadata_Evict(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
metadata *streamMetadata
|
|
cutOff int64
|
|
assignedPartitionIDs []int32
|
|
expectedMetadata map[string]map[int32][]Stream
|
|
expectedEvictions map[string]int
|
|
}{
|
|
{
|
|
name: "all streams active",
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
{Hash: 2, LastSeenAt: now.UnixNano(), TotalSize: 2000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 1),
|
|
},
|
|
cutOff: now.Add(-time.Hour).UnixNano(),
|
|
assignedPartitionIDs: []int32{0},
|
|
expectedMetadata: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
{Hash: 2, LastSeenAt: now.UnixNano(), TotalSize: 2000},
|
|
},
|
|
},
|
|
},
|
|
expectedEvictions: map[string]int{},
|
|
},
|
|
{
|
|
name: "all streams expired",
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 1000},
|
|
{Hash: 2, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 2000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 1),
|
|
},
|
|
cutOff: now.Add(-time.Hour).UnixNano(),
|
|
assignedPartitionIDs: []int32{0},
|
|
expectedMetadata: map[string]map[int32][]Stream{},
|
|
expectedEvictions: map[string]int{
|
|
"tenant1": 2,
|
|
},
|
|
},
|
|
{
|
|
name: "mixed active and expired streams",
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
{Hash: 2, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 2000},
|
|
{Hash: 3, LastSeenAt: now.UnixNano(), TotalSize: 3000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 1),
|
|
},
|
|
cutOff: now.Add(-time.Hour).UnixNano(),
|
|
assignedPartitionIDs: []int32{0},
|
|
expectedMetadata: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
{Hash: 3, LastSeenAt: now.UnixNano(), TotalSize: 3000},
|
|
},
|
|
},
|
|
},
|
|
expectedEvictions: map[string]int{
|
|
"tenant1": 1,
|
|
},
|
|
},
|
|
{
|
|
name: "multiple tenants with mixed streams",
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
{Hash: 2, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 2000},
|
|
},
|
|
},
|
|
"tenant2": {
|
|
0: []Stream{
|
|
{Hash: 3, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 3000},
|
|
{Hash: 4, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 4000},
|
|
},
|
|
},
|
|
"tenant3": {
|
|
0: []Stream{
|
|
{Hash: 5, LastSeenAt: now.UnixNano(), TotalSize: 5000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 1),
|
|
},
|
|
cutOff: now.Add(-time.Hour).UnixNano(),
|
|
assignedPartitionIDs: []int32{0},
|
|
expectedMetadata: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
},
|
|
},
|
|
"tenant3": {
|
|
0: []Stream{
|
|
{Hash: 5, LastSeenAt: now.UnixNano(), TotalSize: 5000},
|
|
},
|
|
},
|
|
},
|
|
expectedEvictions: map[string]int{
|
|
"tenant1": 1,
|
|
"tenant2": 2,
|
|
},
|
|
},
|
|
{
|
|
name: "multiple partitions with some empty after eviction",
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
{Hash: 2, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 2000},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"tenant1": {
|
|
1: []Stream{
|
|
{Hash: 3, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 3000},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"tenant1": {
|
|
2: []Stream{
|
|
{Hash: 4, LastSeenAt: now.UnixNano(), TotalSize: 4000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 3),
|
|
},
|
|
cutOff: now.Add(-time.Hour).UnixNano(),
|
|
assignedPartitionIDs: []int32{0, 1, 2},
|
|
expectedMetadata: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
},
|
|
2: []Stream{
|
|
{Hash: 4, LastSeenAt: now.UnixNano(), TotalSize: 4000},
|
|
},
|
|
},
|
|
},
|
|
expectedEvictions: map[string]int{
|
|
"tenant1": 2,
|
|
},
|
|
},
|
|
{
|
|
name: "unassigned partitions should still be evicted",
|
|
metadata: &streamMetadata{
|
|
stripes: []map[string]map[int32][]Stream{
|
|
{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"tenant1": {
|
|
1: []Stream{
|
|
{Hash: 2, LastSeenAt: now.Add(-2 * time.Hour).UnixNano(), TotalSize: 2000},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
locks: make([]stripeLock, 2),
|
|
},
|
|
cutOff: now.Add(-time.Hour).UnixNano(),
|
|
assignedPartitionIDs: []int32{0},
|
|
expectedMetadata: map[string]map[int32][]Stream{
|
|
"tenant1": {
|
|
0: []Stream{
|
|
{Hash: 1, LastSeenAt: now.UnixNano(), TotalSize: 1000},
|
|
},
|
|
},
|
|
},
|
|
expectedEvictions: map[string]int{
|
|
"tenant1": 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
actualEvictions := tt.metadata.Evict(tt.cutOff)
|
|
|
|
actualMetadata := make(map[string]map[int32][]Stream)
|
|
tt.metadata.All(func(tenant string, partitionID int32, stream Stream) {
|
|
if actualMetadata[tenant] == nil {
|
|
actualMetadata[tenant] = make(map[int32][]Stream)
|
|
}
|
|
if actualMetadata[tenant][partitionID] == nil {
|
|
actualMetadata[tenant][partitionID] = make([]Stream, 0)
|
|
}
|
|
actualMetadata[tenant][partitionID] = append(actualMetadata[tenant][partitionID], stream)
|
|
})
|
|
|
|
require.Equal(t, tt.expectedEvictions, actualEvictions)
|
|
require.Equal(t, tt.expectedMetadata, actualMetadata)
|
|
})
|
|
}
|
|
}
|
|
func TestStreamMetadata_EvictPartitions(t *testing.T) {
|
|
numPartitions := 10
|
|
m := NewStreamMetadata(numPartitions)
|
|
|
|
for i := range numPartitions {
|
|
m.Store("tenant1", int32(i), 1, 1000, time.Now().UnixNano(), time.Now().Truncate(time.Minute).UnixNano(), time.Now().Add(-time.Hour).UnixNano())
|
|
}
|
|
|
|
m.EvictPartitions([]int32{1, 3, 5, 7, 9})
|
|
|
|
expected := []int32{0, 2, 4, 6, 8}
|
|
actual := make([]int32, 0, len(expected))
|
|
m.All(func(_ string, partitionID int32, _ Stream) {
|
|
actual = append(actual, partitionID)
|
|
})
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
|
|
func TestStreamMetadata_EvictPartitions_Concurrent(t *testing.T) {
|
|
numPartitions := 10
|
|
m := NewStreamMetadata(numPartitions)
|
|
|
|
for i := range numPartitions {
|
|
m.Store("tenant1", int32(i), 1, 1000, time.Now().UnixNano(), time.Now().Truncate(time.Minute).UnixNano(), time.Now().Add(-time.Hour).UnixNano())
|
|
}
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(numPartitions / 2)
|
|
for i := range numPartitions {
|
|
if i%2 == 0 {
|
|
continue
|
|
}
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
m.EvictPartitions([]int32{int32(i)})
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
expected := []int32{0, 2, 4, 6, 8}
|
|
actual := make([]int32, 0, len(expected))
|
|
m.All(func(_ string, partitionID int32, _ Stream) {
|
|
actual = append(actual, partitionID)
|
|
})
|
|
require.ElementsMatch(t, expected, actual)
|
|
}
|
|
|