diff --git a/README.md b/README.md index da23a56..53ab8ad 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Use `cronlocker --help` to get the following output: Usage of ./cronlocker: -endpoint string endpoint (default "http://localhost:8500") + -failwithoutlock + Exists with status code 1 if the lock was not received within lockwaittime -key string key to monitor, e.g. cronjobs/any_service/cron_name (default "none") -lockwaittime int diff --git a/command_locker.go b/command_locker.go index fb117a4..2e816a5 100644 --- a/command_locker.go +++ b/command_locker.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "errors" "net/url" "os/exec" "time" @@ -10,24 +11,32 @@ import ( "github.com/hashicorp/consul/api" ) +var errLockWaitTimeExpired = errors.New("wait time for acquiring the lock expired") + +// ConsulCommandLocker is an implementation of a command locker for consul, +// responsible for acquiring a distributed lock and executing a command type ConsulCommandLocker struct { - apiClient *api.Client - lockWaitTime time.Duration - minLockTime time.Duration - maxExecTime time.Duration + apiClient *api.Client + lockWaitTime time.Duration + minLockTime time.Duration + maxExecTime time.Duration + failOnLockWaitExpiration bool } +// NewConsulCommandLocker initializes a new ConsulCommandlocker func NewConsulCommandLocker( endpoint string, token string, lockWaitTime time.Duration, minLockTime time.Duration, maxExecTime time.Duration, + failOnLockWaitExpiration bool, ) (*ConsulCommandLocker, error) { ccl := &ConsulCommandLocker{ - lockWaitTime: lockWaitTime, - minLockTime: minLockTime, - maxExecTime: maxExecTime, + lockWaitTime: lockWaitTime, + minLockTime: minLockTime, + maxExecTime: maxExecTime, + failOnLockWaitExpiration: failOnLockWaitExpiration, } url, err := url.Parse(endpoint) @@ -53,6 +62,7 @@ func NewConsulCommandLocker( return ccl, nil } +// LockAndExecute takes a lock key and executes the command func (ccl *ConsulCommandLocker) LockAndExecute(key, command string) (string, error) { lockOpts := &api.LockOptions{ Key: key, @@ -75,6 +85,10 @@ func (ccl *ConsulCommandLocker) LockAndExecute(key, command string) (string, err // The lock was not acquired if lock channel is empty // Therefore we can simply return if lockCh == nil { + if ccl.failOnLockWaitExpiration { + return "", errLockWaitTimeExpired + } + return "Nothing was executed\n", nil } diff --git a/command_locker_test.go b/command_locker_test.go index 92a6823..8e10243 100644 --- a/command_locker_test.go +++ b/command_locker_test.go @@ -45,6 +45,7 @@ func TestConsulCommandLockerLockAndExecute(t *testing.T) { 300*time.Millisecond, time.Millisecond, 0, + false, ) for _, c := range cases { @@ -85,6 +86,7 @@ func TestConsulCommandLockerMinimumLockAndExecuteTime(t *testing.T) { 300*time.Millisecond, 500*time.Millisecond, 0, + false, ) startTime := time.Now() @@ -103,13 +105,41 @@ func TestConsulCommandLockerMaximumExecutionTime(t *testing.T) { 100*time.Millisecond, 300*time.Millisecond, 500*time.Millisecond, + false, ) startTime := time.Now() - commandLocker.LockAndExecute("test/cron/service/min_time_job", "sleep 5") + commandLocker.LockAndExecute("test/cron/service/max_time_job", "sleep 5") if time.Since(startTime) > 700*time.Millisecond { t.Errorf("Locker did not abort after the maximum execution time was reached") } } + +func TestConsulCommandLockerFailOnLockWaitTimeExpired(t *testing.T) { + commandLocker, _ := NewConsulCommandLocker( + testutils.CONSULURI, + "", // blank token + 300*time.Millisecond, + 500*time.Millisecond, + 0, + true, + ) + + go func() { + commandLocker.LockAndExecute("test/cron/service/lock_wait_time_job", "sleep 10") + }() + + time.Sleep(100 * time.Millisecond) + + output, err := commandLocker.LockAndExecute("test/cron/service/lock_wait_time_job", "sleep 2") + + if output != "" { + t.Errorf("Expected no output but received: %s", output) + } + + if err != errLockWaitTimeExpired { + t.Errorf("Expected to received errLockWaitTimeExpired, but received: %+v", err) + } +} diff --git a/main.go b/main.go index 5e2cd4b..05ce34c 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,11 @@ var ( 500, "Configures the wait time for a lock in milliseconds", ) + failOnLockWaitExpiration = flag.Bool( + "failwithoutlock", + false, + "Exists with status code 1 if the lock was not received within lockwaittime", + ) minLockTimeMS = flag.Int( "minlocktime", 5000, @@ -52,6 +57,7 @@ func main() { time.Duration(*lockWaitTimeMS)*time.Millisecond, time.Duration(*minLockTimeMS)*time.Millisecond, time.Duration(*maxExecutionTimeMS)*time.Millisecond, + *failOnLockWaitExpiration, ) if err != nil { log.Fatalf("%v", err)