Status: Accepted
Author: Rene + Felix
Last Updated: 19 May 2020
Team: Geth
A node.Node
is a collection of services which use shared resources to provide RPC APIs. However, package node is a bit messy because a lot of the "services" have different ways of starting/stopping, using shared resources and providing RPC APIs.
So the effect is that certain "services" whose lifecycles could be managed by package node don't implement the node.Service
interface and are instead managed by eth.Ethereum
because it was/is currently the easiest/fastest way to integrate.
Additionally, there are features in package node that are rarely used and not exactly necessary, such as the concept of the ServiceConstructor
, which only exist to make it possible to restart the node.
The general goal of refactoring is to create a more logical, explicit, and simple architecture for managing "service" lifecycles, shared resource usage and provisioning of APIs.
In summary, make it simpler to understand service behaviors, and therefore, simpler to create new services that can be managed in a relatively generic way by package node.
The purpose of the Lifecycle interface is to simplify the concept of a service and reduce it down to the basic behaviours of just Start()
and Stop()
. So now, instead of the concept of a Service
that would, in addition to having a lifecycle, provide APIs and Protocols, the node would only contain the lifecycles of the services.
type Lifecycle interface {
// Start is called after all services have been constructed and the networking
// layer was also initialized to spawn any goroutines required by the service.
Start() error
// Stop terminates all goroutines belonging to the service, blocking until they
// are all terminated.
Stop() error
}
The rest of the behaviours of services would be handled by the service-specific package itself. For example, if a service provides APIs, it would handle registering its APIs on the node during the construction of the service. Similarly, if a service that requires an http server is specified, it can be registered on the node's default http server during its construction within the service's package and not in package node.
The node.Service
interface will no longer be necessary as Lifecycle
will handle starting and stopping services that are registered on the node. The rest of the behaviours (APIS()
and Protocols()
) will be handled by the service-specific package rather than package node.
In addition to making service-specific packages handle service behaviours outside of Start()
and Stop()
, the responsibility of service construction will also be handed back to the service-specific packages.
Services will be constructed there where they were previously "registered", and instead, their lifecycles will be registered on the node after construction.
As a result, the concept of a ServiceConstructor
will also no longer be necessary since service configuration will now be the responsibility of the service-specific package. The purpose of the ServiceConstructor
was to construct the service and store it on the node so the node can construct and start the services upon start-up and also hold on to the ServiceConstructor
s if the node restarts (a functionality that isn't really used).
Instead of storing the information about individual rpc
and ws
configuration on the node, a new struct called HTTPServer
will neatly store information about ws
, http
and graphql
and the node will store an array of pointers to those HTTPServers
.
httpServers []*HTTPServer
HTTPServer
will also implement the Lifecycle
interface, so after configuration, an HTTPServer
's lifecycle will be registered on the node and will be started in the same manner as the rest of the services whose lifecycles are registered on the node.
This method will allow a service to register the APIs it provides on the node. Currently, package node cycles through all services and appends their APIs upon node start-up.
The responsiblity of registering a service's APIs will be handed back to the service-specific package. So, a service upon construction, will be able to register its provided APIs by calling RegisterAPIs(apis)
on the given node.
func (n *Node) RegisterAPIs([]rpc.APIs)
This method will allow a service (or an HTTPServer
) to register its lifecycle on the node. Currently, this is done by storing the service constructor on the node, constructing the service in package node during node start-up, and then cycling through the created services and calling Start()
on them.
The new Lifecycle
interface would simplify this process further by having the service-specific package handle the registration of its lifecycle on the node just by calling RegisterLifecycle(service)
on the given node after a successful construction.
func (n *Node) RegisterLifecycle(Lifecycle)
This method will allow a service that requires an http server to be able to register a new http server on the node or register itself on the node's default http server. Currently, several different http servers can be started when specified (e.g., websocket, http, and graphql can all be specified on separate servers).
Instead, if a user specifies that they want websocket, http, and graphql provided on the same port, the RegisterHTTP
method would enable websocket, http and graphql requests to be handled on the given port.
func (n *Node) RegisterHTTP(path string, h http.Handler) error
It might also be a good idea to restrict graphQL so that it can only be started on the http server that is specified and not on its own server, since it is a relatively new feature anyway. It would reduce the amount of code required to configure the graphql service and would also prevent the possibility of potentially 3 different http servers being started on the node.
This method will allow a service to register its protocols on the node's p2p.Server
. This must occur before the p2p.Server
is started. Currently, package node is responsible for cycling through services and adding their protocols (if they provide any) to the p2p.Server
during node start-up.
Instead, after a service is constructed, it will call RegisterProtocols(protocols)
on the given node to register its protocols.
func (n *Node) RegisterProtocols([]p2p.Protocol) error
Instead of passing around a ServiceContext
to fetch the backend for services that need a backend for configuration (such as ethstats and graphql), during node configuration, the created backend (either eth.Ethereum
or les.LightEthereum
) will be passed around to the services that require a backend for construction.
This will simplify the responsibilities of package node and also ensure that a service that needs a backend for construction will receive the backend explicitly and sequentially.
p2p.Server
will have to be refactored so that it can be constructed and added to the node without actually starting it until the node is started.
Refactoring would not only make it easier to create and manage new services in the future, but it would also make the backends (eth.Ethereum
or les.LightEthereum
) more lightweight and easy to start. For example, in the future, it would make it possible to forgo javascript tracing in your application if you don't need it (just don't start it).
The goal of refactoring is not to make package node "prettier", but to simplify it as much as possible without compromising functionality.
It is also a non-goal to make cmd/geth startup code nicer.
First, get approval from team on the overall design.
The following will constitute one PR:
- Felix will work on refactoring
p2p.Server
- Rene will work on refactoring the 5 implementations of
node.Service
(eth.Ethereum
,les.LightEthereum
,ethstats
,graphql
, andwhisper
) as well as package node to implement the designs mentioned in this document
Later on, it will be possible to refactor other potential services (like miner
and eth/filters
) so that instead of being handled by the eth package, they can also register their lifecycles on the node and manage their own construction and registration. This would constitute a separate PR.
Another possible PR would be to remove the separation between public
and private
web3 API.
Currently, there is no real use of the distinction between PublicAdminAPI
and PrivateAdminAPI
. It would therefore make sense to condense the two functionalities under PublicAdminAPI
.