diff options
Diffstat (limited to 'src/internal/godebug')
-rw-r--r-- | src/internal/godebug/godebug.go | 164 | ||||
-rw-r--r-- | src/internal/godebug/godebug_test.go | 39 |
2 files changed, 203 insertions, 0 deletions
diff --git a/src/internal/godebug/godebug.go b/src/internal/godebug/godebug.go new file mode 100644 index 0000000..dbcd980 --- /dev/null +++ b/src/internal/godebug/godebug.go @@ -0,0 +1,164 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package godebug makes the settings in the $GODEBUG environment variable +// available to other packages. These settings are often used for compatibility +// tweaks, when we need to change a default behavior but want to let users +// opt back in to the original. For example GODEBUG=http2server=0 disables +// HTTP/2 support in the net/http server. +// +// In typical usage, code should declare a Setting as a global +// and then call Value each time the current setting value is needed: +// +// var http2server = godebug.New("http2server") +// +// func ServeConn(c net.Conn) { +// if http2server.Value() == "0" { +// disallow HTTP/2 +// ... +// } +// ... +// } +package godebug + +import ( + "sync" + "sync/atomic" + _ "unsafe" // go:linkname +) + +// A Setting is a single setting in the $GODEBUG environment variable. +type Setting struct { + name string + once sync.Once + value *atomic.Pointer[string] +} + +// New returns a new Setting for the $GODEBUG setting with the given name. +func New(name string) *Setting { + return &Setting{name: name} +} + +// Name returns the name of the setting. +func (s *Setting) Name() string { + return s.name +} + +// String returns a printable form for the setting: name=value. +func (s *Setting) String() string { + return s.name + "=" + s.Value() +} + +// cache is a cache of all the GODEBUG settings, +// a locked map[string]*atomic.Pointer[string]. +// +// All Settings with the same name share a single +// *atomic.Pointer[string], so that when GODEBUG +// changes only that single atomic string pointer +// needs to be updated. +// +// A name appears in the values map either if it is the +// name of a Setting for which Value has been called +// at least once, or if the name has ever appeared in +// a name=value pair in the $GODEBUG environment variable. +// Once entered into the map, the name is never removed. +var cache sync.Map // name string -> value *atomic.Pointer[string] + +var empty string + +// Value returns the current value for the GODEBUG setting s. +// +// Value maintains an internal cache that is synchronized +// with changes to the $GODEBUG environment variable, +// making Value efficient to call as frequently as needed. +// Clients should therefore typically not attempt their own +// caching of Value's result. +func (s *Setting) Value() string { + s.once.Do(func() { + v, ok := cache.Load(s.name) + if !ok { + p := new(atomic.Pointer[string]) + p.Store(&empty) + v, _ = cache.LoadOrStore(s.name, p) + } + s.value = v.(*atomic.Pointer[string]) + }) + return *s.value.Load() +} + +// setUpdate is provided by package runtime. +// It calls update(def, env), where def is the default GODEBUG setting +// and env is the current value of the $GODEBUG environment variable. +// After that first call, the runtime calls update(def, env) +// again each time the environment variable changes +// (due to use of os.Setenv, for example). +// +//go:linkname setUpdate +func setUpdate(update func(string, string)) + +func init() { + setUpdate(update) +} + +var updateMu sync.Mutex + +// update records an updated GODEBUG setting. +// def is the default GODEBUG setting for the running binary, +// and env is the current value of the $GODEBUG environment variable. +func update(def, env string) { + updateMu.Lock() + defer updateMu.Unlock() + + // Update all the cached values, creating new ones as needed. + // We parse the environment variable first, so that any settings it has + // are already locked in place (did[name] = true) before we consider + // the defaults. + did := make(map[string]bool) + parse(did, env) + parse(did, def) + + // Clear any cached values that are no longer present. + cache.Range(func(name, v any) bool { + if !did[name.(string)] { + v.(*atomic.Pointer[string]).Store(&empty) + } + return true + }) +} + +// parse parses the GODEBUG setting string s, +// which has the form k=v,k2=v2,k3=v3. +// Later settings override earlier ones. +// Parse only updates settings k=v for which did[k] = false. +// It also sets did[k] = true for settings that it updates. +func parse(did map[string]bool, s string) { + // Scan the string backward so that later settings are used + // and earlier settings are ignored. + // Note that a forward scan would cause cached values + // to temporarily use the ignored value before being + // updated to the "correct" one. + end := len(s) + eq := -1 + for i := end - 1; i >= -1; i-- { + if i == -1 || s[i] == ',' { + if eq >= 0 { + name, value := s[i+1:eq], s[eq+1:end] + if !did[name] { + did[name] = true + v, ok := cache.Load(name) + if !ok { + p := new(atomic.Pointer[string]) + p.Store(&empty) + v, _ = cache.LoadOrStore(name, p) + } + v.(*atomic.Pointer[string]).Store(&value) + } + } + eq = -1 + end = i + } else if s[i] == '=' { + eq = i + } + } +} diff --git a/src/internal/godebug/godebug_test.go b/src/internal/godebug/godebug_test.go new file mode 100644 index 0000000..319229d --- /dev/null +++ b/src/internal/godebug/godebug_test.go @@ -0,0 +1,39 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package godebug_test + +import ( + . "internal/godebug" + "testing" +) + +func TestGet(t *testing.T) { + foo := New("foo") + tests := []struct { + godebug string + setting *Setting + want string + }{ + {"", New(""), ""}, + {"", foo, ""}, + {"foo=bar", foo, "bar"}, + {"foo=bar,after=x", foo, "bar"}, + {"before=x,foo=bar,after=x", foo, "bar"}, + {"before=x,foo=bar", foo, "bar"}, + {",,,foo=bar,,,", foo, "bar"}, + {"foodecoy=wrong,foo=bar", foo, "bar"}, + {"foo=", foo, ""}, + {"foo", foo, ""}, + {",foo", foo, ""}, + {"foo=bar,baz", New("loooooooong"), ""}, + } + for _, tt := range tests { + t.Setenv("GODEBUG", tt.godebug) + got := tt.setting.Value() + if got != tt.want { + t.Errorf("get(%q, %q) = %q; want %q", tt.godebug, tt.setting.Name(), got, tt.want) + } + } +} |