-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
app/featureset: implement feature flags #446
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// Copyright © 2022 Obol Labs Inc. | ||
// | ||
// This program is free software: you can redistribute it and/or modify it | ||
// under the terms of the GNU General Public License as published by the Free | ||
// Software Foundation, either version 3 of the License, or (at your option) | ||
// any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, but WITHOUT | ||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
// more details. | ||
// | ||
// You should have received a copy of the GNU General Public License along with | ||
// this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
package featureset | ||
|
||
import ( | ||
"context" | ||
"math" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/obolnetwork/charon/app/errors" | ||
"github.com/obolnetwork/charon/app/log" | ||
"github.com/obolnetwork/charon/app/z" | ||
) | ||
|
||
const ( | ||
enable status = math.MaxInt | ||
disable status = 0 | ||
) | ||
|
||
// Config configures the feature set package. | ||
type Config struct { | ||
// MinStatus defines the minimum enabled status. | ||
MinStatus string | ||
// Enabled overrides min status and enables a list of features. | ||
Enabled []string | ||
// Disabled overrides min status and disables a list of features. | ||
Disabled []string | ||
} | ||
|
||
// DefaultConfig returns the default config enabling only stable features. | ||
func DefaultConfig() Config { | ||
return Config{ | ||
MinStatus: statusStable.String(), | ||
} | ||
} | ||
|
||
// Init initialises the global feature set state. | ||
func Init(ctx context.Context, config Config) error { | ||
var ok bool | ||
for s := statusAlpha; s < statusSentinel; s++ { | ||
if strings.ToLower(config.MinStatus) == strings.ToLower(s.String()) { | ||
minStatus = s | ||
ok = true | ||
|
||
break | ||
} | ||
} | ||
if !ok { | ||
return errors.New("unknown min status", z.Str("min_status", config.MinStatus)) | ||
} | ||
|
||
for _, f := range config.Enabled { | ||
var ok bool | ||
for feature := range state { | ||
if strings.ToLower(string(feature)) == strings.ToLower(f) { | ||
state[feature] = enable | ||
ok = true | ||
} | ||
} | ||
if !ok { | ||
log.Warn(ctx, "Ignoring unknown enabled feature", z.Str("feature", f)) | ||
} | ||
} | ||
|
||
for _, f := range config.Disabled { | ||
var ok bool | ||
for feature := range state { | ||
if strings.ToLower(string(feature)) == strings.ToLower(f) { | ||
state[feature] = disable | ||
ok = true | ||
} | ||
} | ||
if !ok { | ||
log.Warn(ctx, "Ignoring unknown disabled feature", z.Str("feature", f)) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// EnableForT enables a feature for testing. | ||
func EnableForT(t *testing.T, feature Feature) { | ||
t.Helper() | ||
|
||
cache := state[feature] | ||
t.Cleanup(func() { | ||
state[feature] = cache | ||
}) | ||
|
||
state[feature] = enable | ||
} | ||
|
||
// DisableForT disables a feature for testing. | ||
func DisableForT(t *testing.T, feature Feature) { | ||
t.Helper() | ||
|
||
cache := state[feature] | ||
t.Cleanup(func() { | ||
state[feature] = cache | ||
}) | ||
|
||
state[feature] = disable | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// Copyright © 2022 Obol Labs Inc. | ||
// | ||
// This program is free software: you can redistribute it and/or modify it | ||
// under the terms of the GNU General Public License as published by the Free | ||
// Software Foundation, either version 3 of the License, or (at your option) | ||
// any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, but WITHOUT | ||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
// more details. | ||
// | ||
// You should have received a copy of the GNU General Public License along with | ||
// this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
package featureset_test | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/obolnetwork/charon/app/featureset" | ||
) | ||
|
||
// setup initialises global variable per test. | ||
func setup(t *testing.T) { | ||
t.Helper() | ||
|
||
err := featureset.Init(context.Background(), featureset.DefaultConfig()) | ||
require.NoError(t, err) | ||
} | ||
|
||
func TestConfig(t *testing.T) { | ||
setup(t) | ||
|
||
err := featureset.Init(context.Background(), featureset.DefaultConfig()) | ||
require.NoError(t, err) | ||
|
||
err = featureset.Init(context.Background(), featureset.Config{ | ||
MinStatus: "alpha", | ||
Enabled: []string{"ignored"}, | ||
}) | ||
require.NoError(t, err) | ||
|
||
require.True(t, featureset.Enabled(featureset.QBFTConsensus)) | ||
} | ||
|
||
func TestEnableForT(t *testing.T) { | ||
setup(t) | ||
|
||
testFeature := featureset.Feature("test") | ||
require.False(t, featureset.Enabled(testFeature)) | ||
|
||
featureset.EnableForT(t, testFeature) | ||
require.True(t, featureset.Enabled(testFeature)) | ||
|
||
featureset.DisableForT(t, testFeature) | ||
require.False(t, featureset.Enabled(testFeature)) | ||
} | ||
|
||
func TestQBFT(t *testing.T) { | ||
setup(t) | ||
|
||
require.False(t, featureset.Enabled(featureset.QBFTConsensus)) | ||
|
||
featureset.EnableForT(t, featureset.QBFTConsensus) | ||
require.True(t, featureset.Enabled(featureset.QBFTConsensus)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// Copyright © 2022 Obol Labs Inc. | ||
// | ||
// This program is free software: you can redistribute it and/or modify it | ||
// under the terms of the GNU General Public License as published by the Free | ||
// Software Foundation, either version 3 of the License, or (at your option) | ||
// any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, but WITHOUT | ||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
// more details. | ||
// | ||
// You should have received a copy of the GNU General Public License along with | ||
// this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
// Package featureset defines a set of global features and their rollout status. | ||
package featureset | ||
|
||
//go:generate stringer -type=status -trimprefix=status | ||
|
||
// status enumerates the rollout status of a feature. | ||
type status int | ||
|
||
const ( | ||
// statusAlpha is for internal devnet testing. | ||
statusAlpha status = iota + 1 | ||
// statusBeta is for internal and external testnet testing. | ||
statusBeta | ||
// statusStable is for stable feature ready for production. | ||
statusStable | ||
// statusSentinel is an internal tail-end placeholder. | ||
statusSentinel // Must always be last | ||
) | ||
|
||
// Feature is a feature being rolled out. | ||
type Feature string | ||
|
||
const ( | ||
// QBFTConsensus introduces qbft consensus, see https://github.com/ObolNetwork/charon/issues/445. | ||
QBFTConsensus Feature = "qbft_consensus" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we can also have proposer feature here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can add that in next PR |
||
) | ||
|
||
var ( | ||
// state defines the current rollout status of each feature. | ||
state = map[Feature]status{ | ||
QBFTConsensus: statusAlpha, | ||
// Add all features and there status here. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here too proposer feature with |
||
} | ||
|
||
// minStatus defines the minimum enabled status. | ||
minStatus = statusStable | ||
) | ||
|
||
// Enabled returns true if the feature is enabled. | ||
func Enabled(feature Feature) bool { | ||
return state[feature] >= minStatus | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// Copyright © 2022 Obol Labs Inc. | ||
// | ||
// This program is free software: you can redistribute it and/or modify it | ||
// under the terms of the GNU General Public License as published by the Free | ||
// Software Foundation, either version 3 of the License, or (at your option) | ||
// any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, but WITHOUT | ||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
// more details. | ||
// | ||
// You should have received a copy of the GNU General Public License along with | ||
// this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
package featureset | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestAllFeatureStatus(t *testing.T) { | ||
// Add all features to this test | ||
features := []Feature{ | ||
QBFTConsensus, | ||
} | ||
|
||
for _, feature := range features { | ||
status, ok := state[feature] | ||
require.True(t, ok) | ||
require.Greater(t, status, 0) | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so basically minStatus is very important and enabled and disabled are not that important it seems
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup,
minStatus
enables/disables different sets of features. WhileEnable/Disable
is to override individual features