blob: 8245e26b0eb429ca35c3257f5623c1bef25c1265 [file] [log] [blame]
/*
* Copyright 2021-2023 Open Networking Foundation (ONF) and the ONF Contributors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kvstore
import (
"context"
"os"
"strconv"
"sync"
"testing"
"time"
"github.com/opencord/voltha-lib-go/v7/pkg/log"
mocks "github.com/opencord/voltha-lib-go/v7/pkg/mocks/etcd"
"github.com/phayes/freeport"
"github.com/stretchr/testify/assert"
)
const (
embedEtcdServerHost = "localhost"
defaultTimeout = 1 * time.Second
)
var (
embedEtcdServerPort int
)
func TestMain(m *testing.M) {
ctx := context.Background()
var err error
embedEtcdServerPort, err = freeport.GetFreePort()
if err != nil {
logger.Fatal(ctx, err)
}
peerPort, err := freeport.GetFreePort()
if err != nil {
logger.Fatal(ctx, err)
}
etcdServer := mocks.StartEtcdServer(ctx, mocks.MKConfig(ctx,
"voltha.db.kvstore.test",
embedEtcdServerPort,
peerPort,
"voltha.lib.db.kvstore",
"error"))
res := m.Run()
etcdServer.Stop(ctx)
os.Exit(res)
}
func TestNewRoundRobinEtcdClientAllocator(t *testing.T) {
address := embedEtcdServerHost + ":" + strconv.Itoa(embedEtcdServerPort)
capacity := 20
maxUsage := 10
pool, err := NewRoundRobinEtcdClientAllocator([]string{address}, defaultTimeout, capacity, maxUsage, log.ErrorLevel)
assert.NotNil(t, pool)
assert.Nil(t, err)
}
func TestRoundRobin_Get_Put(t *testing.T) {
address := embedEtcdServerHost + ":" + strconv.Itoa(embedEtcdServerPort)
capacity := 20
maxUsage := 10
pool, err := NewRoundRobinEtcdClientAllocator([]string{address}, defaultTimeout, capacity, maxUsage, log.ErrorLevel)
assert.NotNil(t, pool)
assert.Nil(t, err)
// Verify we can obtain the expected number of clients with no errors or waiting time
var wg sync.WaitGroup
for i := 0; i < capacity*maxUsage; i++ {
wg.Add(1)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
c, err := pool.Get(ctx)
assert.NotNil(t, c)
assert.Nil(t, err)
time.Sleep(5 * time.Second)
pool.Put(c)
wg.Done()
}()
}
wg.Wait()
}
func TestRoundRobin_Get_Put_various_capacity(t *testing.T) {
// Test single client with 1 concurrent usage
getPutVaryingCapacity(t, 1, 1)
// Test single client with multiple concurrent usage
getPutVaryingCapacity(t, 1, 100)
// Test multiple clients with single concurrent usage
getPutVaryingCapacity(t, 10, 1)
// Test multiple clients with multiple concurrent usage
getPutVaryingCapacity(t, 10, 10)
}
func getPutVaryingCapacity(t *testing.T, capacity, maxUsage int) {
address := embedEtcdServerHost + ":" + strconv.Itoa(embedEtcdServerPort)
pool, err := NewRoundRobinEtcdClientAllocator([]string{address}, defaultTimeout, capacity, maxUsage, log.ErrorLevel)
assert.NotNil(t, pool)
assert.Nil(t, err)
// Verify we can obtain the expected number of clients with no errors or waiting time
var wg sync.WaitGroup
totalSize := capacity * maxUsage
ch := make(chan struct{})
for i := 0; i < totalSize; i++ {
wg.Add(1)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
c, err := pool.Get(ctx)
assert.NotNil(t, c)
assert.Nil(t, err)
// Inform the waiting loop that a client has been allocated
ch <- struct{}{}
// Keep the client for 5s and then return it to the pool
time.Sleep(5 * time.Second)
pool.Put(c)
wg.Done()
}()
}
// Wait until all clients are allocated
allocated := 0
for range ch {
allocated++
if allocated == totalSize {
break
}
}
// Try to get above capacity/usage with low timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
c, err := pool.Get(ctx)
assert.NotNil(t, err)
assert.Nil(t, c)
cancel()
// Try to get above capacity/usage with longer timeout
ctx, cancel = context.WithTimeout(context.Background(), 6*time.Second)
c, err = pool.Get(ctx)
assert.NotNil(t, c)
assert.Nil(t, err)
pool.Put(c)
cancel()
wg.Wait()
// Close the connection
pool.Close(context.Background())
}
func TestRoundRobin_Close_various_capacity(t *testing.T) {
// Test single client with 1 concurrent usage
closeWithVaryingCapacity(t, 1, 1)
// Test single client with multiple concurrent usage
closeWithVaryingCapacity(t, 1, 100)
// Test multiple clients with single concurrent usage
closeWithVaryingCapacity(t, 10, 1)
// Test multiple clients with multiple concurrent usage
closeWithVaryingCapacity(t, 10, 10)
}
func closeWithVaryingCapacity(t *testing.T, capacity, maxUsage int) {
address := embedEtcdServerHost + ":" + strconv.Itoa(embedEtcdServerPort)
pool, err := NewRoundRobinEtcdClientAllocator([]string{address}, defaultTimeout, capacity, maxUsage, log.ErrorLevel)
assert.NotNil(t, pool)
assert.Nil(t, err)
// Verify we can obtain the expected number of clients with no errors or waiting time
var wg sync.WaitGroup
totalSize := capacity * maxUsage
ch := make(chan struct{})
for i := 0; i < totalSize; i++ {
wg.Add(1)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
c, err := pool.Get(ctx)
assert.NotNil(t, c)
assert.Nil(t, err)
// Inform the waiting loop that a client has been allocated
ch <- struct{}{}
// Keep the client for 5s and then return it to the pool
time.Sleep(5 * time.Second)
pool.Put(c)
wg.Done()
}()
}
// Wait until all clients are allocated
allocated := 0
for range ch {
allocated++
if allocated == totalSize {
break
}
}
// Try to get above capacity/usage with longer timeout
wg.Add(1)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
c, err := pool.Get(ctx)
assert.NotNil(t, err)
expected := err.Error() == "pool-is-closing" || err.Error() == "stop-waiting-pool-is-closing"
assert.True(t, expected)
assert.Nil(t, c)
cancel()
wg.Done()
}()
// Invoke close on the pool
wg.Add(1)
go func() {
pool.Close(context.Background())
wg.Done()
}()
// Try to get new client and ensure we can't get one
c, err := pool.Get(context.Background())
assert.NotNil(t, err)
expected := err.Error() == "pool-is-closing" || err.Error() == "stop-waiting-pool-is-closing"
assert.True(t, expected)
assert.Nil(t, c)
wg.Wait()
}