Skip to content

Conversation

dilyanpalauzov
Copy link
Contributor

I create a file rules/a.rules:

rule "Slow Rule"
when
  Item r changed
then
  logError("A", "SLOW rule start at " + ZonedDateTime.now().toString().substring(11, 25))
  Thread.sleep(10000)
  logError("A", "SLOW rule end at " + ZonedDateTime.now().toString().substring(11, 25))
end

rule "Fast Rule"
when
  Item r changed
then
  logError("A", "FAST rule run at " + ZonedDateTime.now().toString().substring(11, 25))
end

and then change r six times fast. openhab.log contains:

2025-10-04 14:49:03.083 [INFO ] [el.core.internal.ModelRepositoryImpl] - Loading DSL model 'a.rules'
2025-10-04 14:50:04.628 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule start at 14:50:04.62676
2025-10-04 14:50:04.630 [ERROR] [org.openhab.core.model.script.A     ] - FAST rule run at 14:50:04.62817
2025-10-04 14:50:04.797 [ERROR] [org.openhab.core.model.script.A     ] - FAST rule run at 14:50:04.79549
2025-10-04 14:50:05.518 [ERROR] [org.openhab.core.model.script.A     ] - FAST rule run at 14:50:05.51655
2025-10-04 14:50:06.242 [ERROR] [org.openhab.core.model.script.A     ] - FAST rule run at 14:50:06.24042
2025-10-04 14:50:06.948 [ERROR] [org.openhab.core.model.script.A     ] - FAST rule run at 14:50:06.94709
2025-10-04 14:50:07.683 [ERROR] [org.openhab.core.model.script.A     ] - FAST rule run at 14:50:07.68087
2025-10-04 14:50:14.634 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule end at 14:50:14.63184
2025-10-04 14:50:14.643 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule start at 14:50:14.64059
2025-10-04 14:50:24.649 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule end at 14:50:24.64700
2025-10-04 14:50:24.657 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule start at 14:50:24.65547
2025-10-04 14:50:34.663 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule end at 14:50:34.66138
2025-10-04 14:50:34.679 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule start at 14:50:34.67715
2025-10-04 14:50:44.685 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule end at 14:50:44.68312
2025-10-04 14:50:44.700 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule start at 14:50:44.69756
2025-10-04 14:50:54.706 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule end at 14:50:54.70419
2025-10-04 14:50:54.718 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule start at 14:50:54.71562
2025-10-04 14:51:04.725 [ERROR] [org.openhab.core.model.script.A     ] - SLOW rule end at 14:51:04.72241

So both rules are executed six times, but SLOW Rule waits its own execution to end, before starting from the start.

Copy link

netlify bot commented Oct 4, 2025

Deploy Preview for openhab-docs-preview failed. Why did it fail? →

Built without sensitive environment variables

Name Link
🔨 Latest commit aace84a
🔍 Latest deploy log https://app.netlify.com/projects/openhab-docs-preview/deploys/68f5f7c108403900085c48ba

@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

@dilyanpalauzov
Copy link
Contributor Author

If a rule cannot be executed in parallel, why does https://www.openhab.org/docs/configuration/rules-dsl.html#concurrency-guard say “If a rule triggers on UI events it may be necessary to guard against concurrency.”?

@rkoshak
Copy link
Contributor

rkoshak commented Oct 7, 2025

"If a rule triggers on UI events it may be necessary to guard against concurrency.”

That sentence is probably poorly worded.

MainUI widgets can directly call a rule as its action. This usage is similar to running a rule manually (e.g. pressing the play button), running the rule directly through the REST API (POST /rules/{ruleUID]/runnow), or running a rule from another rule. In these cases, the locks you exercised above all get bypassed.

Note, I think that MainUI actually has some logic in place that will prevent you from manually running a rule that's already running on that rule's page. But that's logic built into the web page and it does not extend to UI widgets.

@dilyanpalauzov
Copy link
Contributor Author

I do not understand. Either OpenHAB-core, irrespective of web pages, ensures that a single Rule (or a script) can have a single running instance, or it does not make such promises. In this case, OpenHAB just waits for a rule to finish and if the rule is triggered in the meantime again, after it finishes, it is fired again. This is what the example above shows, this is how it works with transformations.

If OpenHAB lets a rule under circumstances start running, before another instance of the same rule has finished, then this is completely different story and the conditions, when this can happen, should be described, so that locks guard against this.

Do UI-widgets or REST API calls allow a rule (e.g. a rule containing Thread.sleep()) to be restarted, while the same rule is executing (was started in the past and has not yet finished)?

@rkoshak
Copy link
Contributor

rkoshak commented Oct 7, 2025

I do not understand. Either OpenHAB-core, irrespective of web pages, ensures that a single Rule (or a script) can have a single running instance, or it does not make such promises.

There are many ways to run a rule. Some ways there are locks. Other ways there are no locks.

Rules that are run based on a trigger are locked. Rules directly run (i.e. not run based on a trigger) there is no lock.

In this case, OpenHAB just waits for a rule to finish and if the rule is triggered in the meantime again, after it finishes, it is fired again.

Right, the key word is "triggered" here. MainUI doesn't trigger the rule. It directly invokes it. That comment in the docs you point out is referring to this direct invocation.

Do UI-widgets or REST API calls allow a rule (e.g. a rule containing Thread.sleep()) to be restarted, while the same rule is executing (was started in the past and has not yet finished)?

Yes. You will have two of this rule running at the same time.

Rule Action:
image

Widget's Action:
image

From a Script (JS in this case, note that Rules DSL lacks this ability)

rules.runRule('RuleUID', { }, true);

REST API:

image

None of these ways to run a rule have locks. Only when a rule is triggered is it locked.

@dilyanpalauzov
Copy link
Contributor Author

I have changed the wording to state, that rules fired by their triggers cannot be executed in parallel, while rules executed by RuleManager.runNow() - from a script or another rule; or started from a MainUI widget can be executed concurrently.

@Nadahar said at openhab/openhab-addons#19410 (comment), that each rule has a dedicated thread - .

Does it mean, that when a rule is executed by RuleManager.runNow() from another rule, MainUI widget, the rule will run in that very same thread, on runNow() runs the rule in a different thread?

Note, I think that MainUI actually has some logic in place that will prevent you from manually running a rule that's already running on that rule's page. But that's logic built into the web page and it does not extend to UI widgets.

Is this logic in MainUI irrelevant (bypassed), if the same page is opened in two browsers?

@Nadahar
Copy link
Contributor

Nadahar commented Oct 10, 2025

Does it mean, that when a rule is executed by RuleManager.runNow() from another rule, MainUI widget, the rule will run in that very same thread, on runNow() runs the rule in a different thread?

Actually, I believe that there's a "problem" when using runNow() (and all the derived ways to launch a rule), and also when run from timers. I don't think that the "rule thread" is respected/used in this situation, which leads to problems. The very reason one thread was dedicated to each rule was to avoid all the problems/errors experienced when this wasn't the case. Everything within a rule is usually done without any thread-safety, and it would potentially be very complicated to change that, which is why the "one rule one thread" solution was chosen as I understand it.

@dilyanpalauzov
Copy link
Contributor Author

Actually, I believe that there's a "problem" when … run from timers.

I checked for timer triggers, in particular for timer.GenericCronTrigger with Groovy:

logger = org.slf4j.LoggerFactory.getLogger("C")
scriptExtension.importPreset("RuleSupport")
global_i = 0
automationManager.addRule(
  new org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule() {
    @Override
    Object execute(org.openhab.core.automation.Action module, Map<String, ?> inputs) {
        Integer local_i = global_i++
        logger.error("START " + local_i)
        Thread.sleep(15000)
        logger.error("END " + local_i)
    }
    {
      triggers = [org.openhab.core.automation.util.TriggerBuilder.create().withId("a")
          .withTypeUID("timer.GenericCronTrigger").withConfiguration(
             new org.openhab.core.config.core.Configuration([cronExpression: "0/5 * * * * *"])).build()
      ]
    }
})

prints

2025-10-11 09:57:31.091 [INFO ] [ort.loader.AbstractScriptFileWatcher] - (Re-)Loading script '/etc/openhab/automation/jsr223/ff.groovy'                                                                                         
2025-10-11 09:57:35.229 [ERROR] [C                                   ] - START 0 
2025-10-11 09:57:50.234 [ERROR] [C                                   ] - END 0 
2025-10-11 09:57:50.237 [ERROR] [C                                   ] - START 1 
2025-10-11 09:58:05.240 [ERROR] [C                                   ] - END 1 
2025-10-11 09:58:05.244 [ERROR] [C                                   ] - START 2 
2025-10-11 09:58:20.246 [ERROR] [C                                   ] - END 2 
2025-10-11 09:58:20.251 [ERROR] [C                                   ] - START 3 
2025-10-11 09:58:35.254 [ERROR] [C                                   ] - END 3 
2025-10-11 09:58:35.257 [ERROR] [C                                   ] - START 4 
2025-10-11 09:58:50.260 [ERROR] [C                                   ] - END 4 
2025-10-11 09:58:50.264 [ERROR] [C                                   ] - START 5 
2025-10-11 09:59:05.266 [ERROR] [C                                   ] - END 5

This trigger fires every five seconds, but because of Thread.sleep(15000) the system executes the trigger every fifteen seconds.

By their nature the other timer triggers timer.TimeOfDayTrigger and timer.DateTimeTrigger cannot run in parallel. The first runs once a day, the second also. Unless the trigger configuration for TimeOfDayTrigger is changed in a loop, or the item bound to DateTimeTrigger is changed in a loop, which loop repeat every second and the action handler is slow. I am not going to test if timer.TimeOfDayTrigger and timer.DateTimeTrigger can be triggered in parallel.

@Nadahar
Copy link
Contributor

Nadahar commented Oct 11, 2025

@dilyanpalauzov I didn't mean timer triggers, but timers used in scripts. I'm not entirely sure how they are used to execute rules, I've just read it referenced.

However, you should keep an eye on this, it might change how this works: openhab/openhab-core#5069

@dilyanpalauzov
Copy link
Contributor Author

Are profiles also restricted to run sequentially, in the same way as transformations are?

Sequential executions for profiles is stated at https://github.com/dalgwen/openhab-addons/blob/java223/main/bundles/org.openhab.automation.java223/README.md#concurrency with these words:

openHAB also prevents several executions of the same rule at the same time. This means, for example, that a script defined as the 'Action' part of a GUI rule cannot run concurrently, even if triggered by another rule at the same moment (The second one will fail).

openHAB prevents several executions of the same script at the same time. This is also the case for transformations/profile (the second transformation will wait for the first to finish). This can be an issue, especially if your script takes some non-negligible time to execute.

Timers in Rules and Scripts

My understanding is that a timer in a script is in essence a new thread, and then locks can be justified, if the timer-body references variables, which exist outside the timer, and and the rule is retriggered - the rule accesses the same variables at the same time - once after being retriggered, and once from the timer.

That said, I think timers and locks in rules and scripts do not need extra text.

I have such a rule (DSL), which changes a value of a String item (e3_colour) to contain a colour (red, orange, yellow). The longer the device is on, the hotter the colour.

var e3Lock = new java.util.concurrent.locks.ReentrantLock
var Timer timer
val ir = org.openhab.core.model.script.ScriptServiceUtil.getItemRegistry

rule "E3 Power Change"
when
  Item e3_power changed
then
  if (previousState != NULL Math.abs(e3_power.getStateAs(Number).intValue - (previousState as Number).intValue) > 10) {
     val prev = (previousState as Number).floatValue < 60
     val cur = e3_power.getStateAs(Number).floatValue < 60
     if ((!prev && cur) || (prev && !cur))
       e3_posl_promjana.postUpdate(new DateTimeType)
   }

  e3Lock.lock
  try {
    if ((e3_power.state as Number).intValue < 5) {
      if (timer !== null) { timer.cancel; timer = null }
      if (e3_color.state != NULL) e3_color.postUpdate(NULL)
      return
    }

    timer = createTimer("Set E3 color", now.plusMinutes(4)) [|
      e3Lock.lock
      try {
         val last_mod = (e3_posl_promjana.state as DateTimeType).getZonedDateTime(ZoneId.systemDefault)
         var b = ZonedDateTime.now
         for (StringType c: newArrayList(new StringType(''), new StringType('yellow'), new StringType('gold'), new StringType('orange'), new StringType('maroon'))) {
           b = b.minusMinutes(4)
           if (last_mod.isAfter(b)) {
             if (timer !== null) timer.reschedule(now.plusMinutes(1))
             e3_color.postUpdate(if (c == '') NULL else c)
             return
           }
        }
      } finally {
        e3Lock.unlock
      }
      }
      if (e3_color.state != "red") {
        e3_color.postUpdate("red")
        timer = null
      }
    ]
  } finally {
    e3Lock.unlock
  }
end

@dilyanpalauzov
Copy link
Contributor Author

If a profile cannot run in parallel to itself, does this apply only for profiles written in automation language? (There is such statement for transformations written in automation language). Or for all profiles?


Rules can be executed, either when fired by their triggers, or when called explicitly - from a script, another rule, MainUI widget.
When a rule is fired by its trigger, the execution of one and the same rule does not happen in parallel.
If a rule is triggered for execution, while the rule is currently run, it will be queued and run later.
Copy link
Contributor

Choose a reason for hiding this comment

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

If a rule is triggered for execution, while the rule is currently run, it will be queued and run later.

... while a rule is currently running*...

### Concurrency Guard

If a rule triggers on UI events it may be necessary to guard against concurrency.
If a rule is executed not fired by its triggers, but explicitly - from a script, from another rule, from a MainUI widget - the rule can be started, before its previous execution has ended. It may be necessary to guard against concurrency.
Copy link
Contributor

Choose a reason for hiding this comment

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

Each sentence should be on a separate line.

The wording of the first sentence is awkward. I recommend something more like:

"If a rule is explicitly run from another script, rule, or MainUI widget instead of a trigger the rule can be started before the current execution has ended."

@Nadahar
Copy link
Contributor

Nadahar commented Oct 13, 2025

My understanding is that a timer in a script is in essence a new thread

Yes, unless openhab/openhab-core#5069 is merged, in which case it isn't anymore.

That said, I agree that using a lock like you do above isn't hard, but there are some complicating factors. First, according to @rkoshak there are bugs with Rules DSL which means that finally won't always run if an exception occurs. That would leave the lock in question in a permanently locked state, and OH would have to be restarted to release it.

In addition, there are other scripting languages than Rules DSL where doing something like this is much more cumbersome. And, many people don't really understand the concept at all, why there is a need for locks or how you use them. For reasons not clear to me, concurrency seem to be one of those things that only some people "get". There are so many concurrency mistakes made, also by developers that are otherwise very capable.

@dilyanpalauzov
Copy link
Contributor Author

On the text rewording here

I have taken the suggested text over. In:

If a rule is explicitly run from another script, rule, or MainUI widget instead of a trigger the rule can be started before the current execution has ended.

I have removed “or” before MainUI, as the list is not exclusive: HTTP POST /rest/rules/{rule-uid}/runnow/ also executes the rule extraordinary. And I put commata around “instead of a trigger“.

Bugs in Rules DSL …

according to @rkoshak there are bugs with Rules DSL which means that finally won't always run if an exception occurs. That would leave the lock in question in a permanently locked state, and OH would have to be restarted to release it again.

Where are these bugs described? Can I have an example reproducing this? Can a lock be handled as a try-with-resources, so after try is left, the lock is released, without having something in finally?

(Moving Rules DSL out of openhab-core is another solution to the problem, or it lets isolate the problem either in DSL or in all rule executions systems - right now I do not know where it is.)

Can non-script transformations run in parallel?

Sometimes - yes, sometimes - not!

Not only transformations from automations scripts are executed sequentially (one transformation per thread), but all transformations. I modified the MAP transformation to sleep for 5s with
this transform.map.delay5.patch.

the patch to delay transformation.map with 5s
--- a/bundles/org.openhab.transform.map/src/main/java/org/openhab/transform/map/internal/MapTransformationService.java
+++ b/bundles/org.openhab.transform.map/src/main/java/org/openhab/transform/map/internal/MapTransformationService.java
@@ -84,8 +84,13 @@ public class MapTransformationService
 
     @Override
     public @Nullable String transform(String function, String source) throws TransformationException {
+        logger.error("A/" + Long.toString(Thread.currentThread().getId()) + "/ "
+                + java.time.ZonedDateTime.now().toString().substring(11, 22) + " input=[" + source + "]");
+        try {
+            Thread.sleep(5000);
+        } catch (Exception e) {
+        }
         Properties properties = null;
-
         Matcher matcher = INLINE_MAP_CONFIG_PATTERN.matcher(function);
         if (matcher.matches()) {
             properties = cachedInlineMap.computeIfAbsent(function, f -> {
@@ -120,15 +125,22 @@ public class MapTransformationService
             if (target == null) {
                 target = properties.getProperty("");
                 if (target == null) {
+                    logger.error("B/" + Long.toString(Thread.currentThread().getId()) + "/ "
+                            + java.time.ZonedDateTime.now().toString().substring(11, 22) + " input=[" + source
+                            + "] new Exception");
                     throw new TransformationException("Target value not found in map for '" + source + "'");
                 } else if (SOURCE_VALUE.equals(target)) {
                     target = source;
                 }
             }
-
+            logger.error("C/" + Long.toString(Thread.currentThread().getId()) + "/ "
+                    + java.time.ZonedDateTime.now().toString().substring(11, 22) + " input=[" + source + "] output="
+                    + target);
             logger.debug("Transformation resulted in '{}'", target);
             return target;
         }
+        logger.error("D/" + Long.toString(Thread.currentThread().getId()) + "/ "
+                + java.time.ZonedDateTime.now().toString().substring(11, 22) + " input=[" + source + "] new Exception");
         throw new TransformationException("Could not find configuration '" + function + "' or failed to parse it.");
     }
 
I have the MAP transformations called from
# grep -ri  [^e]map items/ things/ sitemaps/
items/r9.items:Switch r9 "9 Реле [MAP(switch.map):%s]"{channel="mqtt:topic:r9:r9", autoupdate="false"}
items/r9.items:Switch r9a "9 Реле [MAP(switch.map):%s]" {channel="mqtt:topic:r9:r9", autoupdate="false"}
items/r9.items:Switch r9b "9 Реле [MAP(switch.map):%s]" {channel="mqtt:topic:r9:r9", autoupdate="false"}
items/r9.items:Switch r9c "9 Реле [MAP(switch.map):%s]" {channel="mqtt:topic:r9:r9", autoupdate="false"}
items/r9.items:Switch r9d "9 Реле [MAP(switch.map):%s]" {channel="mqtt:topic:r9:r9", autoupdate="false"}
items/r9.items:Switch r9e "9 Реле [MAP(switch.map):%s]" {channel="mqtt:topic:r9:r9", autoupdate="false"}
items/r9.items:Switch r9f "9 Реле [MAP(switch.map):%s]" {channel="mqtt:topic:r9:r9", autoupdate="false"}
items/r3.items:Switch r3 "3 Реле [MAP(switch.map):%s]" {channel="mqtt:topic:r3:r3", autoupdate="false"}
sitemaps/r3.sitemap:        Switch item=r3_manual mappings=[ON="Ръчно", OFF="Автоматично"] labelcolor=[OFF="green", ON="red"] visibility=[!=NULL]

and the seven r9, r9a...r9f items appear in r9.sitemaps:

sitemap r9 label="R9" {
  Text item=r9
  Text item=r9a
  Text item=r9b
  Text item=r9c
  Text item=r9d
  Text item=r9e
  Text item=r9f
}

For the results below I visit /settings/items and the sitemap /basicui/app?sitemap=x.

However I do not understand the results.

These are the results from the MAP transformation logging in /var/log/openhab/openhab.log (for brevity I removed the date at the start of the lines and ` [ERROR] `)
15:12:23.613[ap.internal.MapTransformationService] - A/316/ 15:12:23.61 input=[ON]
15:12:28.618[ap.internal.MapTransformationService] - C/316/ 15:12:28.61 input=[ON] output=TurnedOn
15:12:28.654[ap.internal.MapTransformationService] - A/316/ 15:12:28.65 input=[ON]
15:12:33.656[ap.internal.MapTransformationService] - C/316/ 15:12:33.65 input=[ON] output=TurnedOn
15:12:33.660[ap.internal.MapTransformationService] - A/316/ 15:12:33.65 input=[ON]
15:12:38.661[ap.internal.MapTransformationService] - C/316/ 15:12:38.66 input=[ON] output=TurnedOn
15:12:38.674[ap.internal.MapTransformationService] - A/316/ 15:12:38.66 input=[ON]
15:12:38.686[ap.internal.MapTransformationService] - A/485/ 15:12:38.68 input=[ON]
15:12:40.251[ap.internal.MapTransformationService] - A/785/ 15:12:40.25 input=[ON]
15:12:41.655[ap.internal.MapTransformationService] - A/331/ 15:12:41.65 input=[ON]
15:12:43.676[ap.internal.MapTransformationService] - C/316/ 15:12:43.67 input=[ON] output=TurnedOn
15:12:43.680[ap.internal.MapTransformationService] - A/316/ 15:12:43.68 input=[ON]
15:12:43.688[ap.internal.MapTransformationService] - C/485/ 15:12:43.68 input=[ON] output=TurnedOn
15:12:43.697[ap.internal.MapTransformationService] - A/485/ 15:12:43.69 input=[ON]
15:12:45.256[ap.internal.MapTransformationService] - C/785/ 15:12:45.25 input=[ON] output=TurnedOn
15:12:45.263[ap.internal.MapTransformationService] - A/785/ 15:12:45.26 input=[ON]
15:12:46.656[ap.internal.MapTransformationService] - C/331/ 15:12:46.65 input=[ON] output=TurnedOn
15:12:46.664[ap.internal.MapTransformationService] - A/331/ 15:12:46.66 input=[ON]
15:12:48.682[ap.internal.MapTransformationService] - C/316/ 15:12:48.68 input=[ON] output=TurnedOn
15:12:48.691[ap.internal.MapTransformationService] - A/316/ 15:12:48.69 input=[ON]
15:12:48.699[ap.internal.MapTransformationService] - C/485/ 15:12:48.69 input=[ON] output=TurnedOn
15:12:48.705[ap.internal.MapTransformationService] - A/485/ 15:12:48.70 input=[ON]
15:12:50.265[ap.internal.MapTransformationService] - C/785/ 15:12:50.26 input=[ON] output=TurnedOn
15:12:50.272[ap.internal.MapTransformationService] - A/785/ 15:12:50.27 input=[ON]
15:12:51.666[ap.internal.MapTransformationService] - C/331/ 15:12:51.66 input=[ON] output=TurnedOn
15:12:51.673[ap.internal.MapTransformationService] - A/331/ 15:12:51.67 input=[ON]
15:12:53.694[ap.internal.MapTransformationService] - C/316/ 15:12:53.69 input=[ON] output=TurnedOn
15:12:53.697[ap.internal.MapTransformationService] - A/316/ 15:12:53.69 input=[ON]
15:12:53.707[ap.internal.MapTransformationService] - C/485/ 15:12:53.70 input=[ON] output=TurnedOn
15:12:53.711[ap.internal.MapTransformationService] - A/485/ 15:12:53.71 input=[ON]
15:12:55.274[ap.internal.MapTransformationService] - C/785/ 15:12:55.27 input=[ON] output=TurnedOn
15:12:55.278[ap.internal.MapTransformationService] - A/785/ 15:12:55.27 input=[ON]
15:12:56.675[ap.internal.MapTransformationService] - C/331/ 15:12:56.67 input=[ON] output=TurnedOn
15:12:56.679[ap.internal.MapTransformationService] - A/331/ 15:12:56.67 input=[ON]
15:12:58.699[ap.internal.MapTransformationService] - C/316/ 15:12:58.69 input=[ON] output=TurnedOn
15:12:58.713[ap.internal.MapTransformationService] - C/485/ 15:12:58.71 input=[ON] output=TurnedOn
15:12:58.717[ap.internal.MapTransformationService] - A/485/ 15:12:58.71 input=[ON]
15:13:00.279[ap.internal.MapTransformationService] - C/785/ 15:13:00.27 input=[ON] output=TurnedOn
15:13:00.283[ap.internal.MapTransformationService] - A/785/ 15:13:00.28 input=[ON]
15:13:01.681[ap.internal.MapTransformationService] - C/331/ 15:13:01.68 input=[ON] output=TurnedOn
15:13:01.685[ap.internal.MapTransformationService] - A/331/ 15:13:01.68 input=[ON]
15:13:03.718[ap.internal.MapTransformationService] - C/485/ 15:13:03.71 input=[ON] output=TurnedOn
15:13:03.726[ap.internal.MapTransformationService] - A/485/ 15:13:03.72 input=[ON]
15:13:05.284[ap.internal.MapTransformationService] - C/785/ 15:13:05.28 input=[ON] output=TurnedOn
15:13:05.291[ap.internal.MapTransformationService] - A/785/ 15:13:05.29 input=[ON]
15:13:06.686[ap.internal.MapTransformationService] - C/331/ 15:13:06.68 input=[ON] output=TurnedOn
15:13:06.692[ap.internal.MapTransformationService] - A/331/ 15:13:06.69 input=[ON]
15:13:08.728[ap.internal.MapTransformationService] - C/485/ 15:13:08.72 input=[ON] output=TurnedOn
15:13:08.735[ap.internal.MapTransformationService] - A/485/ 15:13:08.73 input=[ON]
15:13:10.294[ap.internal.MapTransformationService] - C/785/ 15:13:10.29 input=[ON] output=TurnedOn
15:13:10.301[ap.internal.MapTransformationService] - A/785/ 15:13:10.30 input=[ON]
15:13:11.695[ap.internal.MapTransformationService] - C/331/ 15:13:11.69 input=[ON] output=TurnedOn
15:13:11.701[ap.internal.MapTransformationService] - A/331/ 15:13:11.70 input=[ON]
15:13:13.739[ap.internal.MapTransformationService] - C/485/ 15:13:13.73 input=[ON] output=TurnedOn
15:13:13.759[ap.internal.MapTransformationService] - A/485/ 15:13:13.75 input=[ON]
15:13:15.304[ap.internal.MapTransformationService] - C/785/ 15:13:15.30 input=[ON] output=TurnedOn
15:13:15.322[ap.internal.MapTransformationService] - A/785/ 15:13:15.32 input=[ON]
15:13:16.703[ap.internal.MapTransformationService] - C/331/ 15:13:16.70 input=[ON] output=TurnedOn
15:13:16.721[ap.internal.MapTransformationService] - A/331/ 15:13:16.72 input=[ON]
15:13:18.761[ap.internal.MapTransformationService] - C/485/ 15:13:18.76 input=[ON] output=TurnedOn
15:13:20.324[ap.internal.MapTransformationService] - C/785/ 15:13:20.32 input=[ON] output=TurnedOn
15:13:21.724[ap.internal.MapTransformationService] - C/331/ 15:13:21.72 input=[ON] output=TurnedOn

That transformation is executed in threads 316, 331, 485, 785. Thread 316 executes the transformation 7 times, the other execute the transformation 8 times. Indeed settings/items/ does not show the items with the transformation in their label. My explanation is that 8 items apply the transformation to their labels, while the sitemap contains 7 items. (Verified, this is the case).

So, one and the same transformation - script or not - is executed several times in the same thread, when

  • several items have the same transformation in their label and all the items are included in a sitemap (seven executions above)
  • several items have the same transformation in their label, are linked to channels (same or different channels) and the channel receives data (eight executions above). (I do not understand how this 8 happens when mqtt:topic:r3:r3 is not updated and mqtt:topic:r9:r9 is updated and ).

How can it be generalized when one and the same transformation runs several times (sequentially) in the same thread, and when in parallel using different threads?

Parallelism in Profiles from transformations

I see no point in looking at how this works, before it is clarified when is one and the same transformation executed several times in the same thread and when in a different thread. (As I cannot know if the findings for profiles will be because of how profiles work, or because of how transformations work.)

@Nadahar
Copy link
Contributor

Nadahar commented Oct 14, 2025

Where are these bugs described? Can I have an example reproducing this? Can a lock be handled as a try-with-resources, so after try is left, the lock is released, without having something in finally?

I'm not sure that they are described except for in forum posts, probably. @rkoshak might have an idea how to trigger it. A Lock isn't AutoClosable, to no, you can't use try-with-resources, but that wouldn't help either from what I understand. As I understand the problem, there are some exceptions, like those from invalid casts, the makes the "Rules DSL parser/processor" crash, so that script execution is aborted there and then. There's nothing wrong with try-finally in itself, but if the execution is aborted during the block, finally will never be executed, and the same would be true for an AutoClosable resource.

From what I understand, this is a pretty fundamental flaw with xtext/xtend (I'm not sure exactly where) that nobody has been able to solve, and it's one of the reasons why focus was shifted away from Rules DSL. If you can't trust a finally block to be executed, you're basically left without options to control what will happen.

(Moving Rules DSL out of openhab-core is another solution to the problem, or it lets isolate the problem either in DSL or in all rule executions systems - right now I do not know where it is.)

As I'm sure you know, I'm very much against moving it out of core, since it's the one BIG benefit of Rules DSL as I see it - that it's NOT an add-on. But, understand one thing: The different script languages have different challenges, some more than others.

No one person have the overview of all this, and what comes up as examples are somewhat random. If you want to follow this to the end, and really find all the problems, I guess you will have to actually try to do it in all the different languages, and see what barriers present themselves. So, the focus on "if the bug is in Rules DSL or not" isn't really that important here. Rules were never meant to or designed to run in parallel with themselves as I understand it, so there are probably lots of small decisions that would have to be different, if they were meant to be.

What I don't understand is why you'd want a rule to run in parallel with itself. It sounds like a mess even without concurrency issues. A rule normally does some evaluation of something and then makes a change. If this evaluation → change process goes on in parallel, how can this be anything but a mess? One instance of the rule is evaluating while the other is applying the action, the next iteration has decided something different partly based on what the first one did, and so it will immediately change the decision made. It sounds like a recipe for "random behavior" to me.

When it comes to parallel execution of transformations, I'm not familiar enough with the code to know how it's done. But, what you're seeing could mean that they, in theory, run in parallel, but that for other reasons (the code that launches the transformations), they often aren't in effect run in parallel. There are lots of moving parts here, and one lock (in the "wrong place") can often be enough to prevent things from happening in parallel.

@rkoshak
Copy link
Contributor

rkoshak commented Oct 14, 2025

Where are these bugs described? Can I have an example reproducing this? Can a lock be handled as a try-with-resources, so after try is left, the lock is released, without having something in finally?Where are these bugs described? Can I have an example reproducing this? Can a lock be handled as a try-with-resources, so after try is left, the lock is released, without having something in finally?

I haven't used Rules DSL in many years and do not care to. But it used to be the case that if you had a type error in the try block (e.g. tried to do an operation with two Objects of incompatible types), Rules DSL would throw and catch and log a NullException. Because it caught its own exception, the rule just exits and is never given the chance to run the finally.

There were other errors that got generated deep in the Xtend call stack which cause the rule to just stop. The exception never bubbles back up to the rule so the catch and finally never execute.

(Moving Rules DSL out of openhab-core is another solution to the problem, or it lets isolate the problem either in DSL or in all rule executions systems - right now I do not know where it is.)

I pushed really hard for this. But it's been decided. It's not going to happen. So we work with what we have. Ultimately, .items files, .things files, .sitemap, et. al. legacy file formats are defined by and parsed using Xtext/Xtend too. Moving Rules DSL to become an add-on doesn't actually remove much of anything from core because just about everything Rules DSL needs is also needed by these other file formats.

But I'm also not 100% certain something like this isn't also possible in other rules languages. There's a whole stack of Java stuff that a rule's scripts are running on top of. If there is an exception anywhere in that stack and the exception doesn't flow up to the script, you can never guarantee that the finally block will be run in any rule script. In practice, it probably doesn't happen that way, certainly not often, but there is also no guarantee.

(As I cannot know if the findings for profiles will be because of how profiles work, or because of how transformations work.)

I suspect it's how the profile is called that makes the difference, same as with rules. The sitemap needs to make some REST API call to apply the transformation in the label so, as when a rule is run directly like that, it probably bypasses any locks. That's my guess anyway.

What I don't understand is why you'd want a rule to run in parallel with itself. It sounds like a mess even without concurrency issues. A rule normally does some evaluation of something and then makes a change. If this evaluation → change process goes on in parallel, how can this be anything but a mess? One instance of the rule is evaluating while the other is applying the action, the next iteration has decided something different partly based on what the first one did, and so it will immediately change the decision made. It sounds like a recipe for "random behavior" to me.

This is exactly how it worked in OH 1.x and 2.x (for those not using the Experimental rule engine). Imagine the fun we had to deal with when using an "Item state updated" trigger on a Group with an aggregation function. The rule got triggered N-1 times where N is the number of members of the Group in rapid succession.

I spent roughly 75% of my time on the forum helping users find, diagnose, and solve errors caused by the same rule running more than once at a time. It was indeed a mess.

As I see it, there are only two ways you can make it so the same rule can run more than once at a time, and this applies to all the languages.

  1. Rules can only be stateless. No more variables, no cache (unless all that locking stuff gets straightened out), simply a few lines of code which neither depend upon nor save any data from one run to the next.

  2. A separate instance (ScriptEngine) is created on the fly for every execution of a rule so if you have the same rule running twice, each has it's own thread, own ScriptEngine, and in all other ways are completely seprate from each other without anything shared. They are essentially two separate rules.

1 isn't feasible because it eliminates pretty much every use case people have for rules.

2 isn't feasible because, particularly on SBCs like RPis, the amount of time it takes to parse and instantiate the script is long. For a poorly created rule in Rules DSL (i.e. forcing types where you don't absolutely need to) it can take dozens of seconds. We can't afford that kind of latency every time the rule runs. I'm certain we could find ways to amortize and reuse stuff so it's not quite that bad, but it's never going to be down to acceptable levels of latency. (Note, it used to be the case not that long ago that rules were not loaded and parsed/compiled until the first time the rule was triggered, and the user experience was horrible).

@dilyanpalauzov
Copy link
Contributor Author

From what I understand, this is a pretty fundamental flaw with xtext/xtend (I'm not sure exactly where)
Ultimately, .items files, .things files, .sitemap, et. al. legacy file formats are defined by and parsed using Xtext/Xtend too.

Let me explain you my understanding of the difference between Xtext and Xbase/Xtend.

XText defines the grammar for sitemaps, items, things, rules files.

In grammar for rules I mean "start possibly with imports, then vars", then accept zero or more times “rule "NAME" when, then triggers, then then, then MAGIC BLOCK, then end”.

Xbase and Xtend are only in charge for the magic block above and DSL transformations. Xbase and Xtend parse the code (only the magic block) and then can either interpret it, or generate .java files. I think in openHAB it is interpreted, as it seems to run even on systems without javac, without implementation of javax.toolx.JavaCompiler (Zulu/Azul Java JRE). If there is a fundamental flaw with Xtend/Xbase, then this fundamental flaw will be preserved in the generated (by Xtend) .java files.

In addition Xtext provides some services for Language Server support, and Xbase/Xtend might also provide such services.

In another addition Xtend/Xbase is used to compile parts of openHAB, e.g. bundles/org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapRuntimeModule.xtend, but this is only compile time dependency. So sitemaps depend on Xtend for their compilation, but at run time (after openHAB is shipped) model.sitemap, model.persistance, model.thing do not need Xtend, they need only Xtext (for parsing files).

All Xtend files in openHAB core
$ find -name '*.xtend'
./org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapRuntimeModule.xtend
./org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/SitemapStandaloneSetup.xtend
./org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/formatting/SitemapFormatter.xtend
./org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/scoping/SitemapScopeProvider.xtend
./org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/generator/SitemapGenerator.xtend
./org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/validation/SitemapValidator.xtend
./org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/serializer/SitemapSyntacticSequencer.xtend
./org.openhab.core.model.sitemap/src/org/openhab/core/model/sitemap/serializer/SitemapSemanticSequencer.xtend
./org.openhab.core.model.script.ide/src/org/openhab/core/model/script/ide/ScriptIdeSetup.xtend
./org.openhab.core.model.script.ide/src/org/openhab/core/model/script/ide/ScriptIdeModule.xtend
./org.openhab.core.model.thing.ide/src/org/openhab/core/model/thing/ide/ThingIdeModule.xtend
./org.openhab.core.model.thing.ide/src/org/openhab/core/model/thing/ide/ThingIdeSetup.xtend
./org.openhab.core.model.item.ide/src/org/openhab/core/model/ide/ItemsIdeSetup.xtend
./org.openhab.core.model.item.ide/src/org/openhab/core/model/ide/ItemsIdeModule.xtend
./org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/PersistenceRuntimeModule.xtend
./org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/PersistenceStandaloneSetup.xtend
./org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/formatting/PersistenceFormatter.xtend
./org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/scoping/PersistenceScopeProvider.xtend
./org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/generator/PersistenceGenerator.xtend
./org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/validation/PersistenceValidator.xtend
./org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/serializer/PersistenceSemanticSequencer.xtend
./org.openhab.core.model.persistence/src/org/openhab/core/model/persistence/serializer/PersistenceSyntacticSequencer.xtend
./org.openhab.core.model.rule.ide/src/org/openhab/core/model/rule/ide/RulesIdeModule.xtend
./org.openhab.core.model.rule.ide/src/org/openhab/core/model/rule/ide/RulesIdeSetup.xtend
./org.openhab.core.model.sitemap.ide/src/org/openhab/core/model/sitemap/ide/SitemapIdeSetup.xtend
./org.openhab.core.model.sitemap.ide/src/org/openhab/core/model/sitemap/ide/SitemapIdeModule.xtend
./org.openhab.core.model.item/src/org/openhab/core/model/ItemsRuntimeModule.xtend
./org.openhab.core.model.item/src/org/openhab/core/model/ItemsStandaloneSetup.xtend
./org.openhab.core.model.item/src/org/openhab/core/model/formatting/ItemsFormatter.xtend
./org.openhab.core.model.item/src/org/openhab/core/model/scoping/ItemsScopeProvider.xtend
./org.openhab.core.model.item/src/org/openhab/core/model/generator/ItemsGenerator.xtend
./org.openhab.core.model.item/src/org/openhab/core/model/validation/ItemsValidator.xtend
./org.openhab.core.model.item/src/org/openhab/core/model/serializer/ItemsSemanticSequencer.xtend
./org.openhab.core.model.item/src/org/openhab/core/model/serializer/ItemsSyntacticSequencer.xtend
./org.openhab.core.model.rule/src/org/openhab/core/model/rule/RulesStandaloneSetup.xtend
./org.openhab.core.model.rule/src/org/openhab/core/model/rule/formatting/RulesFormatter.xtend
./org.openhab.core.model.rule/src/org/openhab/core/model/rule/scoping/RulesScopeProvider.xtend
./org.openhab.core.model.rule/src/org/openhab/core/model/rule/validation/RulesValidator.xtend
./org.openhab.core.model.rule/src/org/openhab/core/model/rule/jvmmodel/RulesJvmModelInferrer.xtend
./org.openhab.core.model.rule/src/org/openhab/core/model/rule/serializer/RulesSemanticSequencer.xtend
./org.openhab.core.model.rule/src/org/openhab/core/model/rule/serializer/RulesSyntacticSequencer.xtend
./org.openhab.core.model.rule/src/org/openhab/core/model/rule/RulesRuntimeModule.xtend
./org.openhab.core.model.persistence.ide/src/org/openhab/core/model/persistence/ide/PersistenceIdeSetup.xtend
./org.openhab.core.model.persistence.ide/src/org/openhab/core/model/persistence/ide/PersistenceIdeModule.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/internal/GenericThingProvider.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingRuntimeModule.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/ThingStandaloneSetup.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/formatting/ThingFormatter.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/scoping/ThingScopeProvider.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/generator/ThingGenerator.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/validation/ThingValidator.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/serializer/ThingSyntacticSequencer.xtend
./org.openhab.core.model.thing/src/org/openhab/core/model/thing/serializer/ThingSemanticSequencer.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/ServiceTrackerThingActionsProvider.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/internal/engine/ServiceTrackerActionServiceProvider.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/ScriptRuntimeModule.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/formatting/ScriptFormatter.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/scoping/ScriptScopeProvider.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/ScriptStandaloneSetup.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/interpreter/ScriptInterpreter.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/validation/ScriptValidator.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/jvmmodel/ScriptJvmModelInferrer.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/serializer/ScriptSyntacticSequencer.xtend
./org.openhab.core.model.script/src/org/openhab/core/model/script/serializer/ScriptSemanticSequencer.xtend

That said, removing Rules DSL and DSL transformation from openhab-core means also removing Xtend and Xbase. It does not mean removig Xtext.

In fact Xtend/Xtext would be very fine, if it can run without the Rules grammar. That is to be able to write .xtend files in automation/jsr223/, which are either interpreted or converted to .java files, but do not have this rule NAME when … then … end grammar extras (or limitations).

Execting a rule to itself in parallel

As I see it, there are only two ways you can make it so the same rule can run more than once at a time, and this applies to all the languages.

  • Rules can only be stateless. No more variables, no cache (unless all that locking stuff gets straightened out), simply a few lines of code which neither depend upon nor save any data from one run to the next.
  • A separate instance (ScriptEngine) is created on the fly for every execution of a rule so if you have the same rule running twice, each has it's own thread, own ScriptEngine, and in all other ways are completely seprate from each other without anything shared. They are essentially two separate rules.

To run a rule in parallel would actually mean that the only thing a rule does is to start a new thread and execute a function in that thread. Then the rule returns (immediately), before the function in the thread has completed. This can be done even now and as it can be done now, the rules can run with the same amount of ScriptEngines as now and share data in the same way as now. The only drawback is that a rule, which returns immediately cannot return a value. But this value is useful only, if a …

Okay, above we talk about rules, but we mean rule actions. So if a rule has many actions, when each action returns, it passes data to the next action. And when the action returns immediately, then the next action cannot use the return value of the previous action. Except the previous action returned a Promise… and who needs several actions to a rule… That said, as it is now, as people can fire threads from (slow) rules and return immeditely, the same rule can run virtually in parallel to itself.

The other only drawback is that starting new threads/executing actions in thread pools will eat more resources than now.

However the above does not apply to transformations, as they need a String return value immediately.

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

@Nadahar
Copy link
Contributor

Nadahar commented Oct 14, 2025

To run a rule in parallel would actually mean that the only thing a rule does is to start a new thread and execute a function in that thread. Then the rule returns (immediately), before the function in the thread has completed. This can be done even now and as it can be done now, the rules can run with the same amount of ScriptEngines as now and share data in the same way as now. The only drawback is that a rule, which returns immediately cannot return a value. But this value is useful only, if a …

Okay, above we talk about rules, but we mean rule actions. So if a rule has many actions, when each action returns, it passes data to the next action. And when the action returns immediately, then the next action cannot use the return value of the previous action. Except the previous action returned a Promise… and who needs several actions to a rule… That said, as it is now, as people can fire threads from (slow) rules and return immeditely, the same rule can run virtually in parallel to itself.

The other only drawback is that starting new threads/executing actions in thread pools will eat more resources than now.

However the above does not apply to transformations, as they need a String return value immediately.

I don't understand what you're trying to say here. If you launch new threads (which you can basically do anywhere where you're allowed to "run code", those new threads aren't "rules". They are just threads, they don't interact with OH as part of a rule, they aren't treated as such, and you're "completely on your own" as to what these achieve. I don't think such "abusive techniques" need to be a part of the regular documentation.

If doing that serves a purpose for you, please go ahead and do it, but for me, it's very difficult to understand what I could achieve by doing something like this.

@rkoshak
Copy link
Contributor

rkoshak commented Oct 14, 2025

As for removing Rules DSL from OH, you can go tilt at that windmill. As far as I'm concerned, the developers/maintainers have spoken definitively. Rules DSL is not going to be removed from core.

To run a rule in parallel would actually mean that the only thing a rule does is to start a new thread and execute a function in that thread.

And those separate runs of the function share data. In fact, they don't just share data, they share memory.

step function 1 function 2
1 starts running
2 sets variable a to "foo"
3 starts running
4 sets variable a to "bar"
5 console.info(a)
6 console.info(a)

What gets logged in step 5? What gets logged in step 6? In both cases "bar". function 2 overwrote "foo" and now all executing functions only see "bar".

This is why you can't run the same script more than once at the same time except under the restrictions I outlined above.

And yes, this is exactly the sorts of things we had to deal with back when rules could run at the same time.

and who needs several actions to a rule…

Anyone using "simple UI rules" (i.e. using any action that isn't an "Inline Script"). Granted this is exceptionally limited right now but Scripts are not the only Rule Action.

@dilyanpalauzov
Copy link
Contributor Author

The focus of the current changeset is to write down what cannot run in parallel, what can run in parallel, what only sometimes runs in parallel - file based rules, UI rules, file-script, UI scripts, actions, profiles, profiles linked to scripting transformations, profiles linked to non-scripting transformations, scripting transformations, non-scripting transformations…

Currently it is not clear when a non-scripting transformation reuses a thread (runs sequentially - is started, after it finished) and when it runs in a new thread (parallel to itself).

But as an exception to what I just said:

developers/maintainers have spoken definitively. Rules DSL is not going to be removed from core.

why does openhab/openhab-core#3233 exist as it is?

@rkoshak
Copy link
Contributor

rkoshak commented Oct 14, 2025

why does openhab/openhab-core#3233 exist as it is?

Because he was also in favor of moving Rules DSL to an add-on. But the consensus of the rest of the maintainers was to leave it part of core. So that Issue was never worked and it probably should have been closed.

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.

4 participants