Skip to content

Commit

Permalink
fix: concurrent-safe unique data generation (#53)
Browse files Browse the repository at this point in the history
* fix: concurent map writing issue

* fix: test for concurrent write on unique field
  • Loading branch information
bxcodec authored Jun 8, 2024
1 parent df18fe6 commit 9cbcda8
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 9 deletions.
17 changes: 10 additions & 7 deletions faker.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (

var (
// Unique values are kept in memory so the generator retries if the value already exists
uniqueValues = map[string][]interface{}{}
uniqueValues = &sync.Map{}
)

// Supported tags
Expand Down Expand Up @@ -266,7 +266,7 @@ func init() {
// ResetUnique is used to forget generated unique values.
// Call this when you're done generating a dataset.
func ResetUnique() {
uniqueValues = map[string][]interface{}{}
uniqueValues = &sync.Map{}
}

var (
Expand Down Expand Up @@ -501,15 +501,16 @@ func getFakedValue(item interface{}, opts *options.Options) (reflect.Value, erro
if retry >= maxRetry {
return reflect.Value{}, fmt.Errorf(fakerErrors.ErrUniqueFailure, reflect.TypeOf(item).Field(i).Name)
}

value := v.Field(i).Interface()
if slice.ContainsValue(uniqueValues[tags.fieldType], value) { // Retry if unique value already found
uniqueVal, _ := uniqueValues.Load(tags.fieldType)
uniqueValArr, _ := uniqueVal.([]interface{})
if slice.ContainsValue(uniqueValArr, value) { // Retry if unique value already found
i--
retry++
continue
}
retry = 0
uniqueValues[tags.fieldType] = append(uniqueValues[tags.fieldType], value)
uniqueValues.Store(tags.fieldType, append(uniqueValArr, value))
} else {
retry = 0
}
Expand Down Expand Up @@ -1380,8 +1381,10 @@ func RandomInt(parameters ...int) (p []int, err error) {
func generateUnique(dataType string, fn func() interface{}) (interface{}, error) {
for i := 0; i < maxRetry; i++ {
value := fn()
if !slice.ContainsValue(uniqueValues[dataType], value) { // Retry if unique value already found
uniqueValues[dataType] = append(uniqueValues[dataType], value)
uniqueVal, _ := uniqueValues.Load(dataType)
uniqueValArr, _ := uniqueVal.([]interface{})
if !slice.ContainsValue(uniqueValArr, value) { // Retry if unique value already found
uniqueValues.Store(dataType, append(uniqueValArr, value))
return value, nil
}
}
Expand Down
38 changes: 36 additions & 2 deletions faker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1503,7 +1503,9 @@ func TestUnique(t *testing.T) {
}

found := []interface{}{}
for _, v := range uniqueValues["word"] {
uniqueVal, _ := uniqueValues.Load("word")
uniqueValArr := uniqueVal.([]interface{})
for _, v := range uniqueValArr {
for _, f := range found {
if f == v {
t.Errorf("expected unique values, found \"%s\" at least twice", v)
Expand All @@ -1517,6 +1519,30 @@ func TestUnique(t *testing.T) {
ResetUnique()
}

func TestUniqueInConcurrentParallel(t *testing.T) {
t.Parallel()

var wg sync.WaitGroup
wg.Add(3)

go func() {
defer wg.Done()
Username(options.WithGenerateUniqueValues(true))
}()

go func() {
defer wg.Done()
IPv4(options.WithGenerateUniqueValues(true))
}()

go func() {
defer wg.Done()
Email(options.WithGenerateUniqueValues(true))
}()

wg.Wait()
}

func TestUniqueReset(t *testing.T) {
type String struct {
StringVal string `faker:"word,unique"`
Expand All @@ -1531,7 +1557,15 @@ func TestUniqueReset(t *testing.T) {
}

ResetUnique()
length := len(uniqueValues)
var getSize = func(m *sync.Map) (count int) {
m.Range(func(_, _ interface{}) bool {
count++
return true
})
return
}

length := getSize(uniqueValues)
if length > 0 {
t.Errorf("expected empty uniqueValues map, but got a length of %d", length)
}
Expand Down

0 comments on commit 9cbcda8

Please sign in to comment.