Skip to content

Commit 1e9ba12

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 1e9ba12

File tree

15 files changed

+453
-47
lines changed

15 files changed

+453
-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

+55
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,29 @@ func (cfg *configYAML) configFromFile(path string) error {
877908
return cfg.Validate()
878909
}
879910

911+
// SetFeatureGatesFromExperimentalFlags sets the feature gate values if the feature gate is not explicitly set
912+
// while their corresponding experimental flags are explicitly set.
913+
// TODO: remove after all experimental flags are deprecated.
914+
func (cfg *Config) SetFeatureGatesFromExperimentalFlags(isExperimentalFlagSet func(string) bool, featureGatesVal string) error {
915+
// verify that the feature gate and its experimental flag are not both set at the same time.
916+
for expFlagName, featureName := range features.ExperimentalFlagToFeatureMap {
917+
if isExperimentalFlagSet(expFlagName) && strings.Contains(featureGatesVal, string(featureName)) {
918+
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)",
919+
expFlagName, featuregate.FlagName, featureName, featuregate.FlagName, featureName)
920+
}
921+
}
922+
923+
m := make(map[string]bool)
924+
defaultEc := NewConfig()
925+
// if a ExperimentalXX config is different from the default value, that means the experimental flag is explicitly set.
926+
// We need to pass that into the feature gate.
927+
// This section should include all the experimental flag configs that are still in use.
928+
if cfg.ExperimentalStopGRPCServiceOnDefrag != defaultEc.ExperimentalStopGRPCServiceOnDefrag {
929+
m[string(features.StopGRPCServiceOnDefrag)] = cfg.ExperimentalStopGRPCServiceOnDefrag
930+
}
931+
return cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).SetFromMap(m)
932+
}
933+
880934
func updateCipherSuites(tls *transport.TLSInfo, ss []string) error {
881935
if len(tls.CipherSuites) > 0 && len(ss) > 0 {
882936
return fmt.Errorf("TLSInfo.CipherSuites is already specified (given %v)", ss)
@@ -907,6 +961,7 @@ func (cfg *Config) Validate() error {
907961
if err := cfg.setupLogging(); err != nil {
908962
return err
909963
}
964+
cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).SetLogger(cfg.logger)
910965
if err := checkBindURLs(cfg.ListenPeerUrls); err != nil {
911966
return err
912967
}

server/embed/config_test.go

+116
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"net"
2222
"net/url"
2323
"os"
24+
"strconv"
2425
"testing"
2526
"time"
2627

@@ -31,6 +32,8 @@ import (
3132
"go.etcd.io/etcd/client/pkg/v3/srv"
3233
"go.etcd.io/etcd/client/pkg/v3/transport"
3334
"go.etcd.io/etcd/client/pkg/v3/types"
35+
"go.etcd.io/etcd/pkg/v3/featuregate"
36+
"go.etcd.io/etcd/server/v3/features"
3437
)
3538

3639
func notFoundErr(service, domain string) error {
@@ -89,6 +92,119 @@ func TestConfigFileOtherFields(t *testing.T) {
8992
assert.Equal(t, false, cfg.SocketOpts.ReuseAddress, "ReuseAddress does not match")
9093
}
9194

95+
func TestConfigFileFeatureGates(t *testing.T) {
96+
testCases := []struct {
97+
name string
98+
featureGatesJSON string
99+
experimentalStopGRPCServiceOnDefrag string
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+
expectErr: true,
115+
},
116+
{
117+
name: "ok to set different experimental flag and feature gate flag",
118+
featureGatesJSON: "DistributedTracing=false",
119+
experimentalStopGRPCServiceOnDefrag: "true",
120+
expectedFeatures: map[featuregate.Feature]bool{
121+
features.DistributedTracing: false,
122+
features.StopGRPCServiceOnDefrag: true,
123+
},
124+
},
125+
{
126+
name: "can set feature gate to true from experimental flag",
127+
experimentalStopGRPCServiceOnDefrag: "true",
128+
expectedFeatures: map[featuregate.Feature]bool{
129+
features.StopGRPCServiceOnDefrag: true,
130+
},
131+
},
132+
{
133+
name: "can set feature gate to false from experimental flag",
134+
experimentalStopGRPCServiceOnDefrag: "false",
135+
expectedFeatures: map[featuregate.Feature]bool{
136+
features.StopGRPCServiceOnDefrag: false,
137+
},
138+
},
139+
{
140+
name: "can set feature gate to true from feature gate flag",
141+
featureGatesJSON: "StopGRPCServiceOnDefrag=true",
142+
expectedFeatures: map[featuregate.Feature]bool{
143+
features.StopGRPCServiceOnDefrag: true,
144+
},
145+
},
146+
{
147+
name: "can set feature gate to false from feature gate flag",
148+
featureGatesJSON: "StopGRPCServiceOnDefrag=false",
149+
expectedFeatures: map[featuregate.Feature]bool{
150+
features.StopGRPCServiceOnDefrag: false,
151+
},
152+
},
153+
}
154+
for _, tc := range testCases {
155+
t.Run(tc.name, func(t *testing.T) {
156+
var b []byte
157+
var err error
158+
if tc.experimentalStopGRPCServiceOnDefrag != "" {
159+
experimentalStopGRPCServiceOnDefrag, perr := strconv.ParseBool(tc.experimentalStopGRPCServiceOnDefrag)
160+
if perr != nil {
161+
t.Fatal(perr)
162+
}
163+
yc := struct {
164+
ExperimentalStopGRPCServiceOnDefrag bool `json:"experimental-stop-grpc-service-on-defrag"`
165+
FeatureGatesJSON string `json:"feature-gates"`
166+
}{
167+
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+
if err != nil {
197+
t.Fatal(err)
198+
}
199+
for k, v := range tc.expectedFeatures {
200+
if cfg.ServerFeatureGate.Enabled(k) != v {
201+
t.Errorf("expected feature gate %s=%v, got %v", k, v, cfg.ServerFeatureGate.Enabled(k))
202+
}
203+
}
204+
})
205+
}
206+
}
207+
92208
// TestUpdateDefaultClusterFromName ensures that etcd can start with 'etcd --name=abc'.
93209
func TestUpdateDefaultClusterFromName(t *testing.T) {
94210
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)