Skip to content

rules_overview.md: elaborate about concurrency support in rule engines#2565

Closed
dilyanpalauzov wants to merge 1 commit intoopenhab:mainfrom
dilyanpalauzov:rule_engines_concurrency
Closed

rules_overview.md: elaborate about concurrency support in rule engines#2565
dilyanpalauzov wants to merge 1 commit intoopenhab:mainfrom
dilyanpalauzov:rule_engines_concurrency

Conversation

@dilyanpalauzov
Copy link
Contributor

Based on discussion at https://community.openhab.org/t/java223-scripting-script-with-java-on-openhab-5-0-1-0-6-0-0-0/159853/27, and looking for git grep [^a-zA-Z]Lock bundles/org.openhab.automation.*, I see that only org.openhab.automation.pythonscripting.internal.PythonScriptEngine and org.openhab.automation.jsscripting.internal.OpenhabGraalJSScriptEngine implement Lock. My understanding is that in these cases there is a single thread, which executes all the Python/GraalVM code, and another thread for executing all the JavaScript/GraalVM code. Hence the last sentence of the changeset.

@netlify
Copy link

netlify bot commented Sep 27, 2025

Thanks for your pull request to the openHAB documentation! The result can be previewed at the URL below (this comment and the preview will be updated if you add more commits).

Built without sensitive environment variables

Name Link
🔨 Latest commit ce9172f
🔍 Latest deploy log https://app.netlify.com/projects/openhab-docs-preview/deploys/68d7a3d54a772900085503e3
😎 Deploy Preview https://deploy-preview-2565--openhab-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@openhab-bot
Copy link
Collaborator

This pull request has been mentioned on openHAB Community. There might be relevant details there:

https://community.openhab.org/t/java223-scripting-script-with-java-on-openhab-5-0-1-0-6-0-0-0/159853/45

@dilyanpalauzov
Copy link
Contributor Author

I asked at https://community.openhab.org/t/java223-scripting-script-with-java-on-openhab-5-0-1-0-6-0-0-0/159853/45:

For clarification, if automation/jsr223/seven.py (or seven.js) installs seven rules and automation/jsr223/three.py (or three.js) contains three rules, does the implements Lock in org.openhab.automation.pythonscripting.internal.PythonScriptEngine (and org.openhab.automation.jsscripting.internal.OpenhabGraalJSScriptEngine) ensure that at most one out of ten rules are executed at once, or does it mean, that one of the seven rules is executed, and at the same time one of the three rules can be executed? That is, does splitting one file in automation/jsr223/.(js|py) into many, independent files, each of which installs a rule, increase the parallelism?

@rkoshak
Copy link
Contributor

rkoshak commented Sep 29, 2025

I'll also answer here.

For all rules no matter the language being used:

  • each rule gets one single thread
  • if a rule gets triggered, it runs in that single thread
  • if the rule gets triggered again, the second trigger will be queued and won't run until the current running of this rule exits

tl;dr: only one instance of any given rule can run at a time. It is impossible for a single rule to be running two or more times in parallel.

All this locking is within a single rule. You can have as many separate rules running at the same time in parallel as your machine is able to handle.

  • timers for most rules languages use a thread from a pool of threads. This limits how many individual timers you can have running at the same time, but that number is large enough for most and it allows timers to be running at the same time as the rule in which the timer was created. (I'm not as certain of this, it's how it used to work but don't know if it works that way toady).

Some rules languages have further restrictions. For example, GraalVM JS has a limitation that disallows any context from being accessed by more than one thread. Each rule represents a separate context. When a context is accessed by more than one thread, a MultiThreadedException when the second parallel access is attempted.

To prevent these exceptions, the GraalVM JS add-on implements additional locking on the timers. The rule/script and the timer created by that rule/script share a context so you cannot have a timer running at the same time as the rule that created it. Therefore, GraalVM JS adds locks so only the rule or only one of its timers can be running at any given time.

There is no locking on the shared cache nor on rules that are run directly instead of being triggered.

If someone puts anything more than a primitive in the shared cache and two separate GraalVM JS Scripting rules attempt to access that Object at the same time, a MultiThreaded exception will occur (a warning is logged when you attempt to put something more than a primitive in the shared cache in GraalVM JS).

When a rule is called directly from MainUI or from another rule, there is no locking so MultiThreaded exceptions will occur if two or more attempt to call the same rule at the same time.

Best practices:

  • make sure rules run and exit quickly
  • if a script or rule can be called from one than one rule or from MainUI and JS Scripting is used, use a triggering Item instead of a direct call so that these calls are synchronized
  • in JS avoid putting anything but primitives into the shared cache (Java Objects may be OK though, need to test that).

@Nadahar
Copy link
Contributor

Nadahar commented Sep 29, 2025

My understanding is that in these cases there is a single thread, which executes all the Python/GraalVM code, and another thread for executing all the JavaScript/GraalVM code.

It's not a single thread, it will be different threads, but the locks make sure that just one of them are allowed inside the locked section at a time. So, there is no concurrency, anything that utilize e.g. JavaScript will have to "wait in line" until other threads are done using the JavaScript engine.

JavaScript doesn't support multithreading as a language, so this isn't really any different from all the JavaScript running in your browser or other places, even though the "web workers" is an attempt to work around this limitation for browsers.

Python supports multiple threads but only allow one of them to run at any time, so it's also without concurrency. This isn't a limitation of JSR223, but of these languages themselves, so there's really nothing that can be done to make them handle multithreading.

If scripts don't "sleep" or do blocking calls to e.g network resources/web pages/APIs, it won't really be that much of a problem, since the scripts will run quickly and thus release the locks quickly. But, if scripts do these things... there will be queues piling up.

@rkoshak
Copy link
Contributor

rkoshak commented Sep 29, 2025

Python supports multiple threads but only allow one of them to run at any time, so it's also without concurrency. This isn't a limitation of JSR223, but of these languages themselves, so there's really nothing that can be done to make them handle multithreading.

I think it's important to separate whether the language can run multithreaded and whether the way GraalVM works with these languages allow access to "stuff" created by the languages to be accessed by multiple threads at the same time. It's the latter that's a limitation for GraalVM JS as we use it (there actually is a way to run GraalVM JS with Java that does allow multuithreaded access to the context, but it's not really something that we can make deployable in an OH context. It requires installing and running a separate process (IIRC) instead of embedding it as an OSGI service.

This all isn't strictly a case where "the language can't do it". It's more of a case of "the way we need to use GraalVM in OH doesn't allow it.

If scripts don't "sleep" or do blocking calls to e.g network resources/web pages/APIs, it won't really be that much of a problem, since the scripts will run quickly and thus release the locks quickly. But, if scripts do these things... there will be queues piling up.

Yes, but if you write a rule that blocks like that and triggers often enough to matter, it will only block that one rule, not all rules. At least that's the way it's supposed to work. I haven't tested it since OH 3 though. Note this is a huge improvement over the way it used to work in older verison of OH where one rogue rule could kill all your rules waiting for access to a thread to run in.

@Nadahar
Copy link
Contributor

Nadahar commented Sep 29, 2025

This all isn't strictly a case where "the language can't do it". It's more of a case of "the way we need to use GraalVM in OH doesn't allow it.

The limitations of GraalVM, and how OH uses it, the restrictions in OH itself, all matter. The reason I've been focusing on these languages lacking multithreading support is that, regardless of all the other limitations, if they didn't exist, you still couldn't let them work with multiple threads at once because the languages lack the mechanisms to do it. They simply don't have syntax/techniques to handle concurrency. So, how could it ever work, regardless of other imposed limitations?

JS and Python seem to have picked a slightly different strategy to avoid having to deal with concurrency, but the result isn't very different. In the world of JS, threads doesn't really exist, and instead it has evolved into a quite complicated asynchronous system via the "event loop", that will call different things from a single thread in sequence. As a consequence, you never really know when something will run, just that it probably will "at some time in the future".

Python allow threads, but to avoid having to deal with concurrency, they have the "Global Interpreter Lock" (GIL). It's a lock/mutex/synchronization that only allows one thread access to the Python interpreter (which is the entity that makes anything happen, much like the "script engine" for JSR223). So, even though you can have threads, they will spend their time waiting for their turn, only one can run at any given time. This means that shared objects can safely be accessed by different threads, since there is no actual concurrency, so the language doesn't have to deal with ways to handle concurrency (like locks/mutexes).

So, despite their approach being different, the end goal seems to be the same: Avoid having to deal with concurrency, with the price being that they can't really utilize multiple cores. But, both being more "light scripting languages" than full programming languages, probably considered this as an acceptable tradeoff. The end result remains that you lack ways to handle concurrency in the languages themselves.

The "web workers" in browsers tries to add threads to JS, but not by integrating it into the language, but my making JS code use an API that basically implements what is lacking in the language itself.

Yes, but if you write a rule that blocks like that and triggers often enough to matter, it will only block that one rule, not all rules. At least that's the way it's supposed to work. I haven't tested it since OH 3 though. Note this is a huge improvement over the way it used to work in older verison of OH where one rogue rule could kill all your rules waiting for access to a thread to run in.

That's not true, as far as I can understand, for JavaScript and Python with these engine-wide locks. One "rogue rule" in one of these languages can "kill"/pause all rules using the same language.

I don't have the full overview, but it might be possible to make it work differently by having one ScriptEngine instance per rule, but I have no idea what it would "cost" or what it would do to "shared context". It is a problem with all the stuff being injected into the contexts of the scripts if these objects can't be safely accesses concurrently, even from different ScriptEngine instances.

In many ways, this is a quite hard "design question" for how all this should ideally work, what solution are possible, desirable, and what the price of the different decisions are. I certainly don't have all the answers.

@Nadahar
Copy link
Contributor

Nadahar commented Sep 29, 2025

Interestingly, GraalVM claims to support concurrency if some rules are followed - so I'm questioning if the "engine lock" is really necessary or desirable:

https://www.graalvm.org/22.0/reference-manual/js/Multithreading/

Exactly what are the circumstances that result in this MultiThreadedException (I can't really find any information about it)? Is it in reality this one?

java.lang.IllegalStateException: Multi threaded access requested by thread..

@dilyanpalauzov
Copy link
Contributor Author

https://www.graalvm.org/22.0/reference-manual/js/Multithreading/ says the limitations of multi-threading for JavaScript:

  • Concurrent access to JavaScript objects is not allowed: any JavaScript object cannot be accessed by more than one thread at a time.

Exactly what are the circumstances that result in this MultiThreadedException?

This happened here openhab/openhab-addons#13022 - A rule starts a timer (setTimeOut(…)). When the timer expires, at the same time the rule is executed again, because it was triggered, reading some JavaScript objects at the same time from different threads - one executing the rule, one executing the setTimeOut() callback. (Or something like this).

Is it in reality this one? java.lang.IllegalStateException: Multi threaded access requested by thread..

openhab/openhab-addons#13022 says “Multi threaded access requested by thread …”

@rkoshak
Copy link
Contributor

rkoshak commented Sep 29, 2025

you still couldn't let them work with multiple threads at once because the languages lack the mechanisms to do it.

But in an OH rules and transformation's context, you have access to all of Java too which can.

For example, if I want to sleep for some reason in a JS rule I can use:

Java.type('java.lang.Thread').sleep(5000);

We are not in a pure environment. It's a hybrid between JS and Java (or Rules DSL and Java, or Groovy and Java, ...).

I'd have to experiment to find the proper syntax, but you should be able to create a thread that way too. And all the locking classes in Java are available too.

That's not true, as far as I can understand, for JavaScript and Python with these engine-wide locks. One "rogue rule" in one of these languages can "kill"/pause all rules using the same language.

It's easy enough to test.

Given the following rules:

configuration: {}
triggers:
  - id: "1"
    configuration:
      cronExpression: "* * * * * ? *"
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: console.info('Rule is running!');
    type: script.ScriptAction
console.info('about to sleep');
Java.type('java.lang.Thread').sleep(3000);
console.info('done sleeping');

I should never see the log "Rule is running!" in between "about to sleep" and "done sleeping". And yet...

2025-09-29 12:27:11.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:27:12.139 [INFO ] [g.openhab.automation.script.ui.block] - about to sleep
2025-09-29 12:27:12.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:27:13.916 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:27:14.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:27:15.140 [INFO ] [g.openhab.automation.script.ui.block] - done sleeping
2025-09-29 12:27:15.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!

Even if I change the sleep to a busy wait we can see that two JS scripting scripts are running at the same time. I'm not sure why it's not sleeping for the full 30 seconds though but that's not relevant to this discussion.

console.info('about to sleep');
//Java.type('java.lang.Thread').sleep(3000);
let end = time.toZDT('PT30S');
let i = 0;
while(time.toZDT().isBefore(end)) {
  i++;
}
console.info('done sleeping');
2025-09-29 12:30:43.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:43.917 [INFO ] [g.openhab.automation.script.ui.block] - about to sleep
2025-09-29 12:30:44.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:45.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:46.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:47.919 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:48.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:49.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:50.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:51.918 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:52.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:53.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:54.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:55.918 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:56.916 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:57.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:58.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:30:59.918 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:00.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:01.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:02.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:03.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:04.916 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:05.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:06.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:07.918 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:08.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:09.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:10.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:11.918 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:12.916 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:13.917 [INFO ] [hab.automation.script.ui.everySecond] - Rule is running!
2025-09-29 12:31:13.918 [INFO ] [g.openhab.automation.script.ui.block] - done sleeping

These would not be possible if only one JS rule could run at a time.

I don't have the full overview, but it might be possible to make it work differently by having one ScriptEngine instance per rule, but I have no idea what it would "cost" or what it would do to "shared context".

I can't speak to the cost, but that is how it appears to work. Something is in place that allows JS rules to run in parallel.

Interestingly, GraalVM claims to support concurrency if some rules are followed

Indeed. I said so above. But we can't deploy GraalVM JS in a way that we can take advantage of that without adding a bunch manual steps beyond installing the add-on. Those steps are not compatible with OSGI.

Is it in reality this one?

I don't know. It might be that the JS Scripting add-on wraps it. I do know in openhab.log we see an exception that starts with "MultiThreaded". It may be MultiThreadedAccessException or just MultiThreadedException. You ccan generate it if you construct a test where a rule gets called (not triggered, called directly, in JS that would be rules.runRule() while it's already running, or if you put a JS Object into the cache.shared and try to access it from two different rules.

Each rule has its own thread. If they all ran in the same thread, there would be no chance for a multithreaded exception.

@dilyanpalauzov
Copy link
Contributor Author

Java.type('java.lang.Thread').sleep(3000);
I'm not sure why it's not sleeping for the full 30 seconds though

Because 3000 milliseconds, as in Thread.sleep() are 3 seconds.

The above example shows, that two JavaScript rules can be executed at the same time. My explanation is that there are two distinct ScriptEngines involved. Can you provide the same example, but putting all logic in in a single automation/jsr223/.js file: printing console.info('Rule is running!'); with cronExpression: "* * * * * ? *" and a script console.info('about to sleep');Java.type('java.lang.Thread').sleep(3000);console.info('done sleeping'); ? So maybe first write "about to sleep", then install the rule, then sleep.

@rkoshak
Copy link
Contributor

rkoshak commented Sep 29, 2025

But getting back to this PR. I don't think it's correct that JS and Python do not have concurrency. Separate rules do run in parallel (see the test above for proof).

All rules languages including GraalVM JS and Python have access to all of Java including ReentrantLock, Thread, etc. So there is access to concurrency primitives.

However, GraalVM JS and maybe Python have a concurrency restriction in that nothing from any one rule (script really) can be accessed from more than one thread at the same time. But that does not mean that these scripting languages do not support concurrency at all. Therefore, the edits of the PR are incorrect.

Can you provide the same example, but putting all logic in in a single

I can, but it shouldn't matter. A rule is a rule once it's created. The only difference is how how the rule gets created in the first place: i.e. loaded from JSONDB or created through code using the APIs. It will take me a bit to do that though. It might be tomorrow but I'll try later.

I thought I had typed 30000. Doh!

and a script console.info('about to sleep');Java.type('java.lang.Thread').sleep(3000);console.info('done sleeping');

This needs to be inside a rule too, right? Otherwise we are not testing the same thing,. If it's not in a rule we are testing whether multiple .js files can be loaded at the same time, not whether multiple rules can run at the same time.

Also, it needs to be in automation/js. GraalVM doesn't use jsr223.

@dilyanpalauzov
Copy link
Contributor Author

This needs to be inside a rule too, right? Otherwise we are not testing the same thing,. If it's not in a rule we are testing whether multiple .js files can be loaded at the same time, not whether multiple rules can run at the same time.

I mean in all cases everything should be in a single .js file, so that one ScriptEngine handles it. I though you start with console.info, outside rule, then install the rule from the same.js code, then sleep and console.info. But you can try also with two rules, as long as they are loaded from a single .js file.

@rkoshak
Copy link
Contributor

rkoshak commented Sep 29, 2025

I though you start with console.info, outside rule, then install the rule from the same.js code, then sleep and console.info.

That won't show anything. It certainly isn't a duplicate of the test above. The test above is two rules, one a full rule and the other just a Script (since it gets run manually). There's no code tab on Scripts in MainUI so just pasted in the code. But it is a rule.

as long as they are loaded from a single .js file.
That shouldn't matter at all. JS works like @HolgerHees described on the forum. Once the file is loaded and the rules from the file are created, they exist independently.

I seem to be having a problem with my script recognizing the automatic imports. This might take longer. I might be hitting a bug. I have to fight that before I can get some results.

One thing that might cause confusion is that when using .js files to create rules, you are actually writing a JS program whose job is to create rule(s) when it's run. OH loads the file and runs the script and obviously it needs a ScriptEngine to execute that rule creation code. But once the rules are created, they are independent from that ScriptEngine and it appears get their own ScriptEngine.

So if I put a sleep outside of a rule in a .js file, all I'll accomplish is adding a delay between the creation of the rules in that file depending on where I put the sleep.

@dilyanpalauzov
Copy link
Contributor Author

I seem to be having a problem with my script recognizing the automatic imports. This might take longer. I might be hitting a bug. I have to fight that before I can get some results.

Yes, the automatic imports are not working with automation/js files with OH 5.1.0 Build 4836, even if for me “Auto injection for all scripts, including file-based scripts and transformations“ is selected. This works /etc/openhab/automation/js/a.js:

const { rules, triggers } = require("openhab")
console.info("START")
rules.JSRule({
    name: 'ABC',
    description: 'ABC',
    triggers: [triggers.GenericCronTrigger("* * * * * ? *")],
    execute: (data) => console.info('Rule is running!'))
})
Java.type('java.lang.Thread').sleep(30000)
console.info("END")

Result:

2025-09-29 21:02:11.994 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/a.js'
2025-09-29 21:02:12.491 [INFO ] [.openhab.automation.script.file.a.js] - START
2025-09-29 21:02:12.499 [INFO ] [.openhab.automation.openhab-js.rules] - Adding rule: ABC
2025-09-29 21:02:42.508 [INFO ] [.openhab.automation.script.file.a.js] - END
2025-09-29 21:02:42.514 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.520 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.526 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.533 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.539 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.545 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.551 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.557 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.563 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.569 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.575 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!
2025-09-29 21:02:42.581 [INFO ] [.openhab.automation.script.file.a.js] - Rule is running!

As long as there is Thread.sleep(30000) running, the rule is not executed. That is because the rule ABC and the script (code in a.js outside of the rule is the same a OH Script) are executed either-or: in the same thread. You could create an example with two rules, started from the same file, to check if the same behaviour is observed, or if the rules can both be executed at the very same time.

@rkoshak
Copy link
Contributor

rkoshak commented Sep 29, 2025

OK, I managed to work around the problem. Indeed there appears to be a bug with automatic injection of the variables for file based rules. I'll file an issue for that later.

But color me surprised. The blocking rule did block the cron triggered rule.

Code:

const { rules } = require('openhab');

rules.when().cron('* * * * * ? *')
     .then( (event) => {
       console.info('Rule is running!');
     })
     .build('Every Minute', 'Logs every minute for testing', [], 'everyMinute');

rules.when().item('TestSwitch').receivedCommand()
     .then( (event) => {
        console.info('About to block');
        Java.type('java.lang.Thread').sleep(15000);
        console.info('Done blocking');
      })
      .build('Blocking', 'Sleeps for 15 seconds', [], 'blocking');

Logs:

2025-09-29 13:56:58.138 [INFO ] [enhab.automation.script.file.test.js] - Rule is running! 
2025-09-29 13:56:59.058 [INFO ] [enhab.automation.script.file.test.js] - About to block
2025-09-29 13:57:14.059 [INFO ] [enhab.automation.script.file.test.js] - Done blocking
2025-09-29 13:57:14.060 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!

I don't like this difference in behavior at all.

I tried moving the blocking rule to its own file.

When the rules are defined in separate files they do not block each other.

2025-09-29 14:02:29.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:30.618 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:30.826 [INFO ] [nhab.automation.script.file.block.js] - About to block
2025-09-29 14:02:31.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:32.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:33.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:34.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:35.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:36.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:37.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:38.618 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:39.618 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:40.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:41.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:42.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:43.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:44.618 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:45.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!
2025-09-29 14:02:45.828 [INFO ] [nhab.automation.script.file.block.js] - Done blocking
2025-09-29 14:02:46.619 [INFO ] [enhab.automation.script.file.test.js] - Rule is running!

I think I understand why, and I should not have been surprised.

All rules defined in a given .js file share the same context. They don't magically get a new context once they are created. At a minimum the Script Actions for all the rules need to be passed to the rules that's created even if everything else gets created as Java in the rules engine (which I don't think is the case). Because they share the same context, none of these script actions can run at the same time.

So to go back to your original scenario:

  • all managed rules are independent and can run in parallel with no intra-rule restrictions
  • all rules defined in different .js files can run in parallel with no intra-rule restrictions
  • all rules defined in the same .js file can only run one at a time, blocking all other rules from that file from running at the same time.

And by "running" I mean executing script conditions and script actions. They can still be triggered but the triggers will be queued and work on in sequence.

@dilyanpalauzov
Copy link
Contributor Author

Ah, bug after bug. With the script below, or something nearly identical to it, when I change ABC to ABC2 and save, I get:

2025-09-29 21:10:23.388 [INFO ] [.openhab.automation.script.file.a.js] - ABC START                              
2025-09-29 21:10:23.389 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/autom
ation/js/a.js'                                                                                                  
2025-09-29 21:10:23.511 [ERROR] [.openhab.automation.script.file.a.js] - Failed to execute rule ABC-4110964a-e42
b-4f68-afa7-ddd4ab70af93: java.lang.InterruptedException: sleep interrupted: sleep interrupted                  
    at java.lang.Thread.sleep0                                                                                  
    at java.lang.Thread.sleep (Thread.java:509:0)                                                               
    at execute (a.js:9:247-287)                                                                                 
    at doExecute (openhab.js:2:58571-58601)                                                                     
2025-09-29 21:10:23.529 [INFO ] [.openhab.automation.script.file.a.js] - DEF START       
2025-09-29 21:10:23.533 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule 'ABC-4110964a-e4
2b-4f68-afa7-ddd4ab70af93': Failed to execute action: 1(Error: Failed to execute rule ABC-4110964a-e42b-4f68-afa
7-ddd4ab70af93: java.lang.InterruptedException: sleep interrupted: sleep interrupted
    at java.lang.Thread.sleep0                                                                                  
    at java.lang.Thread.sleep (Thread.java:509:0)                                                               
    at execute (a.js:9:247-287)                                                                                 
    at doExecute (openhab.js:2:58571-58601))                                                                    
2025-09-29 21:10:23.542 [ERROR] [.openhab.automation.script.file.a.js] - Failed to execute rule DEF-31373cb3-795
e-47dc-8952-672feb18bd60: java.lang.InterruptedException: sleep interrupted: sleep interrupted
    at java.lang.Thread.sleep0                                                                                  
    at java.lang.Thread.sleep (Thread.java:509:0)                                                               
    at execute (a.js:19:501-541)
    at doExecute (openhab.js:2:58571-58601)
2025-09-29 21:10:23.552 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule 'DEF-31373cb3-79
5e-47dc-8952-672feb18bd60': Failed to execute action: 1(Error: Failed to execute rule DEF-31373cb3-795e-47dc-895
2-672feb18bd60: java.lang.InterruptedException: sleep interrupted: sleep interrupted
    at java.lang.Thread.sleep0
    at java.lang.Thread.sleep (Thread.java:509:0)
    at execute (a.js:19:501-541)
    at doExecute (openhab.js:2:58571-58601))
2025-09-29 21:10:24.473 [INFO ] [.openhab.automation.script.file.a.js] - START
2025-09-29 21:10:24.489 [INFO ] [.openhab.automation.openhab-js.rules] - Adding rule: ABC

and when I repeated the above several times, I think I hit a deadlock : nothing happens.

So stopping and starting openhab, putting under /etc/openhab/automation/a.js:

const { rules, triggers } = require("openhab") 
console.info("START")
rules.JSRule({
    name: 'ABC',
    description: 'ABC',
    triggers: [triggers.GenericCronTrigger("* * * * * ? *")],
    execute: (data) => {
        console.info('ABC START')
        Java.type('java.lang.Thread').sleep(3000)
        console.info('ABC END')
    }
})
rules.JSRule({
    name: 'DEF',
    description: 'DEF',
    triggers: [triggers.GenericCronTrigger("* * * * * ? *")],
    execute: (data) => {
        console.info('DEF START')
        Java.type('java.lang.Thread').sleep(3000)
        console.info('DEF END')
    }
})
Java.type('java.lang.Thread').sleep(30000)
console.info("END")

output is

2025-09-29 21:20:09.888 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/a.js'                                                                                                  
2025-09-29 21:20:17.075 [INFO ] [.openhab.automation.script.file.a.js] - START
2025-09-29 21:20:17.199 [INFO ] [.openhab.automation.openhab-js.rules] - Adding rule: ABC
2025-09-29 21:20:17.355 [INFO ] [.openhab.automation.openhab-js.rules] - Adding rule: DEF
2025-09-29 21:20:47.381 [INFO ] [.openhab.automation.script.file.a.js] - END
2025-09-29 21:20:47.437 [INFO ] [.openhab.automation.script.file.a.js] - ABC START
2025-09-29 21:20:50.442 [INFO ] [.openhab.automation.script.file.a.js] - ABC END
2025-09-29 21:20:50.457 [INFO ] [.openhab.automation.script.file.a.js] - DEF START
2025-09-29 21:20:53.462 [INFO ] [.openhab.automation.script.file.a.js] - DEF END
2025-09-29 21:20:53.482 [INFO ] [.openhab.automation.script.file.a.js] - ABC START
2025-09-29 21:20:56.486 [INFO ] [.openhab.automation.script.file.a.js] - ABC END
2025-09-29 21:20:56.503 [INFO ] [.openhab.automation.script.file.a.js] - DEF START
2025-09-29 21:20:59.507 [INFO ] [.openhab.automation.script.file.a.js] - DEF END
2025-09-29 21:20:59.514 [INFO ] [.openhab.automation.script.file.a.js] - ABC START
2025-09-29 21:21:02.518 [INFO ] [.openhab.automation.script.file.a.js] - ABC END
2025-09-29 21:21:02.534 [INFO ] [.openhab.automation.script.file.a.js] - DEF START
2025-09-29 21:21:05.539 [INFO ] [.openhab.automation.script.file.a.js] - DEF END
2025-09-29 21:21:05.551 [INFO ] [.openhab.automation.script.file.a.js] - ABC START
2025-09-29 21:21:08.555 [INFO ] [.openhab.automation.script.file.a.js] - ABC END
2025-09-29 21:21:08.566 [INFO ] [.openhab.automation.script.file.a.js] - DEF START

The blocking rule did block the cron triggered rule. … I tried moving the blocking rule to its own file. When the rules are defined in separate files they do not block each other.

I come to the same conclusion with the example above.

Okay, we found it out! So to my question:

For clarification, if automation/jsr223/seven.py (or seven.js) installs seven rules and automation/jsr223/three.py (or three.js) contains three rules, does the implements Lock in org.openhab.automation.pythonscripting.internal.PythonScriptEngine (and org.openhab.automation.jsscripting.internal.OpenhabGraalJSScriptEngine) ensure that at most one out of ten rules are executed at once, or does it mean, that one of the seven rules is executed, and at the same time one of the three rules can be executed? That is, does splitting one file in automation/jsr223/.(js|py) into many, independent files, each of which installs a rule, increase the parallelism?

Yes, splitting in many files does increase the parallelism!

I will change the current PR.

@Nadahar
Copy link
Contributor

Nadahar commented Sep 29, 2025

We are not in a pure environment. It's a hybrid between JS and Java (or Rules DSL and Java, or Groovy and Java, ...).

True, but I'm not sure if you can get access to the intrinsic locks from Java, they aren't objects, but are rather a part of every object. Maybe it would be possible, but I don't know how to do that from Java either, you use the synchronized keyword to use the intrinsic locks. These are the most commonly used locks, the explicit Lock implementations are relatively rare to use, because they are both slower to execute, produces more "complicated" code in that you must unlock everything using finally and there's simply no point in going through the extra trouble most of the time. The only time I use explicit locks is if I want to have a "read-write lock", one that allows shared read but exclusive write. This is a good idea in theory, but since they are much slower, what you gain by this is often not worth it. They are also complicated to use since you can easily get a deadlock if you try to "upgrade" from a read lock to a write lock.

Maybe there are ways around this, but I can't imagine this whole thing would be very user-friendly or at all viable for most users.

It's easy enough to test.

We're talking about different things. Whenever I talk about a rule, I don't mean a SimpleRule unless explicitly saying so. They just follow completely different rules, because they are basically controlled by the add-on. For SimpleRules, according to @HolgerHees, this applies:

The current behavior for pythonscripting is that for each script file a new PythonScriptEngine is created.

That means that the SimpleRule Actions will run in this ScripeEngine. Since the rest of a SimpleRule (the part that isn't the Action) is just a normal rule, the Conditions probably isn't executed in this ScriptEngine, but in the "rule thread", assuming that Conditions use the "rule thread" (don't remember at the moment).

What I'm talking about is two normal rules. Try to create two of them that use JS and let one sleep, and I'm pretty sure you'll see a different result. These will, as far as I can understand, all share the ScriptEngineManager provided script engine, while the SimpleRule Actions will run in the script engine created by the add-on. Whether that engine is shared between SimpleRules will depend on how the add-on creates these.

Indeed. I said so above. But we can't deploy GraalVM JS in a way that we can take advantage of that without adding a bunch manual steps beyond installing the add-on. Those steps are not compatible with OSGI.

I'm not sure that's true. I'm talking about within the same script engine instance. It says that as long as the context isn't shared between threads, multiple threads can actually run scripts in the same script engine. However, it also says that "JavaScript objects" cannot be accessed by multiple threads, but this is where I don't follow quite... because, if the context isn't shared, how would two threads get access to the same "JavaScript object"?

So, to me it looks like you probably could run things in parallel of you kept the contexts separate. However, I'm not sure how all the stuff the helper libs insert works. Are there "JavaScript objects" in there, are there any form of global variables available across script engines?

Each rule has its own thread. If they all ran in the same thread, there would be no chance for a multithreaded exception.

Aren't you saying that you must do something that breaks this isolation, like using timers or invoking a different rule, to trigger such an exception? In addition there are things like cache.shared that I saw mentioned that I don't know how works, but it sounds like this could allow accessing "JavaScript objects" by multiple threads. Is the "cache.shared" something that lives in the script engine instance?

But getting back to this PR. I don't think it's correct that JS and Python do not have concurrency. Separate rules do run in parallel

IF they use different script engine instances, which is only the case for SimpleRule implementations. So, there's a lot of "ifs" and "buts" here.

One thing that might cause confusion is that when using .js files to create rules, you are actually writing a JS program whose job is to create rule(s) when it's run. OH loads the file and runs the script and obviously it needs a ScriptEngine to execute that rule creation code. But once the rules are created, they are independent from that ScriptEngine and it appears get their own ScriptEngine.

SimpleRules in general cause a lot of confusion. It is correct that you write a JS program that creates the rule, this rule then becomes a Rule in OH like any other rule. The code that creates the Rule runs in the script engine created by the add-on, but that wouldn't really matter if you didn't use SimpleRule. If not using SimpleRule, the rule would behave in the exact same way as e.g an UI created Rule, it would use the ScriptEngineManager provided ScriptEngine. But, with SimpleRules, there's this "trick" where the java Action object isn't really used. Instead, there's a reference to an in-memory compiled code that is executed when the Action is executed. This Action will run in the same script engine as was used to create the rule, not the one provided by ScriptEngineManager. So, this isn't easy to keep track of at all, with both threads and script engine instances to keep track of.

I don't like this difference in behavior at all.

This is all about the different script engine instances, as I already tried to point out. The locks follow the script engine instance, so regardless of which thread is trying to execute, only one will be allowed to run within the same script engine instance.

I'm having more and more doubt if this is the correct solution the more I learn about it.

  • all managed rules are independent and can run in parallel with no intra-rule restrictions

How did you reach that conclusion? This isn't the case, as far as I can understand, for script engines that implement the lock. They will all have to wait for each other, because they share the script engine instance.

  • all rules defined in different .js files can run in parallel with no intra-rule restrictions

This depends on how the add-on creates script engine instances, but seem to be the case for the JS add-on at the moment.

  • all rules defined in the same .js file can only run one at a time, blocking all other rules from that file from running at the same time.

Yes, because there is one script engine instance per file. The reason for this restriction is the Lock on the script engine instance, not really the context. But, since the context can't be shared among threads, you probably could get this discussed exception if two SimpleRule Actions from the same .js file were executed at the same time. The handing of the context isn't entirely clear to me, I remember seeing it being created in Java code, but I didn't really trace how it was handled. Also, when it comes to SimpleRule Actions, I suspect that the Java code don't control the context, but that it comes with the in-memory runnable object that is created with execute() {}.

and when I repeated the above several times, I think I hit a deadlock : nothing happens.

What immediately intrigues me here is: Why is the thread interrupted? A thread that is interrupted (has been interrupted in the past, so that the interrupted flag is set), will immediately throw InterruptedException if you try to sleep(). But the thread that is trying to run this, shouldn't be interrupted... one would think. You normally interrupt a thread if you want it to stop what it's doing. The interrupted flag must then be reset before giving the thread a new task... but this is normally handled by the thread pools AFAIK - so I'm curious how this came about in the first place.

I will change the current PR.

I would actually hold off documenting this until things are straightened out. How much of this has changed recently? Are the script engine locks new? If so, is it even certain that this solution is viable - I assume that it's supposed to prevent the "multithreaded exception", but I fear that it will cause much more problems than it solves. Or, has it been this way for some time, and something else changed in 5.1?

@Nadahar
Copy link
Contributor

Nadahar commented Sep 29, 2025

To throw another wrench into the machinery, there is also this:

https://github.com/openhab/openhab-addons/blob/main/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeWrappingScriptedAutomationManagerDelegate.java

It's supposed to protect the SimpleRule Action context against concurrent access from the same .js file, as far as I can understand, so this must stem from a time when there were no lock protecting the whole script engine instance. There now appears to be two locks guarding against this.

@HolgerHees
Copy link
Contributor

HolgerHees commented Sep 30, 2025

Regarding the mentioned pythonscripting.

Pythonscripting can fully run asynchron in the way it means for Python (GIL)

You can create Threads and Timers inside a script file and they fully run in parallel.

The lock from PythonScriptEngine is only used during initial eval calls. This are internals and does not affect the runtime context of python itself.

The scenario where you have a sleep inside the execute function has nothing to do with python threading. It is just a symptom of how openhab works.

  1. Rules are no Threads. They are just listeners on the event bus.
  2. If a event happens, it is processed from a queue thread. This queue thread is also calling the execute function. If you have a sleep inside this execute function, you block the queue thread of openhab.
  3. The amount of queue threads are limited. e.g. for Item or Command Events, if I remember correctly this are 5 or 10.
  4. For CronEvents, maybe this is just one. It depends on the CronService implementation. In the mentioned scenario from @dilyanpalauzov , he is just blocking the Cron processing thread, with the result, that no other crons are triggered.

This is why you should never to complex things inside the execute statement. If you have something complex todo, just create a Thread and do it there.

@dilyanpalauzov
Copy link
Contributor Author

Python is a programming language. When implemented in Jython it does not have Global Interpreter Lock. The CPython implementation has Global Interpreter Lock and the way to achieve concurrency is using multi-processing (many processes instead of many threads). CPython 3.13 can be compiled without Global Interpreter lock.

Some more tests:

JavaScript: Each rule is executed in its own thread

const { rules, triggers } = require("openhab")
console.info("START " + Java.type('java.lang.Thread').currentThread().getId())
rules.JSRule({
    name: 'ABC',
    triggers: [triggers.GenericCronTrigger("* * * * * ? *")],
    execute: (data) => {
        console.info('ABC START ' + Java.type('java.lang.Thread').currentThread().getId())
        Java.type('java.lang.Thread').sleep(3000)
        console.info('ABC END')
    }
})
rules.JSRule({
    name: 'DEF',
    triggers: [triggers.GenericCronTrigger("* * * * * ? *")],
    execute: (data) => {
        console.info('DEF START ' + Java.type('java.lang.Thread').currentThread().getId())
        Java.type('java.lang.Thread').sleep(3000)
        console.info('DEF END')
    }
})
Java.type('java.lang.Thread').sleep(30000)
console.info("END")
2025-09-30 11:13:17.252 [INFO ] [.openhab.automation.script.file.a.js] - ABC START 10314
2025-09-30 11:13:20.257 [INFO ] [.openhab.automation.script.file.a.js] - ABC END
2025-09-30 11:13:20.262 [INFO ] [.openhab.automation.script.file.a.js] - DEF START 10313
2025-09-30 11:13:23.264 [INFO ] [.openhab.automation.script.file.a.js] - DEF END

The above shows that each rule is executed in its own thread, even if only one of the threads, created by a single ScriptEngine instance, is executed at any given time.

Parallelism of one and the same Transformation

How can I check, if the same Groovy/Java223/JavaScript/Python transformation can run in parallel?

I have created three logging transformations:

  • transform/d.java
public class D {
    public Object main() {
        String s = java.time.ZonedDateTime.now().toString().substring(11, 19);
        org.slf4j.LoggerFactory.getLogger("J").warn(s);
        try {
            java.lang.Thread.sleep(3000);
        } catch (InterruptedException e) {}
        return "A " + s + " - " + java.time.ZonedDateTime.now().toString().substring(11, 19);
    }
}
  • transform/d.groovy
String s = java.time.ZonedDateTime.now().toString().substring(11, 19)
org.slf4j.LoggerFactory.getLogger("L").warn(s)
java.lang.Thread.sleep(3000)
"G " + s + " - " + java.time.ZonedDateTime.now().toString().substring(11, 19)
  • transform/d.js
s = (new Date).toISOString().substr(11)
console.log(s)
Java.type('java.lang.Thread').sleep(3000)
"J " + s + " - " + (new Date).toISOString().substr(11)

Whatever I try I cannot get the same transformation logging in less than 3 seconds gaps (time at the beginnig of the lines in openhab.log).

Parallelism in Rules

I have changed the two rules, which trigger on cron event and are blocking each other, with item change trigger:

const { rules, triggers } = require("openhab")
console.info("START " + Java.type('java.lang.Thread').currentThread().getId())
rules.JSRule({
    name: 'ABC',
    triggers: [triggers.ItemStateChangeTrigger('r9_power')],
    execute: (data) => {
        console.info('ABC START ' + Java.type('java.lang.Thread').currentThread().getId())
        Java.type('java.lang.Thread').sleep(3000)
        console.info('ABC END')
    }
})
rules.JSRule({
    name: 'DEF',
    triggers: [triggers.ItemStateChangeTrigger('r9_power')],
    execute: (data) => {
        console.info('DEF START ' + Java.type('java.lang.Thread').currentThread().getId())
        Java.type('java.lang.Thread').sleep(3000)
        console.info('DEF END')
    }
})
Java.type('java.lang.Thread').sleep(30000)
console.info("END")

This does not print two START or two END in a row, so that each rule blocks the other rule. The equivalent in Groovy:

scriptExtension.importPreset("RuleSupport")

@groovy.transform.Field logger = org.slf4j.LoggerFactory.getLogger("logger2")

automationManager.addRule(new org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule() {
     String name = "GHI"
     List<org.openhab.core.automation.Trigger> triggers = [
         org.openhab.core.automation.util.TriggerBuilder.create().withId("t").withTypeUID("core.ItemStateChangeTrigger")
         .withConfiguration(new org.openhab.core.config.core.Configuration([itemName: "r"])).build()
     ]

     @Override public Object execute(org.openhab.core.automation.Action module, Map<String, ?> inputs) {
	 logger.warn("GHI BEGIN")
	 Thread.sleep(3000)
	 logger.warn("GHI END")
	 return null
     }
})

automationManager.addRule(new org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule() {
     String name = "JKL"
     List<org.openhab.core.automation.Trigger> triggers = [
         org.openhab.core.automation.util.TriggerBuilder.create().withId("t").withTypeUID("core.ItemStateChangeTrigger")
         .withConfiguration(new org.openhab.core.config.core.Configuration([itemName: "r"])).build()
     ]

     @Override public Object execute(org.openhab.core.automation.Action module, Map<String, ?> inputs) {
	 logger.warn("JKL BEGIN")
	 Thread.sleep(3000)
	 logger.warn("JKL END")
	 return null
     }
})

does execute the rules in parallel:

2025-09-30 11:13:14.232 [WARN ] [logger2                             ] - JKL END
2025-09-30 11:13:14.232 [WARN ] [logger2                             ] - GHI END
2025-09-30 11:13:14.235 [WARN ] [logger2                             ] - JKL BEGIN
2025-09-30 11:13:14.235 [WARN ] [logger2                             ] - GHI BEGIN
2025-09-30 11:13:17.237 [WARN ] [logger2                             ] - JKL END
2025-09-30 11:13:17.238 [WARN ] [logger2                             ] - GHI END

The lock from PythonScriptEngine is only used during initial eval calls.

I guess this eval is done after the input is compiled and there is no equivalent of org.openhab.automation.jsscripting.internal.threading.ThreadsafeWrappingScriptedAutomationManagerDelegate.java for Python.

If the above example, presented in JavaScript and Groovy, is rewritten in Python(GraalVM), how does the code look like and are the two rules executed in parallel?

@HolgerHees
Copy link
Contributor

HolgerHees commented Sep 30, 2025

This is whats happen in python scripting

import java

from openhab import rule, Registry
from openhab.triggers import GenericCronTrigger

@rule(
    triggers = [
        GenericCronTrigger("* * * * * ?")
    ]
)
class Notification1:
    def execute(self, module, input):
        self.logger.info('ABC START ' + str(java.type('java.lang.Thread').currentThread().getId()))
        java.type('java.lang.Thread').sleep(3.0)
        self.logger.info('ABC END')

@rule(
    triggers = [
        GenericCronTrigger("* * * * * ?")
    ]
)
class Notification2:
    def execute(self, module, input):
        self.logger.info('DEF START ' + str(java.type('java.lang.Thread').currentThread().getId()))
        java.type('java.lang.Thread').sleep(3.0)
        self.logger.info('DEF END')
025-09-30 12:41:57.586 [INFO ] [n.pythonscripting.test.Notification1] - ABC START 884
2025-09-30 12:41:57.589 [INFO ] [n.pythonscripting.test.Notification2] - DEF START 885
2025-09-30 12:42:00.587 [INFO ] [n.pythonscripting.test.Notification1] - ABC END
2025-09-30 12:42:00.589 [INFO ] [n.pythonscripting.test.Notification2] - DEF END
2025-09-30 12:42:00.590 [INFO ] [n.pythonscripting.test.Notification1] - ABC START 884
2025-09-30 12:42:00.591 [INFO ] [n.pythonscripting.test.Notification2] - DEF START 885
2025-09-30 12:42:03.591 [INFO ] [n.pythonscripting.test.Notification1] - ABC END
2025-09-30 12:42:03.591 [INFO ] [n.pythonscripting.test.Notification2] - DEF END
2025-09-30 12:42:03.592 [INFO ] [n.pythonscripting.test.Notification1] - ABC START 884
2025-09-30 12:42:03.593 [INFO ] [n.pythonscripting.test.Notification2] - DEF START 885
2025-09-30 12:42:06.593 [INFO ] [n.pythonscripting.test.Notification2] - DEF END
2025-09-30 12:42:06.593 [INFO ] [n.pythonscripting.test.Notification1] - ABC END

So it looks like each CronTrigger ist triggered in its own thread. And it works asynchron.

and instead of using Java classes, below is a pure python example of your code, which is doing exactly the same.

from openhab import rule, Registry
from openhab.triggers import GenericCronTrigger

import time
import threading

@rule(
    triggers = [
        GenericCronTrigger("* * * * * ?")
    ]
)
class Notification1:
    def execute(self, module, input):
        self.logger.info('ABC START ' + str(threading.get_native_id()))
        time.sleep(3000)
        self.logger.info('ABC END')

@rule(
    triggers = [
        GenericCronTrigger("* * * * * ?")
    ]
)
class Notification2:
    def execute(self, module, input):
        self.logger.info('DEF START ' + str(threading.get_native_id()))
        time.sleep(3000)
        self.logger.info('DEF END')

@HolgerHees
Copy link
Contributor

Another example using Item Events

from openhab import rule, Registry
from openhab.triggers import GenericCronTrigger, ItemStateUpdateTrigger

import time
import threading

@rule(
    triggers = [
        ItemStateUpdateTrigger("Item1")
    ]
)
class Notification1:
    def execute(self, module, input):
        self.logger.info('ABC START ' + str(threading.get_native_id()))
        time.sleep(3)
        self.logger.info('ABC END')

@rule(
    triggers = [
        ItemStateUpdateTrigger("Item1")
    ]
)
class Notification2:
    def execute(self, module, input):
        self.logger.info('DEF START ' + str(threading.get_native_id()))
        time.sleep(3)
        self.logger.info('DEF END')

@rule(
    triggers = [
        ItemStateUpdateTrigger("Item1")
    ]
)
class Notification3:
    def execute(self, module, input):
        self.logger.info('XYZ TRIGGERED ' + str(threading.get_native_id()))


Registry.getItem("Item1").postUpdate(1)
Registry.getItem("Item1").postUpdate(1)
Registry.getItem("Item1").postUpdate(1)
Registry.getItem("Item1").postUpdate(1)
Registry.getItem("Item1").postUpdate(1)
Registry.getItem("Item1").postUpdate(1)
Registry.getItem("Item1").postUpdate(1)
2025-09-30 12:52:23.949 [INFO ] [n.pythonscripting.test.Notification1] - ABC START 1032
2025-09-30 12:52:23.950 [INFO ] [n.pythonscripting.test.Notification2] - DEF START 1031
2025-09-30 12:52:23.950 [INFO ] [n.pythonscripting.test.Notification3] - XYZ TRIGGERED 1034
2025-09-30 12:52:23.952 [INFO ] [n.pythonscripting.test.Notification3] - XYZ TRIGGERED 1034
2025-09-30 12:52:23.953 [INFO ] [n.pythonscripting.test.Notification3] - XYZ TRIGGERED 1034
2025-09-30 12:52:23.954 [INFO ] [n.pythonscripting.test.Notification3] - XYZ TRIGGERED 1034
2025-09-30 12:52:23.954 [INFO ] [n.pythonscripting.test.Notification3] - XYZ TRIGGERED 1034
2025-09-30 12:52:23.955 [INFO ] [n.pythonscripting.test.Notification3] - XYZ TRIGGERED 1034
2025-09-30 12:52:23.956 [INFO ] [n.pythonscripting.test.Notification3] - XYZ TRIGGERED 1034
2025-09-30 12:52:27.000 [INFO ] [n.pythonscripting.test.Notification1] - ABC END
2025-09-30 12:52:27.000 [INFO ] [n.pythonscripting.test.Notification2] - DEF END
2025-09-30 12:52:27.002 [INFO ] [n.pythonscripting.test.Notification1] - ABC START 1032
2025-09-30 12:52:27.002 [INFO ] [n.pythonscripting.test.Notification2] - DEF START 1031
2025-09-30 12:52:31.001 [INFO ] [n.pythonscripting.test.Notification1] - ABC END
2025-09-30 12:52:31.001 [INFO ] [n.pythonscripting.test.Notification2] - DEF END
2025-09-30 12:52:31.003 [INFO ] [n.pythonscripting.test.Notification2] - DEF START 1031
2025-09-30 12:52:31.003 [INFO ] [n.pythonscripting.test.Notification1] - ABC START 1032
2025-09-30 12:52:35.000 [INFO ] [n.pythonscripting.test.Notification2] - DEF END
2025-09-30 12:52:35.000 [INFO ] [n.pythonscripting.test.Notification1] - ABC END

@HolgerHees
Copy link
Contributor

So, I can only speak for pythonscripting, but please remove the part "Has no concurrency" from pythonscripting.

@dilyanpalauzov
Copy link
Contributor Author

I am convinced, that Python can run things in parallel, but I do not understand what implications has PythonScriptEngine implements Lock. I am closing the current change proposal in favour of openhab/openhab-addons#19410, adding in jsscripting/README:

When many rules are installed from a single .js file in this directory, at most one of these rules runs at a time.

@dilyanpalauzov dilyanpalauzov deleted the rule_engines_concurrency branch September 30, 2025 13:15
@Nadahar
Copy link
Contributor

Nadahar commented Sep 30, 2025

This is very difficult to discuss, as so many different terms are used. Asynchronous and concurrent are entirely different things. Asynchronous only means that things don't run in a sequential order. Concurrent means in parallel, which means "happening at the same time".

Neither Python nor JS can do concurrency in themselves. But, obviously, if you run multiple script engines (essentially isolated JS or Python execution environments), these environments can work in parallel - they aren't even aware of each other. But, that's like starting a single threaded program 5 times and claim that it's multi-threaded, because it runs multiple times at once.

@dilyanpalauzov
Copy link
Contributor Author

openhab/openhab-addons#19410 is about to add in jsscripting/README:

All automation/js/.js files are executed sequentially in a single thread.

Is this the same for all automation/python/*py files?

I agree this at this moment is hard to discuss for many reasons. My suggestion is to propose documentation changes and then these changes can be discussed.

@HolgerHees
Copy link
Contributor

PythonScriptEngine implements Lock just means the engine can be locked during processing the python sources. In this moment the whole engine is locked. This just means, you can't process a second python file. But we don't want to process a second script. Rules running inside this locked context are not affected by this lock. You can run as much in parallel as you want.

For normal endusers of pythonscripting, they can just ignore the internal engine lock.

@dilyanpalauzov
Copy link
Contributor Author

This is very difficult to discuss, as so many different terms are used. Asynchronous and concurrent are entirely different things. Asynchronous only means that things don't run in a sequential order. Concurrent means in parallel, which means "happening at the same time".

I think the purpose is to describe when things block each other.

@Nadahar
Copy link
Contributor

Nadahar commented Sep 30, 2025

I think the purpose is to describe when things block each other.

If you narrowly only focus on that, sure. To me, getting the overview is more important, because that's what you need to see if there can be done improvements to "the architecture" itself, where locks are needed and why. To do that, separating the different concepts/concerns is important.

The old "don't plan, just slap on some locks until it works" approach doesn't really give good results. It usually ends with too much locking, but not necessarily everywhere it's needed, and very high contention. Instead of solving the design problems themselves, workarounds are then often found for some isolated issue, that makes things worse in other areas, and you end up with something that never really works properly or effectively, that is tweaked "constantly" for new, surprising behavior.

That's why I'm in favor of planning the strategy, and then implement that. It will usually give the best result, and the least amount of surprises. But, much of this has been done already, so it's not really up to me to "design" anything here, still, this is where my mind goes when discussing problems: what is the overall strategy to handle this, is the strategy flawed, and if so how? Is the strategy actually followed? Are there things being done that don't conform to the strategy?

@rkoshak
Copy link
Contributor

rkoshak commented Sep 30, 2025

These are the most commonly used locks
But we are not running in Java. We are running in this hybrid environment. But just because they are a pain to use and used less often doesn't mean Using ReentrantLocks et al doesn't count.

if the context isn't shared, how would two threads get access to the same "JavaScript object"?

In OH the shared cache is one way.

Are there "JavaScript objects" in there, are there any form of global variables available across script engines?

I think yes.

Aren't you saying that you must do something that breaks this isolation, like using timers or invoking a different rule, to trigger such an exception?

Yes, a timer created by the same script would and could trigger the exception without the locks in the add-on preventing that.

Is the "cache.shared" something that lives in the script engine instance?

No, it's implemented by core. JS Scripting will print a warning to the log if you attempt to put something more than a primitive into the shared cache though.

IF they use different script engine instances, which is only the case for SimpleRule implementations. So, there's a lot of "ifs" and "buts" here.

Remember this is a PR to update the docs. The docs represent how OH works right now, not how it could be fixed maybe someday in the future. The forum or a PR on the add-on is a much better place for that.

Right now we have definitive answers as to the behaviors. All managed rules can run in parallel. All rules defined in separate .js files can run in parallel. All rules defined in the same .js file run in sequence.

The end users have no control over whether they are creating a SimpleRule or a "normal" rule. So all that discussion is irrelevant to this PR.

How did you reach that conclusion? This isn't the case, as far as I can understand, for script engines that implement the lock. They will all have to wait for each other, because they share the script engine instance.

See my experiment above showing that two managed JS rules running at the same time, Also see @florian-h05's explanation on the forum thread.

but every ScriptAction or ScriptCondition gets its own ScriptEngine instance

Are the script engine locks new?
I don't think there are any new ones. All the ones we've been talking about were implemented prior to OH 4 I think. They've been around for years and years. I could be wrong about one of them which might be a bit more recent (early 4.x versions) but still implemented at least a year or more ago.

To do that, separating the different concepts/concerns is important.

I don't think anyone is saying that isn't important. But this is a documentation PR. It's not appropriate here because this repo isn't where anything will change if we decide something needs to change. And even if we decide nothing needs to change, we still need to document how it works now. Most of the discussion above was relevant in it helped us understand how OH actually works now so we can make appropriate documentation changes. But we can't do anything about whether it could be done better or not in this repo.

@dilyanpalauzov
Copy link
Contributor Author

and when I repeated the above several times, I think I hit a deadlock : nothing happens.
What immediately intrigues me here is: Why is the thread interrupted? A thread that is interrupted (has been interrupted in the past, so that the interrupted flag is set), will immediately throw InterruptedException if you try to sleep(). But the thread that is trying to run this, shouldn't be interrupted... one would think. You normally interrupt a thread if you want it to stop what it's doing. The interrupted flag must then be reset before giving the thread a new task... but this is normally handled by the thread pools AFAIK - so I'm curious how this came about in the first place.

I guess the thread is interrupted, because a rule, loaded from a .js file, was executing Thread.sleep() and then the js file was changed, so the rule had to be destroyed. The source code of the .js files is above, so you can reproduce the thread interruption. Today I reproduced it too, but I could not reproduce the deadlock, so I think the system was just doing Thread.sleep(30seconds) and there was no deadlock.

I would actually hold off documenting this until things are straightened out. How much of this has changed recently? Are the script engine locks new? If so, is it even certain that this solution is viable - I assume that it's supposed to prevent the "multithreaded exception", but I fear that it will cause much more problems than it solves. Or, has it been this way for some time, and something else changed in 5.1?

It is in practice on hold off. I do not know what has changed, it all started with a discussion of multithreading in the Java223 community add-on: many people have different opinion how multi-threading is done in openHAB, so writing some clarifications would be useful. There is no relation to changes in 5.1.

For CronEvents, maybe this is just one. It depends on the CronService implementation. In the mentioned scenario from @dilyanpalauzov , he is just blocking the Cron processing thread, with the result, that no other crons are triggered.

As demonstrated by other examples, there is no Cron processing thread, that was locked, two cron rules can be executed in parallel, provided they are not created from the same .js file.

@Nadahar
Copy link
Contributor

Nadahar commented Sep 30, 2025

But we are not running in Java. We are running in this hybrid environment. But just because they are a pain to use and used less often doesn't mean Using ReentrantLocks et al doesn't count.

I realize that we're probably ending up quite far out on a branch here, but the whole reason I've been talking about intrinsic locks is that if you use an object that already has locking implemented, you must use the same lock as is implemented. Since this is most often the intrinsic lock, you can't actually (as far as I can tell) acquire that lock. But, I realize when thinking about it that this probably isn't that relevant, it gets "too advanced". If you only want to protect some object, you could absolutely use a Java Lock for this. But, I'm not sure if it would be "deadlock safe", it would depend on the scripting language having a try.. finally statement that can't be cheated/broken out of. But yes, you do have access to some locking this way, that's true.

In OH the shared cache is one way.

Is the OH shared cache something that is implemented in OH, across scripting languages? It could probably be made thread-safe (if it isn't already) in the Java code then, so that whenever you retrieve something from the cache, proper locks are applied and a copy is made. That object could then be manipulated locally without locks, but the cache wouldn't automatically be updated as a consequence. For that to happen, you would have to "store" the object in the cache again. If this is how this already works? That said, I don't know how "JavaScript objects" can be stored in Java, but I guess they are serialized to JSON or something like that?

I think yes.

I would think that global variables would live in the context object, and not in the libs. But, again, I could be wrong. These things are essential to understand what must and must not be protected against concurrent access.

Yes, a timer created by the same script would and could trigger the exception without the locks in the add-on preventing that.

The reason I'm asking is that I'm thinking that if the only ways you can get into this "problem" is via timers or calls to other rules, maybe something could be done to avoid that in core instead. I would imagine, without knowing the details, that it should be possible to make a timer also run in the "rule thread" instead of a new one. Calling other rules might be slightly more complicated if this must be blocking/retrieve a result, but if not (if the script just triggers another rule and then move on), it could probably be solved in a similar way to timers.

No, it's implemented by core. JS Scripting will print a warning to the log if you attempt to put something more than a primitive into the shared cache though.

So you have any idea what it's called internally, to improve my chance to actually find and look at how this is done?

Remember this is a PR to update the docs. The docs represent how OH works right now, not how it could be fixed maybe someday in the future. The forum or a PR on the add-on is a much better place for that.

I'm aware that a lot of this discussion goes beyond the PR. The PR is also closed now, so it's not that big of a deal. But, it would probably be better if the discussion was in a place where it would be easier to find later or for others.

Right now we have definitive answers as to the behaviors. All managed rules can run in parallel. All rules defined in separate .js files can run in parallel. All rules defined in the same .js file run in sequence.

I'm not sure about managed rules, did you ever test with two managed rules? It depends on how the script engine instance is handled.

The end users have no control over whether they are creating a SimpleRule or a "normal" rule. So all that discussion is irrelevant to this PR.

I agree that the end user probably have no idea about that, but if you're about to make assertive statements, and the reality is different for the different types of rules, how do you do that without asserting something that is wrong?

See my experiment above showing that two managed JS rules running at the same time, Also see @florian-h05's explanation on the forum thread.

I never saw that you made two managed rules, is this what you mean?

Given the following rules:

configuration: {}
triggers:
  - id: "1"
    configuration:
      cronExpression: "* * * * * ? *"
    type: timer.GenericCronTrigger
conditions: []
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: console.info('Rule is running!');
    type: script.ScriptAction

Because, frankly, you only showed one of the rules, and just the "action part" of the other one - which is why I assumed that the second rule was a SimpleRule.

But, you're right that if it's like @florian-h05 says that a new script engine instance is created for every execution (of "normal" rules), they will be able to run independently (but it sounds like a very expensive/slow way to do it).

I don't think anyone is saying that isn't important. But this is a documentation PR. It's not appropriate here because this repo isn't where anything will change if we decide something needs to change. And even if we decide nothing needs to change, we still need to document how it works now. Most of the discussion above was relevant in it helped us understand how OH actually works now so we can make appropriate documentation changes. But we can't do anything about whether it could be done better or not in this repo.

Fair enough. Parts of me screams "leave this one alone", it will be too much frustration to try to get to the bottom of or make improvements to. But, if I'm unable to listen to myself, where do you think this should be discussed? On the forum, or in a "dedicated issue"?

so the rule had to be destroyed.

Even if the rule was changed and the current execution was attempted terminated, the execution of the new version shouldn't execute from a thread in an interrupted state. But, perhaps I misunderstood what was happening. I'm still somewhat curious where the interrupt came from though.

The source code of the .js files is above, so you can reproduce the thread interruption.

Sadly I can't, because GraalVM won't work in my dev environment, and there has been zero interest in figuring out why this doesn't work. So, to run the script I would have to set up a "regular OH instance", but that would leave me without all my dev tools, so it wouldn't let me examine what's actually going on.

It is in practice on hold off. I do not know what has changed, it all started with a discussion of multithreading in the Java223 community add-on: many people have different opinion how multi-threading is done in openHAB, so writing some clarifications would be useful. There is no relation to changes in 5.1.

👍 All the more important to get what is stated absolutely correct.

@dilyanpalauzov
Copy link
Contributor Author

Is the OH shared cache something that is implemented in OH, across scripting languages? It could probably be made thread-safe (if it isn't already) in the Java code then, so that whenever you retrieve something from the cache, proper locks are applied and a copy is made.

I have not used it, but I think it is implemented in OH-core. Converting it to thread-safe thing ... will introduce more locks, and more mutexes means less parallelism.

Even if the rule was changed and the current execution was attempted terminated, the execution of the new version shouldn't execute from a thread in an interrupted state.

I do not know more, why the thread was interrupted. The only thing I could do is describe how to reproduce this interrupted sleep.

@Nadahar
Copy link
Contributor

Nadahar commented Sep 30, 2025

I have not used it, but I think it is implemented in OH-core. Converting it to thread-safe thing ... will introduce more locks, and more mutexes means less parallelism.

No. The whole point of making the cache thread-safe was so that you could drop the script engine lock, which would increase parallelism. And, I wasn't thinking of a cache that "locked" objects, but one that duplicated them on retrieval - which should only hold a lock for a very short time. But, depending on how the cache is implemented, this might require major changes in how it's used and thus be nonviable.

@Nadahar
Copy link
Contributor

Nadahar commented Sep 30, 2025

I'll add that to see what threads does what, it's useful to add the %thread parameter to the log pattern. An example of such a pattern can be:

		<Console name="STDOUT">
			<PatternLayout pattern="%d{HH:mm:ss.SSS} [%-5.5p] (%-20.20thread) [%-36.36c] - %m%n"/>
		</Console>

The above will print the 20 last characters of the thread name as a part of each log line.

@rkoshak
Copy link
Contributor

rkoshak commented Oct 1, 2025

Is the OH shared cache something that is implemented in OH, across scripting languages?

Yes. In fact, you can access Objects in the cache from different languages. If I put something in it at key "foo" using JS, I could pull it out in a Python script or Rules DSL. It's only JS (as far as I'm aware) that has a restriction that the contexts need to be locked. And any changes made to the cache will impact all the automation add-ons.

It could probably be made thread-safe (if it isn't already) in the Java code then, so that whenever you retrieve something from the cache, proper locks are applied and a copy is made. That object could then be manipulated locally without locks, but the cache wouldn't automatically be updated as a consequence. For that to happen, you would have to "store" the object in the cache again. If this is how this already works? That said, I don't know how "JavaScript objects" can be stored in Java, but I guess they are serialized to JSON or something like that?

I created an issue to implement exactly that. Maintainers decided that it was a misuse of the shared cache to put Objects in it like that so they implemented the warning instead.

I would think that global variables would live in the context object, and not in the libs. But, again, I could be wrong. These things are essential to understand what must and must not be protected against concurrent access.

But if they are written with the assumption that there will be one instance per ScriptEngine that already happens. so there would be no reason to do anything special.

So you have any idea what it's called internally, to improve my chance to actually find and look at how this is done?

I do not, but as I said this idea has already been rejected. Based on how it's used I suspect it's backed by a Map of some sort. But I think it's a Registry (like ItemRegistry).

I'm not sure about managed rules, did you ever test with two managed rules? It depends on how the script engine instance is handled.

Yes of course. That was the first test I did. And I have already said that I did at least twice.

Once again, I tested with two managed rules, both rules in the same .js file , and each rule in it's own .js file.

And @florian-h05 explicitly stated how the script engine is handled for managed rules. Each script action and each script condition gets its own ScriptEngine.

I agree that the end user probably have no idea about that, but if you're about to make assertive statements, and the reality is different for the different types of rules, how do you do that without asserting something that is wrong?

Because I tested every combination of ways to create a rule available right now in JS. I can assert without reservation how JS rules work right now as a result.

When and if a new way to create rules in JS is implemented, these docs can and should be revisited. In the mean time, we have all the information we need from a JS perspective at least, to understand the current behavior.

Because, frankly, you only showed one of the rules, and just the "action part" of the other one - which is why I assumed that the second rule was a SimpleRule.

I explained that above as well. The second one was a MainUI -> Settings -> Script and there is no "Code" tab for Scripts defined that way. But a Script in that context is just a managed rule that consists of a single Script Action, no triggers and no conditions and is tagged with "Script". MainUI presents these two differently. It would be nice if a Script did have a code tab. 🤷‍♂️

where do you think this should be discussed? On the forum, or in a "dedicated issue"?

Either place. Probably the forum first because assuming some changes are to be made, it's not clear whether they would be done in core or the automation add-on(s) or both.

@dilyanpalauzov
Copy link
Contributor Author

Is the OH shared cache something that is implemented in OH, across scripting languages?

In my memories it was OH JavaScripting, which invented the cache mechanism, and later this mechanism was implemented in openhab-core and removed from https://github.com/openhab/openhab-js + org.openhab.automation.jsscripting. You can look in the history of the openhab-js and in particular openhab/openhab-addons@438552d485357d9060862, to see how the cache was implemented in JavaScript and, if the cache is implemented in JavaScript, you should have been able to put inside the old implementation JavaScript objects. (I have never used it.)

How the cache works for all rule (script) engines is described currently at https://www.openhab.org/docs/configuration/jsr223.html#cache-preset.

@florian-h05
Copy link
Contributor

JavaScript objects can be passed to Java, GraalJS takes care of conversion.
For example a JS object consisting of simple key value pairs is converted to a Map. That behaviour is described somewhere in the GraalJS docs.

Though I don‘t think there are many complex types that end up in the shared cache, as language interoperability only really works for primitive types.
The only exception being commonly stored into the shared cache are openHAB timers. When sharing them from JS Scripting, accessing them from another thread at the same time cause a illegal multithread access requested for context … exception. Seems like there is a reference from the Java timer object to the context, at least if the timer is created through that script. But that reference might be something internal to Graal …

@openhab-bot
Copy link
Collaborator

This pull request has been mentioned on openHAB Community. There might be relevant details there:

https://community.openhab.org/t/rules-and-concurrency/166577/2

@openhab-bot
Copy link
Collaborator

This pull request has been mentioned on openHAB Community. There might be relevant details there:

https://community.openhab.org/t/rules-and-concurrency/166577/33

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants