Make SetHeaderNormalizer safe to call while decoding
SetHeaderNormalizer reassigned the normalizer and copied a fresh sync.Map over the struct info cache while other goroutines were using it. Keep both in one value swapped atomically instead. Fixes #237.
This commit is contained in:
1 parent
391e8f0876
commit
f87ec7522c
3 files changed
+60
-8
No files matched your search
@@ -16,6 +16,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// FailIfUnmatchedStructTags indicates whether it is considered an error when there is an unmatched
|
||||
@@ -47,17 +48,36 @@ type Normalizer func(string) string
|
||||
|
||||
type ErrorHandler func(*csv.ParseError) bool
|
||||
|
||||
// normalizeName function initially set to a nop Normalizer.
|
||||
var normalizeName = DefaultNameNormalizer()
|
||||
// headerNormalization is the Normalizer in use together with the struct infos
|
||||
// built with it. SetHeaderNormalizer replaces both at once, so it is safe to
|
||||
// call while other goroutines are reading or writing CSV.
|
||||
type headerNormalization struct {
|
||||
normalize Normalizer
|
||||
structInfos sync.Map
|
||||
}
|
||||
|
||||
// currentHeaderNormalization holds a *headerNormalization, initially with a nop Normalizer.
|
||||
var currentHeaderNormalization atomic.Value
|
||||
|
||||
func init() {
|
||||
currentHeaderNormalization.Store(&headerNormalization{normalize: DefaultNameNormalizer()})
|
||||
}
|
||||
|
||||
func loadHeaderNormalization() *headerNormalization {
|
||||
return currentHeaderNormalization.Load().(*headerNormalization)
|
||||
}
|
||||
|
||||
func normalizeName(name string) string {
|
||||
return loadHeaderNormalization().normalize(name)
|
||||
}
|
||||
|
||||
// DefaultNameNormalizer is a nop Normalizer.
|
||||
func DefaultNameNormalizer() Normalizer { return func(s string) string { return s } }
|
||||
|
||||
// SetHeaderNormalizer sets the normalizer used to normalize struct and header field names.
|
||||
func SetHeaderNormalizer(f Normalizer) {
|
||||
normalizeName = f
|
||||
// Need to clear the cache hen the header normalizer changes.
|
||||
structInfoCache = sync.Map{}
|
||||
// The struct infos are rebuilt from scratch, as they hold normalized names.
|
||||
currentHeaderNormalization.Store(&headerNormalization{normalize: f})
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
+32
@@ -2,6 +2,8 @@ package gocsv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -25,6 +27,36 @@ func TestUnmarshalToCallback_ReaderError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetHeaderNormalizerWhileUnmarshaling(t *testing.T) {
|
||||
defer SetHeaderNormalizer(DefaultNameNormalizer())
|
||||
|
||||
type row struct {
|
||||
ID string `csv:"id"`
|
||||
Name string `csv:"name"`
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
SetHeaderNormalizer(strings.ToLower)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var rows []row
|
||||
if err := UnmarshalString("id,name\n1,foo\n", &rows); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if len(rows) != 1 || rows[0].ID != "1" || rows[0].Name != "foo" {
|
||||
t.Errorf("unexpected rows: %+v", rows)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read([]byte) (n int, err error) {
|
||||
|
||||
+3
-3
@@ -58,19 +58,19 @@ func removeZeroWidthChars(s string) string {
|
||||
}, s)
|
||||
}
|
||||
|
||||
var structInfoCache sync.Map
|
||||
var structMap = make(map[reflect.Type]*structInfo)
|
||||
var structMapMutex sync.RWMutex
|
||||
|
||||
func getStructInfo(rType reflect.Type) *structInfo {
|
||||
stInfo, ok := structInfoCache.Load(rType)
|
||||
structInfos := &loadHeaderNormalization().structInfos
|
||||
stInfo, ok := structInfos.Load(rType)
|
||||
if ok {
|
||||
return stInfo.(*structInfo)
|
||||
}
|
||||
|
||||
fieldsList, err := getFieldInfos(rType, []int{}, []string{}, make(map[reflect.Type]bool))
|
||||
stInfo = &structInfo{Fields: fieldsList, Err: err}
|
||||
structInfoCache.Store(rType, stInfo)
|
||||
structInfos.Store(rType, stInfo)
|
||||
|
||||
return stInfo.(*structInfo)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user