From 1fb2b790d8228a1167ea04a7e0aa643dd71182fd Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 28 Jun 2022 19:10:33 +0800 Subject: [PATCH 1/5] Make i18n live-reload work for default language --- modules/translation/i18n/i18n.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index acce5f19fb0dc..9c428ad88fa02 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -138,6 +138,13 @@ func (l *locale) Tr(trKey string, trArgs ...interface{}) string { if l.store.reloadMu != nil { l.store.reloadMu.Lock() defer l.store.reloadMu.Unlock() + } + msg, _ := l.tryTr(trKey, trArgs...) + return msg +} + +func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found bool) { + if l.store.reloadMu != nil { now := time.Now() if now.Sub(l.lastReloadCheckTime) >= time.Second && l.sourceFileInfo != nil && l.sourceFileName != "" { l.lastReloadCheckTime = now @@ -150,11 +157,6 @@ func (l *locale) Tr(trKey string, trArgs ...interface{}) string { } } } - msg, _ := l.tryTr(trKey, trArgs...) - return msg -} - -func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found bool) { trMsg := trKey textIdx, ok := l.store.textIdxMap[trKey] if ok { From b4c1230c531774732e1de8c1a04fbf4180ed4dc3 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 28 Jun 2022 23:53:05 +0800 Subject: [PATCH 2/5] use RWMutex instead of Mutex --- modules/translation/i18n/i18n.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index 9c428ad88fa02..4fddeab3537fd 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -35,7 +35,7 @@ type locale struct { } type LocaleStore struct { - reloadMu *sync.Mutex // for non-prod(dev), use a mutex for live-reload. for prod, no mutex, no live-reload. + reloadMu *sync.RWMutex // for non-prod(dev), use a mutex for live-reload. for prod, no mutex, no live-reload. langNames []string langDescs []string @@ -49,7 +49,7 @@ type LocaleStore struct { func NewLocaleStore(isProd bool) *LocaleStore { ls := &LocaleStore{localeMap: make(map[string]*locale), textIdxMap: make(map[string]int)} if !isProd { - ls.reloadMu = &sync.Mutex{} + ls.reloadMu = &sync.RWMutex{} } return ls } @@ -136,8 +136,8 @@ func (ls *LocaleStore) Tr(lang, trKey string, trArgs ...interface{}) string { // Tr translates content to locale language. fall back to default language. func (l *locale) Tr(trKey string, trArgs ...interface{}) string { if l.store.reloadMu != nil { - l.store.reloadMu.Lock() - defer l.store.reloadMu.Unlock() + l.store.reloadMu.RLock() + defer l.store.reloadMu.RUnlock() } msg, _ := l.tryTr(trKey, trArgs...) return msg @@ -147,6 +147,8 @@ func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found b if l.store.reloadMu != nil { now := time.Now() if now.Sub(l.lastReloadCheckTime) >= time.Second && l.sourceFileInfo != nil && l.sourceFileName != "" { + l.store.reloadMu.RUnlock() // if the locale file should be reloaded, then we release the read-lock + l.store.reloadMu.Lock() // and acquire the write-lock l.lastReloadCheckTime = now if sourceFileInfo, err := os.Stat(l.sourceFileName); err == nil && !sourceFileInfo.ModTime().Equal(l.sourceFileInfo.ModTime()) { if err = l.store.reloadLocaleByIni(l.langName, l.sourceFileName); err == nil { @@ -155,6 +157,8 @@ func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found b log.Error("unable to live-reload the locale file %q, err: %v", l.sourceFileName, err) } } + l.store.reloadMu.Unlock() // release the write-lock + l.store.reloadMu.RLock() // and re-acquire the read-lock, which was managed by outer Tr function } } trMsg := trKey From 1b61adf5110c0cff9b385c9035879ece99af21a6 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 29 Jun 2022 00:19:05 +0800 Subject: [PATCH 3/5] reduce write-lock duration, a few quick memory operations for most calls within the write-lock time --- modules/translation/i18n/i18n.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index 4fddeab3537fd..04f30b2c8c247 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -147,10 +147,11 @@ func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found b if l.store.reloadMu != nil { now := time.Now() if now.Sub(l.lastReloadCheckTime) >= time.Second && l.sourceFileInfo != nil && l.sourceFileName != "" { + sourceFileInfo, err := os.Stat(l.sourceFileName) l.store.reloadMu.RUnlock() // if the locale file should be reloaded, then we release the read-lock l.store.reloadMu.Lock() // and acquire the write-lock l.lastReloadCheckTime = now - if sourceFileInfo, err := os.Stat(l.sourceFileName); err == nil && !sourceFileInfo.ModTime().Equal(l.sourceFileInfo.ModTime()) { + if err == nil && !sourceFileInfo.ModTime().Equal(l.sourceFileInfo.ModTime()) { if err = l.store.reloadLocaleByIni(l.langName, l.sourceFileName); err == nil { l.sourceFileInfo = sourceFileInfo } else { From 4d3ebce98b3a78ca817fcbe4e9bb77673586ab06 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 29 Jun 2022 00:20:48 +0800 Subject: [PATCH 4/5] fine tune error message if os.Stat failed --- modules/translation/i18n/i18n.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index 04f30b2c8c247..28a3610ed0f1d 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -157,6 +157,8 @@ func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found b } else { log.Error("unable to live-reload the locale file %q, err: %v", l.sourceFileName, err) } + } else if err != nil { + log.Error("unable to stat the locale file %q, err: %v", l.sourceFileName, err) } l.store.reloadMu.Unlock() // release the write-lock l.store.reloadMu.RLock() // and re-acquire the read-lock, which was managed by outer Tr function From c3409e71f91a22cd32cb4633e7d3f4c91753f6a8 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 29 Jun 2022 01:29:58 +0800 Subject: [PATCH 5/5] write-lock-free --- modules/translation/i18n/i18n.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index 28a3610ed0f1d..268aca3ae8c43 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -10,6 +10,7 @@ import ( "os" "reflect" "sync" + "sync/atomic" "time" "code.gitea.io/gitea/modules/log" @@ -31,7 +32,7 @@ type locale struct { sourceFileName string sourceFileInfo os.FileInfo - lastReloadCheckTime time.Time + lastReloadCheckTime atomic.Value } type LocaleStore struct { @@ -66,6 +67,7 @@ func (ls *LocaleStore) AddLocaleByIni(langName, langDesc string, source interfac if fileName, ok := source.(string); ok { lc.sourceFileName = fileName lc.sourceFileInfo, _ = os.Stat(fileName) // live-reload only works for regular files. the error can be ignored + lc.lastReloadCheckTime.Store(&time.Time{}) } ls.langNames = append(ls.langNames, langName) @@ -146,22 +148,27 @@ func (l *locale) Tr(trKey string, trArgs ...interface{}) string { func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found bool) { if l.store.reloadMu != nil { now := time.Now() - if now.Sub(l.lastReloadCheckTime) >= time.Second && l.sourceFileInfo != nil && l.sourceFileName != "" { + lastCheckTime, ok := l.lastReloadCheckTime.Load().(*time.Time) + if ok && now.Sub(*lastCheckTime) >= time.Second && l.sourceFileInfo != nil { + lastModTime := l.sourceFileInfo.ModTime() + l.store.reloadMu.RUnlock() // the file may need to be reloaded with a write-lock, release the read-lock first + sourceFileInfo, err := os.Stat(l.sourceFileName) - l.store.reloadMu.RUnlock() // if the locale file should be reloaded, then we release the read-lock - l.store.reloadMu.Lock() // and acquire the write-lock - l.lastReloadCheckTime = now - if err == nil && !sourceFileInfo.ModTime().Equal(l.sourceFileInfo.ModTime()) { + casOk := l.lastReloadCheckTime.CompareAndSwap(lastCheckTime, &now) + if err == nil && casOk && !sourceFileInfo.ModTime().Equal(lastModTime) { + l.store.reloadMu.Lock() // acquire the write-lock only if the file need to be reloaded if err = l.store.reloadLocaleByIni(l.langName, l.sourceFileName); err == nil { l.sourceFileInfo = sourceFileInfo + log.Info("reloaded locale file %q", l.sourceFileName) } else { log.Error("unable to live-reload the locale file %q, err: %v", l.sourceFileName, err) } + l.store.reloadMu.Unlock() // release the write-lock } else if err != nil { log.Error("unable to stat the locale file %q, err: %v", l.sourceFileName, err) } - l.store.reloadMu.Unlock() // release the write-lock - l.store.reloadMu.RLock() // and re-acquire the read-lock, which was managed by outer Tr function + + l.store.reloadMu.RLock() // re-acquire the read-lock, which was managed by outer Tr function } } trMsg := trKey