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"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FailIfUnmatchedStructTags indicates whether it is considered an error when there is an unmatched
|
// 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
|
type ErrorHandler func(*csv.ParseError) bool
|
||||||
|
|
||||||
// normalizeName function initially set to a nop Normalizer.
|
// headerNormalization is the Normalizer in use together with the struct infos
|
||||||
var normalizeName = DefaultNameNormalizer()
|
// 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.
|
// DefaultNameNormalizer is a nop Normalizer.
|
||||||
func DefaultNameNormalizer() Normalizer { return func(s string) string { return s } }
|
func DefaultNameNormalizer() Normalizer { return func(s string) string { return s } }
|
||||||
|
|
||||||
// SetHeaderNormalizer sets the normalizer used to normalize struct and header field names.
|
// SetHeaderNormalizer sets the normalizer used to normalize struct and header field names.
|
||||||
func SetHeaderNormalizer(f Normalizer) {
|
func SetHeaderNormalizer(f Normalizer) {
|
||||||
normalizeName = f
|
// The struct infos are rebuilt from scratch, as they hold normalized names.
|
||||||
// Need to clear the cache hen the header normalizer changes.
|
currentHeaderNormalization.Store(&headerNormalization{normalize: f})
|
||||||
structInfoCache = sync.Map{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|||||||
+32
@@ -2,6 +2,8 @@ package gocsv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"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{}
|
type errorReader struct{}
|
||||||
|
|
||||||
func (e *errorReader) Read([]byte) (n int, err error) {
|
func (e *errorReader) Read([]byte) (n int, err error) {
|
||||||
|
|||||||
+3
-3
@@ -58,19 +58,19 @@ func removeZeroWidthChars(s string) string {
|
|||||||
}, s)
|
}, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
var structInfoCache sync.Map
|
|
||||||
var structMap = make(map[reflect.Type]*structInfo)
|
var structMap = make(map[reflect.Type]*structInfo)
|
||||||
var structMapMutex sync.RWMutex
|
var structMapMutex sync.RWMutex
|
||||||
|
|
||||||
func getStructInfo(rType reflect.Type) *structInfo {
|
func getStructInfo(rType reflect.Type) *structInfo {
|
||||||
stInfo, ok := structInfoCache.Load(rType)
|
structInfos := &loadHeaderNormalization().structInfos
|
||||||
|
stInfo, ok := structInfos.Load(rType)
|
||||||
if ok {
|
if ok {
|
||||||
return stInfo.(*structInfo)
|
return stInfo.(*structInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldsList, err := getFieldInfos(rType, []int{}, []string{}, make(map[reflect.Type]bool))
|
fieldsList, err := getFieldInfos(rType, []int{}, []string{}, make(map[reflect.Type]bool))
|
||||||
stInfo = &structInfo{Fields: fieldsList, Err: err}
|
stInfo = &structInfo{Fields: fieldsList, Err: err}
|
||||||
structInfoCache.Store(rType, stInfo)
|
structInfos.Store(rType, stInfo)
|
||||||
|
|
||||||
return stInfo.(*structInfo)
|
return stInfo.(*structInfo)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user