2022-12-05 23:40:45 +00:00
package internal
import (
"context"
"encoding/json"
"fmt"
2023-06-16 14:29:04 +00:00
"io"
2023-06-01 07:38:03 +00:00
"io/fs"
2022-12-05 23:40:45 +00:00
"math/rand"
"sync"
"testing"
"time"
2023-06-19 13:57:25 +00:00
"github.com/databricks/cli/libs/filer"
2023-05-16 16:35:39 +00:00
lockpkg "github.com/databricks/cli/libs/locker"
2022-12-05 23:40:45 +00:00
"github.com/databricks/databricks-sdk-go"
2023-04-21 08:30:20 +00:00
"github.com/databricks/databricks-sdk-go/service/workspace"
2022-12-05 23:40:45 +00:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TODO: create a utility function to create an empty test repo for tests and refactor sync_test integration test
const EmptyRepoUrl = "https://github.com/shreyas-goenka/empty-repo.git"
func createRemoteTestProject ( t * testing . T , projectNamePrefix string , wsc * databricks . WorkspaceClient ) string {
ctx := context . TODO ( )
me , err := wsc . CurrentUser . Me ( ctx )
assert . NoError ( t , err )
remoteProjectRoot := fmt . Sprintf ( "/Repos/%s/%s" , me . UserName , RandomName ( projectNamePrefix ) )
2023-04-21 08:30:20 +00:00
repoInfo , err := wsc . Repos . Create ( ctx , workspace . CreateRepo {
2022-12-05 23:40:45 +00:00
Path : remoteProjectRoot ,
Url : EmptyRepoUrl ,
Provider : "gitHub" ,
} )
assert . NoError ( t , err )
t . Cleanup ( func ( ) {
err := wsc . Repos . DeleteByRepoId ( ctx , repoInfo . Id )
assert . NoError ( t , err )
} )
return remoteProjectRoot
}
func TestAccLock ( t * testing . T ) {
t . Log ( GetEnvOrSkipTest ( t , "CLOUD_ENV" ) )
ctx := context . TODO ( )
2023-01-11 12:32:02 +00:00
wsc , err := databricks . NewWorkspaceClient ( )
require . NoError ( t , err )
2022-12-05 23:40:45 +00:00
remoteProjectRoot := createRemoteTestProject ( t , "lock-acc-" , wsc )
2023-05-25 13:22:51 +00:00
// 5 lockers try to acquire a lock at the same time
numConcurrentLocks := 5
2022-12-05 23:40:45 +00:00
2022-12-15 16:16:07 +00:00
// Keep single locker unlocked.
// We use this to check on the current lock through GetActiveLockState.
2023-03-22 15:37:26 +00:00
locker , err := lockpkg . CreateLocker ( "humpty.dumpty@databricks.com" , remoteProjectRoot , wsc )
2022-12-15 16:16:07 +00:00
require . NoError ( t , err )
2022-12-05 23:40:45 +00:00
lockerErrs := make ( [ ] error , numConcurrentLocks )
2023-03-22 15:37:26 +00:00
lockers := make ( [ ] * lockpkg . Locker , numConcurrentLocks )
2022-12-05 23:40:45 +00:00
for i := 0 ; i < numConcurrentLocks ; i ++ {
2023-03-22 15:37:26 +00:00
lockers [ i ] , err = lockpkg . CreateLocker ( "humpty.dumpty@databricks.com" , remoteProjectRoot , wsc )
2022-12-15 16:16:07 +00:00
require . NoError ( t , err )
2022-12-05 23:40:45 +00:00
}
var wg sync . WaitGroup
for i := 0 ; i < numConcurrentLocks ; i ++ {
wg . Add ( 1 )
currentIndex := i
go func ( ) {
defer wg . Done ( )
time . Sleep ( time . Duration ( rand . Intn ( 100 ) ) * time . Millisecond )
2022-12-15 16:16:07 +00:00
lockerErrs [ currentIndex ] = lockers [ currentIndex ] . Lock ( ctx , false )
2022-12-05 23:40:45 +00:00
} ( )
}
wg . Wait ( )
countActive := 0
indexOfActiveLocker := 0
indexOfAnInactiveLocker := - 1
for i := 0 ; i < numConcurrentLocks ; i ++ {
if lockers [ i ] . Active {
countActive += 1
assert . NoError ( t , lockerErrs [ i ] )
indexOfActiveLocker = i
} else {
if indexOfAnInactiveLocker == - 1 {
indexOfAnInactiveLocker = i
}
assert . ErrorContains ( t , lockerErrs [ i ] , "lock acquired by" )
2023-08-15 19:03:43 +00:00
assert . ErrorContains ( t , lockerErrs [ i ] , "Use --force-lock to override" )
2022-12-05 23:40:45 +00:00
}
}
assert . Equal ( t , 1 , countActive , "Exactly one locker should successfull acquire the lock" )
// test remote lock matches active lock
2022-12-15 16:16:07 +00:00
remoteLocker , err := locker . GetActiveLockState ( ctx )
2022-12-05 23:40:45 +00:00
require . NoError ( t , err )
assert . Equal ( t , remoteLocker . ID , lockers [ indexOfActiveLocker ] . State . ID , "remote locker id does not match active locker" )
assert . True ( t , remoteLocker . AcquisitionTime . Equal ( lockers [ indexOfActiveLocker ] . State . AcquisitionTime ) , "remote locker acquisition time does not match active locker" )
// test all other locks (inactive ones) do not match the remote lock and Unlock fails
for i := 0 ; i < numConcurrentLocks ; i ++ {
if i == indexOfActiveLocker {
continue
}
assert . NotEqual ( t , remoteLocker . ID , lockers [ i ] . State . ID )
2022-12-15 16:16:07 +00:00
err := lockers [ i ] . Unlock ( ctx )
2022-12-05 23:40:45 +00:00
assert . ErrorContains ( t , err , "unlock called when lock is not held" )
}
// test inactive locks fail to write a file
for i := 0 ; i < numConcurrentLocks ; i ++ {
if i == indexOfActiveLocker {
continue
}
2023-06-16 14:29:04 +00:00
err := lockers [ i ] . Write ( ctx , "foo.json" , [ ] byte ( ` ' { "surname":"Khan", "name":"Shah Rukh"} ` ) )
2022-12-05 23:40:45 +00:00
assert . ErrorContains ( t , err , "failed to put file. deploy lock not held" )
}
// active locker file write succeeds
2023-06-16 14:29:04 +00:00
err = lockers [ indexOfActiveLocker ] . Write ( ctx , "foo.json" , [ ] byte ( ` { "surname":"Khan", "name":"Shah Rukh"} ` ) )
2022-12-05 23:40:45 +00:00
assert . NoError ( t , err )
2023-06-16 14:29:04 +00:00
// read active locker file
r , err := lockers [ indexOfActiveLocker ] . Read ( ctx , "foo.json" )
require . NoError ( t , err )
2023-06-19 13:57:25 +00:00
defer r . Close ( )
2023-06-16 14:29:04 +00:00
b , err := io . ReadAll ( r )
require . NoError ( t , err )
// assert on active locker content
2022-12-05 23:40:45 +00:00
var res map [ string ] string
2023-06-16 14:29:04 +00:00
json . Unmarshal ( b , & res )
2022-12-05 23:40:45 +00:00
assert . NoError ( t , err )
assert . Equal ( t , "Khan" , res [ "surname" ] )
assert . Equal ( t , "Shah Rukh" , res [ "name" ] )
// inactive locker file reads fail
for i := 0 ; i < numConcurrentLocks ; i ++ {
if i == indexOfActiveLocker {
continue
}
2023-06-16 14:29:04 +00:00
_ , err = lockers [ i ] . Read ( ctx , "foo.json" )
2022-12-05 23:40:45 +00:00
assert . ErrorContains ( t , err , "failed to get file. deploy lock not held" )
}
// Unlock active lock and check it becomes inactive
2022-12-15 16:16:07 +00:00
err = lockers [ indexOfActiveLocker ] . Unlock ( ctx )
2022-12-05 23:40:45 +00:00
assert . NoError ( t , err )
2022-12-15 16:16:07 +00:00
remoteLocker , err = locker . GetActiveLockState ( ctx )
2023-06-01 07:38:03 +00:00
assert . ErrorIs ( t , err , fs . ErrNotExist , "remote lock file not deleted on unlock" )
2022-12-05 23:40:45 +00:00
assert . Nil ( t , remoteLocker )
assert . False ( t , lockers [ indexOfActiveLocker ] . Active )
// A locker that failed to acquire the lock should now be able to acquire it
assert . False ( t , lockers [ indexOfAnInactiveLocker ] . Active )
2022-12-15 16:16:07 +00:00
err = lockers [ indexOfAnInactiveLocker ] . Lock ( ctx , false )
2022-12-05 23:40:45 +00:00
assert . NoError ( t , err )
assert . True ( t , lockers [ indexOfAnInactiveLocker ] . Active )
}
2023-06-19 13:57:25 +00:00
func setupLockerTest ( ctx context . Context , t * testing . T ) ( * lockpkg . Locker , filer . Filer ) {
t . Log ( GetEnvOrSkipTest ( t , "CLOUD_ENV" ) )
w , err := databricks . NewWorkspaceClient ( )
require . NoError ( t , err )
// create temp wsfs dir
tmpDir := temporaryWorkspaceDir ( t , w )
f , err := filer . NewWorkspaceFilesClient ( w , tmpDir )
require . NoError ( t , err )
// create locker
locker , err := lockpkg . CreateLocker ( "redfoo@databricks.com" , tmpDir , w )
require . NoError ( t , err )
return locker , f
}
func TestAccLockUnlockWithoutAllowsLockFileNotExist ( t * testing . T ) {
ctx := context . Background ( )
locker , f := setupLockerTest ( ctx , t )
var err error
// Acquire lock on tmp directory
err = locker . Lock ( ctx , false )
require . NoError ( t , err )
// Assert lock file is created
_ , err = f . Stat ( ctx , "deploy.lock" )
assert . NoError ( t , err )
// Manually delete lock file
err = f . Delete ( ctx , "deploy.lock" )
assert . NoError ( t , err )
// Assert error, because lock file does not exist
err = locker . Unlock ( ctx )
assert . ErrorIs ( t , err , fs . ErrNotExist )
}
func TestAccLockUnlockWithAllowsLockFileNotExist ( t * testing . T ) {
ctx := context . Background ( )
locker , f := setupLockerTest ( ctx , t )
var err error
// Acquire lock on tmp directory
err = locker . Lock ( ctx , false )
require . NoError ( t , err )
assert . True ( t , locker . Active )
// Assert lock file is created
_ , err = f . Stat ( ctx , "deploy.lock" )
assert . NoError ( t , err )
// Manually delete lock file
err = f . Delete ( ctx , "deploy.lock" )
assert . NoError ( t , err )
// Assert error, because lock file does not exist
err = locker . Unlock ( ctx , lockpkg . AllowLockFileNotExist )
assert . NoError ( t , err )
assert . False ( t , locker . Active )
}