-
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
Introduce generalized multi node client #10907
Conversation
I see that you haven't updated any README files. Would it make sense to do so? |
Secondary | ||
) | ||
|
||
func (n NodeTier) String() string { |
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.
Is it ok that fields of type NodeTier are only used in logging and no other logic depends on it?
common/client/node.go
Outdated
} | ||
|
||
func (n *node[CHAIN_ID, HEAD, RPC]) String() string { | ||
s := fmt.Sprintf("(primary)%s:%s", n.name, n.ws.String()) |
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 we use client.NodeTier?
// verifyLoop may only be triggered once, on Start, if initial chain ID check | ||
// fails. | ||
// | ||
// It will continue checking until success and then exit permanently. |
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.
Could you elaborate on why it's ok?
common/client/send_only_node.go
Outdated
"github.com/smartcontractkit/chainlink/v2/core/utils" | ||
) | ||
|
||
type SendOnlyClient[ |
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 type is quite confusing:
- It does not require implementation to have any kind of
SendTransaction
- It's not used, as in NewMultiNode we require
sendOnlyNodes
to use same RPC asnodes
I understand that it just defines methods used by sendOnlyNode
, but high number of public types might confuse users of the package. Can we use RPC as single dependency across all implementations? As alternative we can create package documentation explaining which types are essential for the users and which are internals.
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 for now we can unexport this interface so it doesn't pollute the API of the package. In the future though, once we decide on a testing strategy for this package, it will make more sense to have a targeted client API that can be easily mocked instead of having an entire RPC interface.
|
||
String() string | ||
// State returns nodeState | ||
State() nodeState |
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 possible to inject custom implementation of SendOnlyNode|Node
as nodeState
is private.
Even if it's not needed, it feels weird
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 I understand this. Even if nodeState
is changed internally, State()
is used by the MultiNode to access the state. Let me know if you have something else on your mind.
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.
During integration of new chain someone might benefit from possibility to create their own implementation of SendOnlyNode
or Node
and still use it with MultiNode. But it's not possible to create custom implementation in different package as there is no way to implement State() nodeState
method as nodeState
is private for common/client
package.
One way to resolve it is to make SendOnlyNode
and Node
private types and create instances of them in NewMultiNode
using passed RPCs.
Another is to keep nodeState public
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.
Personally, I would go in the exact opposite direction and totally abstract Node from the user. Then they would only have to instantiate the MultiNode and the RPCs. The aim of this package is for the user to use the exact same API as Geth or other clients, without worrying about the complexity of internal node states. The only issue I see with that is that currently Node instantiation is coupled with toml.Node
config and it might need some additional changes.
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 having trouble following this thread, but there are two different concerns here:
- Whether or not the method is exported affects who can call it.
- Whether or the returned type is exported affects who can implement it.
Isn't it critical for this to be implemented by types outside of this 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.
The aim of this package is for the user to use the exact same API as Geth or other clients, without worrying about the complexity of internal node states.
Can you elaborate on this? That doesn't fit my mental model. The Geth API is the thing that we want to abstract away, in order to reuse all the stuff that is not specific to geth's API, with other chain clients. Aren't the 'user's of this package the developers integrating those other chain clients? Do they need to implement this method? Call it?
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.
Maybe there are two separate uses here? Unexported & strongly-typed, and exported string?
State() nodeState | |
state() nodeState |
func (s *sendOnlyNode[CHAIN_ID, RPC]) State() string {
return string(s.state())
}
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.
Yes, we do abstract the geth-specific calls into more generic ones, but at the same time, we should aim to abstract the Node as much as possible as well. I see two use cases here:
- If a user wants to interact with a chain, instead of utilizing a Geth library or some other chain-specific one, we give them a slightly more generalized version of it that can apply to most chains. We should still aim to provide familiar methods like
BlockByHash
because the average user just wants to have methods as close as possible to the chain client. - To me the user shouldn't be worried about the internal node lifecycle and ideally about the creation and implementation of the node. The transition should be as simple as: "Instead of using a generalized API that makes direct calls to an RPC, you can pass multiple URLs and still use the same calls and we will take care of how the multi-node spins up nodes and handles them. Ideally, the nodes should be totally abstracted.
One example I can think of is the RTSP team. They want to have a client that interacts with a chain without worrying about a node's lifecycle and state.
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.
By 'abstract away' I mean exclude. Meaning that the multi-node code can be used by any kind of client. This is a separate concern from providing a chain-agnostic API layer for clients to adapt. Each has distinctly different ideas of what a user is. Entangling them comes at a cost.
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 think the problem starts from the fact that the original code exports all these fields and methods we are trying to generalize because they are used in different packages for mocking and testing (perhaps it's also on purpose to allow users to extend?).
So when we transferred everything we kept the same convention, but common/client
doesn't have tests on the same package as it requires concrete types, and that means we could unexport everything and let the MultiNode act as the interface with the user.
But the question here is do we want the nodes to be extended by the user or totally abstracted? Also, something to consider is that once we agree on a strategy to test generalized packages we might end up going back to exporting those methods and fields.
common/client/multi_node.go
Outdated
NodeSelectionMode_HighestHead = "HighestHead" | ||
NodeSelectionMode_RoundRobin = "RoundRobin" | ||
NodeSelectionMode_TotalDifficulty = "TotalDifficulty" | ||
NodeSelectionMode_PriorityLevel = "PriorityLevel" |
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 know what the best way to name these is, but let's drop the un-idiomatic underscore?
NodeSelectionMode_HighestHead = "HighestHead" | |
NodeSelectionMode_RoundRobin = "RoundRobin" | |
NodeSelectionMode_TotalDifficulty = "TotalDifficulty" | |
NodeSelectionMode_PriorityLevel = "PriorityLevel" | |
NodeSelectionModeHighestHead = "HighestHead" | |
NodeSelectionModeRoundRobin = "RoundRobin" | |
NodeSelectionModeTotalDifficulty = "TotalDifficulty" | |
NodeSelectionModePriorityLevel = "PriorityLevel" |
NodeSelectionMode_HighestHead = "HighestHead" | |
NodeSelectionMode_RoundRobin = "RoundRobin" | |
NodeSelectionMode_TotalDifficulty = "TotalDifficulty" | |
NodeSelectionMode_PriorityLevel = "PriorityLevel" | |
HighestHeadNodeSelectionMode = "HighestHead" | |
RoundRobinNodeSelectionMode = "RoundRobin" | |
TotalDifficultyNodeSelectionMode = "TotalDifficulty" | |
PriorityLevelNodeSelectionMode = "PriorityLevel" |
NodeSelectionMode_HighestHead = "HighestHead" | |
NodeSelectionMode_RoundRobin = "RoundRobin" | |
NodeSelectionMode_TotalDifficulty = "TotalDifficulty" | |
NodeSelectionMode_PriorityLevel = "PriorityLevel" | |
HighestHeadSelectionMode = "HighestHead" | |
RoundRobinSelectionMode = "RoundRobin" | |
TotalDifficultySelectionMode = "TotalDifficulty" | |
PriorityLevelSelectionMode = "PriorityLevel" |
|
||
// TODO-1663: change this to actual ChainID() call once client.go is deprecated. | ||
func (c *chainClient) ChainID() (*big.Int, error) { | ||
//return c.multiNode.ChainID(ctx), 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.
//return c.multiNode.ChainID(ctx), nil |
Duplicate #9900, originally drafted by Silas.
common/chains/client
should also be moved undercommon/client
in a new PR.