Skip to content

Commit f757c9f

Browse files
emosbaughnicksnyder
authored andcommitted
Fixes #54 - make bundle safe for concurrent map reads and writes
1 parent 991e81c commit f757c9f

File tree

2 files changed

+92
-13
lines changed

2 files changed

+92
-13
lines changed

i18n/bundle/bundle.go

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ package bundle
44
import (
55
"encoding/json"
66
"fmt"
7-
"gopkg.in/yaml.v2"
87
"io/ioutil"
9-
"reflect"
10-
118
"path/filepath"
9+
"reflect"
10+
"sync"
1211

1312
"github.com/nicksnyder/go-i18n/i18n/language"
1413
"github.com/nicksnyder/go-i18n/i18n/translation"
14+
"gopkg.in/yaml.v2"
1515
)
1616

1717
// TranslateFunc is a copy of i18n.TranslateFunc to avoid a circular dependency.
@@ -24,6 +24,8 @@ type Bundle struct {
2424

2525
// Translations that can be used when an exact language match is not possible.
2626
fallbackTranslations map[string]map[string]translation.Translation
27+
28+
sync.RWMutex
2729
}
2830

2931
// New returns an empty bundle.
@@ -108,6 +110,8 @@ func parseTranslations(filename string, buf []byte) ([]translation.Translation,
108110
//
109111
// It is useful if your translations are in a format not supported by LoadTranslationFile.
110112
func (b *Bundle) AddTranslation(lang *language.Language, translations ...translation.Translation) {
113+
b.Lock()
114+
defer b.Unlock()
111115
if b.translations[lang.Tag] == nil {
112116
b.translations[lang.Tag] = make(map[string]translation.Translation, len(translations))
113117
}
@@ -128,24 +132,37 @@ func (b *Bundle) AddTranslation(lang *language.Language, translations ...transla
128132

129133
// Translations returns all translations in the bundle.
130134
func (b *Bundle) Translations() map[string]map[string]translation.Translation {
131-
return b.translations
135+
t := make(map[string]map[string]translation.Translation)
136+
b.RLock()
137+
for tag, translations := range b.translations {
138+
t[tag] = make(map[string]translation.Translation)
139+
for id, translation := range translations {
140+
t[tag][id] = translation
141+
}
142+
}
143+
b.RUnlock()
144+
return t
132145
}
133146

134147
// LanguageTags returns the tags of all languages that that have been added.
135148
func (b *Bundle) LanguageTags() []string {
136149
var tags []string
150+
b.RLock()
137151
for k := range b.translations {
138152
tags = append(tags, k)
139153
}
154+
b.RUnlock()
140155
return tags
141156
}
142157

143158
// LanguageTranslationIDs returns the ids of all translations that have been added for a given language.
144159
func (b *Bundle) LanguageTranslationIDs(languageTag string) []string {
145160
var ids []string
161+
b.RLock()
146162
for id := range b.translations[languageTag] {
147163
ids = append(ids, id)
148164
}
165+
b.RUnlock()
149166
return ids
150167
}
151168

@@ -212,6 +229,8 @@ func (b *Bundle) supportedLanguage(pref string, prefs ...string) *language.Langu
212229

213230
func (b *Bundle) translatedLanguage(src string) *language.Language {
214231
langs := language.Parse(src)
232+
b.RLock()
233+
defer b.RUnlock()
215234
for _, lang := range langs {
216235
if len(b.translations[lang.Tag]) > 0 ||
217236
len(b.fallbackTranslations[lang.Tag]) > 0 {
@@ -226,15 +245,7 @@ func (b *Bundle) translate(lang *language.Language, translationID string, args .
226245
return translationID
227246
}
228247

229-
translations := b.translations[lang.Tag]
230-
if translations == nil {
231-
translations = b.fallbackTranslations[lang.Tag]
232-
if translations == nil {
233-
return translationID
234-
}
235-
}
236-
237-
translation := translations[translationID]
248+
translation := b.translation(lang, translationID)
238249
if translation == nil {
239250
return translationID
240251
}
@@ -280,6 +291,19 @@ func (b *Bundle) translate(lang *language.Language, translationID string, args .
280291
return s
281292
}
282293

294+
func (b *Bundle) translation(lang *language.Language, translationID string) translation.Translation {
295+
b.RLock()
296+
defer b.RUnlock()
297+
translations := b.translations[lang.Tag]
298+
if translations == nil {
299+
translations = b.fallbackTranslations[lang.Tag]
300+
if translations == nil {
301+
return nil
302+
}
303+
}
304+
return translations[translationID]
305+
}
306+
283307
func isNumber(n interface{}) bool {
284308
switch n.(type) {
285309
case int, int8, int16, int32, int64, string:

i18n/bundle/bundle_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package bundle
22

33
import (
44
"fmt"
5+
"strconv"
6+
"sync"
57
"testing"
68

79
"reflect"
@@ -160,6 +162,59 @@ func TestTfuncAndLanguage(t *testing.T) {
160162
}
161163
}
162164

165+
func TestConcurrent(t *testing.T) {
166+
b := New()
167+
// bootstrap bundle
168+
translationID := "translation_id" // +1
169+
englishLanguage := languageWithTag("en-US")
170+
addFakeTranslation(t, b, englishLanguage, translationID)
171+
172+
tf, err := b.Tfunc(englishLanguage.Tag)
173+
if err != nil {
174+
t.Errorf("Tfunc(%v) = error{%q}; expected no error", []string{englishLanguage.Tag}, err)
175+
}
176+
177+
const iterations = 1000
178+
var wg sync.WaitGroup
179+
wg.Add(iterations)
180+
181+
// Using go routines insert 1000 ints into our map.
182+
go func() {
183+
for i := 0; i < iterations/2; i++ {
184+
// Add item to map.
185+
translationID := strconv.FormatInt(int64(i), 10)
186+
addFakeTranslation(t, b, englishLanguage, translationID)
187+
188+
// Retrieve item from map.
189+
tf(translationID)
190+
191+
wg.Done()
192+
} // Call go routine with current index.
193+
}()
194+
195+
go func() {
196+
for i := iterations / 2; i < iterations; i++ {
197+
// Add item to map.
198+
translationID := strconv.FormatInt(int64(i), 10)
199+
addFakeTranslation(t, b, englishLanguage, translationID)
200+
201+
// Retrieve item from map.
202+
tf(translationID)
203+
204+
wg.Done()
205+
} // Call go routine with current index.
206+
}()
207+
208+
// Wait for all go routines to finish.
209+
wg.Wait()
210+
211+
// Make sure map contains 1000+1 elements.
212+
count := len(b.Translations()[englishLanguage.Tag])
213+
if count != iterations+1 {
214+
t.Error("Expecting 1001 elements, got", count)
215+
}
216+
}
217+
163218
func addFakeTranslation(t *testing.T, b *Bundle, lang *language.Language, translationID string) string {
164219
translation := fakeTranslation(lang, translationID)
165220
b.AddTranslation(lang, testNewTranslation(t, map[string]interface{}{

0 commit comments

Comments
 (0)