I have approximately 15 message handlers in one of my projects, I used message handlers so that I could use tag event scripts to call local project scripts. This was a choice made so that I could use a single Ignition project and replicate it without having to create a new project for each instance, I just create a new UDT instance and pass my unique parameters in and the rest is a template with tag event scripts in the UDT.
My issue is with performance, this worked great with 1,5, or even 20 of my instances, but now I have roughly 200 instances and I am seeing performance bottlenecks with my message handlers. I selected âDedicatedâ for the threading option in the gateway events, which gives each message handler itâs own dedicated thread so that it doesnât wait on the others, however I interpreted this incorrectly. I understood it as, every time the message event handler is called, it spawns a new thread for it to execute in, but the reality is that it is a single thread that process all of the events for this specific message handler. So what I end up with is a single congested message handler, while the other âless-usedâ message handlers can continue to execute.
Is there a way that I can spawn a thread for each call of the message handler rather than have one thread process them all? At this point the project is so large and widely distributed itâs hard to make a large framework change. I guess I could add more message handlers and use a memory tag to index through them for each time it is called to distribute the load to different threads.
Thanks for any help
No, but in your handler you can spawn your own thread using system.util.invokeAsynchronous
, but this will only work if you are sending messages and not requests (no return from the handler). You will also run the risk of blowing up your system by spawning too many threads if things get out of hand.
Luckily I donât need any return from the handlers. Thanks for the help, I will cautiously proceed with that option, is there a way to programmatically get the list of threads via scripting like I can view them in the gateway? My thought is that I could count the threads and only allow spawning new ones if the count is under a specified value.
You can, via Java APIs, doing something like this:
Thread.getAllStackTraces().keySet().size();
But I would not recommend doing this because itâs an expensive call.
A cheaper way to get all threads is to enumerate them, like so:
import jarray
from java.lang import Thread
def getAllThreads():
tcEstimate = Thread.currentThread().threadGroup.activeCount()
threads = jarray.zeros(tcEstimate*2, Thread)
threadCount = Thread.enumerate(threads)
return list(threads)[:threadCount]
Note that retrieves the threads as objects. Youâll probably want to iterate through that list retrieving thread names. And donât keep that list around longer than necessaryâthat would be a bad memory leak.
1 Like
Rather than using our invokeAsync
directly, you could create your own ExecutorService
(cache it in system.util.getGlobals()
on the gateway) and immediately delegate from your message handlers to this pool.
3 Likes
Hi Paul,
Thanks for this solution. I was able to successfully create a fixed thread pool named âthreadPoolâ with a maximum thread count of 15. It works like it is supposed to except for when I try to pass variables through to the script I am calling. For example:
Hereâs how I am assigning threads to my pool:
system.util.getGlobals()['threadPool'].execute(testRun('hello'))
I have a function named testRun which accepts a value variable as defined below:
def testRun(value):
print value
This combination gives me a Null Pointer Exception.
However, getting rid of the value and just printing a blank line works fine, like this:
def testRun():
print ''
system.util.getGlobals()['threadPool'].execute(testRun())
Could you please tell me what I am doing wrong here and how I can pass variables into my thread? Thanks!
When submitting a function to run under another context, you have to pass the bare function name. Parentheses cause function execution. That is, including parentheses and any parameters runs your function on the spot and delivers the return value to run in the Thread Pool or other execution manager. (This is also true for Ignitionâs built-in system.util.invokeAsynchronous and .invokeLater.) Your return value is None if you donât actually return something, which explains your null pointer exception. Your code, as written, tries to execute a null as a task.
Pythonâs partial()
function can assemble a callable (function) and parameters into a single callable object that will do what you want. It would look something like this:
system.util.getGlobals()['threadPool'].execute(partial(testRun, 'hello'))
Also note that Ignitionâs built-ins were recently enhanced to do this partial assembly for you. Much easier than closures or default values, which were the most common way to deliver arguments to functions in these cases.
1 Like
Thanks Phil, I think Iâm one step closer. Iâm now getting a:
TypeError: execute(): 1st arg can't be coerced to java.lang.Runnable
When trying to execute the following:
system.util.getGlobals()['threadPool'].execute(partial(testRun, 'hello'))
Try something like this; Jython isnât able to perform the single-abstract-method conversion necessary to pass your Python function directly to the Java executor. In a utils
(or whatever) project script, add this code:
from java.lang import Runnable
class JythonRunnable(Runnable):
def __init__(self, fn, *args, **kwargs):
self.fn = fn
self.args = args
self.kwargs = kwargs
def run(self):
self.fn(*self.args, **self.kwargs)
def invokeAsync(function, *args, **kwargs):
system.util.getGlobals()["threadPool"].execute(JythonRunnable(function, args, kwargs))
Then in your handlers, just call utils.invokeAsync(testRun, "hello")
.
For background, everything in Java is a class or interface; Runnable
is probably the most common âfunctionalâ interface, and it basically just defines a âsingle abstract methodâ (named run
) that has no arguments and returns no results. By defining a Jython class with Runnable
as the superclass, you can do the same thing as partial
, but also return the right âtypeâ so the rest of Java just sees a Runnable
and doesnât care it came out of Jython.
2 Likes
Thanks again Paul. This is working perfectly.