180 lines
5.2 KiB
Go
180 lines
5.2 KiB
Go
package serverlock
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/util/testutil"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIntegrationServerLock_LockAndExecute(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
sl := createTestableServerLock(t)
|
|
|
|
counter := 0
|
|
fn := func(context.Context) { counter++ }
|
|
atInterval := time.Hour
|
|
ctx := context.Background()
|
|
|
|
// this time `fn` should be executed
|
|
require.Nil(t, sl.LockAndExecute(ctx, "test-operation", atInterval, fn))
|
|
require.Equal(t, 1, counter)
|
|
|
|
// this should not execute `fn`
|
|
require.Nil(t, sl.LockAndExecute(ctx, "test-operation", atInterval, fn))
|
|
require.Nil(t, sl.LockAndExecute(ctx, "test-operation", atInterval, fn))
|
|
require.Equal(t, 1, counter)
|
|
|
|
atInterval = time.Millisecond
|
|
|
|
// now `fn` should be executed again
|
|
err := sl.LockAndExecute(ctx, "test-operation", atInterval, fn)
|
|
require.Nil(t, err)
|
|
require.Equal(t, 2, counter)
|
|
}
|
|
|
|
func TestIntegrationServerLock_LockExecuteAndRelease(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
t.Run("lock is released", func(t *testing.T) {
|
|
sl := createTestableServerLock(t)
|
|
|
|
counter := 0
|
|
fn := func(context.Context) { counter++ }
|
|
atInterval := time.Hour
|
|
ctx := context.Background()
|
|
|
|
err := sl.LockExecuteAndRelease(ctx, "test-operation", atInterval, fn)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, counter)
|
|
|
|
// the function will be executed again, as everytime the lock is released
|
|
err = sl.LockExecuteAndRelease(ctx, "test-operation", atInterval, fn)
|
|
require.NoError(t, err)
|
|
err = sl.LockExecuteAndRelease(ctx, "test-operation", atInterval, fn)
|
|
require.NoError(t, err)
|
|
err = sl.LockExecuteAndRelease(ctx, "test-operation", atInterval, fn)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 4, counter)
|
|
})
|
|
|
|
t.Run("lock is released when context is cancelled", func(t *testing.T) {
|
|
sl := createTestableServerLock(t)
|
|
operationUID := "test-operation-context-cancel"
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
err := sl.LockExecuteAndRelease(ctx, operationUID, time.Hour, func(ctx context.Context) {
|
|
cancel()
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = sl.SQLStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
|
lockRows := []*serverLock{}
|
|
err := sess.Where("operation_uid = ?", operationUID).Find(&lockRows)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(lockRows), "Lock should be released even when context is cancelled")
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestIntegrationServerLock_LockExecuteAndReleaseWithRetries(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
t.Run("retries when lock is already taken", func(t *testing.T) {
|
|
sl := createTestableServerLock(t)
|
|
|
|
retries := 0
|
|
expectedRetries := 10
|
|
funcRuns := 0
|
|
fn := func(context.Context) {
|
|
funcRuns++
|
|
}
|
|
lockTimeConfig := LockTimeConfig{
|
|
MaxInterval: time.Hour,
|
|
MinWait: 0 * time.Millisecond,
|
|
MaxWait: 1 * time.Millisecond,
|
|
}
|
|
ctx := context.Background()
|
|
actionName := "test-operation"
|
|
|
|
// Acquire lock so that when `LockExecuteAndReleaseWithRetries` runs, it is forced
|
|
// to retry
|
|
err := sl.acquireForRelease(ctx, actionName, lockTimeConfig.MaxInterval)
|
|
require.NoError(t, err)
|
|
|
|
wgRetries := sync.WaitGroup{}
|
|
wgRetries.Add(expectedRetries)
|
|
wgRelease := sync.WaitGroup{}
|
|
wgRelease.Add(1)
|
|
wgCompleted := sync.WaitGroup{}
|
|
wgCompleted.Add(1)
|
|
|
|
onRetryFn := func(int) error {
|
|
retries++
|
|
wgRetries.Done()
|
|
if retries == expectedRetries {
|
|
// When we reach `expectedRetries`, wait for the lock to be released
|
|
// to guarantee that next try will succeed
|
|
wgRelease.Wait()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
go func() {
|
|
err := sl.LockExecuteAndReleaseWithRetries(ctx, actionName, lockTimeConfig, fn, onRetryFn)
|
|
require.NoError(t, err)
|
|
wgCompleted.Done()
|
|
}()
|
|
|
|
// Wait to release the lock until `LockExecuteAndReleaseWithRetries` has retried `expectedRetries` times.
|
|
wgRetries.Wait()
|
|
err = sl.releaseLock(ctx, actionName)
|
|
require.NoError(t, err)
|
|
wgRelease.Done()
|
|
|
|
// `LockExecuteAndReleaseWithRetries` has run completely.
|
|
// Check that it had to retry because the lock was already taken.
|
|
wgCompleted.Wait()
|
|
require.Equal(t, expectedRetries, retries)
|
|
require.Equal(t, 1, funcRuns)
|
|
})
|
|
|
|
t.Run("lock is released when context is cancelled", func(t *testing.T) {
|
|
sl := createTestableServerLock(t)
|
|
operationUID := "test-operation-context-cancel-retries"
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
lockTimeConfig := LockTimeConfig{
|
|
MaxInterval: time.Hour,
|
|
MinWait: 0 * time.Millisecond,
|
|
MaxWait: 1 * time.Millisecond,
|
|
}
|
|
|
|
err := sl.LockExecuteAndReleaseWithRetries(ctx, operationUID, lockTimeConfig, func(ctx context.Context) {
|
|
cancel()
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = sl.SQLStore.WithDbSession(context.Background(), func(sess *db.Session) error {
|
|
lockRows := []*serverLock{}
|
|
err := sess.Where("operation_uid = ?", operationUID).Find(&lockRows)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, len(lockRows), "Lock should be released even when context is cancelled")
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|