-
Notifications
You must be signed in to change notification settings - Fork 370
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
Delta Diff MVP: Plugins manager #5153
Conversation
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.
Thanks! Very nice & useful.
A bunch of minor-but-important things that I think we should improve.
pkg/plugins/manager.go
Outdated
if m == nil || m.pluginApplicationClients == nil { | ||
return ErrUninitializedManager | ||
} |
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.
Do we ever need an uninitialized plugin manager? I'd just remove it if that's impossible. It's not like the caller can do anything on receipt of this error other than panic
. And a nil pointer dereference here will be more informative than the error message for a programming error.
pkg/plugins/manager.go
Outdated
} | ||
|
||
// RegisterPlugin is used to register a new plugin client with the corresponding plugin type. | ||
func (m *Manager[T]) RegisterPlugin(name string, id PluginIdentity, auth PluginHandshake, p plugin.Plugin) error { |
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.
func (m *Manager[T]) RegisterPlugin(name string, id PluginIdentity, auth PluginHandshake, p plugin.Plugin) error { | |
func (m *Manager[T]) RegisterPlugin(name string, id PluginIdentity, handshake PluginHandshake, p plugin.Plugin) error { |
It's a handshake not an auth:
- No secrets, can and should log it.
- Comes from regular configuration (e.g. K8s configmap not secret).
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.
it kinda is- the plugin client and server agree on a certain key and value that identify one another
Cmd *exec.Cmd | ||
} | ||
|
||
// PluginHandshake includes handshake properties for the plugin. |
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.
I wonder, isn't this tied 1:1 with PluginIdentity? I know we've been over this before, I don't see in the code why they need to be separate.
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.
the previous argument was that a handshake is more of an auth entity rather than an identifier.
pkg/plugins/manager.go
Outdated
} | ||
|
||
// LoadPluginClient loads a Client of type T. | ||
func (m *Manager[T]) LoadPluginClient(name string) (*T, error) { |
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.
Consider spec'cing the manager on *T
when you create it, and returning T
here. For instance, if T
is an interface (it often will be!) then this is a pointer-to-interface, which is really weird. Even if the current implementation in the library returns a struct, there is no requirement for it to stay that way.
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.
What do you mean by Consider **spec'cing** the manager on *T
?
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.
The type Manager[T]
returns clients of type *T
. This is technically correct if the client is a struct, and we instantiate it on the struct type:
type MyClient struct { /* stuff */ } // But really the client is *MyClient
func a(m Manager[MyClient], name string) {
var client *MyClient = m.LoadPluginClient(name)
// client stuff
This works mostly because *MyClient
satisfies the MyClient
interface (the converse is not really true!).
BUT if T
is an interface type, then Manager[T].LoadPluginClient
returns a *T
, which is a pretty useless type.
AFAIU the more Go-like interface would be to return a T
from Manager[T].LoadPluginClient
. In the *MyClient
case above, you would need to use Manager[*MyClient]
as the manager type. But that makes sense: the client type really is *MyClient
there!
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.
got it
pkg/plugins/manager.go
Outdated
c, ok := m.pluginApplicationClients[name] | ||
if !ok { | ||
return nil, ErrPluginNameNotFound | ||
} | ||
grpcPluginClient, err := c.Client() | ||
if err != nil { | ||
return nil, err | ||
} | ||
rawGrpcClientStub, err := grpcPluginClient.Dispense(name) // Returns an implementation of the stub service. | ||
if err != nil { | ||
return nil, err | ||
} |
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.
This code needs a mutex to be thread-safe.
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.
True, it's implemented on the next PR 🤫
pkg/plugins/manager_test.go
Outdated
return cmd | ||
} | ||
|
||
// This is not a real test. This is the plugin server that will be initialized by the tests |
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.
Then it shouldn't be a test. Perhaps this needs to be TestMain
?
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.
It's triggered by the tests themselves, not initialized at the beginning of the tests
…` -> `uint` Co-authored-by: Ariel Shaqed (Scolnicov) <ariels@treeverse.io>
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.
Thanks! added a few minor comments
pkg/plugins/manager_test.go
Outdated
m *Manager[PingPongStub] | ||
name string | ||
p plugin.Plugin | ||
err error |
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.
err error | |
expectedErr error |
Co-authored-by: talSofer <tal.sofer@treeverse.io>
Co-authored-by: talSofer <tal.sofer@treeverse.io>
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.
The TestPluginServer hook is a really strange way to run the test plugin server. It should just be a separate program. Failing that, if building 2 binaries for a test is too hard because Go, it might execute as a TestMain and then do one of 2 things.
But I want to question even that. Why run the plugin server as a separate process? It's just a web server, can't we run it in a separate goroutine?
pkg/plugins/manager.go
Outdated
} | ||
|
||
// LoadPluginClient loads a Client of type T. | ||
func (m *Manager[T]) LoadPluginClient(name string) (*T, error) { |
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.
The type Manager[T]
returns clients of type *T
. This is technically correct if the client is a struct, and we instantiate it on the struct type:
type MyClient struct { /* stuff */ } // But really the client is *MyClient
func a(m Manager[MyClient], name string) {
var client *MyClient = m.LoadPluginClient(name)
// client stuff
This works mostly because *MyClient
satisfies the MyClient
interface (the converse is not really true!).
BUT if T
is an interface type, then Manager[T].LoadPluginClient
returns a *T
, which is a pretty useless type.
AFAIU the more Go-like interface would be to return a T
from Manager[T].LoadPluginClient
. In the *MyClient
case above, you would need to use Manager[*MyClient]
as the manager type. But that makes sense: the client type really is *MyClient
there!
pkg/plugins/manager.go
Outdated
if m == nil || m.pluginApplicationClients == nil { | ||
return | ||
} |
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.
Not sure when this is useful.
// This is not a real test. This is the plugin server that will be triggered by the tests | ||
func TestPluginServer(*testing.T) { |
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.
If this is not a test, it should start with something other than Test
. Also not sure why it takes *testing.T
parameter.
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.
It's not a test, yet the command that runs it is go test
which accepts the -test.run
flag that specifies the tests it should run. Unfortunately, golang don't allow tests to be named other than Test...
and must accept *testing.T
as an argument:
You write a test by creating a file with a name ending in
_test.go
that contains functions namedTestXXX
with signaturefunc (t *testing.T)
.
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.
I like the ping-pong protobuf and additional classes, since it explains very well how plugins and grcp work. However, I'm wondering about this methodology, because this test doesn't (and shouldn't) test the logic of the plugins. I would expect this test to mock both plugin.Plugin
and plugin.Client
, so it can become a strict unit test.
One problem is the plugin.Client
is a struct and not an interface, but I suggest creating an interface on our side in order to allow mocking it as well:
type Client interface {
Client() (ClientProtocol, error)
}
return | ||
} | ||
|
||
defer os.Exit(0) |
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.
Not sure why a test helper should ever exit. This is even worse, it exits successfully when something strange happens. But if that happens, the test succeeds even though it should fail.
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.
true this is useless
@arielshaqed, the plugin manager is dependent on the |
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.
Thanks for your patience explaining the go-plugin test horridness to me!
While we're there, can you use one of the go-plugin test plugin plugins, maybe this one? |
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.
Mainly cleanups and naming. Also, I really enjoyed reading the tests, they helped me understand the world of go-plugins. However, not sure the methodology is optimal for these specific tests.
IsDebugging() bool | ||
IsInfo() bool | ||
IsError() bool | ||
IsWarn() bool |
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.
What are these for?
Also - these are some very confusing name, each in its own way.
@@ -0,0 +1,64 @@ | |||
package plugins | |||
|
|||
import ( |
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.
filename should be in lowercase
Sound sound = 1; | ||
} | ||
|
||
service PingPong { |
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.
Love it
return nil | ||
} | ||
|
||
// GRPCClient will return the Delta diff GRPC custom client |
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.
I think the comment is outdated?
} | ||
|
||
// GRPCClient will return the Delta diff GRPC custom client | ||
func (p GRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { |
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.
func (p GRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { | |
func (p GRPCPlugin) GRPCClient(_ context.Context, _ *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { |
Also in other places in the file
// This is not a real test. This is the plugin server that will be triggered by the tests | ||
func TestPluginServer(*testing.T) { |
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.
I like the ping-pong protobuf and additional classes, since it explains very well how plugins and grcp work. However, I'm wondering about this methodology, because this test doesn't (and shouldn't) test the logic of the plugins. I would expect this test to mock both plugin.Plugin
and plugin.Client
, so it can become a strict unit test.
One problem is the plugin.Client
is a struct and not an interface, but I suggest creating an interface on our side in order to allow mocking it as well:
type Client interface {
Client() (ClientProtocol, error)
}
MagicCookieKey: auth.Key, | ||
MagicCookieValue: auth.Value, | ||
} | ||
cmd := id.Cmd |
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.
It's confusing to me that an "id" contains a command.
} | ||
|
||
// LoadPluginClient loads a Client of type T. | ||
func (m *Manager[T]) LoadPluginClient(name string) (T, error) { |
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.
func (m *Manager[T]) LoadPluginClient(name string) (T, error) { | |
func (m *Manager[T]) DispensePluginClient(name string) (T, error) { |
I think is more accurate?
// grpcPluginClient, err := c.Client() // Returns a plugin.GRPCClient | ||
// rawGrpcClientStub, err := grpcPluginClient.Dispense(name) // Calls grpcPluginClient's GRPCClient method and returns the gRPC stub. | ||
// grpcClient, ok := rawGrpcClientStub.(Differ) // Asserts the expected type of stub client. |
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.
This is an explanation of an implementation detail.
The plugin manager will be used to store
go-plugin
application clients and to load gRPC clients needed to communicate with gRPC server.Each type of "plugin service" (like Diff Service for example) will initialize its own plugin manager with its plugin type.
This PR is the final version of #5024
Optional
This is a "
go-plugin
implementation" diagram to help understand this package:protoc
go-plugin
codeCloses #4965