Skip to content

Commit c726d69

Browse files
Add "feature-gates" flag and field in config file and add feature gate for StopGRPCServiceOnDefrag
Signed-off-by: Siyuan Zhang <[email protected]>
1 parent d80d0f0 commit c726d69

File tree

15 files changed

+444
-47
lines changed

15 files changed

+444
-47
lines changed

etcd.conf.yml.sample

+3
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,6 @@ cipher-suites: [
155155
# Limit etcd to specific TLS protocol versions
156156
tls-min-version: 'TLS1.2'
157157
tls-max-version: 'TLS1.3'
158+
159+
# Comma-separated list of feature gate enablement in the format of feature=true|false
160+
feature-gates: StopGRPCServiceOnDefrag=false,DistributedTracing=false

pkg/featuregate/feature_gate.go

+17-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package featuregate
1717

1818
import (
19+
"flag"
1920
"fmt"
2021
"sort"
2122
"strconv"
@@ -30,7 +31,7 @@ import (
3031
type Feature string
3132

3233
const (
33-
flagName = "feature-gates"
34+
FlagName = "feature-gates"
3435

3536
// allAlphaGate is a global toggle for alpha features. Per-feature key
3637
// values override the default set by allAlphaGate. Examples:
@@ -98,7 +99,7 @@ type MutableFeatureGate interface {
9899
FeatureGate
99100

100101
// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
101-
AddFlag(fs *pflag.FlagSet)
102+
AddFlag(fs *flag.FlagSet)
102103
// Set parses and stores flag gates for known features
103104
// from a string like feature1=true,feature2=false,...
104105
Set(value string) error
@@ -121,6 +122,8 @@ type MutableFeatureGate interface {
121122
// overriding its default to true for a limited number of components without simultaneously
122123
// changing its default for all consuming components.
123124
OverrideDefault(name Feature, override bool) error
125+
// SetLogger replaces the logger with the provided logger.
126+
SetLogger(lg *zap.Logger)
124127
}
125128

126129
// featureGate implements FeatureGate as well as pflag.Value for flag parsing.
@@ -165,6 +168,9 @@ func setUnsetBetaGates(known map[Feature]FeatureSpec, enabled map[Feature]bool,
165168
var _ pflag.Value = &featureGate{}
166169

167170
func New(name string, lg *zap.Logger) *featureGate {
171+
if lg == nil {
172+
lg = zap.NewNop()
173+
}
168174
known := map[Feature]FeatureSpec{}
169175
for k, v := range defaultFeatures {
170176
known[k] = v
@@ -349,7 +355,7 @@ func (f *featureGate) Enabled(key Feature) bool {
349355
}
350356

351357
// AddFlag adds a flag for setting global feature gates to the specified FlagSet.
352-
func (f *featureGate) AddFlag(fs *pflag.FlagSet) {
358+
func (f *featureGate) AddFlag(fs *flag.FlagSet) {
353359
f.lock.Lock()
354360
// TODO(mtaufen): Shouldn't we just close it on the first Set/SetFromMap instead?
355361
// Not all components expose a feature gates flag using this AddFlag method, and
@@ -359,7 +365,7 @@ func (f *featureGate) AddFlag(fs *pflag.FlagSet) {
359365
f.lock.Unlock()
360366

361367
known := f.KnownFeatures()
362-
fs.Var(f, flagName, ""+
368+
fs.Var(f, FlagName, ""+
363369
"A set of key=value pairs that describe feature gates for alpha/experimental features. "+
364370
"Options are:\n"+strings.Join(known, "\n"))
365371
}
@@ -409,3 +415,10 @@ func (f *featureGate) DeepCopy() MutableFeatureGate {
409415

410416
return fg
411417
}
418+
419+
// SetLogger replaces the logger with the provided logger.
420+
func (f *featureGate) SetLogger(lg *zap.Logger) {
421+
f.lock.Lock()
422+
defer f.lock.Unlock()
423+
f.lg = lg
424+
}

pkg/featuregate/feature_gate_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
package featuregate
1616

1717
import (
18+
"flag"
1819
"fmt"
1920
"strings"
2021
"testing"
2122

22-
"github.com/spf13/pflag"
2323
"github.com/stretchr/testify/assert"
2424
"go.uber.org/zap/zaptest"
2525
)
@@ -203,15 +203,15 @@ func TestFeatureGateFlag(t *testing.T) {
203203
}
204204
for i, test := range tests {
205205
t.Run(test.arg, func(t *testing.T) {
206-
fs := pflag.NewFlagSet("testfeaturegateflag", pflag.ContinueOnError)
206+
fs := flag.NewFlagSet("testfeaturegateflag", flag.ContinueOnError)
207207
f := New("test", zaptest.NewLogger(t))
208208
f.Add(map[Feature]FeatureSpec{
209209
testAlphaGate: {Default: false, PreRelease: Alpha},
210210
testBetaGate: {Default: false, PreRelease: Beta},
211211
})
212212
f.AddFlag(fs)
213213

214-
err := fs.Parse([]string{fmt.Sprintf("--%s=%s", flagName, test.arg)})
214+
err := fs.Parse([]string{fmt.Sprintf("--%s=%s", FlagName, test.arg)})
215215
if test.parseError != "" {
216216
if !strings.Contains(err.Error(), test.parseError) {
217217
t.Errorf("%d: Parse() Expected %v, Got %v", i, test.parseError, err)
@@ -603,7 +603,7 @@ func TestFeatureGateOverrideDefault(t *testing.T) {
603603

604604
t.Run("returns error if already added to flag set", func(t *testing.T) {
605605
f := New("test", zaptest.NewLogger(t))
606-
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
606+
fs := flag.NewFlagSet("test", flag.ContinueOnError)
607607
f.AddFlag(fs)
608608

609609
if err := f.OverrideDefault("TestFeature", true); err == nil {

server/config/config.go

-3
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,6 @@ type ServerConfig struct {
193193
// a shared buffer in its readonly check operations.
194194
ExperimentalTxnModeWriteWithSharedBuffer bool `json:"experimental-txn-mode-write-with-shared-buffer"`
195195

196-
// ExperimentalStopGRPCServiceOnDefrag enables etcd gRPC service to stop serving client requests on defragmentation.
197-
ExperimentalStopGRPCServiceOnDefrag bool `json:"experimental-stop-grpc-service-on-defrag"`
198-
199196
// ExperimentalBootstrapDefragThresholdMegabytes is the minimum number of megabytes needed to be freed for etcd server to
200197
// consider running defrag during bootstrap. Needs to be set to non-zero value to take effect.
201198
ExperimentalBootstrapDefragThresholdMegabytes uint `json:"experimental-bootstrap-defrag-threshold-megabytes"`

server/embed/config.go

+51
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"go.etcd.io/etcd/client/pkg/v3/transport"
4343
"go.etcd.io/etcd/client/pkg/v3/types"
4444
clientv3 "go.etcd.io/etcd/client/v3"
45+
"go.etcd.io/etcd/pkg/v3/featuregate"
4546
"go.etcd.io/etcd/pkg/v3/flags"
4647
"go.etcd.io/etcd/pkg/v3/netutil"
4748
"go.etcd.io/etcd/server/v3/config"
@@ -50,6 +51,7 @@ import (
5051
"go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp"
5152
"go.etcd.io/etcd/server/v3/etcdserver/api/v3compactor"
5253
"go.etcd.io/etcd/server/v3/etcdserver/api/v3discovery"
54+
"go.etcd.io/etcd/server/v3/features"
5355
)
5456

5557
const (
@@ -455,6 +457,9 @@ type Config struct {
455457

456458
// V2Deprecation describes phase of API & Storage V2 support
457459
V2Deprecation config.V2DeprecationEnum `json:"v2-deprecation"`
460+
461+
// ServerFeatureGate is a server level feature gate
462+
ServerFeatureGate featuregate.FeatureGate
458463
}
459464

460465
// configYAML holds the config suitable for yaml parsing
@@ -476,6 +481,8 @@ type configJSON struct {
476481

477482
ClientSecurityJSON securityConfig `json:"client-transport-security"`
478483
PeerSecurityJSON securityConfig `json:"peer-transport-security"`
484+
485+
FeatureGatesJSON string `json:"feature-gates"`
479486
}
480487

481488
type securityConfig struct {
@@ -576,6 +583,7 @@ func NewConfig() *Config {
576583
},
577584

578585
AutoCompactionMode: DefaultAutoCompactionMode,
586+
ServerFeatureGate: features.NewDefaultServerFeatureGate(DefaultName, nil),
579587
}
580588
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
581589
return cfg
@@ -762,6 +770,9 @@ func (cfg *Config) AddFlags(fs *flag.FlagSet) {
762770
// unsafe
763771
fs.BoolVar(&cfg.UnsafeNoFsync, "unsafe-no-fsync", false, "Disables fsync, unsafe, will cause data loss.")
764772
fs.BoolVar(&cfg.ForceNewCluster, "force-new-cluster", false, "Force to create a new one member cluster.")
773+
774+
// featuregate
775+
cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).AddFlag(fs)
765776
}
766777

767778
func ConfigFromFile(path string) (*Config, error) {
@@ -785,6 +796,26 @@ func (cfg *configYAML) configFromFile(path string) error {
785796
return err
786797
}
787798

799+
if cfg.configJSON.FeatureGatesJSON != "" {
800+
err = cfg.Config.ServerFeatureGate.(featuregate.MutableFeatureGate).Set(cfg.configJSON.FeatureGatesJSON)
801+
if err != nil {
802+
return err
803+
}
804+
}
805+
var cfgMap map[string]interface{}
806+
err = yaml.Unmarshal(b, &cfgMap)
807+
if err != nil {
808+
return err
809+
}
810+
isExperimentalFlagSet := func(expFlag string) bool {
811+
_, ok := cfgMap[expFlag]
812+
return ok
813+
}
814+
err = cfg.SetFeatureGatesFromExperimentalFlags(isExperimentalFlagSet, cfg.configJSON.FeatureGatesJSON)
815+
if err != nil {
816+
return err
817+
}
818+
788819
if cfg.configJSON.ListenPeerURLs != "" {
789820
u, err := types.NewURLs(strings.Split(cfg.configJSON.ListenPeerURLs, ","))
790821
if err != nil {
@@ -877,6 +908,25 @@ func (cfg *configYAML) configFromFile(path string) error {
877908
return cfg.Validate()
878909
}
879910

911+
// SetFeatureGatesFromExperimentalFlags sets the feature gate values if their corresponding experimental flags are
912+
// explicitly set.
913+
func (cfg *Config) SetFeatureGatesFromExperimentalFlags(isExperimentalFlagSet func(string) bool, featureGatesVal string) error {
914+
// verify that the feature gate and its experimental flag are not both set at the same time.
915+
for expFlagName, featureName := range features.ExperimentalFlagToFeatureMap {
916+
if isExperimentalFlagSet(expFlagName) && strings.Contains(featureGatesVal, string(featureName)) {
917+
return fmt.Errorf("cannot specify both flags: --%s=(true|false) and --%s=%s=(true|false) at the same time, please just use --%s=%s=(true|false)",
918+
expFlagName, featuregate.FlagName, featureName, featuregate.FlagName, featureName)
919+
}
920+
}
921+
922+
m := make(map[string]bool)
923+
defaultEc := NewConfig()
924+
if cfg.ExperimentalStopGRPCServiceOnDefrag != defaultEc.ExperimentalStopGRPCServiceOnDefrag {
925+
m[string(features.StopGRPCServiceOnDefrag)] = cfg.ExperimentalStopGRPCServiceOnDefrag
926+
}
927+
return cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).SetFromMap(m)
928+
}
929+
880930
func updateCipherSuites(tls *transport.TLSInfo, ss []string) error {
881931
if len(tls.CipherSuites) > 0 && len(ss) > 0 {
882932
return fmt.Errorf("TLSInfo.CipherSuites is already specified (given %v)", ss)
@@ -907,6 +957,7 @@ func (cfg *Config) Validate() error {
907957
if err := cfg.setupLogging(); err != nil {
908958
return err
909959
}
960+
cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).SetLogger(cfg.logger)
910961
if err := checkBindURLs(cfg.ListenPeerUrls); err != nil {
911962
return err
912963
}

server/embed/config_test.go

+113
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"go.etcd.io/etcd/client/pkg/v3/srv"
3232
"go.etcd.io/etcd/client/pkg/v3/transport"
3333
"go.etcd.io/etcd/client/pkg/v3/types"
34+
"go.etcd.io/etcd/pkg/v3/featuregate"
35+
"go.etcd.io/etcd/server/v3/features"
3436
)
3537

3638
func notFoundErr(service, domain string) error {
@@ -89,6 +91,117 @@ func TestConfigFileOtherFields(t *testing.T) {
8991
assert.Equal(t, false, cfg.SocketOpts.ReuseAddress, "ReuseAddress does not match")
9092
}
9193

94+
func TestConfigFileFeatureGates(t *testing.T) {
95+
testCases := []struct {
96+
name string
97+
featureGatesJSON string
98+
experimentalStopGRPCServiceOnDefrag bool
99+
setExperimentalStopGRPCServiceOnDefrag bool
100+
expectErr bool
101+
expectedFeatures map[featuregate.Feature]bool
102+
}{
103+
{
104+
name: "default",
105+
expectedFeatures: map[featuregate.Feature]bool{
106+
features.DistributedTracing: false,
107+
features.StopGRPCServiceOnDefrag: false,
108+
},
109+
},
110+
{
111+
name: "cannot set both experimental flag and feature gate flag",
112+
featureGatesJSON: "StopGRPCServiceOnDefrag=true",
113+
experimentalStopGRPCServiceOnDefrag: false,
114+
setExperimentalStopGRPCServiceOnDefrag: true,
115+
expectErr: true,
116+
},
117+
{
118+
name: "ok to set different experimental flag and feature gate flag",
119+
featureGatesJSON: "DistributedTracing=false",
120+
experimentalStopGRPCServiceOnDefrag: true,
121+
setExperimentalStopGRPCServiceOnDefrag: true,
122+
expectedFeatures: map[featuregate.Feature]bool{
123+
features.DistributedTracing: false,
124+
features.StopGRPCServiceOnDefrag: true,
125+
},
126+
},
127+
{
128+
name: "can set feature gate to true from experimental flag",
129+
experimentalStopGRPCServiceOnDefrag: true,
130+
setExperimentalStopGRPCServiceOnDefrag: true,
131+
expectedFeatures: map[featuregate.Feature]bool{
132+
features.StopGRPCServiceOnDefrag: true,
133+
},
134+
},
135+
{
136+
name: "can set feature gate to false from experimental flag",
137+
experimentalStopGRPCServiceOnDefrag: false,
138+
setExperimentalStopGRPCServiceOnDefrag: true,
139+
expectedFeatures: map[featuregate.Feature]bool{
140+
features.StopGRPCServiceOnDefrag: false,
141+
},
142+
},
143+
{
144+
name: "can set feature gate to true from feature gate flag",
145+
featureGatesJSON: "StopGRPCServiceOnDefrag=true",
146+
expectedFeatures: map[featuregate.Feature]bool{
147+
features.StopGRPCServiceOnDefrag: true,
148+
},
149+
},
150+
{
151+
name: "can set feature gate to false from feature gate flag",
152+
featureGatesJSON: "StopGRPCServiceOnDefrag=false",
153+
expectedFeatures: map[featuregate.Feature]bool{
154+
features.StopGRPCServiceOnDefrag: false,
155+
},
156+
},
157+
}
158+
for _, tc := range testCases {
159+
t.Run(tc.name, func(t *testing.T) {
160+
var b []byte
161+
var err error
162+
if tc.setExperimentalStopGRPCServiceOnDefrag {
163+
yc := struct {
164+
ExperimentalStopGRPCServiceOnDefrag bool `json:"experimental-stop-grpc-service-on-defrag"`
165+
FeatureGatesJSON string `json:"feature-gates"`
166+
}{
167+
tc.experimentalStopGRPCServiceOnDefrag,
168+
tc.featureGatesJSON,
169+
}
170+
b, err = yaml.Marshal(&yc)
171+
} else {
172+
yc := struct {
173+
FeatureGatesJSON string `json:"feature-gates"`
174+
}{
175+
176+
tc.featureGatesJSON,
177+
}
178+
b, err = yaml.Marshal(&yc)
179+
}
180+
if err != nil {
181+
t.Fatal(err)
182+
}
183+
184+
tmpfile := mustCreateCfgFile(t, b)
185+
defer os.Remove(tmpfile.Name())
186+
187+
cfg, err := ConfigFromFile(tmpfile.Name())
188+
if tc.expectErr {
189+
if err == nil {
190+
if err == nil {
191+
t.Fatal("expect parse error")
192+
}
193+
}
194+
return
195+
}
196+
for k, v := range tc.expectedFeatures {
197+
if cfg.ServerFeatureGate.Enabled(k) != v {
198+
t.Errorf("expected feature gate %s=%v, got %v", k, v, cfg.ServerFeatureGate.Enabled(k))
199+
}
200+
}
201+
})
202+
}
203+
}
204+
92205
// TestUpdateDefaultClusterFromName ensures that etcd can start with 'etcd --name=abc'.
93206
func TestUpdateDefaultClusterFromName(t *testing.T) {
94207
cfg := NewConfig()

server/embed/etcd.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,11 @@ func StartEtcd(inCfg *Config) (e *Etcd, err error) {
223223
WarningUnaryRequestDuration: cfg.WarningUnaryRequestDuration,
224224
ExperimentalMemoryMlock: cfg.ExperimentalMemoryMlock,
225225
ExperimentalTxnModeWriteWithSharedBuffer: cfg.ExperimentalTxnModeWriteWithSharedBuffer,
226-
ExperimentalStopGRPCServiceOnDefrag: cfg.ExperimentalStopGRPCServiceOnDefrag,
227226
ExperimentalBootstrapDefragThresholdMegabytes: cfg.ExperimentalBootstrapDefragThresholdMegabytes,
228227
ExperimentalMaxLearners: cfg.ExperimentalMaxLearners,
229228
V2Deprecation: cfg.V2DeprecationEffective(),
230229
ExperimentalLocalAddress: cfg.InferLocalAddr(),
230+
ServerFeatureGate: cfg.ServerFeatureGate,
231231
}
232232

233233
if srvcfg.ExperimentalEnableDistributedTracing {

0 commit comments

Comments
 (0)