Skip to content
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

Merged
merged 4 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/eth2wrap"
"github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/lifecycle"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/retry"
Expand Down Expand Up @@ -64,6 +65,7 @@ import (
type Config struct {
P2P p2p.Config
Log log.Config
Feature featureset.Config
ManifestFile string
DataDir string
MonitoringAddr string
Expand Down Expand Up @@ -116,6 +118,10 @@ func Run(ctx context.Context, conf Config) (err error) {
return err
}

if err := featureset.Init(ctx, conf.Feature); err != nil {
return err
}

hash, timestamp := GitCommit()
log.Info(ctx, "Charon starting",
z.Str("version", version.Version),
Expand Down
2 changes: 2 additions & 0 deletions app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

"github.com/obolnetwork/charon/app"
"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/cmd"
"github.com/obolnetwork/charon/p2p"
Expand Down Expand Up @@ -168,6 +169,7 @@ func pingCluster(t *testing.T, test pingTest) {
for i := 0; i < n; i++ {
conf := app.Config{
Log: log.DefaultConfig(),
Feature: featureset.DefaultConfig(),
SimnetBMock: true,
MonitoringAddr: testutil.AvailableAddr(t).String(), // Random monitoring address
ValidatorAPIAddr: testutil.AvailableAddr(t).String(), // Random validatorapi address
Expand Down
117 changes: 117 additions & 0 deletions app/featureset/config.go
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 {
Copy link
Contributor

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

Copy link
Contributor Author

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. While Enable/Disable is to override individual features

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
}
70 changes: 70 additions & 0 deletions app/featureset/config_test.go
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))
}
57 changes: 57 additions & 0 deletions app/featureset/featureset.go
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we can also have proposer feature here

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too proposer feature with statusAlpha

}

// 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
}
35 changes: 35 additions & 0 deletions app/featureset/featureset_internal_test.go
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)
}
}
41 changes: 41 additions & 0 deletions app/featureset/status_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/simnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

"github.com/obolnetwork/charon/app"
"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/core"
"github.com/obolnetwork/charon/core/leadercast"
Expand Down Expand Up @@ -132,6 +133,7 @@ func testSimnet(t *testing.T, args simnetArgs, propose bool) {
for i := 0; i < args.N; i++ {
conf := app.Config{
Log: log.DefaultConfig(),
Feature: featureset.DefaultConfig(),
SimnetBMock: true,
SimnetVMock: args.VMocks[i],
MonitoringAddr: testutil.AvailableAddr(t).String(), // Random monitoring address
Expand Down
Loading