-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Conversation
if len(enabledKeys) == 0 { | ||
return errors.New("no available keys found") | ||
} | ||
signerKey := enabledKeys[0].ToEcdsaPrivKey() |
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.
Open question: how do we handle multiple keys being enabled on the node? Will we always pick the same key?
80ba68a
to
e951bdb
Compare
} | ||
|
||
func (e *serviceWrapper) Close() error { | ||
return e.StopOnce("WorkflowConnectorHandler", func() (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.
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() |
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.
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) |
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.
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 { |
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.
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 { |
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.
remove this
|
||
var ( | ||
_ connector.Signer = &workflowConnectorHandler{} | ||
// _ connector.GatewayConnectorHandler = &workflowConnectorHandler{} |
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.
remove those
fd6ae00
to
7784141
Compare
lggr logger.Logger | ||
} | ||
|
||
var ( |
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.
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) { |
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.
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 { |
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.
also here in those strings, just "ConnectorSigner"
r.WsClientConfig = network.WebSocketClientConfig{HandshakeTimeoutMillis: f.WSHandshakeTimeoutMillis()} | ||
} | ||
|
||
// 0 are valid values |
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.
nit: unnecessary comment
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 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() != "" { |
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.
You don't need those checks. If it's empty, it's empty.
} | ||
} | ||
|
||
if !reflect.ValueOf(f.WSHandshakeTimeoutMillis).IsZero() { |
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.
same here
} | ||
} | ||
|
||
func (e *serviceWrapper) Start(ctx context.Context) 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.
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 |
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 believe the convention is to use gateway_connector here.
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.
lint barfed and said no underscores in package names.
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.
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:
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.
In this case, perhaps something like gatewayconn
.
We should keep the directory aligned too, rather than just rename the package.
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.
Interesting... we have a lot of package names with underscores throughout core. I'll remember for the future, thanks!
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 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. |
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. 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" |
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.
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? |
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.
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) |
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 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. |
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.
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}}) |
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.
EnabledKeysForChain takes two arguments, not one.
assert.NoError(t, handler.Close()) | ||
}) | ||
|
||
t.Run("Start & Stop Success", func(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.
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()) |
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 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) |
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.
Should be named wrapper, not handler.
"github.com/test-go/testify/mock" | ||
) | ||
|
||
func generateConfig(addr common.Address) (chainlink.GeneralConfig, 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.
You can put more shared logic in this helper. Instead of returning config, return the whole Wrapper object with mocks already set.
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.
Ok, I'd wanted to do that but apparently misunderstood your earlier comment.
type serviceWrapper struct { | ||
services.StateMachine | ||
|
||
config *config.GatewayConnector |
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.
No pointer here - GatewayConnector in an interface.
|
||
var _ connector.Signer = &connectorSigner{} | ||
|
||
func NewConnectorSigner(config *config.GatewayConnector, signerKey *ecdsa.PrivateKey, lggr logger.Logger) (*connectorSigner, 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.
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 { |
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.
GatewayConnector is an interface, so you don't need a pointer (*)
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" | ||
) | ||
|
||
type serviceWrapper struct { |
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 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.
d787b7b
to
2b66be0
Compare
8c73f6b
to
493f94c
Compare
return err | ||
} | ||
translated := translateConfigs(conf) | ||
e.connector, err = connector.NewGatewayConnector(&translated, signer, clockwork.NewRealClock(), e.lggr) |
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.
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.
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.
@bolekk your thoughts?
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.
makes sense to me
|
||
var _ connector.Signer = &connectorSigner{} | ||
|
||
func NewConnectorSigner(config config.GatewayConnector, signerKey *ecdsa.PrivateKey, lggr logger.Logger) (*connectorSigner, 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.
Does it make sense for this to live in its own file?
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.
Bolek had asked for this func to be moved into here fwiw.
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 don't care too much. It's ultra-small, can be anywhere you guys prefer.
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'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 :)
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 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.
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.
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" |
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.
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) |
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.
nit:
signer, err := NewConnectorSigner(e.config, signerKey, e.lggr) | |
signer, err := NewConnectorSigner(conf, signerKey, e.lggr) |
}) | ||
} | ||
|
||
func (e *ServiceWrapper) Ready() 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.
This is overwriting services.StateMachine
's implementation right? Probably better to leave this as the default.
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.
Oh nice, I didn't realize it has defaults.
} | ||
|
||
func (e *ServiceWrapper) HealthReport() map[string]error { | ||
return nil |
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.
To be thorough, how about filling this in from services.StateMachine
?
return nil | |
return map[string]error{e.Name(): e.Healthy()} |
} | ||
|
||
// 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 { |
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.
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.
Quality Gate passedIssues Measures |
Add serviceWrapper for gatewayconnector, cappl-19