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.

I’m still getting these errors even when adding some random nano seconds.

Is there anything else I can try to avoid this? Or is there any solution in sight?

I think it all depends on why the rule is attempting to run while it’s already running. I’ve only seen this error persist for me in cases where I have a rule called from multiple other rules. The locks that are in place on the context cannot span rules in this way. I’ve reworked some things to make them less likely to occur but I still see them from time to time during startup.

There also might be problems if you share something like a Timer in the sharedCache I can imagine. I haven’t tried that yet, no need.

But within a given rule, there are now locks in place so the rule and it’s Timers cannot run at the same time.

It is true that multiple rules run. But the timers should be offset by my random nano seconds.

I have two rules that deal with motion sensors controlling fans, and another controlling lights. The main offender is in a room that has both a fan and a light, so two different rules will trigger at the same time when that particular motion sensor triggers.
However, the key should be different as it is defined by the triggering item name. (The triggering names are different because the motion sensor items are within a group to allow multiple sensors to trigger a single zone, and it is these zone items that trigger the rules.) Also, these rules do work most of the time. Lights and fans generally turn on and off when they are supposed to.

I have been using the shared cache. I can’t remember now, but I’m pretty sure I followed directions you wrote somewhere. Should it use private? Would it not be useful to have access to timers across different rules?

Each rule has a context. The context contains all the variables, data structures, and code that makes up a rule, including timers.

Locks have been put in place around that context so a rule, or something created from that rule (e.g. a Timer), will have sole access to the context at any given time.

But these locks only work with a single rule. But that means that if you have two timers scheduled to run at the same time in a given rule, one will wait until they can acquire the lock on the context. If a Timer is running when the rule is triggered again, that trigger will wait for the Timer to exit. This ensures that only one thread is active in a given rule at a time.

However, these locks do not work across rules. The lock on rule Foo won’t prevent rule Bar from messing with the data. This is where the multithreaded problems arise. If you have two rules Foo and Bar that each call rule Baz at nearly the same time, when the second rule calls Baz it won’t be blocked and the exception will occur.

If you have a Timer in the sharedCache created by rule Foo and also accessed by rule Bar. If Foo happens to be running when Bar tries to interact with the Timer (e.g. to cancel it or reschedule it) or the Timer happens to be running when Bar tries to interact with the Timer, the exception will occur.

It only happens occasionally because it’s all a matter of timing.

The key doesn’t matter. All that matters is when code running in one thread (i.e. a rule or timers created by a rule) tries to access anything created by another thread while that other thread is running.

If you are seeing these errors, you need to adjust your approach to eliminate or at least reduce the instances and how often one rule accesses stuff from another rule or calls another rule.