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

Refactor: Introduce a Global Registry #7392

Merged
merged 9 commits into from
Jun 26, 2018
Merged
Show file tree
Hide file tree
Changes from 3 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
55 changes: 55 additions & 0 deletions libbeat/feature/bundle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package feature

import "fmt"

// Bundle defines a list of features available in the current beat.
type Bundle struct {
Features []Featurable
}

// NewBundle creates a new Bundle of feature to be registered.
func NewBundle(features []Featurable) *Bundle {
return &Bundle{Features: features}
}

// Filter creates a new bundle with only the feature matching the requested stability.
func (b *Bundle) Filter(stabilities ...Stability) *Bundle {
var filtered []Featurable

for _, feature := range b.Features {
for _, stability := range stabilities {
if feature.Stability() == stability {
filtered = append(filtered, feature)
break
}
}
}
return NewBundle(filtered)
}

// MustBundle takes existing bundle or features and create a new Bundle with all the merged Features,
// will panic on errors.
func MustBundle(features ...interface{}) *Bundle {
b, err := BundleFeature(features...)
if err != nil {
panic(err)
}
return b
}

// BundleFeature takes existing bundle or features and create a new Bundle with all the merged
// Features,
func BundleFeature(features ...interface{}) (*Bundle, error) {
var merged []Featurable
for _, feature := range features {
switch v := feature.(type) {
case Featurable:
merged = append(merged, v)
case *Bundle:
merged = append(merged, v.Features...)
default:
return nil, fmt.Errorf("unknown type, expecting 'Featurable' or 'Bundle' and received '%T'", v)
}
}
return NewBundle(merged), nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will change this, I will remove the switch case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The following might be better.

diff --git a/libbeat/feature/bundle.go b/libbeat/feature/bundle.go
index 174c2f973..9886b68b5 100644
--- a/libbeat/feature/bundle.go
+++ b/libbeat/feature/bundle.go
@@ -1,22 +1,26 @@
 package feature
 
-import "fmt"
+// Groupable allows features and Bundle to be merged together.
+type Groupable interface {
+	// Features returns a slice of features.
+	Features() []Featurable
+}
 
 // Bundle defines a list of features available in the current beat.
 type Bundle struct {
-	Features []Featurable
+	features []Featurable
 }
 
 // NewBundle creates a new Bundle of feature to be registered.
 func NewBundle(features []Featurable) *Bundle {
-	return &Bundle{Features: features}
+	return &Bundle{features: features}
 }
 
 // Filter creates a new bundle with only the feature matching the requested stability.
 func (b *Bundle) Filter(stabilities ...Stability) *Bundle {
 	var filtered []Featurable
 
-	for _, feature := range b.Features {
+	for _, feature := range b.features {
 		for _, stability := range stabilities {
 			if feature.Stability() == stability {
 				filtered = append(filtered, feature)
@@ -27,29 +31,16 @@ func (b *Bundle) Filter(stabilities ...Stability) *Bundle {
 	return NewBundle(filtered)
 }
 
-// MustBundle takes existing bundle or features and create a new Bundle with all the merged Features,
-// will panic on errors.
-func MustBundle(features ...interface{}) *Bundle {
-	b, err := BundleFeature(features...)
-	if err != nil {
-		panic(err)
-	}
-	return b
+// Features returns a slice of features.
+func (b *Bundle) Features() []Featurable {
+	return b.features
 }
 
-// BundleFeature takes existing bundle or features and create a new Bundle with all the merged
-// Features,
-func BundleFeature(features ...interface{}) (*Bundle, error) {
+// MustBundle takes existing bundle or features and create a new Bundle with all the merged Features.
+func MustBundle(features ...Groupable) *Bundle {
 	var merged []Featurable
 	for _, feature := range features {
-		switch v := feature.(type) {
-		case Featurable:
-			merged = append(merged, v)
-		case *Bundle:
-			merged = append(merged, v.Features...)
-		default:
-			return nil, fmt.Errorf("unknown type, expecting 'Featurable' or 'Bundle' and received '%T'", v)
-		}
+		merged = append(merged, feature.Features()...)
 	}
-	return NewBundle(merged), nil
+	return NewBundle(merged)
 }
diff --git a/libbeat/feature/bundle_test.go b/libbeat/feature/bundle_test.go
index df293cc4b..e675a7f09 100644
--- a/libbeat/feature/bundle_test.go
+++ b/libbeat/feature/bundle_test.go
@@ -16,25 +16,25 @@ func TestBundle(t *testing.T) {
 
 	t.Run("Creates a new Bundle", func(t *testing.T) {
 		b := NewBundle(features)
-		assert.Equal(t, 3, len(b.Features))
+		assert.Equal(t, 3, len(b.Features()))
 	})
 
 	t.Run("Filters feature based on stability", func(t *testing.T) {
 		b := NewBundle(features)
 		new := b.Filter(Experimental)
-		assert.Equal(t, 1, len(new.Features))
+		assert.Equal(t, 1, len(new.Features()))
 	})
 
 	t.Run("Filters feature based on multiple different stability", func(t *testing.T) {
 		b := NewBundle(features)
 		new := b.Filter(Experimental, Stable)
-		assert.Equal(t, 2, len(new.Features))
+		assert.Equal(t, 2, len(new.Features()))
 	})
 
 	t.Run("Creates a new Bundle from specified feature", func(t *testing.T) {
 		f1 := New("libbeat.outputs", "elasticsearch", factory, Stable)
 		b := MustBundle(f1)
-		assert.Equal(t, 1, len(b.Features))
+		assert.Equal(t, 1, len(b.Features()))
 	})
 
 	t.Run("Creates a new Bundle with grouped features", func(t *testing.T) {
@@ -49,6 +49,6 @@ func TestBundle(t *testing.T) {
 			MustBundle(f3, f4),
 		)
 
-		assert.Equal(t, 4, len(b.Features))
+		assert.Equal(t, 4, len(b.Features()))
 	})
 }
diff --git a/libbeat/feature/feature.go b/libbeat/feature/feature.go
index 0931bbe53..1d2621982 100644
--- a/libbeat/feature/feature.go
+++ b/libbeat/feature/feature.go
@@ -59,6 +59,11 @@ func (f *Feature) Stability() Stability {
 	return f.stability
 }
 
+// Features allows to Bundle a Feature with a bundle of features.
+func (f *Feature) Features() []Featurable {
+	return []Featurable{f}
+}
+
 // Equal return true if both object are equals.
 func (f *Feature) Equal(other Featurable) bool {
 	// There is no safe way to compare function in go,

54 changes: 54 additions & 0 deletions libbeat/feature/bundle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package feature

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestBundle(t *testing.T) {
factory := func() {}
features := []Featurable{
New("libbeat.outputs", "elasticsearch", factory, Stable),
New("libbeat.outputs", "edge", factory, Experimental),
New("libbeat.input", "tcp", factory, Beta),
}

t.Run("Creates a new Bundle", func(t *testing.T) {
b := NewBundle(features)
assert.Equal(t, 3, len(b.Features))
})

t.Run("Filters feature based on stability", func(t *testing.T) {
b := NewBundle(features)
new := b.Filter(Experimental)
assert.Equal(t, 1, len(new.Features))
})

t.Run("Filters feature based on multiple different stability", func(t *testing.T) {
b := NewBundle(features)
new := b.Filter(Experimental, Stable)
assert.Equal(t, 2, len(new.Features))
})

t.Run("Creates a new Bundle from specified feature", func(t *testing.T) {
f1 := New("libbeat.outputs", "elasticsearch", factory, Stable)
b := MustBundle(f1)
assert.Equal(t, 1, len(b.Features))
})

t.Run("Creates a new Bundle with grouped features", func(t *testing.T) {
f1 := New("libbeat.outputs", "elasticsearch", factory, Stable)
f2 := New("libbeat.outputs", "edge", factory, Experimental)
f3 := New("libbeat.input", "tcp", factory, Beta)
f4 := New("libbeat.input", "udp", factory, Beta)

b := MustBundle(
MustBundle(f1),
MustBundle(f2),
MustBundle(f3, f4),
)

assert.Equal(t, 4, len(b.Features))
})
}
120 changes: 120 additions & 0 deletions libbeat/feature/feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package feature

import (
"fmt"
"reflect"
)

//go:generate stringer -type=Stability
Copy link

Choose a reason for hiding this comment

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

How about the 'generate stringer' command next to the type declaration or in a separate 0gen.go file. The generated file already contains the type name. Having the generate command next to the type declaration makes it easier to find when navigating the code. Like:

// Comment...
// go:generate stringer -type=Stability
type Stability int

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was added automatically when you call generate stringer -type=Stability in the feature directory :)


// Registry is the global plugin registry, this variable is mean to be temporary to move all the
Copy link

Choose a reason for hiding this comment

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

s/mean/meant/

// internal factory to receive a context that include the current beat registry.
var Registry = newRegistry()

// Featurable implements the description of a feature.
type Featurable interface {
// Namespace returns the namespace of the Feature.
Namespace() string

// Name returns the name of the feature. The name must be unique for each namespace.
Name() string

// Factory returns the factory func.
Factory() interface{}

// Stability returns the stability of the feature.
Stability() Stability

// Equal returns true if the two object are equal.
Equal(other Featurable) bool

String() string
}
Copy link

Choose a reason for hiding this comment

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

'X returns x' is kind of redundant. Plus godoc will not generate any docs for the methods. Instead the type will be shown in the docs as is in code. Rather then stating the obvious, describing how Featurable interacts with the Bundle and Registry would be more helpful. What's the purpose of Namespace. Why do I need to implement 'equal'?


// Feature contains the information for a specific feature
type Feature struct {
namespace string
name string
factory interface{}
stability Stability
}

// Namespace return the namespace of the feature.
func (f *Feature) Namespace() string {
return f.namespace
}

// Name returns the name of the feature.
func (f *Feature) Name() string {
return f.name
}

// Factory returns the factory for the feature.
func (f *Feature) Factory() interface{} {
return f.factory
}

// Stability returns the stability level of the feature, current: stable, beta, experimental.
func (f *Feature) Stability() Stability {
return f.stability
}

// Equal return true if both object are equals.
func (f *Feature) Equal(other Featurable) bool {
// There is no safe way to compare function in go,
// but since the method are global it should be stable.
Copy link

Choose a reason for hiding this comment

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

s/method/function pointers/

if f.Name() == other.Name() &&
f.Namespace() == other.Namespace() &&
reflect.ValueOf(f.Factory()).Pointer() == reflect.ValueOf(other.Factory()).Pointer() {
return true
}

return false
}

// String return the debug information
func (f *Feature) String() string {
return fmt.Sprintf("%s/%s (stability: %s)", f.namespace, f.name, f.stability)
}

// Stability defines the stability of the feature, this value can be used to filter a bundler.
type Stability int

// List all the available stability for a feature.
const (
Stable Stability = iota
Beta
Experimental
Undefined
)
Copy link

@urso urso Jun 25, 2018

Choose a reason for hiding this comment

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

Nice trick:

const (
  Undefined Stability = iota
  Stable
  Beta
  Experimental
)

This way the 'zero value' of Stability becomes Undefiend -> force developer to use a constant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I have added undefined at the end, will make the change.


// New returns a new Feature.
func New(namespace, name string, factory interface{}, stability Stability) *Feature {
return &Feature{
namespace: namespace,
name: name,
factory: factory,
stability: stability,
}
}

// RegisterBundle registers a bundle of features.
func RegisterBundle(bundle Bundle) error {
for _, f := range bundle.Features {
Registry.Register(f)
}
return nil
}

// Register register a new feature on the global registry.
func Register(feature Featurable) error {
return Registry.Register(feature)
}

// MustRegister register a new Feature on the global registry and panic on error.
func MustRegister(feature Featurable) {
err := Register(feature)
if err != nil {
panic(err)
}
}
141 changes: 141 additions & 0 deletions libbeat/feature/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package feature

import (
"fmt"
"sync"

"github.com/elastic/beats/libbeat/logp"
)

type mapper map[string]map[string]Featurable

// Registry implements a global registry for any kind of feature in beats.
// feature are grouped by namespace, a namespace is a kind of plugin like outputs, inputs, or queue.
// The feature name must be unique.
type registry struct {
sync.RWMutex
namespaces mapper
log *logp.Logger
}

// NewRegistry returns a new registry.
func newRegistry() *registry {
return &registry{
namespaces: make(mapper),
log: logp.NewLogger("registry"),
}
}

// Register registers a new feature into a specific namespace, namespace are lazy created.
// Feature name must be unique.
func (r *registry) Register(feature Featurable) error {
r.Lock()
defer r.Unlock()

// Lazy create namespaces
_, found := r.namespaces[feature.Namespace()]
if !found {
r.namespaces[feature.Namespace()] = make(map[string]Featurable)
}

f, found := r.namespaces[feature.Namespace()][feature.Name()]
if found {
if feature.Equal(f) {
Copy link

@urso urso Jun 25, 2018

Choose a reason for hiding this comment

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

We can remove Equal from the public interface by providing a helper function checking for equal:

func featuresEq(f1, f2 Featurable) bool {
  type comparable interface { Equal(other Featurable) bool }
  if cmp, ok := f1.(comparable); ok {
    return cmp.Equal(f2)
  }
  return false
} 

// Allow both old style and new style of plugin to work together.
r.log.Debugw(
"ignoring, feature '%s' is already registered in the namespace '%s'",
feature.Name(),
feature.Namespace(),
)
return nil
}

return fmt.Errorf(
"could not register new feature '%s' in namespace '%s', feature name must be unique",
feature.Name(),
feature.Namespace(),
)
}

r.log.Debugw(
"registering new feature",
"namespace",
feature.Namespace(),
"name",
feature.Name(),
)

r.namespaces[feature.Namespace()][feature.Name()] = feature
Copy link

Choose a reason for hiding this comment

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

Instead of calling a function that might potentially generate a string on every call (it's all interfaces), let's introduce some local vars: ns, name := feature.Namespace(), feature.Name().


return nil
}

// Unregister removes a feature from the registry.
func (r *registry) Unregister(namespace, name string) error {
r.Lock()
defer r.Unlock()

v, found := r.namespaces[namespace]
if !found {
return fmt.Errorf("unknown namespace named '%s'", namespace)
}

_, found = v[name]
if !found {
return fmt.Errorf("unknown feature '%s' in namespace '%s'", name, namespace)
}

delete(r.namespaces[namespace], name)
return nil
}

// Find returns a specific Find from a namespace or an error if not found.
Copy link

Choose a reason for hiding this comment

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

Find searches for a Feature by the namespace-name pair.

Alternative name: Lookup

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm OK for using Lookup instead.

func (r *registry) Find(namespace, name string) (Featurable, error) {
r.RLock()
defer r.RUnlock()

v, found := r.namespaces[namespace]
if !found {
return nil, fmt.Errorf("unknown namespace named '%s'", namespace)
}

m, found := v[name]
if !found {
return nil, fmt.Errorf("unknown feature '%s' in namespace '%s'", name, namespace)
}

return m, nil
}

// FindAll returns all the features for a specific namespace.
func (r *registry) FindAll(namespace string) ([]Featurable, error) {
r.RLock()
defer r.RUnlock()

v, found := r.namespaces[namespace]
if !found {
return nil, fmt.Errorf("unknown namespace named '%s'", namespace)
}

list := make([]Featurable, len(v))
c := 0
for _, feature := range v {
list[c] = feature
c++
}

return list, nil
}

// Size returns the number of registered features in the registry.
func (r *registry) Size() int {
r.RLock()
defer r.RUnlock()

c := 0
for _, namespace := range r.namespaces {
c += len(namespace)
}

return c
}
Loading