Skip to content

Commit

Permalink
Merge pull request #11 from gruntwork-io/sigint
Browse files Browse the repository at this point in the history
Handle CTRL+C. If no args, show help. Fix concurrency test.
  • Loading branch information
brikis98 authored Jun 20, 2016
2 parents d9de426 + 20340f3 commit 3862999
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 40 deletions.
10 changes: 5 additions & 5 deletions cli/cli_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,17 @@ func CreateTerragruntCli(version string) *cli.App {
func runApp(cliContext *cli.Context) (finalErr error) {
defer errors.Recover(func(cause error) { finalErr = cause })

terragruntConfig, err := config.ReadTerragruntConfig()
if err != nil {
return err
}

// If someone calls us with no args at all, show the help text and exit
if !cliContext.Args().Present() {
cli.ShowAppHelp(cliContext)
return nil
}

terragruntConfig, err := config.ReadTerragruntConfig()
if err != nil {
return err
}

if err := downloadModules(cliContext); err != nil {
return err
}
Expand Down
72 changes: 37 additions & 35 deletions dynamodb/dynamo_lock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sync"
"github.com/gruntwork-io/terragrunt/errors"
"reflect"
"github.com/aws/aws-sdk-go/service/dynamodb"
)

func TestAcquireLockHappyPath(t *testing.T) {
Expand Down Expand Up @@ -83,39 +84,40 @@ func TestAcquireAndReleaseLock(t *testing.T) {
func TestAcquireLockConcurrency(t *testing.T) {
t.Parallel()

client := createDynamoDbClientForTest(t)
stateFileId := uniqueId()
lock := DynamoDbLock{
StateFileId: stateFileId,
AwsRegion: DEFAULT_TEST_REGION,
TableName: uniqueTableNameForTest(),
MaxLockRetries: 1,
}

defer cleanupTable(t, lock.TableName, client)

// Use a WaitGroup to ensure the test doesn't exit before all goroutines finish.
var waitGroup sync.WaitGroup
// This will count how many of the goroutines were able to acquire a lock. We use Go's atomic package to
// ensure all modifications to this counter are atomic operations.
locksAcquired := int32(0)

// Launch a bunch of goroutines who will all try to acquire the lock at more or less the same time.
// Only one should succeed.
for i := 0; i < 20; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
err := lock.AcquireLock()
if err == nil {
atomic.AddInt32(&locksAcquired, 1)
} else {
assert.True(t, errors.IsError(err, AcquireLockRetriesExceeded{ItemId: stateFileId, Retries: 1}), "Unexpected error of type %s: %s", reflect.TypeOf(err), err)
}
}()
}

waitGroup.Wait()

assert.Equal(t, int32(1), locksAcquired, "Only one of the goroutines should have been able to acquire a lock")
concurrency := 20

withLockTableProvisionedUnits(t, concurrency, concurrency, func(tableName string, client *dynamodb.DynamoDB) {
stateFileId := uniqueId()
lock := DynamoDbLock{
StateFileId: stateFileId,
AwsRegion: DEFAULT_TEST_REGION,
TableName: uniqueTableNameForTest(),
MaxLockRetries: 1,
}

// Use a WaitGroup to ensure the test doesn't exit before all goroutines finish.
var waitGroup sync.WaitGroup
// This will count how many of the goroutines were able to acquire a lock. We use Go's atomic package to
// ensure all modifications to this counter are atomic operations.
locksAcquired := int32(0)

// Launch a bunch of goroutines who will all try to acquire the lock at more or less the same time.
// Only one should succeed.
for i := 0; i < concurrency; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
err := lock.AcquireLock()
if err == nil {
atomic.AddInt32(&locksAcquired, 1)
} else {
assert.True(t, errors.IsError(err, AcquireLockRetriesExceeded{ItemId: stateFileId, Retries: 1}), "Unexpected error of type %s: %s", reflect.TypeOf(err), err)
}
}()
}

waitGroup.Wait()

assert.Equal(t, int32(1), locksAcquired, "Only one of the goroutines should have been able to acquire a lock")
})
}
11 changes: 11 additions & 0 deletions locks/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package locks
import (
"github.com/gruntwork-io/terragrunt/util"
"github.com/gruntwork-io/terragrunt/errors"
"os"
"os/signal"
)

// Every type of lock must implement this interface
Expand Down Expand Up @@ -39,5 +41,14 @@ func WithLock(lock Lock, action func() error) (finalErr error) {
}
}()

// When Go receives the interrupt signal SIGINT (e.g. from someone hitting CTRL+C), the default behavior is to
// exit the program immediately. Here, we override that behavior, which ensures our deferred code has a chance
// to run and release the lock. Note that we don't have to do anything to cancel the running action, as
// Terraform itself automatically detects SIGINT and does a graceful shutdown in response, so we can just allow
// the blocking call to action() to return normally.
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, os.Interrupt)
go func() { util.Logger.Printf("Caught signal '%s'. Terraform should be shutting down gracefully now.", <- signalChannel) }()

return action()
}

0 comments on commit 3862999

Please sign in to comment.