[英]viper dynamically loading config file has data race
我想动态加载配置文件而不是重新启动我的 Go 应用程序。 我写了下面的文件,它运行但有数据竞争。
配置文件
package main
import (
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"log"
"sync"
"time"
)
var (
reloadConfig = make(chan string)
reloadConfig2 = make(chan string)
viperLock1 sync.Mutex
viperLock2 sync.Mutex
)
func setUpConfig(file string, merge bool, v *viper.Viper) {
v.AddConfigPath("./")
v.SetConfigName(file)
v.SetConfigType("yml")
if merge {
err1 := v.MergeInConfig()
checkForFatalError("fatal error occurred while reading config file!", err1)
} else {
err := v.ReadInConfig()
checkForFatalError("fatal error occurred while reading config file!", err)
}
log.Println("Initial config value: ", v.GetString("env"))
}
func loadConfigDynamically(configChannel chan string, viperLock *sync.Mutex, vipe *viper.Viper) {
viperLock.Lock()
vipe.OnConfigChange(func(e fsnotify.Event) {
viperLock.Lock()
log.Println("config file changed", e.Name)
environment := vipe.GetString("env")
configChannel <- environment
viperLock.Unlock()
})
viperLock.Unlock()
vipe.WatchConfig()
}
func loadMultipleConfigsDynamically() {
go func() {
time.Sleep(time.Millisecond * 50)
vipe2 := viper.New()
setUpConfig("config_base", false, vipe2)
loadConfigDynamically(reloadConfig2, &viperLock2, vipe2)
time.Sleep(time.Millisecond * 50)
vipe1 := viper.New()
setUpConfig("config", false, vipe1)
loadConfigDynamically(reloadConfig, &viperLock1, vipe1)
}()
}
main.go
package main
import (
log "github.com/sirupsen/logrus"
"os"
"os/signal"
"syscall"
)
var reloadConfigNow = make(chan bool)
var reloadConfigAgain = make(chan bool)
var newConfigValue string
func main() {
loadMultipleConfigsDynamically()
go printUpdatedValueOnly()
go justAnotherGoroutine()
go yetAnotherGoroutine()
shutdownAppGracefully()
}
func printUpdatedValueOnly() {
for {
select {
case updatedValue := <-reloadConfig:
newConfigValue = updatedValue
log.Println("dynamically loaded config value: ", updatedValue)
reloadConfigNow <-true
reloadConfigAgain <-true
case updatedValue1 := <-reloadConfig2:
newConfigValue = updatedValue1
log.Println("dynamically loaded config value: ", updatedValue1)
reloadConfigNow <-true
reloadConfigAgain <-true
default:
}
}
}
func justAnotherGoroutine(){
existingConfigValue := ""
for {
select {
case <-reloadConfigNow:
existingConfigValue = newConfigValue
log.Println("justAnotherGoroutine: ", existingConfigValue)
default:
}
}
}
func yetAnotherGoroutine() {
existingConfigValue := ""
for {
select {
case <-reloadConfigAgain:
existingConfigValue = newConfigValue
log.Println("yetAnotherGoroutine: ", existingConfigValue)
default:
}
}
}
func checkForFatalError(errorMsg string, err error) {
if err != nil {
log.Fatal(errorMsg, err)
}
}
func shutdownAppGracefully() {
killSignal := make(chan os.Signal, 1)
signal.Notify(killSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
k := <-killSignal
log.Info("OS Interrupt Signal received, application is shutting down!")
logSystemInterruptType(k)
}
func logSystemInterruptType(osInterrupt os.Signal) {
switch osInterrupt {
case syscall.SIGHUP:
log.Info("SIGHUP")
case syscall.SIGINT:
log.Info("SIGINT")
case syscall.SIGTERM:
log.Info("SIGTERM")
case syscall.SIGQUIT:
log.Info("SIGQUIT")
default:
log.Info("Unknown OS Interrupt")
}
}
配置文件
env : "LOCAL"
config_base.yml
env : "dev15"
去.mod
module reload_config
go 1.16
require (
github.com/fsnotify/fsnotify v1.4.9
github.com/spf13/viper v1.8.1
)
我最近了解到 viper 不是线程安全的,因此我需要用互斥锁包装它。 我也尝试这样做。 在 config.go 文件中,func loadConfigDynamically,我设置的 OnConfigChange 是读取的数据竞争。 并且在同一行的同一函数中是先前的写数据竞争。 我运行上面的包
go run -race reload_config
并更改 config.yml 中 env 的值以测试配置文件是否正在动态加载。此数据竞争仅在第一次动态重新加载配置时发生。 对于随后的时间,它工作得很好。
您锁定viperLock
称为vipe.WatchConfig()
和集vipe.OnConfigChange
与功能,其亦锁定viperLock
。
因为你已经调用了vipe.WatchConfig()
然后它开始在单独的 go 例程中调用vipe.OnConfigChange
。 它也尝试获取相同的锁。 这就是为什么存在竞争条件。
在设置vipe.OnConfigChange
和释放锁后调用vipe.WatchConfig()
。
应该如下进行更正。
func loadConfigDynamically() {
go func() {
time.Sleep(time.Second)
viperLock.Lock()
vipe.OnConfigChange(func(e fsnotify.Event) {
viperLock.Lock()
log.Println("config file changed", e.Name)
environment := vipe.GetString("env")
reloadConfig <- environment
viperLock.Unlock()
})
viperLock.Unlock()
vipe.WatchConfig() //this starting call vipe.OnConfigChange
}()
}
可能是 go 认为一个变量正在同时被两个 goroutine 修改和访问,并且修改和访问的地方没有锁定。 类似于以下示例:
package main
import (
"time"
)
type Foo struct {
f func(string)
}
func (f *Foo) Watch() {
go func() {
for {
time.Sleep(time.Second * 2)
if f.f != nil {
f.f("hello world")
}
}
}()
}
func (f *Foo) SetF(fun func(string)) {
f.f = fun
}
func main() {
f := Foo{}
f.Watch()
f.SetF(func(s string) {
})
time.Sleep(time.Second * 5)
}
它有一个数据竞赛。 如果我在修改和读取的地方都设置了相同的锁,就不会有数据竞争:
package main
import (
"sync"
"time"
)
var lock sync.Mutex
type Foo struct {
f func(string)
}
func (f *Foo) Watch() {
go func() {
for {
time.Sleep(time.Second * 2)
lock.Lock() // read places
if f.f != nil {
f.f("hello world")
}
lock.Unlock()
}
}()
}
func (f *Foo) SetF(fun func(string)) {
f.f = fun
}
func main() {
f := Foo{}
f.Watch()
lock.Lock() // write places
f.SetF(func(s string) {
})
lock.Unlock()
time.Sleep(time.Second * 5)
}
或者消除两个 goroutine 同时读写的可能性会很好:
func main() {
f := Foo{}
f.SetF(func(s string) {
})
f.Watch()
time.Sleep(time.Second * 5)
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.