// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package api
import (
"log"
"net/http"
"net/http/httptest"
"net/http/httputil"
"strings"
"sync"
"testing"
"time"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
)
func createTestLock ( t testutil . TestingTB , c * Client , key string ) ( * Lock , * Session ) {
t . Helper ( )
session := c . Session ( )
se := & SessionEntry {
Name : DefaultLockSessionName ,
TTL : DefaultLockSessionTTL ,
Behavior : SessionBehaviorDelete ,
}
id , _ , err := session . CreateNoChecks ( se , nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
opts := & LockOptions {
Key : key ,
Session : id ,
SessionName : se . Name ,
SessionTTL : se . TTL ,
}
lock , err := c . LockOpts ( opts )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
return lock , session
}
func TestAPI_LockLockUnlock ( t * testing . T ) {
t . Parallel ( )
c , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
lock , session := createTestLock ( t , c , "test/lock" )
defer session . Destroy ( lock . opts . Session , nil )
// Initial unlock should fail
err := lock . Unlock ( )
if err != ErrLockNotHeld {
t . Fatalf ( "err: %v" , err )
}
// Should work
leaderCh , err := lock . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if leaderCh == nil {
t . Fatalf ( "not leader" )
}
// Double lock should fail
_ , err = lock . Lock ( nil )
if err != ErrLockHeld {
t . Fatalf ( "err: %v" , err )
}
// Should be leader
select {
case <- leaderCh :
t . Fatalf ( "should be leader" )
default :
}
// Initial unlock should work
err = lock . Unlock ( )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
// Double unlock should fail
err = lock . Unlock ( )
if err != ErrLockNotHeld {
t . Fatalf ( "err: %v" , err )
}
// Should lose leadership
select {
case <- leaderCh :
case <- time . After ( time . Second ) :
t . Fatalf ( "should not be leader" )
}
}
func TestAPI_LockForceInvalidate ( t * testing . T ) {
t . Parallel ( )
c , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
retry . Run ( t , func ( r * retry . R ) {
lock , session := createTestLock ( r , c , "test/lock" )
defer session . Destroy ( lock . opts . Session , nil )
// Should work
leaderCh , err := lock . Lock ( nil )
if err != nil {
r . Fatalf ( "err: %v" , err )
}
if leaderCh == nil {
r . Fatalf ( "not leader" )
}
defer lock . Unlock ( )
go func ( ) {
// Nuke the session, simulator an operator invalidation
// or a health check failure
session := c . Session ( )
session . Destroy ( lock . lockSession , nil )
} ( )
// Should loose leadership
select {
case <- leaderCh :
case <- time . After ( time . Second ) :
r . Fatalf ( "should not be leader" )
}
} )
}
func TestAPI_LockDeleteKey ( t * testing . T ) {
t . Parallel ( )
c , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
// This uncovered some issues around special-case handling of low index
// numbers where it would work with a low number but fail for higher
// ones, so we loop this a bit to sweep the index up out of that
// territory.
for i := 0 ; i < 10 ; i ++ {
func ( ) {
lock , session := createTestLock ( t , c , "test/lock" )
defer session . Destroy ( lock . opts . Session , nil )
// Should work
leaderCh , err := lock . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if leaderCh == nil {
t . Fatalf ( "not leader" )
}
defer lock . Unlock ( )
go func ( ) {
// Nuke the key, simulate an operator intervention
kv := c . KV ( )
kv . Delete ( "test/lock" , nil )
} ( )
// Should loose leadership
select {
case <- leaderCh :
case <- time . After ( 10 * time . Second ) :
t . Fatalf ( "should not be leader" )
}
} ( )
}
}
func TestAPI_LockContend ( t * testing . T ) {
t . Parallel ( )
c , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
wg := & sync . WaitGroup { }
acquired := make ( [ ] bool , 3 )
for idx := range acquired {
wg . Add ( 1 )
go func ( idx int ) {
defer wg . Done ( )
lock , session := createTestLock ( t , c , "test/lock" )
defer session . Destroy ( lock . opts . Session , nil )
// Should work eventually, will contend
leaderCh , err := lock . Lock ( nil )
if err != nil {
t . Errorf ( "err: %v" , err )
return
}
if leaderCh == nil {
t . Errorf ( "not leader" )
return
}
defer lock . Unlock ( )
log . Printf ( "Contender %d acquired" , idx )
// Set acquired and then leave
acquired [ idx ] = true
} ( idx )
}
// Wait for termination
doneCh := make ( chan struct { } )
go func ( ) {
wg . Wait ( )
close ( doneCh )
} ( )
// Wait for everybody to get a turn
select {
case <- doneCh :
case <- time . After ( 3 * DefaultLockRetryTime ) :
t . Fatalf ( "timeout" )
}
for idx , did := range acquired {
if ! did {
t . Fatalf ( "contender %d never acquired" , idx )
}
}
}
func TestAPI_LockDestroy ( t * testing . T ) {
t . Parallel ( )
c , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
lock , session := createTestLock ( t , c , "test/lock" )
defer session . Destroy ( lock . opts . Session , nil )
// Should work
leaderCh , err := lock . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if leaderCh == nil {
t . Fatalf ( "not leader" )
}
// Destroy should fail
if err := lock . Destroy ( ) ; err != ErrLockHeld {
t . Fatalf ( "err: %v" , err )
}
// Should be able to release
err = lock . Unlock ( )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
// Acquire with a different lock
l2 , session := createTestLock ( t , c , "test/lock" )
defer session . Destroy ( lock . opts . Session , nil )
// Should work
leaderCh , err = l2 . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if leaderCh == nil {
t . Fatalf ( "not leader" )
}
// Destroy should still fail
if err := lock . Destroy ( ) ; err != ErrLockInUse {
t . Fatalf ( "err: %v" , err )
}
// Should release
err = l2 . Unlock ( )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
// Destroy should work
err = lock . Destroy ( )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
// Double destroy should work
err = l2 . Destroy ( )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
}
func TestAPI_LockConflict ( t * testing . T ) {
t . Parallel ( )
c , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
sema , session := createTestSemaphore ( t , c , "test/lock/" , 2 )
defer session . Destroy ( sema . opts . Session , nil )
// Should work
lockCh , err := sema . Acquire ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if lockCh == nil {
t . Fatalf ( "not hold" )
}
defer sema . Release ( )
lock , session := createTestLock ( t , c , "test/lock/.lock" )
defer session . Destroy ( lock . opts . Session , nil )
// Should conflict with semaphore
_ , err = lock . Lock ( nil )
if err != ErrLockConflict {
t . Fatalf ( "err: %v" , err )
}
// Should conflict with semaphore
err = lock . Destroy ( )
if err != ErrLockConflict {
t . Fatalf ( "err: %v" , err )
}
}
func TestAPI_LockReclaimLock ( t * testing . T ) {
t . Parallel ( )
c , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
s . WaitForSerfCheck ( t )
session , _ , err := c . Session ( ) . Create ( & SessionEntry { } , nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
lock , err := c . LockOpts ( & LockOptions { Key : "test/lock" , Session : session } )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
// Should work
leaderCh , err := lock . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if leaderCh == nil {
t . Fatalf ( "not leader" )
}
defer lock . Unlock ( )
l2 , err := c . LockOpts ( & LockOptions { Key : "test/lock" , Session : session } )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
reclaimed := make ( chan ( <- chan struct { } ) , 1 )
go func ( ) {
l2Ch , err := l2 . Lock ( nil )
if err != nil {
t . Errorf ( "not locked: %v" , err )
}
reclaimed <- l2Ch
} ( )
// Should reclaim the lock
var leader2Ch <- chan struct { }
select {
case leader2Ch = <- reclaimed :
case <- time . After ( time . Second ) :
t . Fatalf ( "should have locked" )
}
// unlock should work
err = l2 . Unlock ( )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
//Both locks should see the unlock
select {
case <- leader2Ch :
case <- time . After ( time . Second ) :
t . Fatalf ( "should not be leader" )
}
select {
case <- leaderCh :
case <- time . After ( time . Second ) :
t . Fatalf ( "should not be leader" )
}
}
func TestAPI_LockMonitorRetry ( t * testing . T ) {
t . Parallel ( )
raw , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
s . WaitForSerfCheck ( t )
// Set up a server that always responds with 500 errors.
failer := func ( w http . ResponseWriter , req * http . Request ) {
w . WriteHeader ( 500 )
}
outage := httptest . NewServer ( http . HandlerFunc ( failer ) )
defer outage . Close ( )
// Set up a reverse proxy that will send some requests to the
// 500 server and pass everything else through to the real Consul
// server.
var mutex sync . Mutex
errors := 0
director := func ( req * http . Request ) {
mutex . Lock ( )
defer mutex . Unlock ( )
req . URL . Scheme = "http"
if errors > 0 && req . Method == "GET" && strings . Contains ( req . URL . Path , "/v1/kv/test/lock" ) {
req . URL . Host = outage . URL [ 7 : ] // Strip off "http://".
errors --
} else {
req . URL . Host = raw . config . Address
}
}
proxy := httptest . NewServer ( & httputil . ReverseProxy { Director : director } )
defer proxy . Close ( )
// Make another client that points at the proxy instead of the real
// Consul server.
config := raw . config
config . Address = proxy . URL [ 7 : ] // Strip off "http://".
c , err := NewClient ( & config )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
// Set up a lock with retries enabled.
opts := & LockOptions {
Key : "test/lock" ,
SessionTTL : "60s" ,
MonitorRetries : 3 ,
}
lock , err := c . LockOpts ( opts )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
// Make sure the default got set.
if lock . opts . MonitorRetryTime != DefaultMonitorRetryTime {
t . Fatalf ( "bad: %d" , lock . opts . MonitorRetryTime )
}
// Now set a custom time for the test.
opts . MonitorRetryTime = 250 * time . Millisecond
lock , err = c . LockOpts ( opts )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if lock . opts . MonitorRetryTime != 250 * time . Millisecond {
t . Fatalf ( "bad: %d" , lock . opts . MonitorRetryTime )
}
// Should get the lock.
leaderCh , err := lock . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if leaderCh == nil {
t . Fatalf ( "not leader" )
}
// Poke the key using the raw client to force the monitor to wake up
// and check the lock again. This time we will return errors for some
// of the responses.
mutex . Lock ( )
errors = 2
mutex . Unlock ( )
pair , _ , err := raw . KV ( ) . Get ( "test/lock" , & QueryOptions { } )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
pair . Value = [ ] byte { 1 }
if _ , err := raw . KV ( ) . Put ( pair , & WriteOptions { } ) ; err != nil {
t . Fatalf ( "err: %v" , err )
}
time . Sleep ( 5 * opts . MonitorRetryTime )
// Should still be the leader.
select {
case <- leaderCh :
t . Fatalf ( "should be leader" )
default :
}
// Now return an overwhelming number of errors.
mutex . Lock ( )
errors = 10
mutex . Unlock ( )
pair . Value = [ ] byte { 2 }
if _ , err := raw . KV ( ) . Put ( pair , & WriteOptions { } ) ; err != nil {
t . Fatalf ( "err: %v" , err )
}
time . Sleep ( 5 * opts . MonitorRetryTime )
// Should lose leadership.
select {
case <- leaderCh :
case <- time . After ( time . Second ) :
t . Fatalf ( "should not be leader" )
}
}
func TestAPI_LockOneShot ( t * testing . T ) {
t . Parallel ( )
c , s := makeClientWithoutConnect ( t )
defer s . Stop ( )
s . WaitForSerfCheck ( t )
// Set up a lock as a one-shot.
opts := & LockOptions {
Key : "test/lock" ,
LockTryOnce : true ,
}
lock , err := c . LockOpts ( opts )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
// Make sure the default got set.
if lock . opts . LockWaitTime != DefaultLockWaitTime {
t . Fatalf ( "bad: %d" , lock . opts . LockWaitTime )
}
// Now set a custom time for the test.
opts . LockWaitTime = 250 * time . Millisecond
lock , err = c . LockOpts ( opts )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if lock . opts . LockWaitTime != 250 * time . Millisecond {
t . Fatalf ( "bad: %d" , lock . opts . LockWaitTime )
}
// Should get the lock.
ch , err := lock . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if ch == nil {
t . Fatalf ( "not leader" )
}
// Now try with another session.
contender , err := c . LockOpts ( opts )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
start := time . Now ( )
ch , err = contender . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if ch != nil {
t . Fatalf ( "should not be leader" )
}
diff := time . Since ( start )
if diff < contender . opts . LockWaitTime || diff > 2 * contender . opts . LockWaitTime {
t . Fatalf ( "time out of bounds: %9.6f" , diff . Seconds ( ) )
}
// Unlock and then make sure the contender can get it.
if err := lock . Unlock ( ) ; err != nil {
t . Fatalf ( "err: %v" , err )
}
ch , err = contender . Lock ( nil )
if err != nil {
t . Fatalf ( "err: %v" , err )
}
if ch == nil {
t . Fatalf ( "should be leader" )
}
}