Customizing a Node

How to customize a Node

There are times when we would like a Node to be customized to our needs.

For e.g., we may like to do some additional setup before a session begins execution and some clean-up after a session runs to completion.

Following steps can be followed for this:

  • Create a class that extends org.openqa.selenium.grid.node.Node

  • Add a static method (this will be our factory method) to the newly created class whose signature looks like this:

    public static Node create(Config config). Here:

    • Node is of type org.openqa.selenium.grid.node.Node
    • Config is of type org.openqa.selenium.grid.config.Config
  • Within this factory method, include logic for creating your new Class.

  • To wire in this new customized logic into the hub, start the node and pass in the fully qualified class name of the above class to the argument --node-implementation

Let’s see an example of all this:

Custom Node as an uber jar

  1. Create a sample project using your favourite build tool (Maven|Gradle).
  2. Add the below dependency to your sample project.
  3. Add your customized Node to the project.
  4. Build an uber jar to be able to start the Node using java -jar command.
  5. Now start the Node using the command:
java -jar custom_node-server.jar node \
--node-implementation org.seleniumhq.samples.DecoratedLoggingNode

Note: If you are using Maven as a build tool, please prefer using maven-shade-plugin instead of maven-assembly-plugin because maven-assembly plugin seems to have issues with being able to merge multiple Service Provider Interface files (META-INF/services)

Custom Node as a regular jar

  1. Create a sample project using your favourite build tool (Maven|Gradle).
  2. Add the below dependency to your sample project.
  3. Add your customized Node to the project.
  4. Build a jar of your project using your build tool.
  5. Now start the Node using the command:
java -jar selenium-server-4.6.0.jar \
--ext custom_node-1.0-SNAPSHOT.jar node \
--node-implementation org.seleniumhq.samples.DecoratedLoggingNode

Below is a sample that just prints some messages on to the console whenever there’s an activity of interest (session created, session deleted, a webdriver command executed etc.,) on the Node.

Sample customized node
packageorg.seleniumhq.samples;importjava.io.IOException;importjava.net.URI;importjava.util.UUID;importjava.util.function.Supplier;importorg.openqa.selenium.Capabilities;importorg.openqa.selenium.NoSuchSessionException;importorg.openqa.selenium.WebDriverException;importorg.openqa.selenium.grid.config.Config;importorg.openqa.selenium.grid.data.CreateSessionRequest;importorg.openqa.selenium.grid.data.CreateSessionResponse;importorg.openqa.selenium.grid.data.NodeId;importorg.openqa.selenium.grid.data.NodeStatus;importorg.openqa.selenium.grid.data.Session;importorg.openqa.selenium.grid.log.LoggingOptions;importorg.openqa.selenium.grid.node.HealthCheck;importorg.openqa.selenium.grid.node.Node;importorg.openqa.selenium.grid.node.local.LocalNodeFactory;importorg.openqa.selenium.grid.security.Secret;importorg.openqa.selenium.grid.security.SecretOptions;importorg.openqa.selenium.grid.server.BaseServerOptions;importorg.openqa.selenium.internal.Either;importorg.openqa.selenium.io.TemporaryFilesystem;importorg.openqa.selenium.remote.SessionId;importorg.openqa.selenium.remote.http.HttpRequest;importorg.openqa.selenium.remote.http.HttpResponse;importorg.openqa.selenium.remote.tracing.Tracer;publicclass DecoratedLoggingNodeextendsNode{privateNodenode;protectedDecoratedLoggingNode(Tracertracer,NodeIdnodeId,URIuri,SecretregistrationSecret,DurationsessionTimeout){super(tracer,nodeId,uri,registrationSecret,sessionTimeout);}publicstaticNodecreate(Configconfig){LoggingOptionsloggingOptions=newLoggingOptions(config);BaseServerOptionsserverOptions=newBaseServerOptions(config);URIuri=serverOptions.getExternalUri();SecretOptionssecretOptions=newSecretOptions(config);NodeOptionsnodeOptions=newNodeOptions(config);DurationsessionTimeout=nodeOptions.getSessionTimeout();// Refer to the foot notes for additional context on this line.Nodenode=LocalNodeFactory.create(config);DecoratedLoggingNodewrapper=newDecoratedLoggingNode(loggingOptions.getTracer(),node.getId(),uri,secretOptions.getRegistrationSecret(),sessionTimeout);wrapper.node=node;returnwrapper;}@OverridepublicEither<WebDriverException,CreateSessionResponse>newSession(CreateSessionRequestsessionRequest){returnperform(()->node.newSession(sessionRequest),"newSession");}@OverridepublicHttpResponseexecuteWebDriverCommand(HttpRequestreq){returnperform(()->node.executeWebDriverCommand(req),"executeWebDriverCommand");}@OverridepublicSessiongetSession(SessionIdid)throwsNoSuchSessionException{returnperform(()->node.getSession(id),"getSession");}@OverridepublicHttpResponseuploadFile(HttpRequestreq,SessionIdid){returnperform(()->node.uploadFile(req,id),"uploadFile");}@OverridepublicHttpResponsedownloadFile(HttpRequestreq,SessionIdid){returnperform(()->node.downloadFile(req,id),"downloadFile");}@OverridepublicTemporaryFilesystemgetDownloadsFilesystem(UUIDuuid){returnperform(()->{try{returnnode.getDownloadsFilesystem(uuid);}catch(IOExceptione){thrownewRuntimeException(e);}},"downloadsFilesystem");}@OverridepublicTemporaryFilesystemgetUploadsFilesystem(SessionIdid)throwsIOException{returnperform(()->{try{returnnode.getUploadsFilesystem(id);}catch(IOExceptione){thrownewRuntimeException(e);}},"uploadsFilesystem");}@Overridepublicvoidstop(SessionIdid)throwsNoSuchSessionException{perform(()->node.stop(id),"stop");}@OverridepublicbooleanisSessionOwner(SessionIdid){returnperform(()->node.isSessionOwner(id),"isSessionOwner");}@OverridepublicbooleanisSupporting(Capabilitiescapabilities){returnperform(()->node.isSupporting(capabilities),"isSupporting");}@OverridepublicNodeStatusgetStatus(){returnperform(()->node.getStatus(),"getStatus");}@OverridepublicHealthCheckgetHealthCheck(){returnperform(()->node.getHealthCheck(),"getHealthCheck");}@Overridepublicvoiddrain(){perform(()->node.drain(),"drain");}@OverridepublicbooleanisReady(){returnperform(()->node.isReady(),"isReady");}privatevoidperform(Runnablefunction,Stringoperation){try{System.err.printf("[COMMENTATOR] Before %s()%n",operation);function.run();}finally{System.err.printf("[COMMENTATOR] After %s()%n",operation);}}private<T>Tperform(Supplier<T>function,Stringoperation){try{System.err.printf("[COMMENTATOR] Before %s()%n",operation);returnfunction.get();}finally{System.err.printf("[COMMENTATOR] After %s()%n",operation);}}}

Foot Notes:

In the above example, the line Node node = LocalNodeFactory.create(config); explicitly creates a LocalNode.

There are basically 2 types of user facing implementations of org.openqa.selenium.grid.node.Node available.

These classes are good starting points to learn how to build a custom Node and also to learn the internals of a Node.

  • org.openqa.selenium.grid.node.local.LocalNode - Used to represent a long running Node and is the default implementation that gets wired in when you start a node.
    • It can be created by calling LocalNodeFactory.create(config);, where:
      • LocalNodeFactory belongs to org.openqa.selenium.grid.node.local
      • Config belongs to org.openqa.selenium.grid.config
  • org.openqa.selenium.grid.node.k8s.OneShotNode - This is a special reference implementation wherein the Node gracefully shuts itself down after servicing one test session. This class is currently not available as part of any pre-built maven artifact.
    • You can refer to the source code here to understand its internals.
    • To build it locally refer here.
    • It can be created by calling OneShotNode.create(config), where:
      • OneShotNode belongs to org.openqa.selenium.grid.node.k8s
      • Config belongs to org.openqa.selenium.grid.config