Driver Module Scope Best Practice

Hello,

I have been working on a module that creates a graph database driver and adds some simple querying functionality to said graph database.

I currently only have the capability to connect to one database, but I would like to extend that to have multiple connections, as well as an embedded database instead of just remote ones.

I know that I can instantiate the database in the same JVM in ignition, and then reference it locally with scripting that way. However I am curious where is the best place to do that?

I know that I could instantiate it in the gateway hook and then get reference to it through rpc in the common and client scopes, but is that the best place to put it? I am relatively new to java so if there is a common term for what I am looking for that would be great to know as well!

Thanks,
Keith G.

You definitely want to expose it via scripting. Java can (and Ignition’s gateway does) isolate subsystems from each other with separate classloaders. The designer and vision client scopes don’t use separate classloaders, so all java classes are exposed there. In the gateway, each module gets its own classloader, and can’t see other modules’ classes (unless you mark a dependency). Registering with Script Managers puts your key resources in a public scope.

Maybe I misrepresented what I was asking, or maybe I didnt understand your response, however I have already exposed most of my functionality to scripting

For instance I have a system.neo4j.selectQuery and system.neo4j.updateQuery methods, very similar to system.db.runPrepUpdate and system.db.runQuery.

In a static block at the beginning of the script module I instantiate the driver class, that is used in the aforementioned query methods.

This works great, assuming that I already have the settings for connection provided, and that I only want one connection.

In an optimal world I would create multiple connectors, to multiple databases, and parameterize the query methods with a connector to execute them on.

I theoretically could instantiate an array of connectors in the script module, and then update that array via a listener on the persistent records, then just identify wether its local or remote in the query methods?

Does this sound like I am headed in the right direction or am I missing something?

Sounds like you need GW config page for connection properties, like existing DB connections. For the script modules in the different scopes I would instantiate with a reference to the RPC implementation for that scope. Then the common implementation calls the appropriate RPC methods and the scope is hidden. The GW RPC implementation would look up the connection details and actually make the calls.

1 Like

If you want to roughly follow the model of Ignition’s DB connections, then you’ll want to make your PersistentRecord part of an ExtensionPoint hierarchy - then you’ll get an automatic ‘wizard’ to select a type, and it’ll fit very nicely into the model end users are used to from the rest of Ignition.
I would internally set up a single ‘Manager’ instance in your GatewayHook, and have that start up/shut down instances of actual DB connections as records are added/updated.

Some reference:

1 Like

This looks closer to what I am looking for!

Currently I have a settings page already setup and its a single page, but the hierarchy will definitely be important for what I need. However some of those connections will be remote and some will be local.

The local ones will be a database instance instantiated in ignitions JVM, however the part I don’t know is should I instantiate them in the gateway scope and access them through rpc, or instantiate them in the common scope?

Thats the part I am missing, however that extension-point-example is perfect to help me get the “multiple connection setup” stuff going!

If a reference to the local instance I am describing helps, this is the documentation of how its instantiated: https://neo4j.com/docs/java-reference/current/java-embedded/include-neo4j/

Definitely use RPC, or a remote designer may not work. Nor any Vision clients. (I strongly believe generic infrastructure should support both client technologies.)

1 Like

Question for you based off of your example,

How do you get the instantiated table to have a different text other than the table name?
i.e. it saying “databaseConnection” here

public static final RecordMeta<BaseRecord> META = new RecordMeta<>(BaseRecord.class, "databaseConnection").setNounKey("Connection").setNounPluralKey("Connections");

image

And making it say something like “No Database Connections” instead of “No databaseConnection” similar to the built in tables.
image

That should be the nounKey or nounPluralKey - normally, by convention, you’ll have a .properties file with the same name as your PersistentRecord class at the same package location. Note that if you don’t specify a different key, RecordMeta will automatically populate the keys in the constructor.

1 Like

Separate question off your example, figured I would put it here for anyone that does the same thing.

When I remove the instantiation of the ReferenceField from the two child records I end up with an error when it tries to load the form for said record type.

I have dug through all of the different scripts and from the stack trace cannot figure out what is trying to reference that field when creating that form.

Any ideas?

StackTrace
org.apache.wicket.WicketRuntimeException: Method onFormSubmitted of interface org.apache.wicket.markup.html.form.IFormSubmitListener targeted at [CsrfPreventingForm [Component id = form]] on component [CsrfPreventingForm [Component id = form]] threw an exception

at org.apache.wicket.RequestListenerInterface.internalInvoke(RequestListenerInterface.java:268)

at org.apache.wicket.RequestListenerInterface.invoke(RequestListenerInterface.java:216)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.invokeListener(ListenerInterfaceRequestHandler.java:240)

at org.apache.wicket.core.request.handler.ListenerInterfaceRequestHandler.respond(ListenerInterfaceRequestHandler.java:226)

at org.apache.wicket.request.cycle.RequestCycle$HandlerExecutor.respond(RequestCycle.java:814)

at org.apache.wicket.request.RequestHandlerStack.execute(RequestHandlerStack.java:64)

at org.apache.wicket.request.cycle.RequestCycle.execute(RequestCycle.java:253)

at org.apache.wicket.request.cycle.RequestCycle.processRequest(RequestCycle.java:210)

at org.apache.wicket.request.cycle.RequestCycle.processRequestAndDetach(RequestCycle.java:281)

at org.apache.wicket.protocol.http.WicketFilter.processRequest(WicketFilter.java:188)

at org.apache.wicket.protocol.http.WicketFilter.doFilter(WicketFilter.java:245)

at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1596)

at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:545)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)

at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)

at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1607)

at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)

at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1297)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)

at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:485)

at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1577)

at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)

at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1212)

at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:322)

at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59)

at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:146)

at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)

at org.eclipse.jetty.server.Server.handle(Server.java:500)

at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)

at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:547)

at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)

at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:270)

at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)

at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)

at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)

at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)

at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:388)

at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)

at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)

at java.base/java.lang.Thread.run(Unknown Source)

Caused by: java.lang.reflect.InvocationTargetException: null

at jdk.internal.reflect.GeneratedMethodAccessor23.invoke(Unknown Source)

at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)

at java.base/java.lang.reflect.Method.invoke(Unknown Source)

at org.apache.wicket.RequestListenerInterface.internalInvoke(RequestListenerInterface.java:258)

... 46 common frames omitted

java.lang.NullPointerException: null

It’s probably implicit, by the RecordActionTable or some other mechanism of a well-formed extension point. Are you able to put your code somewhere? I can’t really picture what you’re doing, exactly.

Well I kind of solved what I needed by just making the ReferenceField invisible, however I am guessing that’s not what I should be doing.

The simplest way I have found to create the error is by just commenting out the two lines in your example that create the ReferenceField, it causes the same error.

Here is a zip of your example that I modified to include my change. I have no problem putting my code here as well, just not exactly sure which piece of it you would want.

extension-point-example.zip (215.0 KB)

You need the reference field, and it needs to be hidden. Dereferencing it is how you get the driver or device generic record wherever your code needs it.

Sorry, I keep getting thrown off by your use of “Driver” in the title.

This is actually still probably the answer to my problem. I wasn’t realizing that was how I would identify the connection down the road.

I noticed that the IdentityField and the ReferenceField in my two records are given the same value in the idb when the ReferenceField is instantiated. However I am curious how do I recall the Extended Record from my base record?

I tried the following, but because the two types are different I don’t get anything back.

context.getPersistenceInterface().queryOne(new SQuery<>(ExtendedRecord.META).eq(ExtendedRecord.ReferenceId, baseRecordInstance.Id));

Is there something inherently built in to receive the extended record from the base?

Found what I was looking for in a separate post!

2 Likes

I set all of this up to instantiate the connections in the gateway scope and store them in a hash map so that I can access them via name. However now I am having issues recalling the instantiated connectors with rpc. I am getting an error that the object is not serializable, and from some light research I found this comment that signifies I am potentially doing something wrong here.

Currently I am instantiating and storing the connector instance in the gateway scope, and then getting it via its name and passing it back through RPC. Is the issue here that I am trying to execute all of my functionality in the common scope, and instead I need to move my functionality to the gateway scope and just add methods to reach it in the common scope? Or Is passing it back via something like the following just not the correct implementation:

@Override
    protected DatabaseConnector getDatabaseConnectorImpl(String connectionName) {
        return rpc.getDatabaseConnector(connectionName);
    }

Here is the stack trace as well if it helps:

Stacktrace
Caused by: com.inductiveautomation.ignition.client.gateway_interface.GatewayException: class com.bwdesigngroup.neo4j.driver.DatabaseConnector cannot be cast to class java.io.Serializable (com.bwdesigngroup.neo4j.driver.DatabaseConnector is in unnamed module of loader com.inductiveautomation.ignition.gateway.modules.ModuleClassLoader @451c1f9f; java.io.Serializable is in module java.base of loader 'bootstrap')
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.newGatewayException(GatewayInterface.java:359)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.sendMessage(GatewayInterface.java:333)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.sendMessage(GatewayInterface.java:286)
	at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.moduleInvokeSafe(GatewayInterface.java:905)
	at com.inductiveautomation.ignition.client.gateway_interface.ModuleRPCFactory$DynamicRPCHandler.invoke(ModuleRPCFactory.java:53)
	... 27 more
Caused by: java.lang.ClassCastException: class com.bwdesigngroup.neo4j.driver.DatabaseConnector cannot be cast to class java.io.Serializable (com.bwdesigngroup.neo4j.driver.DatabaseConnector is in unnamed module of loader com.inductiveautomation.ignition.gateway.modules.ModuleClassLoader @451c1f9f; java.io.Serializable is in module java.base of loader 'bootstrap')
	at com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ModuleInvoke.invoke(ModuleInvoke.java:172)
	at com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost(Gateway.java:405)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
	at com.inductiveautomation.ignition.gateway.bootstrap.MapServlet.service(MapServlet.java:86)
	at org.eclipse.jetty.servlet.ServletHolder$NotAsyncServlet.service(ServletHolder.java:1391)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:760)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:547)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:590)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1607)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1297)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:485)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1577)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1212)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
	at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:322)
	at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:59)
	at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:146)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
	at org.eclipse.jetty.server.Server.handle(Server.java:500)
	at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:383)
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:547)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:375)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:270)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
	at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:117)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:336)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:313)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:171)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:129)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:388)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:806)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:938)
	at java.lang.Thread.run(null)

Thanks for any help,
Keith

Trying to hand your entire connection object over RPC is probably the wrong approach. Provide/implement methods to interact with a connection, then delegate the actual implementation of those to RPC in client/designer scope.

2 Likes

Fixed it. RPC defines an Interface that you can use in any scope. Implementations of that interface that cannot execute the methods directly use RPC to delegate to a (the) implementation that can execute the methods. If you need to track unique connection instances for the delegators, you will probably need to use numeric handles under the hood (in the RPC calls).

2 Likes

I apologize for continuing to add to this post but there is a ton of context here that would be a pain to add into a new one!

I set this all up and its working great, I implemented the status pages from the HomeConnect example with the sdk, and am working on a maintained polling status to the databases.

I am able to query my status on load of that page, and poll within the page through javascript, however I am trying to figure out the best way to set something up that will manage the status field of the PersistentRecord without a specific page having to be open.

Is there an example of the best way to do this? I went through the gateway examples and didn’t find anything obvious. I am sure that its something that I schedule in the gateway hook, I am just not sure what it is!

Thanks for any help

TDLR: How do I execute a method in the gateway scope at a set rate?

com.inductiveautomation.ignition.gateway.model.GatewayContext#getScheduledExecutorService?

1 Like