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

[CAPPL-19] Gateway Connector service wrapper #14356

Merged
merged 29 commits into from
Sep 11, 2024

Conversation

DavidOrchard
Copy link
Contributor

@DavidOrchard DavidOrchard commented Sep 5, 2024

Add serviceWrapper for gatewayconnector, cappl-19

if len(enabledKeys) == 0 {
return errors.New("no available keys found")
}
signerKey := enabledKeys[0].ToEcdsaPrivKey()
Copy link
Contributor

Choose a reason for hiding this comment

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

Open question: how do we handle multiple keys being enabled on the node? Will we always pick the same key?

@DavidOrchard DavidOrchard force-pushed the feature/CAPPL-19-connector-service-3 branch from 80ba68a to e951bdb Compare September 6, 2024 18:03
}

func (e *serviceWrapper) Close() error {
return e.StopOnce("WorkflowConnectorHandler", func() (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.

should be "GatewayConnectorServiceWrapper"

}
// cannot assign to conf.NodeAddress (neither addressable nor a map index expression
// I think I'm still confused about mixing interfaces and structures, and how to use a setter on an interface.
conf.NodeAddress = enabledKeys[idx].ID()
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't have to do this at all. You already used the configured address to find the needed key. No need to modify the config struct.

return err
}
translated := translateConfigs(conf)
e.connector, err = connector.NewGatewayConnector(&translated, handler, handler, clockwork.NewRealClock(), e.lggr)
Copy link
Contributor

Choose a reason for hiding this comment

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

With the proposed changes to GatewayConnector, soon you won't need to provide the handler here any more in the constructor (there will be an AddHandler() method to do it dynamically and possibly multiple times). For now, we should strip the workflowConnectorHandler into being only a Signer like I suggested above. Let's also move it into the service_wrapper.go file for simplicity.

"github.com/smartcontractkit/chainlink/v2/core/services/gateway/connector"
)

type workflowConnectorHandler struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's rename it to connectorSigner, strip down as much as we can and put it together with the Wrapper in the same file. Soon the handler portion will go away as I explained in the other comment below.

lggr logger.Logger
}

type Response struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

remove this


var (
_ connector.Signer = &workflowConnectorHandler{}
// _ connector.GatewayConnectorHandler = &workflowConnectorHandler{}
Copy link
Contributor

Choose a reason for hiding this comment

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

remove those

@DavidOrchard DavidOrchard force-pushed the feature/CAPPL-19-connector-service-3 branch from fd6ae00 to 7784141 Compare September 6, 2024 20:02
@DavidOrchard DavidOrchard marked this pull request as ready for review September 6, 2024 20:30
@DavidOrchard DavidOrchard requested a review from a team as a code owner September 6, 2024 20:30
@DavidOrchard DavidOrchard requested review from bolekk and removed request for a team September 6, 2024 20:30
lggr logger.Logger
}

var (
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: you can simplify to one line by removing the parentheses

_ connector.Signer = &workflowConnectorSigner{}
)

func NewWorkflowConnectorSigner(config *config.GatewayConnector, signerKey *ecdsa.PrivateKey, lggr logger.Logger) (*workflowConnectorSigner, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's remove the word "workflow" from everywhere. Just ConnectorSigner.

func (h *workflowConnectorSigner) HandleGatewayMessage(ctx context.Context, gatewayID string, msg *api.Message) {
}
func (h *workflowConnectorSigner) Start(ctx context.Context) error {
return h.StartOnce("WorkflowConnectorHandler", func() error {
Copy link
Contributor

Choose a reason for hiding this comment

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

also here in those strings, just "ConnectorSigner"

r.WsClientConfig = network.WebSocketClientConfig{HandshakeTimeoutMillis: f.WSHandshakeTimeoutMillis()}
}

// 0 are valid values
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: unnecessary comment

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 got the response that I was looking for, which is you confirming that 0 is a valid value :-)


func translateConfigs(f config.GatewayConnector) connector.ConnectorConfig {
r := connector.ConnectorConfig{}
if f.NodeAddress() != "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need those checks. If it's empty, it's empty.

}
}

if !reflect.ValueOf(f.WSHandshakeTimeoutMillis).IsZero() {
Copy link
Contributor

Choose a reason for hiding this comment

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

same here

}
}

func (e *serviceWrapper) Start(ctx context.Context) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a unit test that creates the ServiceWrapper object and then calls Start() can Close() on it, please?
You can take inspiration from functions/plugin_test.go and functions/connector_handler_test.go on how to mock the dependencies.

Ideally you'd test valid NodeAddress and an invalid one (i.e. key doesn't exit).

@@ -0,0 +1,164 @@
package gatewayConnector
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the convention is to use gateway_connector 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.

lint barfed and said no underscores in package names.

Copy link
Contributor

Choose a reason for hiding this comment

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

Typically Go package names are not even full words, let alone multiple words.

Good package names are short and clear. They are lower case, with no under_scores or mixedCaps. They are often simple nouns...

Some resources:

Copy link
Contributor

Choose a reason for hiding this comment

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

In this case, perhaps something like gatewayconn.

We should keep the directory aligned too, rather than just rename the package.

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting... we have a lot of package names with underscores throughout core. I'll remember for the future, thanks!

Copy link
Contributor Author

@DavidOrchard DavidOrchard Sep 6, 2024

Choose a reason for hiding this comment

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

I find this guidance a bit confusing. gatewayConnector is bad but gatewayconn or gatewayconnector is good? I find the 2nd & 3rd a bit harder to read because it is 2 words and almost all the same height. Obviously I have to respect the lint standards, but I'm not bought into the standard.

I think you are also suggesting the directory should change from gateway_connector to gatewayconnector, though that's not in the lint afaik.

_, addr := testutils.NewPrivateKeyAndAddress(t)

config, err := chainlink.GeneralConfigOpts{}.New()
// I don't think this is the right way to get the GatewayConnector in.
Copy link
Contributor

@bolekk bolekk Sep 9, 2024

Choose a reason for hiding this comment

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

It's not. You should do it in the line above. Instead of creating an empty GeneralConfigOpts{} object, you create one with desired config already in it. Something like:

config, err := GeneralConfigOpts{
	Config: chainlink.Config{
		Core: toml.Core{
			Capabilities: toml.Capabilities{
				GatewayConnector: toml.GatewayConnector{
					// all the fields here
				},
			},
		},
	},
}.New()


"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
"github.com/smartcontractkit/chainlink/v2/core/logger"
chainlink "github.com/smartcontractkit/chainlink/v2/core/services/chainlink/config"
Copy link
Contributor

Choose a reason for hiding this comment

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

No such package, I think you just wanted "(...)/services/chainlink"


t.Run("Start Error", func(t *testing.T) {
// Question, what's the best practices when testing a different configuration, is it 2 configs and then
// 2 handlers, or does the test do the initialization as in copy from line 27 to 41 into here?
Copy link
Contributor

Choose a reason for hiding this comment

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

Initialization is executed only once and then both .Run() sections re-use it. So you need to somehow create 2 configs, one valid and one invalid.

return err
}
translated := translateConfigs(conf)
e.connector, err = connector.NewGatewayConnector(&translated, signer, signer, clockwork.NewRealClock(), e.lggr)
Copy link
Contributor

Choose a reason for hiding this comment

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

I changed the signature of NewGatewayConnector() in this PR: #14367

}

// Unit test that creates the ServiceWrapper object and then calls Start() can Close() on it.
// Take inspiration from functions/plugin_test.go and functions/connector_handler_test.go on how to mock the dependencies.
Copy link
Contributor

Choose a reason for hiding this comment

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

You can remove all these comments and instead name the tests:

  • TestGatewayConnectorServiceWrapper_CleanStartClose
  • TestGatewayConnectorServiceWrapper_NonexistentKey


config, err := generateConfig(addr)
ethKeystore := ksmocks.NewEth(t)
ethKeystore.On("EnabledKeysForChain", mock.Anything).Return([]ethkey.KeyV2{{Address: addr}})
Copy link
Contributor

Choose a reason for hiding this comment

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

EnabledKeysForChain takes two arguments, not one.

assert.NoError(t, handler.Close())
})

t.Run("Start & Stop Success", 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.

You don't really need this t.Run() section here, especially that it's only a single test case.

require.NoError(t, err)

t.Cleanup(func() {
assert.NoError(t, handler.Close())
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 you can't have that and also call Close() manually below (Close will error if called more than once) - but please confirm.

ethKeystore.On("EnabledKeysForChain", mock.Anything).Return([]ethkey.KeyV2{{Address: addr2}})

gc := config.Capabilities().GatewayConnector()
handler := NewGatewayConnectorServiceWrapper(&gc, ethKeystore, logger)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be named wrapper, not handler.

"github.com/test-go/testify/mock"
)

func generateConfig(addr common.Address) (chainlink.GeneralConfig, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can put more shared logic in this helper. Instead of returning config, return the whole Wrapper object with mocks already set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I'd wanted to do that but apparently misunderstood your earlier comment.

type serviceWrapper struct {
services.StateMachine

config *config.GatewayConnector
Copy link
Contributor

Choose a reason for hiding this comment

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

No pointer here - GatewayConnector in an interface.


var _ connector.Signer = &connectorSigner{}

func NewConnectorSigner(config *config.GatewayConnector, signerKey *ecdsa.PrivateKey, lggr logger.Logger) (*connectorSigner, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

No pointer needed for config.

}

// NOTE: this wrapper is needed to make sure that our services are started after Keystore.
func NewGatewayConnectorServiceWrapper(config *config.GatewayConnector, keystore keystore.Eth, lggr logger.Logger) *serviceWrapper {
Copy link
Contributor

Choose a reason for hiding this comment

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

GatewayConnector is an interface, so you don't need a pointer (*)

"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey"
)

type serviceWrapper struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

I realized that the Wrapper also needs to have a getter for the underlying Connector object (we need to extract it once it's created). Please add it and also make ServiceWrapper public (uppercase) so we can pass it around in application.go.

@DavidOrchard DavidOrchard force-pushed the feature/CAPPL-19-connector-service-3 branch from d787b7b to 2b66be0 Compare September 10, 2024 14:57
@DavidOrchard DavidOrchard force-pushed the feature/CAPPL-19-connector-service-3 branch from 8c73f6b to 493f94c Compare September 10, 2024 18:34
return err
}
translated := translateConfigs(conf)
e.connector, err = connector.NewGatewayConnector(&translated, signer, clockwork.NewRealClock(), e.lggr)
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of using a real clock here, we may want to use a parameterized clock, such as keeping one on ServiceWrapper. This way we can pass a fake clock for testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bolekk your thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

makes sense to me


var _ connector.Signer = &connectorSigner{}

func NewConnectorSigner(config config.GatewayConnector, signerKey *ecdsa.PrivateKey, lggr logger.Logger) (*connectorSigner, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense for this to live in its own file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Bolek had asked for this func to be moved into here fwiw.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't care too much. It's ultra-small, can be anywhere you guys prefer.

Copy link
Contributor

@bolekk bolekk Sep 10, 2024

Choose a reason for hiding this comment

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

I'm now realizing that Signer doesn't need Start()/Close() so you can remove those to further simplify.
Same for SetConnector().

Honestly maybe even go a step further and make ServiceWrapper just implement connector.Signer interface :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I also don't have a strong opinion here, reads slightly cleaner to me being separate, but I'll leave it up to your opinion David.

Copy link
Contributor

Choose a reason for hiding this comment

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

Honestly maybe even go a step further and make ServiceWrapper just implement connector.Signer interface :)

+1 for this, would be a lot cleaner

"github.com/smartcontractkit/chainlink/v2/core/config"
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/gateway/api"
gwCommon "github.com/smartcontractkit/chainlink/v2/core/services/gateway/common"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: stick to go conventional package naming
https://go.dev/blog/package-names

return errors.New("node address mismatch")
}

signer, err := NewConnectorSigner(e.config, signerKey, e.lggr)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
signer, err := NewConnectorSigner(e.config, signerKey, e.lggr)
signer, err := NewConnectorSigner(conf, signerKey, e.lggr)

})
}

func (e *ServiceWrapper) Ready() error {
Copy link
Contributor

@justinkaseman justinkaseman Sep 10, 2024

Choose a reason for hiding this comment

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

This is overwriting services.StateMachine's implementation right? Probably better to leave this as the default.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh nice, I didn't realize it has defaults.

}

func (e *ServiceWrapper) HealthReport() map[string]error {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

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

To be thorough, how about filling this in from services.StateMachine?

Suggested change
return nil
return map[string]error{e.Name(): e.Healthy()}

bolekk
bolekk previously requested changes Sep 11, 2024
}

// NOTE: this wrapper is needed to make sure that our services are started after Keystore.
func NewGatewayConnectorServiceWrapper(config config.GatewayConnector, signerKey *ecdsa.PrivateKey, keystore keystore.Eth, clock clockwork.Clock, lggr logger.Logger) *ServiceWrapper {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can't have signerKey as a parameter to the constructor. We extract it from Keystore inside Start(). The caller of the constructor doesn't have it.

@DavidOrchard DavidOrchard requested a review from bolekk September 11, 2024 03:46
@DavidOrchard DavidOrchard dismissed bolekk’s stale review September 11, 2024 03:47

code changed per request.

@bolekk bolekk added this pull request to the merge queue Sep 11, 2024
Merged via the queue into develop with commit 51cb378 Sep 11, 2024
136 of 137 checks passed
@bolekk bolekk deleted the feature/CAPPL-19-connector-service-3 branch September 11, 2024 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants