Skip to content

Commit a40b24e

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 a40b24e

File tree

15 files changed

+457
-48
lines changed

15 files changed

+457
-48
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 server feature gate enablement in the format of feature=true|false
160+
server-feature-gates: StopGRPCServiceOnDefrag=false,DistributedTracing=false

pkg/featuregate/feature_gate.go

+19-3
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+
defaultFlagName = "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, flagName string)
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,10 @@ 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, flagName string) {
359+
if flagName == "" {
360+
flagName = defaultFlagName
361+
}
353362
f.lock.Lock()
354363
// TODO(mtaufen): Shouldn't we just close it on the first Set/SetFromMap instead?
355364
// Not all components expose a feature gates flag using this AddFlag method, and
@@ -409,3 +418,10 @@ func (f *featureGate) DeepCopy() MutableFeatureGate {
409418

410419
return fg
411420
}
421+
422+
// SetLogger replaces the logger with the provided logger.
423+
func (f *featureGate) SetLogger(lg *zap.Logger) {
424+
f.lock.Lock()
425+
defer f.lock.Unlock()
426+
f.lg = lg
427+
}

pkg/featuregate/feature_gate_test.go

+6-6
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
})
212-
f.AddFlag(fs)
212+
f.AddFlag(fs, defaultFlagName)
213213

214-
err := fs.Parse([]string{fmt.Sprintf("--%s=%s", flagName, test.arg)})
214+
err := fs.Parse([]string{fmt.Sprintf("--%s=%s", defaultFlagName, 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,8 +603,8 @@ 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)
607-
f.AddFlag(fs)
606+
fs := flag.NewFlagSet("test", flag.ContinueOnError)
607+
f.AddFlag(fs, defaultFlagName)
608608

609609
if err := f.OverrideDefault("TestFeature", true); err == nil {
610610
t.Error("expected a non-nil error to be returned")

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

+65
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 (
@@ -108,6 +110,8 @@ const (
108110
maxElectionMs = 50000
109111
// backend freelist map type
110112
freelistArrayType = "array"
113+
114+
ServerFeatureGateFlagName = "server-feature-gates"
111115
)
112116

113117
var (
@@ -455,6 +459,9 @@ type Config struct {
455459

456460
// V2Deprecation describes phase of API & Storage V2 support
457461
V2Deprecation config.V2DeprecationEnum `json:"v2-deprecation"`
462+
463+
// ServerFeatureGate is a server level feature gate
464+
ServerFeatureGate featuregate.FeatureGate
458465
}
459466

460467
// configYAML holds the config suitable for yaml parsing
@@ -476,6 +483,8 @@ type configJSON struct {
476483

477484
ClientSecurityJSON securityConfig `json:"client-transport-security"`
478485
PeerSecurityJSON securityConfig `json:"peer-transport-security"`
486+
487+
ServerFeatureGatesJSON string `json:"server-feature-gates"`
479488
}
480489

481490
type securityConfig struct {
@@ -576,6 +585,7 @@ func NewConfig() *Config {
576585
},
577586

578587
AutoCompactionMode: DefaultAutoCompactionMode,
588+
ServerFeatureGate: features.NewDefaultServerFeatureGate(DefaultName, nil),
579589
}
580590
cfg.InitialCluster = cfg.InitialClusterFromName(cfg.Name)
581591
return cfg
@@ -762,6 +772,9 @@ func (cfg *Config) AddFlags(fs *flag.FlagSet) {
762772
// unsafe
763773
fs.BoolVar(&cfg.UnsafeNoFsync, "unsafe-no-fsync", false, "Disables fsync, unsafe, will cause data loss.")
764774
fs.BoolVar(&cfg.ForceNewCluster, "force-new-cluster", false, "Force to create a new one member cluster.")
775+
776+
// featuregate
777+
cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).AddFlag(fs, ServerFeatureGateFlagName)
765778
}
766779

767780
func ConfigFromFile(path string) (*Config, error) {
@@ -785,6 +798,26 @@ func (cfg *configYAML) configFromFile(path string) error {
785798
return err
786799
}
787800

801+
if cfg.configJSON.ServerFeatureGatesJSON != "" {
802+
err = cfg.Config.ServerFeatureGate.(featuregate.MutableFeatureGate).Set(cfg.configJSON.ServerFeatureGatesJSON)
803+
if err != nil {
804+
return err
805+
}
806+
}
807+
var cfgMap map[string]interface{}
808+
err = yaml.Unmarshal(b, &cfgMap)
809+
if err != nil {
810+
return err
811+
}
812+
isExperimentalFlagSet := func(expFlag string) bool {
813+
_, ok := cfgMap[expFlag]
814+
return ok
815+
}
816+
err = cfg.SetFeatureGatesFromExperimentalFlags(cfg.ServerFeatureGate, isExperimentalFlagSet, ServerFeatureGateFlagName, cfg.configJSON.ServerFeatureGatesJSON)
817+
if err != nil {
818+
return err
819+
}
820+
788821
if cfg.configJSON.ListenPeerURLs != "" {
789822
u, err := types.NewURLs(strings.Split(cfg.configJSON.ListenPeerURLs, ","))
790823
if err != nil {
@@ -877,6 +910,37 @@ func (cfg *configYAML) configFromFile(path string) error {
877910
return cfg.Validate()
878911
}
879912

913+
// SetFeatureGatesFromExperimentalFlags sets the feature gate values if the feature gate is not explicitly set
914+
// while their corresponding experimental flags are explicitly set.
915+
// TODO: remove after all experimental flags are deprecated.
916+
func (cfg *Config) SetFeatureGatesFromExperimentalFlags(fg featuregate.FeatureGate, isExperimentalFlagSet func(string) bool, flagName, featureGatesVal string) error {
917+
// verify that the feature gate and its experimental flag are not both set at the same time.
918+
for expFlagName, featureName := range features.ExperimentalFlagToFeatureMap {
919+
if isExperimentalFlagSet(expFlagName) && strings.Contains(featureGatesVal, string(featureName)) {
920+
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)",
921+
expFlagName, flagName, featureName, flagName, featureName)
922+
}
923+
}
924+
925+
m := make(map[featuregate.Feature]bool)
926+
defaultEc := NewConfig()
927+
// if a ExperimentalXX config is different from the default value, that means the experimental flag is explicitly set.
928+
// We need to pass that into the feature gate.
929+
// This section should include all the experimental flag configs that are still in use.
930+
if cfg.ExperimentalStopGRPCServiceOnDefrag != defaultEc.ExperimentalStopGRPCServiceOnDefrag {
931+
m[features.StopGRPCServiceOnDefrag] = cfg.ExperimentalStopGRPCServiceOnDefrag
932+
}
933+
// filter out unknown features for fg
934+
allFeatures := fg.(featuregate.MutableFeatureGate).GetAll()
935+
mFiltered := make(map[string]bool)
936+
for k, v := range m {
937+
if _, ok := allFeatures[k]; ok {
938+
mFiltered[string(k)] = v
939+
}
940+
}
941+
return fg.(featuregate.MutableFeatureGate).SetFromMap(mFiltered)
942+
}
943+
880944
func updateCipherSuites(tls *transport.TLSInfo, ss []string) error {
881945
if len(tls.CipherSuites) > 0 && len(ss) > 0 {
882946
return fmt.Errorf("TLSInfo.CipherSuites is already specified (given %v)", ss)
@@ -907,6 +971,7 @@ func (cfg *Config) Validate() error {
907971
if err := cfg.setupLogging(); err != nil {
908972
return err
909973
}
974+
cfg.ServerFeatureGate.(featuregate.MutableFeatureGate).SetLogger(cfg.logger)
910975
if err := checkBindURLs(cfg.ListenPeerUrls); err != nil {
911976
return err
912977
}

server/embed/config_test.go

+107
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,110 @@ 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+
serverFeatureGatesJSON 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+
serverFeatureGatesJSON: "StopGRPCServiceOnDefrag=true",
113+
experimentalStopGRPCServiceOnDefrag: "false",
114+
expectErr: true,
115+
},
116+
{
117+
name: "ok to set different experimental flag and feature gate flag",
118+
serverFeatureGatesJSON: "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+
serverFeatureGatesJSON: "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+
serverFeatureGatesJSON: "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+
yc := struct {
157+
ExperimentalStopGRPCServiceOnDefrag *bool `json:"experimental-stop-grpc-service-on-defrag,omitempty"`
158+
ServerFeatureGatesJSON string `json:"server-feature-gates"`
159+
}{
160+
ServerFeatureGatesJSON: tc.serverFeatureGatesJSON,
161+
}
162+
163+
if tc.experimentalStopGRPCServiceOnDefrag != "" {
164+
experimentalStopGRPCServiceOnDefrag, err := strconv.ParseBool(tc.experimentalStopGRPCServiceOnDefrag)
165+
if err != nil {
166+
t.Fatal(err)
167+
}
168+
yc.ExperimentalStopGRPCServiceOnDefrag = &experimentalStopGRPCServiceOnDefrag
169+
}
170+
b, err := yaml.Marshal(&yc)
171+
if err != nil {
172+
t.Fatal(err)
173+
}
174+
175+
tmpfile := mustCreateCfgFile(t, b)
176+
defer os.Remove(tmpfile.Name())
177+
178+
cfg, err := ConfigFromFile(tmpfile.Name())
179+
if tc.expectErr {
180+
if err == nil {
181+
if err == nil {
182+
t.Fatal("expect parse error")
183+
}
184+
}
185+
return
186+
}
187+
if err != nil {
188+
t.Fatal(err)
189+
}
190+
for k, v := range tc.expectedFeatures {
191+
if cfg.ServerFeatureGate.Enabled(k) != v {
192+
t.Errorf("expected feature gate %s=%v, got %v", k, v, cfg.ServerFeatureGate.Enabled(k))
193+
}
194+
}
195+
})
196+
}
197+
}
198+
92199
// TestUpdateDefaultClusterFromName ensures that etcd can start with 'etcd --name=abc'.
93200
func TestUpdateDefaultClusterFromName(t *testing.T) {
94201
cfg := NewConfig()

0 commit comments

Comments
 (0)