Multi threaded access exception (rule using JavaScript/ECMAScript-2021, utilising a timer within an npm module)

Hi all,

I have created a rule within the openHAB UI that is used by a IKEA Tradfri remote control. It increases/decreases a light’s brightness.

The action part of the rule uses ECMAScript-2021 running on the GraalVM. As I would like to use the main part of the code by multiple remote controls, I put that into an npm module.

The code does what it should do: It increases and decreases the brightness of a connected light item. But the log shows now and then an error message:

2022-01-09 14:49:02.237 [WARN ] [ore.internal.scheduler.SchedulerImpl] - Scheduled job failed and stopped
java.lang.IllegalStateException: Multi threaded access requested by thread Thread[OH-scheduler-38,5,main] but is not allowed for language(s) js.
        at com.oracle.truffle.polyglot.PolyglotEngineException.illegalState(PolyglotEngineException.java:129) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotContextImpl.throwDeniedThreadAccess(PolyglotContextImpl.java:940) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotContextImpl.checkAllThreadAccesses(PolyglotContextImpl.java:799) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotContextImpl.enterThreadChanged(PolyglotContextImpl.java:629) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotEngineImpl.enterCached(PolyglotEngineImpl.java:1885) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.HostToGuestRootNode.execute(HostToGuestRootNode.java:112) ~[bundleFile:?]
        at com.oracle.truffle.api.impl.DefaultCallTarget.callDirectOrIndirect(DefaultCallTarget.java:85) ~[bundleFile:?]
        at com.oracle.truffle.api.impl.DefaultCallTarget.call(DefaultCallTarget.java:102) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotFunctionProxyHandler.invoke(PolyglotFunctionProxyHandler.java:154) ~[bundleFile:?]
        at com.sun.proxy.$Proxy216.apply(Unknown Source) ~[?:?]
        at org.openhab.core.model.script.actions.ScriptExecution.lambda$1(ScriptExecution.java:100) ~[bundleFile:?]
        at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$12(SchedulerImpl.java:184) ~[?:?]
        at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$1(SchedulerImpl.java:87) ~[?:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) [?:?]
        at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) [?:?]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
        at java.lang.Thread.run(Thread.java:829) [?:?]
Caused by: com.oracle.truffle.api.TruffleStackTrace$LazyStackTrace

Although the rule works in spite of this warning, I wonder how I can avoid getting it. The warning is mostly written, if the button on the remote control gets pressed multiple times within a short period of time. I am aware that the GraalVM does not allow the multi threaded access.

The code utilises a timer that checks every 100 ms whether a button on the remote control is still pressed (“hold”). If so, it increases/decreases the brightness by a certain brightness step and reschedules the timer, otherwise it gets cancelled. I suppose, the timer is causing the problem but I cannot figure out why that happens.

Please find below the simplified code. “Simplified” means that it does not send any command to the light item, but it also throws the warning message from above:

Rule from the WebUI
configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: RemoteControl01_Action
    type: core.ItemStateChangeTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        console.log ('rule: start of rule ------------');

        var remoteControlAction;
        var tt; // timerTest instance
        var npmTimerTest;

        remoteControlAction = items.getItem (event.itemName).state;

        if (remoteControlAction == "brightness_up_hold")
        {
          npmTimerTest = (npmTimerTest === undefined ? require ('timertest') : npmTimerTest);

          if (tt === undefined)
          {
            tt = new npmTimerTest.TimerTest ();
          }

          tt.runTimer (this);
        }

        console.log ('rule: end of rule ------------');
    type: script.ScriptAction
npm module: /opt/openhab/conf/automation/js/node_modules/timertest/TimerTest.js
// enable logging by running this command within the karaf console:
// log:set DEBUG org.openhab.automation.script.TimerTest

"use strict";
var zdt = Java.type ("java.time.ZonedDateTime");
//var thr = Java.type ("java.lang.Thread");
var remoteControlHoldActionTimer;

var CLASS_NAME = "TimerTest";

let logger = log (CLASS_NAME);

class TimerTest 
{
  constructor ()
  {
    logger.debug ("constructor: Building TimerTest instance.");
    this.remoteControlHoldActionTimer = (this.remoteControlHoldActionTimer === undefined) ? null : this.remoteControlHoldActionTimer;
    logger.debug ("constructor: TimerTest instance is ready to operate");
  };

  runTimer = function ()
  {
    logger.debug ("runTimer: entering");
    this.remoteControlHoldActionTimer = actions.ScriptExecution.createTimerWithArgument (
      zdt.now ().plusNanos (100 * 1000000), 
      this,
      function (context)
      {
        logger.debug ("callback: entering vvvvvvv");
        logger.debug ("callback: " + items.getItem (event.itemName).state);

        if (items.getItem (event.itemName).state == "brightness_up_hold")
        {
          logger.debug ("callback: reschedule");
          try {
            context.remoteControlHoldActionTimer.cancel ();
            context.remoteControlHoldActionTimer.reschedule (zdt.now().plusNanos (100 * 1000000));
          } catch (e) {
            logger.debug ("callback: ****** reschedule error = {}", e);
          }
        }
        else
        {
          logger.debug ("callback: cancel");
          try {
            context.remoteControlHoldActionTimer.cancel ();
            context.remoteControlHoldActionTimer = null;
          } catch (e) {
            logger.debug ("callback: ****** cancel error = {}", e);
          }
      }
        logger.debug ("callback: leaving ^^^^^^^^^");
      }
    );
    logger.debug ("runTimer: leaving");
  }
} // end of class definition

var scriptUnloaded = function () {
  console.log ("scriptUnloaded: entering");
  console.log ("scriptUnloaded: this.remoteControlHoldActionTimer = " + this.remoteControlHoldActionTimer);
  if (!(this.remoteControlHoldActionTimer === undefined))
  {
    this.remoteControlHoldActionTimer.cancel ();
    this.remoteControlHoldActionTimer = null;
  }
  console.log ("scriptUnloaded: leaving");
}

module.exports = { TimerTest };
npm module: /opt/openhab/conf/automation/js/node_modules/timertest/index.js
console.log ("loaded index.js");
module.exports = require ("./TimerTest");
npm module: /opt/openhab/conf/automation/js/node_modules/timertest/package.json
{
  "name": "timertest",
  "version": "0.1.0",
  "description": "...",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "...",
  "license": "ISC"
}

Could someone please point me into the right direction? Why does another thread access the callback(?) function?

Regards,
watt01

Platform information:

  • Hardware: Raspberry 4
  • OS: Raspian, openHAB running within a docker container
  • openHAB version: Snapshot, openHAB 3.3.0, Build #2682

In GraalVM it is forbidden to mess with Java threading directly. I’ve only ever seen that error when trying to call java.lang.Thread.x (x is most often sleep()).

The one other place I’ve seen this error is if you attempt to cancel a Timer from inside the function called by the Timer being cancelled. I only did a quick scan but suspect that’s what’s happening here. You need to check .isRunning() and only call cancel if that’s false. If it’s true, the Timer will need to run to completion and terminate.

Hi Rich, thank you for your prompt response.

I have removed the cancel () function from the callback, but I am still able to raise that exception now and then:

Adapted callback function
function (context)
      {
        logger.debug ("callback: entering vvvvvvv");
        logger.debug ("callback: " + items.getItem (event.itemName).state);

        if (items.getItem (event.itemName).state == "brightness_up_hold")
        {
          logger.debug ("callback: reschedule");
          try {
            context.remoteControlHoldActionTimer.cancel ();
            context.remoteControlHoldActionTimer.reschedule (zdt.now().plusNanos (100 * 1000000));
          } catch (e) {
            logger.debug ("callback: ****** reschedule error = {}", e);
          }
        }
        else
        {
          logger.debug ("callback: cancel");
          // removed call of cancel function ------------------------------
          // try {
          //  context.remoteControlHoldActionTimer.cancel ();
          //  context.remoteControlHoldActionTimer = null;
          //} catch (e) {
          //  logger.debug ("callback: ****** cancel error = {}", e);
          //}
      }
        logger.debug ("callback: leaving ^^^^^^^^^");
      }

The exception is only thrown when the rule runs again because of a changed state of the remote control item (“brightness_up_release” after “brightness_up_hold”, written to the variable “remoteControlAction”).

Log output

2022-01-10 20:00:43.535 [INFO ] [org.openhab.automation.script       ] - rule: start of rule ------------
2022-01-10 20:00:43.539 [INFO ] [org.openhab.automation.script       ] - action = brightness_up_hold
2022-01-10 20:00:43.582 [INFO ] [org.openhab.automation.script       ] - rule: end of rule ------------
2022-01-10 20:00:43.674 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:43.677 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:43.679 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:43.683 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:43.783 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:43.786 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:43.789 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:43.793 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:43.894 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:43.897 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:43.901 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:43.905 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:44.005 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:44.008 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:44.010 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:44.013 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:44.114 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:44.117 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:44.120 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:44.123 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:44.223 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:44.226 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:44.229 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:44.233 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:44.334 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:44.338 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:44.342 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:44.347 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:44.447 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:44.451 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:44.456 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:44.461 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:44.561 [DEBUG] [.openhab.automation.script.timertest] - callback: entering vvvvvvv
2022-01-10 20:00:44.565 [DEBUG] [.openhab.automation.script.timertest] - callback: brightness_up_hold
2022-01-10 20:00:44.569 [DEBUG] [.openhab.automation.script.timertest] - callback: try to reschedule
2022-01-10 20:00:44.574 [DEBUG] [.openhab.automation.script.timertest] - callback: leaving ^^^^^^^^^
2022-01-10 20:00:44.671 [INFO ] [org.openhab.automation.script       ] - rule: start of rule ------------
2022-01-10 20:00:44.674 [INFO ] [org.openhab.automation.script       ] - action = brightness_up_release   <<<<<<< new state
2022-01-10 20:00:44.678 [INFO ] [org.openhab.automation.script       ] - rule: end of rule ------------
2022-01-10 20:00:44.674 [WARN ] [ore.internal.scheduler.SchedulerImpl] - Scheduled job failed and stopped
java.lang.IllegalStateException: Multi threaded access requested by thread Thread[OH-scheduler-19,5,main] but is not allowed for language(s) js.
        at com.oracle.truffle.polyglot.PolyglotEngineException.illegalState(PolyglotEngineException.java:129) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotContextImpl.throwDeniedThreadAccess(PolyglotContextImpl.java:940) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotContextImpl.checkAllThreadAccesses(PolyglotContextImpl.java:799) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotContextImpl.enterThreadChanged(PolyglotContextImpl.java:629) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotEngineImpl.enterCached(PolyglotEngineImpl.java:1885) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.HostToGuestRootNode.execute(HostToGuestRootNode.java:112) ~[bundleFile:?]
        at com.oracle.truffle.api.impl.DefaultCallTarget.callDirectOrIndirect(DefaultCallTarget.java:85) ~[bundleFile:?]
        at com.oracle.truffle.api.impl.DefaultCallTarget.call(DefaultCallTarget.java:102) ~[bundleFile:?]
        at com.oracle.truffle.polyglot.PolyglotFunctionProxyHandler.invoke(PolyglotFunctionProxyHandler.java:154) ~[bundleFile:?]
        at com.sun.proxy.$Proxy216.apply(Unknown Source) ~[?:?]
        at org.openhab.core.model.script.actions.ScriptExecution.lambda$1(ScriptExecution.java:100) ~[bundleFile:?]
        at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$12(SchedulerImpl.java:184) ~[?:?]
        at org.openhab.core.internal.scheduler.SchedulerImpl.lambda$1(SchedulerImpl.java:87) ~[?:?]
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) [?:?]
        at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]
        at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) [?:?]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]
        at java.lang.Thread.run(Thread.java:829) [?:?]
Caused by: com.oracle.truffle.api.TruffleStackTrace$LazyStackTrace

The new value of “remoteControlAction” does not trigger any call to the TimerTest object (the new value is not equal to “brightness_up_hold”):

if (remoteControlAction == "brightness_up_hold")
{
   ... call to TimerTest object ...

Because of the new value the callback function does not reschedule the timer again and the loop stops.

Seems that I need to further investigate what is happening. But currently I have no idea where to look.

Regards,
watt01

By any chance would the rule be running at the same time that the timer goes off? I’ve been writing some unit tests for my TimerMgr library and I suspect that causes problems too. I’m still experimenting to see if that’s the case but so far all signs point to the fact that the rule cannot be running when the timer goes off or else that exception is thrown.

I should be more precise in my terminology though. I can’t say if it’s a problem with the whole rule. It might just be that one Script Action, in which case you could store the Timer in the cache and check in a Condition Script to not run the rule when the timer exists, or something like that.

1 Like

Update, it’s not just when the rule is running. If one have two timers created in the same script that run at the same time this exception occurs too.

The GraalJS runtime does not allow two threads to enter the same context (which is the script in this case) concurrently. This is because JS is typically single-threaded. Nashorn did permit this, but with undefined behaviour.

Currently the jsscripting add-on does attempt to prevent concurrent execution like this by synchronising/queuing executions, although this is done specifically for Rule executions rather than arbitrary access to the script, which could also come in via things like Timers as discovered here.

Without any explicit queuing for Timers, you will get these exceptions sometimes, and the Timer that caused them will not execute properly as it will be denied access to run the script code.

If we want to fix this properly we will need to extend synchronisation to Timers created from within the script itself, most likely by explicitly adding Timers to the JS API which return special timers that coordinate with the existing synchronisation locks.

If you would like this fixed please file a ticket :slight_smile:

Already planned to file a ticket (started one yesterday but was still gathering info). I was just experimenting some more to make sure I understood what was going on.

It’s good to read that @rlkoshak has investigated the issue and that he can reproduce the exception. As I experienced it randomly, I thought it was caused by my system environment.

My initial intention was to run this kind of script:

1. Start of script
2. Do something
3. Wait a certain (short) period of time.
4. Check an item's state: Certain condition met?
   If no, continue with 2.
   If yes, continue with 5.
5. Do something else
6. End of script 

Step 3 is done by a timer that causes the exception. In spite of the exception the timer script works and for now I could live with that.

Before the issue maybe gets fixed: Does there exist any workaround/another way to achieve what I would like to get and without causing the exception? Or would it be the best to wait for a fix?

If you are implementing step 3 with a timer that is not how your rule is running. Remember that a Timer doesn’t block. You might be able to put it together as:

  1. start of script
  2. do something
  3. create a timer to wait a certain amount of time
  4. end of script

Time passes and timer goes off. Inside the timer’s function
5. check an item’s state …

Even without this exception you can’t achieve this with a Timer. If you are trying to use Thread.sleep to wait, well that’s a different problem and that’s never going to work. Messing with the thread is forbidden in GraalVM.

It sounds like what you are really after is a Design Pattern: Looping Timers. That gives you the concept. Here is a partially tested JS Scripting implementation I’m working on.

const {timeUtils} = require('openhab_rules_tools');


/**
 * @namespace LoopingTimer
 */

/**
 * Implements a looping Timer which is passed a function that is expected to return
 * a when supported by timeUtils.toDateTime. The loop will reschedule the timer based
 * on that returned when or, if it return null the looping stops.
 */
class LoopingTimer {

  /**
   * Constructor, creates a logger.
   */
  constructor() {
    this.logger = log('rules_toools.LoopingTimer');
    this.logger.debug("Looping Timer created");
  }

  /**
   * Kicks off the timer loop. Schedules a timer to call func at when
   * @param {function} func function to call at when, must return a when to continue the loop or null to stop
   * @param {*} when any of the types supported by timeUtils.toDateTime
   */
  loop(func, when) {
    this.logger.debug('Looping timer - loop called with {}', when);

    this.func = func;
    if(!when) this.expired();
    else {
      this.logger.debug('Creating new timer from loop');
      this.timer = actions.ScriptExecution.createTimer(
                                             timeUtils.toDateTime(when), 
                                             () => this.expired());
    }
  }

  /**
   * Called when the timer expires. Calls the passed in function and
   * reschedules it based on the returned when value, or ends if null was
   * returned.
   */
  expired() {
    this.logger.debug('expired called');
    var when = this.func();
    if(when) {
      this.logger.debug('Creating new timer in expired')
      this.timer = actions.ScriptExecution.createTimer(
                                             timeUtils.toDateTime(when),
                                             () => this.expired());
    }
  }

  /**
   * Cancels the timer if it exists and hasn't already terminated.
   */
  cancel() {
    if(this.timer && !this.hasTerminated()) {
      this.timer.cancel();
    }
  }

  /**
   * Returns true of the timer doesn't exist or has terminated.
   */
  hasTerminated() {
    return !this.timer || this.timer.hasTerminated();
  }
}

module.exports = {
  LoopingTimer
}

Instantiate the LoopingTimer Object and call loop with the function to call and time to run it. The function needs to return null when it’s time to stop looping or with when to run the function again.

This will be a part of my openhab_rules_tools library sometime soon.

@rlkoshak, you are right regarding my outlined script; it was too simple.

Thanks for your LoopingTimer, that’s helpful. I will give it a try (probably not before the weekend).

I’ve been working on some JavaScript to schedule timers for lights based on motion detectors and have come across this issue too.

In my case I have a Hue system with a combination of Hue and IKEA bulbs. The IKEA bulbs aren’t able to set the colour and the brightness at the same time. To work around this I wrote a function that sets the state of the bulbs, waits for two seconds then sets the state again. This resolves the problem with the bulbs but means that the function can end up needing to run more than once at the same time if there’s movement in multiple parts of the house. The longer the function takes to run the greater the chance of two calls overlapping.

I’ve also seen the issue with a function I’ve written that is triggered by movement detected by outdoor cameras. If movement is detected on more than one camera at almost the same time then the function that gets an animated GIF from the camera and sends it via Pushover can need to run more than once at the same time.

Not a big deal for me but it would be nice if the same function could be called multiple times simultaneously.

Given the way that GraalVM works, this is never going to be supportable. However, we can at least prevent errors and exceptions by queueing the runs so they occur in sequence instead of in parallel, which isn’t allowed.

Based on what I’ve read, this is a hard and fast problem that we don’t have control over.

One thing you can do to avoid conflicts in the mean time is to add a random amount of milliseconds to the Timer’s time to minimize the chance that two run at exactly the same time.

Ah, I see. OH would need to maintain a queue to ensure one call to a function has finished executing before another call to the same function can begin.

What I’ve found in my own functions is that the longer a timer function takes to execute the greater the chance there is of a multi thread exception occuring. I.e. a function that takes a millisecond to execute is unlikely to clash with another call of itself. A function that takes an hour (grossly exaggerated example) would be far more likely to clash.

Do you know if it’s possible to call functions written in other languages from JS Script?

Yes, of course. The longer the timer function takes to run, the wider the window that exists for something else to come along and want to run. But assuming a relatively uniform amount of time spent in the timer functions, adding some randomness to the times can further reduce the chances that two will try to run at the same time. But as the length of time the timer function runs grows, the effectiveness of adding that random value decreases.

You can pull in Java classes but are limited to core Java and openHAB classes (and a few libraries that openHAB depends upon).

But no, you can’t call a jRuby function from a JS Scripting Script/Condition Action for example. And even if you could, the Script is running in the GraalVM JS Scripting engine so the single thread limitation would not go away even if you could.