Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/go-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Go Tests

on:
push:
branches: [ 'master' ]
pull_request:
branches: [ '*' ]

jobs:
test:
name: Run Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Go 1.18
uses: actions/setup-go@v3
with:
go-version: 1.18
cache: true

- name: Install dependencies
run: go mod download

- name: Run tests
run: |
find . -type f -name "*_test.go" -not -path "./vendor/*" |
xargs dirname |
sort -u |
xargs -I{} sh -c 'echo "Testing {}"; cd {} && go test -v ./...'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
vendor/
.idea
297 changes: 297 additions & 0 deletions concurrent_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
package goconfig_test

import (
"github.com/gojekfarm/goconfig/v2"
"runtime"
"sync"
"testing"
"time"
)

func TestConcurrentGetValue(t *testing.T) {
Comment thread
tejasjadhav marked this conversation as resolved.
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 100
const numOperations = 1000
var wg sync.WaitGroup

// Test concurrent access to the same key
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < numOperations; j++ {
value := baseConfig.GetValue("foo")
if value != "bar" {
t.Errorf("Expected 'bar', got '%s'", value)
}
}
}()
}

wg.Wait()
}

func TestConcurrentGetValueDifferentKeys(t *testing.T) {
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 50
var wg sync.WaitGroup

// Test concurrent access to different keys
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
if goroutineID%2 == 0 {
value := baseConfig.GetValue("foo")
if value != "bar" {
t.Errorf("Expected 'bar', got '%s'", value)
}
} else {
value := baseConfig.GetValue("new_relic_app_name")
if value != "foo" {
t.Errorf("Expected 'foo', got '%s'", value)
}
}
}(i)
}

wg.Wait()
}

func TestConcurrentGetIntValue(t *testing.T) {
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 100
const numOperations = 500
var wg sync.WaitGroup

for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < numOperations; j++ {
value := baseConfig.GetIntValue("someInt")
if value != 1 {
t.Errorf("Expected 1, got %d", value)
}
}
}()
}

wg.Wait()
}

func TestConcurrentGetOptionalValue(t *testing.T) {
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 50
var wg sync.WaitGroup

for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Test existing key
value := baseConfig.GetOptionalValue("foo", "default")
if value != "bar" {
t.Errorf("Expected 'bar', got '%s'", value)
}

// Test non-existing key with default
value = baseConfig.GetOptionalValue("nonexistent", "default")
if value != "default" {
t.Errorf("Expected 'default', got '%s'", value)
}
}()
}

wg.Wait()
}

func TestConcurrentGetOptionalIntValue(t *testing.T) {
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 50
var wg sync.WaitGroup

for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Test existing key
value := baseConfig.GetOptionalIntValue("someInt", 999)
if value != 1 {
t.Errorf("Expected 1, got %d", value)
}

// Test non-existing key with default
value = baseConfig.GetOptionalIntValue("nonexistentInt", 999)
if value != 999 {
t.Errorf("Expected 999, got %d", value)
}
}()
}

wg.Wait()
}

func TestConcurrentGetFeature(t *testing.T) {
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 50
var wg sync.WaitGroup

for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Test feature that is true
value := baseConfig.GetFeature("someFeature")
if !value {
t.Errorf("Expected true, got %v", value)
}

// Test feature that is false
value = baseConfig.GetFeature("someOtherFeature")
if value {
t.Errorf("Expected false, got %v", value)
}
}()
}

wg.Wait()
}

func TestConcurrentMixedOperations(t *testing.T) {
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 20
const numOperations = 100
var wg sync.WaitGroup

// Test mixed concurrent operations
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < numOperations; j++ {
switch goroutineID % 5 {
case 0:
baseConfig.GetValue("foo")
case 1:
baseConfig.GetIntValue("someInt")
case 2:
baseConfig.GetOptionalValue("foo", "default")
case 3:
baseConfig.GetOptionalIntValue("someInt", 999)
case 4:
baseConfig.GetFeature("someFeature")
}
}
}(i)
}

wg.Wait()
}

func TestNoDeadlockScenario(t *testing.T) {
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 100
var wg sync.WaitGroup
timeout := time.After(10 * time.Second)
done := make(chan bool)

// Start goroutines
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Rapidly access different methods that use the same mutex
for j := 0; j < 100; j++ {
baseConfig.GetValue("foo")
baseConfig.GetIntValue("someInt")
baseConfig.GetOptionalValue("nonexistent", "default")
baseConfig.GetOptionalIntValue("nonexistentInt", 999)
baseConfig.GetFeature("someFeature")
}
}()
}

// Wait for completion or timeout
go func() {
wg.Wait()
done <- true
}()

select {
case <-done:
// Test completed successfully, no deadlock
case <-timeout:
t.Fatal("Test timed out - possible deadlock detected")
}
}

func TestRaceConditionDetection(t *testing.T) {
baseConfig := goconfig.NewBaseConfig()
baseConfig.Load()

const numGoroutines = 50
var wg sync.WaitGroup

// Create high contention scenario
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
// All goroutines access the same key to create contention
baseConfig.GetValue("foo")
runtime.Gosched() // Yield to increase chance of race conditions
}
}()
}

wg.Wait()
}

func TestConcurrentCacheEviction(t *testing.T) {
// Test scenario where cache might be cleared while being accessed
baseConfig := goconfig.BaseConfig{}
baseConfig.Load()

const numGoroutines = 20
var wg sync.WaitGroup

for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(goroutineID int) {
defer wg.Done()
if goroutineID == 0 {
// One goroutine periodically clears cache
for j := 0; j < 10; j++ {
time.Sleep(50 * time.Millisecond)
baseConfig.Load()
}
} else {
// Other goroutines continuously access values
for j := 0; j < 100; j++ {
baseConfig.GetValue("foo")
time.Sleep(10 * time.Millisecond)
}
}
}(i)
}

wg.Wait()
}
Loading