Skip to content

[jsscripting] Clarify that rules from one automation/js/.js file cannot run in parallel#19410

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

[jsscripting] Clarify that rules from one automation/js/.js file cannot run in parallel#19410
dilyanpalauzov wants to merge 1 commit intoopenhab:mainfrom
dilyanpalauzov:jsscripting_readme_noparallel

Conversation

@dilyanpalauzov
Copy link
Contributor

This is follow up of the discussions at openhab/openhab-docs#2565 . The conclusion is that if many rules are installed from many files in automation/js/.js then these rules can run in parallel. But when installing the same rules from a single automation/js/.js file, then at any time at most one of the rules can be executed.

For me it is still unclear, how to test, if transformations can run in parallel and how to see on this differences, when using e.g. JS vs Groovy.

@dilyanpalauzov
Copy link
Contributor Author

This change is incomplete. If I create at the same time a file automation/js/a.js

console.info("START2 " + Java.type('java.lang.Thread').currentThread().getId())
Java.type('java.lang.Thread').sleep(30000)
console.info("END2")

and automation/js/b.js

console.info("START1 " + Java.type('java.lang.Thread').currentThread().getId())
Java.type('java.lang.Thread').sleep(30000)
console.info("END1")

openhab.log contains:

2025-09-30 14:30:06.370 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/a.js'
2025-09-30 14:30:06.413 [INFO ] [.openhab.automation.script.file.a.js] - START2 799
2025-09-30 14:30:36.416 [INFO ] [.openhab.automation.script.file.a.js] - END2
2025-09-30 14:30:36.418 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/b.js'
2025-09-30 14:30:36.445 [INFO ] [.openhab.automation.script.file.b.js] - START1 799
2025-09-30 14:31:06.447 [INFO ] [.openhab.automation.script.file.b.js] - END1

So all /js/*.js files are executed in a single thread, but if they create rules, the rules get their own thread.

@dilyanpalauzov dilyanpalauzov force-pushed the jsscripting_readme_noparallel branch from 1510363 to ca2d006 Compare September 30, 2025 13:37
@dilyanpalauzov
Copy link
Contributor Author

So all /js/*.js files are executed in a single thread, but if they create rules, the rules get their own thread.

… their own threads, plural. (one thread per rule, or each rule uses a thread from a pool).

@dilyanpalauzov dilyanpalauzov force-pushed the jsscripting_readme_noparallel branch from ca2d006 to d134386 Compare September 30, 2025 14:27
@dilyanpalauzov dilyanpalauzov changed the title [jsscripting] Clarify that rules one automation/js/.js file cannot run in parallel [jsscripting] Clarify that rules from one automation/js/.js file cannot run in parallel Sep 30, 2025
@Nadahar
Copy link
Contributor

Nadahar commented Sep 30, 2025

or each rule uses a thread from a pool

No - each rule has a dedicated thread.

@dilyanpalauzov
Copy link
Contributor Author

No - each thread has a dedicated thread.

I guess you mean each rule has a dedicated thread.

Copy link
Contributor

@rkoshak rkoshak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions on the additions.


For [file based rules](#file-based-rules), scripts will be loaded from `automation/js` in the user configuration directory.
When many rules are installed from a single `.js` file in this directory, at most one of these rules runs at a time.
Irrespective of how JavaScript installs rules, each rule runs in a separate thread.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend removing this line. All rules have their own thread and that's implemented and controlled by core. It's not JS specific.

Furthermore, it's not meaningful nor useful information for JS Scripting users. They can't do anything with this information. There is nothing a user can or should do with this information they are not already doing. It just raises questions.

For [file based rules](#file-based-rules), scripts will be loaded from `automation/js` in the user configuration directory.
When many rules are installed from a single `.js` file in this directory, at most one of these rules runs at a time.
Irrespective of how JavaScript installs rules, each rule runs in a separate thread.
All `automation/js/.js` files are executed sequentially in a single thread.
Copy link
Contributor

@rkoshak rkoshak Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this means. Are you referring to how the files themselves are loaded or restating the first sentence in another way? I assume the former. Maybe:

All automation/js/*.js files are loaded and executed sequentially and alphabetically in a single thread.

The "in a single thread" is kind of redundant, but I leave it up to you whether to keep it.

Conceptually, most end users are not really aware that these files are executed to create the rules (it should be obvious but for many it's not or at least they don't use that word). By adding "loaded" that should catch those users who think of these files being loaded and don't think about them being executed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All automation/js/.js files are executed sequentially in a single thread. isn't true.

Each rule is run by a dedicated thread, regardless of how the .js files are organized, how many rules are in the same file etc. But, threads aren't the only thing that matters here.

There are also script engine instances and context objects to worry about. Script engine instances now have a global lock that only lets one thread use any given instance at a time. In addition, for SimpleRules, the Action execution is guarded by a lock to protect the context object that is shared amongst all SimpleRules that exist in the same file.

I know that you don't like to care about whether it's a SimpleRule or not, but they behave fundamentally different. For non-SimpleRules, you can have as many as you want in the same *.js file, and they will be independent of each other. For SimpleRules that's not the case, because they implicitly end up sharing the context, which is the context in which the original "build script" runs.

@dilyanpalauzov dilyanpalauzov force-pushed the jsscripting_readme_noparallel branch from d134386 to 980c865 Compare September 30, 2025 17:04
@dilyanpalauzov
Copy link
Contributor Author

The purpose of

Irrespective of how JavaScript installs rules, each rule runs in a separate thread.
All automation/js/.js files are executed sequentially in a single thread.

was to create a contrast: one thread for all js/.js files, but separate thread for each rule. That is: all rules, started from the single thread, implicitly run in many threads.

By adding "loaded" that should catch those users who think of these files being loaded and don't think about them being executed.

Adding “loaded” where? Suggest a sentence.

I have deleted the Irrespective… line.

“All automation/js/.js files are executed sequentially in a single thread.” isn't true.
Each rule is run by a dedicated thread, regardless of how the .js files are organized, how many rules are in the same file etc. But, threads aren't the only thing that matters here.

How do you interpret #19410 (comment) ? My sentence does not say anything about rules, it speaks only about the automation/js/.js files.

The openhab-js utility creates SimpleRule, so it matters here.

@Nadahar
Copy link
Contributor

Nadahar commented Sep 30, 2025

was to create a contrast: one thread for all js/.js files, but separate thread for each rule. That is: all rules, started from the single thread, implicitly run in many threads.

Ah, I see what you mean now. It's easy to "read it wrong" though, at least for me 😉

How do you interpret #19410 (comment) ? My sentence does not say anything about rules, it speaks only about the automation/js/.js files.

The openhab-js utility creates SimpleRule, so it matters here.

Yes, it's the same misunderstanding as above. The file (aka rule-creation) is executed by a single thread. But the resulting rules aren't. I think it's very difficult to describe this precisely in a way that isn't easy to misunderstand.

The way I read "All automation/js/.js files are executed sequentially in a single thread." was that all rules from that file by run by a single thread, which isn't the case. But, they still can't run concurrently because they share the context for the Action (execute()) section, so locks are in place to prevent them from doing so. They are still run by different threads, but they have to wait for each other.

In a sense, it might be better not to mention threads at all in the documentation, and just say what can and cannot run in parallel - to reduce potential confusion.

@rkoshak
Copy link
Contributor

rkoshak commented Oct 1, 2025

Adding “loaded” where? Suggest a sentence.

I did

All automation/js/*.js files are loaded and executed sequentially and alphabetically in a single thread.

Though based on

In a sense, it might be better not to mention threads at all in the documentation, and just say what can and cannot run in parallel - to reduce potential confusion.

I might reword it further to:

All automation/js/*.js files are loaded and executed sequentially and alphabetically.

The full change would therefor be:

When many rules are installed from a single .js file in this directory, at most one of these rules runs at a time.
All automation/js/.js files are loaded and executed sequentially and alphabetically.

As I said above, a lot of users will be confused because they see the rules being "loaded" from these files. They do not understand that they are "executed" in order to create the rules. And I agree, talking about threads here just brings up a lot of details that end users can't do anything with.

@dilyanpalauzov dilyanpalauzov force-pushed the jsscripting_readme_noparallel branch from 980c865 to bd2b3d7 Compare October 1, 2025 15:22
@dilyanpalauzov
Copy link
Contributor Author

I changed it to:

When many rules are installed from a single .js file in this directory, at most one of these rules runs at a time.
All automation/js/.js files are loaded and executed sequentially and alphabetically.

But the previous sentence is

For file based rules, scripts will be loaded from automation/js in the user configuration directory.

So it creates a discrepancy between loading a script, without executing it, and executing a script, without loading it. In any case, as it is, is an improvement.

Copy link
Contributor

@florian-h05 florian-h05 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please open this PR in openhab-js, as the README in the add-ons repo is mainly a copy from there.

### Paths

For [file based rules](#file-based-rules), scripts will be loaded from `automation/js` in the user configuration directory.
When many rules are installed from a single `.js` file in this directory, at most one of these rules runs at a time.
Copy link
Contributor

@florian-h05 florian-h05 Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this sentence into the file based rules section, and rephrase it a bit.

When multiple rules are defined in a single script file, at most one of these rules can run at a time.

And add a note:

When writing rules that extensively query persistence or wait for other I/O, it can make sense to split them across multiple files to allow parallel execution of those.


For [file based rules](#file-based-rules), scripts will be loaded from `automation/js` in the user configuration directory.
When many rules are installed from a single `.js` file in this directory, at most one of these rules runs at a time.
All `automation/js/.js` files are loaded and executed sequentially and alphabetically.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be the same for everything in the automation folder, as file loading is handled by core and not the add-on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the other add-ons do not implement Lock when executing eval() in the files. My understanding is that in consequence when many files are present, these are evaluated/loaded/executed in parallel.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, it just doesn't work to "pretend" that SimpleRule isn't a part of the equation. Only SimpleRules have this "one per file" limitation, because they share the context. You can create as many non-SimpleRules as you want from one file, and they will run independently (with different ScriptEngines).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide an example, where one .js file installs two rules, which are not SimpleRule, and these rules can work in parallel? My understanding is that JS can create indirectly as many threads for handling javascript code, asit wants, but only one of the thread is executed per ScriptEngine insance.

Also, suggest text for inclusion in README, which you think is right, then the proposed addition can be discussed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can create as many non-SimpleRules as you want from one file, and they will run independently (with different ScriptEngines).

Again, you can't create anything but a SimpleRule with JS. We are not "pretending". SimpleRule is the only part of the equation for JS.

And even then, the only way "normal" rules, if it were possible to create them, could work is if each rule in the file is wholly self-contained, which is not a guarantee. Any reference to any variable not defined in the rule's action itself (e.g. defined at the top of the file, such as the import of items from openhab) is referencing another context (i.e. the context that created the rule in the first place). Do that at the same time from two different rules and you get an exception.

So called "normal" rules are not a Thing for JS. Only SimpleRule is possible at this time. It would be confusing at best to bring it up in the docs for JS something that isn't even possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, you can't create anything but a SimpleRule with JS. We are not "pretending". SimpleRule is the only part of the equation for JS.

You can create "normal" rules from JavaScript. It took my quite some time to figure out a syntax that works, because I don't use JS scripting and before the documentation wasn't exactly "helpful" for anything but SimpleRule/JSRule (which is the same thing AFAICU), but here it is:

Object.assign(this, require('@runtime'));
Object.assign(this, require('@runtime/RuleSupport'));

const TriggerBuilder = Java.type('org.openhab.core.automation.util.TriggerBuilder');
const ConditionBuiilder = Java.type('org.openhab.core.automation.util.ConditionBuilder');
const ActionBuilder = Java.type('org.openhab.core.automation.util.ActionBuilder');
const RuleBuilder = Java.type('org.openhab.core.automation.util.RuleBuilder');

let rule = RuleBuilder.create('complex_rule').withName('Complex Rule')
    .withDescription('A complex rule example')
    .withTriggers(
        [TriggerBuilder.create()
            .withId('aTimerTrigger').withTypeUID('timer.GenericCronTrigger')
            .withConfiguration(new Configuration({ 'cronExpression': '0/10 * * * * ? *' })).build(),
        TriggerBuilder.create()
            .withId('systemStart').withTypeUID('core.SystemStartlevelTrigger')
            .withConfiguration(new Configuration({ 'startlevel': 100 })).build(),
        TriggerBuilder.create()
            .withId('windowOpened').withTypeUID('core.ItemStateChangeTrigger')
            .withConfiguration(new Configuration({ 'itemName': 'OpenWindowActive', 'state': 'ON' })).build()
    ]).withConditions(
        [ConditionBuiilder.create()
            .withId('timeRange').withTypeUID('core.TimeOfDayCondition')
            .withConfiguration(new Configuration({ 'startTime': '14:00', 'endTime': '16:23' })).build(),
        ConditionBuiilder.create()
            .withId('ephemeris').withTypeUID('ephemeris.NotHolidayCondition').build()
    ]).withActions(
        [ActionBuilder.create()
            .withId('preventHeating').withTypeUID('core.ItemCommandAction')
            .withConfiguration(new Configuration({ 'itemName': 'LimitedHeatingPower', 'command': '0' })).build(),
        ActionBuilder.create()
            .withId('announce').withTypeUID('script.ScriptAction')
            .withConfiguration(new Configuration({ 'type': 'application/x-javascript', 'script': 'console.info("The window is open, heating turned off");' })).build()
    ])
    .build();

automationManager.addRule(rule);
console.info('Complex Rule added');

This is a valid, functioning (if you have the Items) Rule that is not limited by the limitations of SimpleRule. It could have been much easier to write such rules if the "helper lib" was written to utilize it, but it isn't, and neither is the documentation, so it's hard to figure out. That doesn't mean that it "can't be done". It's completely within the realms of what the scripting add-on can do, it's just that you must more or less do it without any "help".

And even then, the only way "normal" rules, if it were possible to create them, could work is if each rule in the file is wholly self-contained, which is not a guarantee. Any reference to any variable not defined in the rule's action itself (e.g. defined at the top of the file, such as the import of items from openhab) is referencing another context (i.e. the context that created the rule in the first place). Do that at the same time from two different rules and you get an exception.

I understand what you mean, but I don't know if it's possible to write "normal" rules like that. You can supply the script as a text without a problem, but if you try to make "separate scripts" from functions, you'll meet some obstacles. I don't know if that can be done or not, but it's obviously to get around this that SimpleRule was created. But, it also comes with a lot of drawbacks and limitations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide an example, where one .js file installs two rules, which are not SimpleRule, and these rules can work in parallel? My understanding is that JS can create indirectly as many threads for handling javascript code, asit wants, but only one of the thread is executed per ScriptEngine insance.

Yes, create one more rule in the same way as the first, in the example in my previous comment.

Also, suggest text for inclusion in README, which you think is right, then the proposed addition can be discussed.

That's hard, because Rule and SimpleRule are blurred together so many other places. Trying to give this a name without a broader explanation would be hard in this context. So, I'd think that the most important thing here is to not establish "facts" that aren't in fact, facts. On "easy way out" could be to just state what is being stated, but use JSRule instead of just applying it generally, e.g.:
"All JSRules in automation/js/.js files are loaded and executed sequentially and alphabetically."

PS! Are they actually executed alphabetically? That sounds strange, locks are usually "random", they will just let some waiting thread through when the lock is released, there is no system to it.

@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/1

@dilyanpalauzov
Copy link
Contributor Author

Are they actually executed alphabetically?

I create automation/js/a.js with content console.log('a'), automation/js/b.js with content console.log('b') and automation/js/c.js with content console.log('c'). When I start OpenHAB it logs

2025-10-02 07:23:38.258 [INFO ] [e.automation.internal.RuleEngineImpl] - Rule engine started.
2025-10-02 07:23:38.464 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/a.js'
2025-10-02 07:23:48.534 [INFO ] [.openhab.automation.script.file.a.js] - a
2025-10-02 07:23:48.568 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/b.js'
2025-10-02 07:23:48.652 [INFO ] [.openhab.automation.script.file.b.js] - b
2025-10-02 07:23:48.655 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/c.js'
2025-10-02 07:23:48.747 [INFO ] [.openhab.automation.script.file.c.js] - c

Are files under automation/ loaded sequentially only by JS, differently compared to Groovy?

With

$ cat /etc/openhab/automation/jsr223/m.groovy
org.slf4j.LoggerFactory.getLogger("G").warn("M1 " + Thread.currentThread().getId())
Thread.sleep(3000)
org.slf4j.LoggerFactory.getLogger("G").warn("M2")
$ cat /etc/openhab/automation/jsr223/n.groovy
org.slf4j.LoggerFactory.getLogger("G").warn("N1 " + Thread.currentThread().getId())
Thread.sleep(3000)
org.slf4j.LoggerFactory.getLogger("G").warn("N2")

starting openHAB prints

2025-10-02 07:42:48.803 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/jsr223/m.groovy'
2025-10-02 07:42:53.584 [WARN ] [G                                   ] - M1 755
2025-10-02 07:42:56.666 [WARN ] [G                                   ] - M2
2025-10-02 07:42:56.712 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/jsr223/n.groovy'
2025-10-02 07:42:56.809 [WARN ] [G                                   ] - N1 755
2025-10-02 07:42:59.818 [WARN ] [G                                   ] - N2

So sequential loading everything in a single thread is not specific to JavaScript/Groovy.

All automation/js/.js files are loaded and executed sequentially and alphabetically.
This should be the same for everything in the automation folder, as file loading is handled by core and not the add-on.

Yes, it is the same as in Groovy.

What example demostrates, that the locks around eval() for GraalVM (Python and JavaScript) are a bottleneck, compared to Groovy or JRuby?

MIME Type application/x-javascript

With OH 5.1 build 4836 I tried the supplied example:

Object.assign(this, require('@runtime'), require('@runtime/RuleSupport'))

automationManager.addRule(Java.type('org.openhab.core.automation.util.RuleBuilder').create('U').withName('U')
    .withTriggers(
        [Java.type('org.openhab.core.automation.util.TriggerBuilder').create().withId('i').withTypeUID('core.ItemStateChangeTrigger')
            .withConfiguration(new Configuration({ 'itemName': 'r' })).build()
    ]).withActions(
        [Java.type('org.openhab.core.automation.util.ActionBuilder').create().withId('a').withTypeUID('script.ScriptAction')
         .withConfiguration(new Configuration({ 'type': 'application/x-javascript', 'script': 'console.log("R")' })).build()
    ])
    .build()
)

When triggered, openHAB logs:

2025-10-02 08:15:14.260 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - ScriptEngine for language 'application/x-javascript' could not be found for identifier: 7f97433d-73d9-4acc-910b-610662fd810c

Can you explain how have you got new Configuration({ 'type': 'application/x-javascript', 'script': '…' }) running?

Creating several rules from a single .js file, which are not SimpleRule/JSRule

Object.assign(this, require('@runtime'), require('@runtime/RuleSupport'))

const TriggerBuilder = Java.type('org.openhab.core.automation.util.TriggerBuilder')
const ActionBuilder = Java.type('org.openhab.core.automation.util.ActionBuilder')
const RuleBuilder = Java.type('org.openhab.core.automation.util.RuleBuilder')

automationManager.addRule(RuleBuilder.create('ruleA').withName('Rule A')
    .withTriggers(
        [TriggerBuilder.create().withId('i').withTypeUID('core.ItemStateChangeTrigger')
            .withConfiguration(new Configuration({ 'itemName': 'r9_power' })).build()
    ]).withActions(
        [ActionBuilder.create().withId('a').withTypeUID('script.ScriptAction')
         .withConfiguration(new Configuration({ 'type': 'graaljs', 'script': 'console.info("A START");Java.type("java.lang.Thread").sleep(5000);console.info("A END")' })).build()
    ])
    .build()
)

automationManager.addRule(RuleBuilder.create('ruleB').withName('Rule B')
    .withTriggers(
        [TriggerBuilder.create().withId('i').withTypeUID('core.ItemStateChangeTrigger')
            .withConfiguration(new Configuration({ 'itemName': 'r9_power' })).build()
    ]).withActions(
        [ActionBuilder.create().withId('a').withTypeUID('script.ScriptAction')
         .withConfiguration(new Configuration({ 'type': 'application/javascript;version=ECMAScript-2021', 'script': 'console.info("B START");Java.type("java.lang.Thread").sleep(5000);console.info("B END")' })).build()
    ])
    .build()
)

automationManager.addRule(RuleBuilder.create('ruleC').withName('Rule C')
    .withTriggers(
        [TriggerBuilder.create().withId('i').withTypeUID('core.ItemStateChangeTrigger')
            .withConfiguration(new Configuration({ 'itemName': 'r9_power' })).build()
    ]).withActions(
        [ActionBuilder.create().withId('a').withTypeUID('script.ScriptAction')
         .withConfiguration(new Configuration({ 'type': 'application/javascript', 'script': 'console.info("C START");Java.type("java.lang.Thread").sleep(5000);console.info("C END")' })).build()
    ])
    .build()
)

When triggered, OpenHAB logs:

2025-10-02 11:03:15.832 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/complex.js'
2025-10-02 11:03:33.066 [INFO ] [ab.automation.script.file.complex.js] - Ready
2025-10-02 11:08:45.574 [INFO ] [g.openhab.automation.script.ui.ruleA] - A START
2025-10-02 11:08:45.574 [INFO ] [g.openhab.automation.script.ui.ruleC] - C START
2025-10-02 11:08:45.574 [INFO ] [g.openhab.automation.script.ui.ruleB] - B START
2025-10-02 11:08:50.589 [INFO ] [g.openhab.automation.script.ui.ruleC] - C END
2025-10-02 11:08:50.589 [INFO ] [g.openhab.automation.script.ui.ruleA] - A END
2025-10-02 11:08:50.589 [INFO ] [g.openhab.automation.script.ui.ruleB] - B END
2025-10-02 11:09:00.146 [INFO ] [g.openhab.automation.script.ui.ruleA] - A START
2025-10-02 11:09:00.152 [INFO ] [g.openhab.automation.script.ui.ruleC] - C START
2025-10-02 11:09:00.153 [INFO ] [g.openhab.automation.script.ui.ruleB] - B START
2025-10-02 11:09:05.151 [INFO ] [g.openhab.automation.script.ui.ruleA] - A END
2025-10-02 11:09:05.155 [INFO ] [g.openhab.automation.script.ui.ruleC] - C END
2025-10-02 11:09:05.157 [INFO ] [g.openhab.automation.script.ui.ruleB] - B END

So many rules from the same .js file, which are not JSRule/SimpleRule can run in parallel.

About this example:

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

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

it looks like it utilizes openhab-js/RuleBuilder. So

const { rules } = require('openhab')

rules.when().item('r9_power').changed()
     .then( () => {
       console.info('A START')
       Java.type('java.lang.Thread').sleep(3000)
       console.info('A END')
     })
     .build('R changed (A)')

rules.when().item('r9_power').changed()
     .then( () => {
       console.info('B START')
       Java.type('java.lang.Thread').sleep(3000)
       console.info('B END')
     })
     .build('R changed (B)')

produces

2025-10-02 13:19:30.472 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/js/builder.js'
2025-10-02 13:19:33.007 [INFO ] [.openhab.automation.openhab-js.rules] - Adding rule: R changed (A)
2025-10-02 13:19:33.150 [INFO ] [.openhab.automation.openhab-js.rules] - Adding rule: R changed (B)
2025-10-02 13:19:48.148 [INFO ] [ab.automation.script.file.builder.js] - A START
2025-10-02 13:19:51.152 [INFO ] [ab.automation.script.file.builder.js] - A END
2025-10-02 13:19:51.161 [INFO ] [ab.automation.script.file.builder.js] - B START
2025-10-02 13:19:54.166 [INFO ] [ab.automation.script.file.builder.js] - B END
2025-10-02 13:19:54.172 [INFO ] [ab.automation.script.file.builder.js] - A START
2025-10-02 13:19:57.174 [INFO ] [ab.automation.script.file.builder.js] - A END
2025-10-02 13:19:57.180 [INFO ] [ab.automation.script.file.builder.js] - B START
2025-10-02 13:20:00.182 [INFO ] [ab.automation.script.file.builder.js] - B END
2025-10-02 13:20:03.971 [INFO ] [ab.automation.script.file.builder.js] - A START
2025-10-02 13:20:06.973 [INFO ] [ab.automation.script.file.builder.js] - A END
2025-10-02 13:20:06.978 [INFO ] [ab.automation.script.file.builder.js] - B START
2025-10-02 13:20:09.981 [INFO ] [ab.automation.script.file.builder.js] - B END

Here no SimpleRule is involved, but the rules created by openhab-js/RuleBuilder are executed sequentially under JavaScript(GraalVM). openhab-js/RuleBuilder does not seem to allow setting action for a different mime-type type, thus different JSR223 language. And openhab-js seems not to import anyhow interface Rule or claas RuleBuilder from openhab-core:

$ git grep -i java.type src|grep rule
src/rules/rules.js: const SimpleRule = Java.extend(Java.type('org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule'));

For this reason I will reword, stating only JSRule and openhab-js’ Rule Builder.

When writing rules that extensively query persistence or wait for other I/O, it can make sense to split them across multiple files to allow parallel execution of those.

As “extensively” is subjective, I have removed this word.

Please open this PR in openhab-js, as the README in the add-ons repo is mainly a copy from there.

openhab/openhab-js#479

@dilyanpalauzov dilyanpalauzov force-pushed the jsscripting_readme_noparallel branch from bd2b3d7 to d102a71 Compare October 2, 2025 10:50
@dilyanpalauzov dilyanpalauzov force-pushed the jsscripting_readme_noparallel branch from d102a71 to e262d21 Compare October 2, 2025 13:23
@Nadahar
Copy link
Contributor

Nadahar commented Oct 2, 2025

I create automation/js/a.js with content console.log('a'), automation/js/b.js with content console.log('b') and automation/js/c.js with content console.log('c'). When I start OpenHAB it logs

I thought you were referring to the sequence in which individual rules within the same JS file were being executed. That, given that they share the same lock, will be random I think (if they are all triggered at the same time). So, I misunderstood what you meant.

When it comes to the order in which the files are parsed, that is probably likely to be alphabetical. But, I'm pretty sure that this isn't in any way enforced by OH, and is probably OS dependent. What happens in reality is that OH asks the OS for a list of files in a certain folder, and then goes on to process them in the order they are presented in the list. The OS will most likely return the list in whatever is it's "native order", i.e. the same order that ls/dir will return results by default. But, OH doesn't do anything to make this order alphabetical, so I don't think that should be documented as any sort of "rule".

Actually, when looking at the code in AbstractScriptFileWatcher, I'm not sure how to interpret this. Sorting is applied here, but to me, it looks like it is applied to Optional instances, which as far as I understand don't have "a natural order", and I'm thus not sure if this sorting actually does anything:

    private CompletableFuture<Void> addFiles(Collection<Path> files) {
        return CompletableFuture.allOf(files.stream().map(this::getScriptFileReference).flatMap(Optional::stream)
                .sorted().map(this::addScriptFileReference).toArray(CompletableFuture<?>[]::new));
    }

The file watcher will pick up the changed files and hand them off to different parts of the system, to the scripting add-on in this case. The file watcher itself does this in sequence from a single thread. That doesn't mean that the further processing of those files is done in a single thread.

However, when looking in AbstractScriptFileWatcher, a thread pool is used, but the pool only has one thread:

    protected ScheduledExecutorService getScheduler() {
        return Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("scriptwatcher"));
    }

This method can be overridden by subclasses, so that doesn't have to be the case for all add-ons that use AbstractScriptFileWatcher, and the add-on don't have to use AbstractScriptFileWatcher at all, so this behavior isn't "mandated" even though all current implementations might do it this way.

As far the rationale for making the choices that have been made, I can't answer. I can only speculate that there have been issues loading the scripts in parallel, and that making it single-threaded has been "easier" than to actually figure out how to solve the problems.

When triggered, openHAB logs:

2025-10-02 08:15:14.260 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - ScriptEngine for language 'application/x-javascript' could not be found for identifier: 7f97433d-73d9-4acc-910b-610662fd810c

Can you explain how have you got new Configuration({ 'type': 'application/x-javascript', 'script': '…' }) running?

JavaScript syntax and import rules really isn't "my area of expertise". Add JSR223 and access to Java objects into the mix, and I'm reduced to trial and error. So, I can't really say why you get the error you get. But, if you paste the script I made, it should work, and then you can tweak that until it "breaks" to maybe see what causes the error:

Object.assign(this, require('@runtime'));
Object.assign(this, require('@runtime/RuleSupport'));

const TriggerBuilder = Java.type('org.openhab.core.automation.util.TriggerBuilder');
const ConditionBuiilder = Java.type('org.openhab.core.automation.util.ConditionBuilder');
const ActionBuilder = Java.type('org.openhab.core.automation.util.ActionBuilder');
const RuleBuilder = Java.type('org.openhab.core.automation.util.RuleBuilder');

let rule = RuleBuilder.create('complex_rule').withName('Complex Rule')
    .withDescription('A complex rule example')
    .withTriggers(
        [TriggerBuilder.create()
            .withId('aTimerTrigger').withTypeUID('timer.GenericCronTrigger')
            .withConfiguration(new Configuration({ 'cronExpression': '0/10 * * * * ? *' })).build(),
        TriggerBuilder.create()
            .withId('systemStart').withTypeUID('core.SystemStartlevelTrigger')
            .withConfiguration(new Configuration({ 'startlevel': 100 })).build(),
        TriggerBuilder.create()
            .withId('windowOpened').withTypeUID('core.ItemStateChangeTrigger')
            .withConfiguration(new Configuration({ 'itemName': 'OpenWindowActive', 'state': 'ON' })).build()
    ]).withConditions(
        [ConditionBuiilder.create()
            .withId('timeRange').withTypeUID('core.TimeOfDayCondition')
            .withConfiguration(new Configuration({ 'startTime': '14:00', 'endTime': '16:23' })).build(),
        ConditionBuiilder.create()
            .withId('ephemeris').withTypeUID('ephemeris.NotHolidayCondition').build()
    ]).withActions(
        [ActionBuilder.create()
            .withId('preventHeating').withTypeUID('core.ItemCommandAction')
            .withConfiguration(new Configuration({ 'itemName': 'LimitedHeatingPower', 'command': '0' })).build(),
        ActionBuilder.create()
            .withId('announce').withTypeUID('script.ScriptAction')
            .withConfiguration(new Configuration({ 'type': 'application/x-javascript', 'script': 'console.info("The window is open, heating turned off");' })).build()
    ])
    .build();

automationManager.addRule(rule);
console.info('Complex Rule added');

Regarding openhab-js/RuleBuilder I was confused by this as well. It does seem to have some support for Conditions (which I think is otherwise very poorly supported by the scripting add-ons), but I couldn't actually get a condition working. And, even though it has for some strange reason chosen to call "actions" for "operation", it seemed to also support non-scripted actions.

But, I couldn't get any of that to work. openhab-js/RuleBuilder seems unfinished to me. It's a pity, because it could have offered more flexibility than JSRule as far as I can understand.

When creating the rule you posted above, and querying the REST API for that rule, this is what I get:

{
  "status": {
    "status": "IDLE",
    "statusDetail": "NONE"
  },
  "editable": false,
  "triggers": [
    {
      "id": "c9a13e69-2522-4d43-8dcf-b9752759e8bb",
      "configuration": {
        "cronExpression": "* * * * * ? *"
      },
      "type": "timer.GenericCronTrigger"
    }
  ],
  "conditions": [],
  "actions": [
    {
      "inputs": {},
      "id": "1",
      "configuration": {
        "privId": "i8",
        "type": "application/javascript",
        "script": "// Code to run when the rule fires:\n// Note that Rule Builder is currently not supported!\n\nfunction(e){o(e)}"
      },
      "type": "jsr223.ScriptedAction"
    }
  ],
  "configuration": {},
  "configDescriptions": [],
  "templateState": "no-template",
  "uid": "everyMinute",
  "name": "Every Minute",
  "tags": [],
  "visibility": "VISIBLE",
  "description": "Logs every minute for testing"
}

jsr223.ScriptedAction is the "internal code" for SimpleRule, it's when this type is used that the "special handling" for SimpleRule is triggered. Also note privId, this is the "hack". There is no actual content in the action, but the privId reference goes to another registry that then maps this to runnable bytecode in memory. The memory reference is different for each launch of OH, which is why SimpleRules must be destroyed and recreated every time OH restarts.

Thus, it seems that openhab-js/RuleBuilder use SimpleRule as well, despite appearances. That also explains why you can't specify a MIME type - the embedded Action is actually a part of the "creation script", and thus you have shared context/locks, just as with JSRule.

@florian-h05
Copy link
Contributor

Closing in favour of changing the README in openhab-js.

@florian-h05 florian-h05 closed this Oct 3, 2025
@dilyanpalauzov dilyanpalauzov deleted the jsscripting_readme_noparallel branch October 3, 2025 22:35
@Nadahar
Copy link
Contributor

Nadahar commented Oct 8, 2025

@rkoshak I can't find your comment anywhere, so I'm answering here.

I'm pretty sure that the only lines in that example that isn't Java are the console.info() lines, and the second one is really just a String. If we are going to try to cover everything that you can do with Java and the raw Java OH API, then yes, you can create a "normal" rule. But you aren't really using JS as documented, as expected, nor in a supported way. You aren't really using JS at all.

I think I understand what you mean, but it's wrong. It isn't Java at all, it won't compile as Java. It is pure JavaScript, using OH's public API (which is made in Java, but that's irrelevant). It goes to the core of what JSR223 is, it allows you to call APIs across languages. What you construe as Java is simply the structure that is OH, OH core in particular. So, you just can't claim that this "isn't as documented" or "isn't supported". I'd say that if this is somehow "outside" anything, why use JSR223 at all, when it's what it does at its core.

The only thing it "goes against" is the way the "helper lib" has developed. As I wrote, this could have been done much easier with a bit of "support" from the helper lib, but for some reason, it has only focused on SimpleRule. I can't quite answer why that has been done, I've repeatedly pointed out that pretending that SimpleRule is the only thing that exists is quite limiting in some scenarios, although I do understand why it's used for simple tasks: It's simple to use, hence the name.

JS also has a builder interface for creating rules.

I know, and I think it's unfortunate that the same name was picked as what is used in OHs API. I've looked a little bit at it, it seemed to have some support for Conditions, which I was pleasantly surprised by, but I couldn't get it to work. I don't know if that because I did something wrong or not, but to me, the whole implementation looked very unfinished. In addition, I verified that it produces SimpleRules as well, so it was a dead end for my use. I also found it unfortunate that it has dubbed Actions "operations".

@florian-h05
Copy link
Contributor

I think the reason for JSRule to use SimpleRule (and rule builder is based on JSRule, so it’s using Simple Rule as well) instead of using ScriptActions and ScriptConditions is, that with SimpleRule it’s possible for the callback of the rule to reference anything outside of the callback in the same JS file, making it extremely powerful, as you can maintain shared state across multiple rules, write shared functions that multiple rules can use etc.
I agree though that rule builder isn’t perfect and has its quirks, I never found the time and motivation to rewrite it though.

@Nadahar
Copy link
Contributor

Nadahar commented Oct 8, 2025

that with SimpleRule it’s possible for the callback of the rule to reference anything outside of the callback in the same JS file, making it extremely powerful, as you can maintain shared state across multiple rules, write shared functions that multiple rules can use etc.

I'm not saying that it doesn't have its merits, and I understand perfectly well why it's used a lot. What I don't "appreciate", is that it seems to have become "the only things that exists" with some of the scripting languages.

@florian-h05
Copy link
Contributor

I'm not saying that it doesn't have its merits, and I understand perfectly well why it's used a lot. What I don't "appreciate", is that it seems to have become "the only things that exists" with some of the scripting languages.

I have been working hard to make sure your statement is wrong ;-)
See openhab/openhab-js#490.

@rkoshak
Copy link
Contributor

rkoshak commented Oct 8, 2025

@rkoshak I can't find your comment anywhere, so I'm answering here.

I deleted my commend because the issue is closed and I'm tired of arguing about it. I don't care any more. But what I meant was every Class you are using there is Java and JS Scripting makes it clear in the docs one should avoid using raw Java. If we want to make the docs super complicated for the end users covering cases we don't really support and actively recommend against using I won't stand in the way. It would be a net negative contribution to the docs but 🤷‍♂️ .

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.

5 participants