Skip to content

Instantly share code, notes, and snippets.

@renaynay
Last active August 13, 2020 13:05
Show Gist options
  • Save renaynay/5bec2de19fde66f4d04c535fd24f0775 to your computer and use it in GitHub Desktop.
Save renaynay/5bec2de19fde66f4d04c535fd24f0775 to your computer and use it in GitHub Desktop.
Proposal to Refactor Package Node

Proposal to Refactor Package Node

Status: Accepted

Author: Rene + Felix

Last Updated: 19 May 2020

Team: Geth

Problems with Package Node

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.

Goals of Refactoring

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.

New Design

Lifecycle Interface

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.

Removing the concept of a node.Service

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.

Remove the responsibility of service construction

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 ServiceConstructors if the node restarts (a functionality that isn't really used).

Information about HTTP servers

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.

RegisterXXX methods

RegisterAPIs()

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)

RegisterLifecycle()

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)

RegisterHTTP()

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.

RegisterProtocols()

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

Node configuration

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.

Refactorp2p.Server

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.

Benefits of Refactoring

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).

Non-goals of Refactoring

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.

Implementation Plan

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, and whisper) 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment