package util import ( "fmt" "runtime" "strconv" "sync" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/atomic" ) func TestActiveUser(t *testing.T) { as := NewActiveUsers() as.UpdateUserTimestamp("test1", 5) as.UpdateUserTimestamp("test2", 10) as.UpdateUserTimestamp("test3", 15) require.Nil(t, as.PurgeInactiveUsers(2)) require.Equal(t, []string{"test1"}, as.PurgeInactiveUsers(5)) require.Nil(t, as.PurgeInactiveUsers(7)) require.Equal(t, []string{"test2"}, as.PurgeInactiveUsers(12)) as.UpdateUserTimestamp("test1", 17) require.Equal(t, []string{"test3"}, as.PurgeInactiveUsers(16)) require.Equal(t, []string{"test1"}, as.PurgeInactiveUsers(20)) } func TestActiveUserConcurrentUpdateAndPurge(t *testing.T) { count := 10 as := NewActiveUsers() done := sync.WaitGroup{} stop := atomic.NewBool(false) latestTS := atomic.NewInt64(0) for j := 0; j < count; j++ { done.Add(1) go func() { defer done.Done() for !stop.Load() { ts := latestTS.Inc() // In each cycle, we update different user. as.UpdateUserTimestamp(fmt.Sprintf("%d", ts), ts) time.Sleep(1 * time.Millisecond) } }() } previousLatest := int64(0) for i := 0; i < 10; i++ { time.Sleep(100 * time.Millisecond) latest := latestTS.Load() require.True(t, latest > previousLatest) previousLatest = latest purged := as.PurgeInactiveUsers(latest) require.NotEmpty(t, purged) } stop.Store(true) done.Wait() // Final purge. latest := latestTS.Load() as.PurgeInactiveUsers(latest) // Purging again doesn't do anything. purged := as.PurgeInactiveUsers(latest) require.Empty(t, purged) } func BenchmarkActiveUsers_UpdateUserTimestamp(b *testing.B) { for _, c := range []int{0, 5, 10, 25, 50, 100} { b.Run(strconv.Itoa(c), func(b *testing.B) { as := NewActiveUsers() startGoroutinesDoingUpdates(b, c, as) for i := 0; i < b.N; i++ { as.UpdateUserTimestamp("test", int64(i)) } }) } } func BenchmarkActiveUsers_Purge(b *testing.B) { for _, c := range []int{0, 5, 10, 25, 50, 100} { b.Run(strconv.Itoa(c), func(b *testing.B) { as := NewActiveUsers() startGoroutinesDoingUpdates(b, c, as) for i := 0; i < b.N; i++ { as.PurgeInactiveUsers(int64(i)) } }) } } func startGoroutinesDoingUpdates(b *testing.B, count int, as *ActiveUsers) { done := sync.WaitGroup{} stop := atomic.NewBool(false) started := sync.WaitGroup{} for j := 0; j < count; j++ { done.Add(1) started.Add(1) userID := fmt.Sprintf("user-%d", j) go func() { defer done.Done() started.Done() ts := int64(0) for !stop.Load() { ts++ as.UpdateUserTimestamp(userID, ts) // Give other goroutines a chance too. if ts%1000 == 0 { runtime.Gosched() } } }() } started.Wait() b.Cleanup(func() { // Ask goroutines to stop, and then wait until they do. stop.Store(true) done.Wait() }) }