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

Introduce generalized multi node client #10907

Merged
merged 12 commits into from
Nov 1, 2023
Merged

Conversation

dimriou
Copy link
Collaborator

@dimriou dimriou commented Oct 11, 2023

Duplicate #9900, originally drafted by Silas.

common/chains/client should also be moved under common/client in a new PR.

@github-actions
Copy link
Contributor

I see that you haven't updated any README files. Would it make sense to do so?

@dimriou dimriou requested a review from dhaidashenko October 11, 2023 15:38
common/client/node_fsm.go Outdated Show resolved Hide resolved
Secondary
)

func (n NodeTier) String() string {
Copy link
Collaborator

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/multi_node.go Show resolved Hide resolved
common/client/node_fsm.go Show resolved Hide resolved
}

func (n *node[CHAIN_ID, HEAD, RPC]) String() string {
s := fmt.Sprintf("(primary)%s:%s", n.name, n.ws.String())
Copy link
Collaborator

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?

common/chains/client/models.go Show resolved Hide resolved
common/client/node_lifecycle.go Show resolved Hide resolved
common/client/node_lifecycle.go Show resolved Hide resolved
// verifyLoop may only be triggered once, on Start, if initial chain ID check
// fails.
//
// It will continue checking until success and then exit permanently.
Copy link
Collaborator

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/multi_node.go Show resolved Hide resolved
"github.com/smartcontractkit/chainlink/v2/core/utils"
)

type SendOnlyClient[
Copy link
Collaborator

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:

  1. It does not require implementation to have any kind of SendTransaction
  2. It's not used, as in NewMultiNode we require sendOnlyNodes to use same RPC as nodes

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.

Copy link
Collaborator Author

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

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

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Copy link
Collaborator Author

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.

Copy link
Contributor

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:

  1. Whether or not the method is exported affects who can call it.
  2. 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?

Copy link
Contributor

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?

Copy link
Contributor

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?

Suggested change
State() nodeState
state() nodeState
func (s *sendOnlyNode[CHAIN_ID, RPC]) State() string {
	return string(s.state())
}

Copy link
Collaborator Author

@dimriou dimriou Oct 17, 2023

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:

  1. 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.
  2. 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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

dhaidashenko
dhaidashenko previously approved these changes Oct 16, 2023
Comment on lines 34 to 37
NodeSelectionMode_HighestHead = "HighestHead"
NodeSelectionMode_RoundRobin = "RoundRobin"
NodeSelectionMode_TotalDifficulty = "TotalDifficulty"
NodeSelectionMode_PriorityLevel = "PriorityLevel"
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 know what the best way to name these is, but let's drop the un-idiomatic underscore?

Suggested change
NodeSelectionMode_HighestHead = "HighestHead"
NodeSelectionMode_RoundRobin = "RoundRobin"
NodeSelectionMode_TotalDifficulty = "TotalDifficulty"
NodeSelectionMode_PriorityLevel = "PriorityLevel"
NodeSelectionModeHighestHead = "HighestHead"
NodeSelectionModeRoundRobin = "RoundRobin"
NodeSelectionModeTotalDifficulty = "TotalDifficulty"
NodeSelectionModePriorityLevel = "PriorityLevel"
Suggested change
NodeSelectionMode_HighestHead = "HighestHead"
NodeSelectionMode_RoundRobin = "RoundRobin"
NodeSelectionMode_TotalDifficulty = "TotalDifficulty"
NodeSelectionMode_PriorityLevel = "PriorityLevel"
HighestHeadNodeSelectionMode = "HighestHead"
RoundRobinNodeSelectionMode = "RoundRobin"
TotalDifficultyNodeSelectionMode = "TotalDifficulty"
PriorityLevelNodeSelectionMode = "PriorityLevel"
Suggested change
NodeSelectionMode_HighestHead = "HighestHead"
NodeSelectionMode_RoundRobin = "RoundRobin"
NodeSelectionMode_TotalDifficulty = "TotalDifficulty"
NodeSelectionMode_PriorityLevel = "PriorityLevel"
HighestHeadSelectionMode = "HighestHead"
RoundRobinSelectionMode = "RoundRobin"
TotalDifficultySelectionMode = "TotalDifficulty"
PriorityLevelSelectionMode = "PriorityLevel"

@cl-sonarqube-production
Copy link

SonarQube Quality Gate

Quality Gate failed

Failed condition 45.2% 45.2% Coverage on New Code (is less than 75%)

See analysis details on SonarQube


// 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
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
//return c.multiNode.ChainID(ctx), nil

@prashantkumar1982 prashantkumar1982 added this pull request to the merge queue Nov 1, 2023
Merged via the queue into develop with commit 08c9f89 Nov 1, 2023
83 of 84 checks passed
@prashantkumar1982 prashantkumar1982 deleted the generalize_multinode branch November 1, 2023 20:56
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.

4 participants