blob: 6325dd907553c55e25b91e9035fd7be6fb95733c [file] [log] [blame]
package reconnectingmemcached
import (
"errors"
"sync"
"testing"
"time"
"github.com/bradfitz/gomemcache/memcache"
"github.com/stretchr/testify/assert"
)
func TestGetMulti_BooleanRepresentsConnectionState(t *testing.T) {
hc, fmc := makeClientWithFakeMemcache()
_, ok := hc.GetMulti([]string{"whatever"})
assert.True(t, ok)
fmc.isDown = true
_, ok = hc.GetMulti([]string{"whatever"})
assert.False(t, ok)
}
func TestSet_BooleanRepresentsConnectionState(t *testing.T) {
hc, fmc := makeClientWithFakeMemcache()
assert.True(t, hc.Set(&memcache.Item{}))
fmc.isDown = true
assert.False(t, hc.Set(&memcache.Item{}))
}
func TestPing_ReturnsErrorOnBadConnection(t *testing.T) {
hc, fmc := makeClientWithFakeMemcache()
assert.NoError(t, hc.Ping())
fmc.isDown = true
assert.Error(t, hc.Ping())
}
func TestRecovery_ConnectionReattemptedAfterAFewSeconds(t *testing.T) {
hc, fmc := makeClientWithFakeMemcache()
hc.numFailures = 5
hc.recoveryDuration = time.Second
fmc.isDown = true
// Connection hasn't been detect as down yet
assert.True(t, hc.ConnectionAvailable())
// Inject a few more failures than required to make sure we don't block until healed.
const failuresToInject = 10
wc := sync.WaitGroup{}
wc.Add(failuresToInject)
for i := 0; i < failuresToInject; i++ {
go func(isSet bool) {
defer wc.Done()
if isSet {
assert.False(t, hc.Set(&memcache.Item{}))
} else {
_, ok := hc.GetMulti([]string{"whatever"})
assert.False(t, ok)
}
}(i%2 == 0)
}
wc.Wait()
// Connection should be down and healing
assert.False(t, hc.ConnectionAvailable())
// Things should be returning false
assert.False(t, hc.Set(&memcache.Item{}))
_, ok := hc.GetMulti([]string{"whatever"})
assert.False(t, ok)
assert.Error(t, hc.Ping())
// Wait until we are sure the connection has been restored.
time.Sleep(hc.recoveryDuration*2 + time.Second)
// Connection should be back up
assert.True(t, hc.ConnectionAvailable())
// Things should be returning true again
assert.True(t, hc.Set(&memcache.Item{}))
_, ok = hc.GetMulti([]string{"whatever"})
assert.True(t, ok)
assert.NoError(t, hc.Ping())
}
func TestRecovery_HealsAfterThirdTry(t *testing.T) {
const requiredRecoveryAttempts = 3
recoveryAttempts := 0
hc, fmc := makeClientWithFakeMemcache()
hc.numFailures = 0
hc.clientFactory = func(_ Options) memcachedClient {
recoveryAttempts++
if recoveryAttempts >= requiredRecoveryAttempts {
fmc.recover()
}
return fmc
}
hc.recoveryDuration = time.Millisecond
fmc.isDown = true
_, ok := hc.GetMulti([]string{"whatever"})
assert.False(t, ok)
time.Sleep(time.Second)
assert.True(t, hc.ConnectionAvailable())
assert.Equal(t, 3, recoveryAttempts)
}
func makeClientWithFakeMemcache() (*healingClientImpl, *fakeMemcacheClient) {
fmc := &fakeMemcacheClient{}
return &healingClientImpl{
client: fmc,
clientFactory: func(_ Options) memcachedClient {
// Call recover to signal connection restored and then return the
// same client to make it easy to handle assertions.
fmc.recover()
return fmc
},
}, fmc
}
type fakeMemcacheClient struct {
isDown bool
mutex sync.RWMutex
}
func (f *fakeMemcacheClient) Ping() error {
f.mutex.RLock()
defer f.mutex.RUnlock()
if f.isDown {
return errors.New("down")
}
return nil
}
func (f *fakeMemcacheClient) GetMulti(_ []string) (map[string]*memcache.Item, error) {
f.mutex.RLock()
defer f.mutex.RUnlock()
if f.isDown {
return nil, errors.New("down")
}
return map[string]*memcache.Item{}, nil
}
func (f *fakeMemcacheClient) Set(_ *memcache.Item) error {
f.mutex.RLock()
defer f.mutex.RUnlock()
if f.isDown {
return errors.New("down")
}
return nil
}
func (f *fakeMemcacheClient) recover() {
f.mutex.Lock()
defer f.mutex.Unlock()
f.isDown = false
}