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

Delta Diff MVP: Plugins manager #5153

Merged
merged 21 commits into from
Feb 5, 2023
Merged

Conversation

Jonathan-Rosenberg
Copy link
Contributor

@Jonathan-Rosenberg Jonathan-Rosenberg commented Jan 31, 2023

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:

go-plugin

  • Generated code: Code generated by running protoc
  • Implemented code: Code needed to be implemented by us.
  • go-plugin code: Some related go-plugin code

Closes #4965

@Jonathan-Rosenberg Jonathan-Rosenberg added the exclude-changelog PR description should not be included in next release changelog label Jan 31, 2023
Copy link
Contributor

@arielshaqed arielshaqed left a 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.

Comment on lines 56 to 58
if m == nil || m.pluginApplicationClients == nil {
return ErrUninitializedManager
}
Copy link
Contributor

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.

}

// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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).

Copy link
Contributor Author

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

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.

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 previous argument was that a handshake is more of an auth entity rather than an identifier.

}

// LoadPluginClient loads a Client of type T.
func (m *Manager[T]) LoadPluginClient(name string) (*T, error) {
Copy link
Contributor

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.

Copy link
Contributor Author

@Jonathan-Rosenberg Jonathan-Rosenberg Feb 2, 2023

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?

Copy link
Contributor

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!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

got it

Comment on lines 95 to 106
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
}
Copy link
Contributor

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.

Copy link
Contributor Author

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 🤫

return cmd
}

// This is not a real test. This is the plugin server that will be initialized by the tests
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

@talSofer talSofer left a 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

m *Manager[PingPongStub]
name string
p plugin.Plugin
err error
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
err error
expectedErr error

Copy link
Contributor

@arielshaqed arielshaqed left a 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?

}

// LoadPluginClient loads a Client of type T.
func (m *Manager[T]) LoadPluginClient(name string) (*T, error) {
Copy link
Contributor

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!

Comment on lines 56 to 58
if m == nil || m.pluginApplicationClients == nil {
return
}
Copy link
Contributor

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.

Comment on lines +97 to +98
// This is not a real test. This is the plugin server that will be triggered by the tests
func TestPluginServer(*testing.T) {
Copy link
Contributor

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.

Copy link
Contributor Author

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 named TestXXX with signature func (t *testing.T).

Copy link
Contributor

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)
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

true this is useless

@Jonathan-Rosenberg
Copy link
Contributor Author

@arielshaqed, the plugin manager is dependent on the go-plugin (Hashicorp's) package which receives an exec.Command as the plugin server's binary it should run. It also kills it when the plugin's job is over.
It means that a running go routine is not an option.
We can implement a separate process, but I think it's quite an overkill.

Copy link
Contributor

@arielshaqed arielshaqed left a 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!

@arielshaqed
Copy link
Contributor

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?

Jonathan-Rosenberg and others added 3 commits February 5, 2023 13:10
Co-authored-by: Ariel Shaqed (Scolnicov) <ariels@treeverse.io>
Co-authored-by: Ariel Shaqed (Scolnicov) <ariels@treeverse.io>
@Jonathan-Rosenberg Jonathan-Rosenberg merged commit d996d9e into master Feb 5, 2023
@Jonathan-Rosenberg Jonathan-Rosenberg deleted the feature/grpc-plugin-manager branch February 5, 2023 13:26
Copy link
Contributor

@johnnyaug johnnyaug left a 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.

Comment on lines +176 to +179
IsDebugging() bool
IsInfo() bool
IsError() bool
IsWarn() bool
Copy link
Contributor

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 (
Copy link
Contributor

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 {
Copy link
Contributor

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
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 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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

Comment on lines +97 to +98
// This is not a real test. This is the plugin server that will be triggered by the tests
func TestPluginServer(*testing.T) {
Copy link
Contributor

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

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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
func (m *Manager[T]) LoadPluginClient(name string) (T, error) {
func (m *Manager[T]) DispensePluginClient(name string) (T, error) {

I think is more accurate?

Comment on lines +39 to +41
// 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.
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
exclude-changelog PR description should not be included in next release changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Delta Diff MVP - Task: Create a go-plugin client abstraction
4 participants