Files
seaweedfs/weed/util/config.go
T
Chris LuGitHubDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
80a26020d7 util: serialize all ViperProxy access so startup cannot hit concurrent map read/write (#11470)
* util: serialize every ViperProxy method; stop promoting unlocked viper calls

ViperProxy embedded *viper.Viper, so only the five declared methods took
the mutex while every promoted call — GetStringMap in backend.LoadConfiguration
was the reported crash — touched viper's maps unsynchronized. `weed server`
starts the volume server (SetDefault writer) and the master (GetStringMap
reader) back to back, and a race build reports the pair on a plain start.

The wrapped viper is now a named field: a method must be declared here to
exist on the proxy, so unsynchronized access fails at compile time rather
than at runtime. Every promoted use in the tree (GetStringMap, GetUint32,
GetFloat64, GetDuration, IsSet, AllKeys, Set) gets a locked wrapper;
NewViperProxy replaces struct literals for local vipers. GetStringMap
deep-copies its result — viper hands back the internal subtree, so
iterating it after the lock is released would race the next writer.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: take the shared lock while LoadConfiguration merges a config file

viper.MergeInConfig rewrites the same maps the proxy serializes; without
the lock a merge can race a concurrent SetDefault or reader exactly like
the reported startup crash.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: deep-copy slice elements in the GetStringMap snapshot

A slice of maps inside the returned subtree still shared the inner maps —
copy elements recursively so nothing the caller mutates is viper's
internal state.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: add the missing AutomaticEnv wrapper used by tests

sse_reader_test reaches it through GetViper(); without the wrapper the
call no longer exists once the viper field stopped being embedded.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* util: return a fresh slice from GetStringSlice

A stored []string comes back uncast from viper — the backing array is
shared internal state like the GetStringMap subtree, so copy it while
holding the lock.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-26 16:09:29 +08:00

217 lines
5.6 KiB
Go

package util
import (
"strings"
"sync"
"time"
"github.com/spf13/viper"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
var (
ConfigurationFileDirectory DirectoryValueType
loadSecurityConfigOnce sync.Once
)
type DirectoryValueType string
func (s *DirectoryValueType) Set(value string) error {
*s = DirectoryValueType(value)
return nil
}
func (s *DirectoryValueType) String() string {
return string(*s)
}
type Configuration interface {
GetString(key string) string
GetBool(key string) bool
GetInt(key string) int
GetStringSlice(key string) []string
SetDefault(key string, value interface{})
}
func LoadSecurityConfiguration() {
loadSecurityConfigOnce.Do(func() {
LoadConfiguration("security", false)
})
}
func LoadConfiguration(configFileName string, required bool) (loaded bool) {
// MergeInConfig mutates the shared viper that ViperProxy serializes;
// take the same lock so a merge cannot race a reader or SetDefault.
vp.Lock()
defer vp.Unlock()
// find a filer store
viper.SetConfigName(configFileName) // name of config file (without extension)
viper.AddConfigPath(ResolvePath(ConfigurationFileDirectory.String())) // path to look for the config file in
viper.AddConfigPath(".") // optionally look for config in the working directory
viper.AddConfigPath("$HOME/.seaweedfs") // call multiple times to add many search paths
viper.AddConfigPath("/usr/local/etc/seaweedfs/") // search path for bsd-style config directory in
viper.AddConfigPath("/etc/seaweedfs/") // path to look for the config file in
if err := viper.MergeInConfig(); err != nil { // Handle errors reading the config file
if strings.Contains(err.Error(), "Not Found") {
glog.V(1).Infof("Reading %s: %v", viper.ConfigFileUsed(), err)
} else {
// If the config is required, fail immediately
if required {
glog.Fatalf("Reading %s: %v", viper.ConfigFileUsed(), err)
}
// If the config is optional, log a warning but don't crash
glog.Warningf("Reading %s: %v. Skipping optional configuration.", viper.ConfigFileUsed(), err)
}
if required {
glog.Fatalf("Failed to load %s.toml file from current directory, or $HOME/.seaweedfs/, or /etc/seaweedfs/"+
"\n\nPlease use this command to generate the default %s.toml file\n"+
" weed scaffold -config=%s -output=.\n\n\n",
configFileName, configFileName, configFileName)
}
return false
}
glog.V(1).Infof("Reading %s.toml from %s", configFileName, viper.ConfigFileUsed())
return true
}
// ViperProxy serializes access to the global viper. The wrapped Viper is a
// named field, not embedded, so every method must be declared here under the
// mutex — a promoted method would take no lock and race, e.g. GetStringMap
// against a concurrent SetDefault during `weed server` startup.
type ViperProxy struct {
v *viper.Viper
sync.Mutex
}
var (
vp = &ViperProxy{}
)
func (vp *ViperProxy) SetDefault(key string, value interface{}) {
vp.Lock()
defer vp.Unlock()
vp.v.SetDefault(key, value)
}
func (vp *ViperProxy) AutomaticEnv() {
vp.Lock()
defer vp.Unlock()
vp.v.AutomaticEnv()
}
func (vp *ViperProxy) GetString(key string) string {
vp.Lock()
defer vp.Unlock()
return vp.v.GetString(key)
}
func (vp *ViperProxy) GetBool(key string) bool {
vp.Lock()
defer vp.Unlock()
return vp.v.GetBool(key)
}
func (vp *ViperProxy) GetInt(key string) int {
vp.Lock()
defer vp.Unlock()
return vp.v.GetInt(key)
}
func (vp *ViperProxy) GetStringSlice(key string) []string {
vp.Lock()
defer vp.Unlock()
// a []string value comes back uncast — the same shared subtree issue
// GetStringMap has, so hand the caller a copy
return append([]string(nil), vp.v.GetStringSlice(key)...)
}
func (vp *ViperProxy) GetStringMap(key string) map[string]interface{} {
vp.Lock()
defer vp.Unlock()
// viper hands back its internal subtree, so a caller iterating it after
// the lock is released would race the next SetDefault — copy it out.
return deepCopyStringMap(vp.v.GetStringMap(key))
}
func deepCopyStringMap(m map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(m))
for k, v := range m {
out[k] = deepCopyValue(v)
}
return out
}
func deepCopyValue(v interface{}) interface{} {
switch nested := v.(type) {
case map[string]interface{}:
return deepCopyStringMap(nested)
case []interface{}:
out := make([]interface{}, len(nested))
for i, item := range nested {
out[i] = deepCopyValue(item)
}
return out
default:
return v
}
}
func (vp *ViperProxy) GetUint32(key string) uint32 {
vp.Lock()
defer vp.Unlock()
return vp.v.GetUint32(key)
}
func (vp *ViperProxy) GetFloat64(key string) float64 {
vp.Lock()
defer vp.Unlock()
return vp.v.GetFloat64(key)
}
func (vp *ViperProxy) GetDuration(key string) time.Duration {
vp.Lock()
defer vp.Unlock()
return vp.v.GetDuration(key)
}
func (vp *ViperProxy) Set(key string, value interface{}) {
vp.Lock()
defer vp.Unlock()
vp.v.Set(key, value)
}
func (vp *ViperProxy) IsSet(key string) bool {
vp.Lock()
defer vp.Unlock()
return vp.v.IsSet(key)
}
func (vp *ViperProxy) AllKeys() []string {
vp.Lock()
defer vp.Unlock()
return vp.v.AllKeys()
}
// NewViperProxy wraps a specific viper — for configuration loaded from a
// source other than the shared instance GetViper returns.
func NewViperProxy(v *viper.Viper) *ViperProxy {
return &ViperProxy{v: v}
}
func GetViper() *ViperProxy {
vp.Lock()
defer vp.Unlock()
if vp.v == nil {
vp.v = viper.GetViper()
vp.v.AutomaticEnv()
vp.v.SetEnvPrefix("weed")
vp.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
}
return vp
}