JS Scripting (ECMAScript 262 Edition 11) concurrecy problems

I have the following ECMAScript 262 Edition 11 which has multiple triggers:

triggers:
  - id: "1"
    configuration:
      itemName: Test_Switch1
    type: core.ItemCommandTrigger
  - id: "2"
    configuration:
      itemName: Test_Switch2
    type: core.ItemCommandTrigger

JSScript:

async function performSomeAsyncTask() {
  console.info("Starting task");
  await new Promise(resolve => setTimeout(resolve, 2000));
  console.info("Task completed");
}

async function waitForCompletion() {
  console.info("Waiting for task");
  await performSomeAsyncTask();
  console.info("Waiting completed");
}

async function main() {
  try {
    await waitForCompletion();
  } catch (error) {
    console.error("Error:", error);
  }
}

main();

If two triggers receive a command almost simultaneously, the log looks like this:

2023-10-09 15:09:42.005 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Test_Switch1' received command ON
2023-10-09 15:09:42.070 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Test_Switch2' received command ON
2023-10-09 15:09:42.172 [INFO ] [nhab.automation.script.ui.778e94a830] - Waiting for task
2023-10-09 15:09:42.174 [INFO ] [nhab.automation.script.ui.778e94a830] - Starting task
2023-10-09 15:09:42.179 [INFO ] [nhab.automation.script.ui.778e94a830] - Waiting for task
2023-10-09 15:09:42.181 [INFO ] [nhab.automation.script.ui.778e94a830] - Starting task
2023-10-09 15:09:44.175 [INFO ] [nhab.automation.script.ui.778e94a830] - Task completed
2023-10-09 15:09:44.176 [INFO ] [nhab.automation.script.ui.778e94a830] - Waiting completed
2023-10-09 15:09:44.182 [INFO ] [nhab.automation.script.ui.778e94a830] - Task completed
2023-10-09 15:09:44.183 [INFO ] [nhab.automation.script.ui.778e94a830] - Waiting completed

You can see that the same rule is running in parallel.

How can I prevent this in JS Scripting to wait until the script in the rule is finished?

I would like the result to look like this. One is done after the other:

2023-10-09 15:00:27.535 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Test_Switch1' received command ON
2023-10-09 15:00:27.539 [INFO ] [openhab.event.ItemCommandEvent      ] - Item 'Test_Switch2' received command ON
2023-10-09 15:00:27.541 [INFO ] [nhab.automation.script.ui.778e94a830] - Waiting for task
2023-10-09 15:00:27.542 [INFO ] [nhab.automation.script.ui.778e94a830] - Starting task
2023-10-09 15:00:29.544 [INFO ] [nhab.automation.script.ui.778e94a830] - Task completed
2023-10-09 15:00:29.545 [INFO ] [nhab.automation.script.ui.778e94a830] - Waiting completed
2023-10-09 15:00:32.206 [INFO ] [nhab.automation.script.ui.778e94a830] - Waiting for task
2023-10-09 15:00:32.208 [INFO ] [nhab.automation.script.ui.778e94a830] - Starting task
2023-10-09 15:00:34.210 [INFO ] [nhab.automation.script.ui.778e94a830] - Task completed
2023-10-09 15:00:34.211 [INFO ] [nhab.automation.script.ui.778e94a830] - Waiting completed

You are trying too hard.

Don’t use async, await and promises. By using those you push the execution into the background and the rule exits.

function performSomeTask() {
  console.info("Starting task");
  java.Type('java.lang.Thread').sleep(2000);
  console.info("Task completed");
}

performSomeTask();

This isn’t a web page nor is it a Node module. There’s no event loop you need to wait around for (and in fact I wasn’t even aware that Promise even worked now, it didn’t use to.

If you want the rule to block while it’s running just write your straight forward code and while that code is running, OH will queue up subsequent runs of the rule and work them off in series.

If you do in fact want to run something in the background and not block the rule, the standard approach is to use setTimeout or use an openHAB Timer. Once advantage of using an openHAB Timer is it can be shared across rules using the shared cache.

In OH3.4 I had used java.util.concurrent.CompletableFuture. With JSScripting in OH4 this doesn`t work anymore, so I wanted to switch to async / await. But this is probably also not purposeful.

ECMA - 262 Edition 5.1:

var logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.core.model.script." + ctx.ruleUID);
var ZonedDateTime = Java.type("java.time.ZonedDateTime");
var ScriptExecution = Java.type("org.openhab.core.model.script.actions.ScriptExecution");
var CompletableFuture = Java.type("java.util.concurrent.CompletableFuture");


function exampleFunction1() {
  var future1 = new CompletableFuture();
  // Simulate an asynchronous operation
  ScriptExecution.createTimer(ZonedDateTime.now().plusSeconds(2), function() {
    logger.info("Completing Task 1");
    future1.complete("Result 1");
  });
  return future1;
}

function exampleFunction2() {
  var future2 = new CompletableFuture();
  // Simulate an asynchronous operation
  ScriptExecution.createTimer(ZonedDateTime.now().plusSeconds(3), function() {
    logger.info("Completing Task 2");
    future2.complete("Result 2");
  });
  return future2;
}

var future1 = exampleFunction1();
var future2 = exampleFunction2();

CompletableFuture.allOf(future1, future2).join();

logger.info("Both tasks completed");
logger.info("Result of Task 1: " + future1.get());
logger.info("Result of Task 2: " + future2.get());

I have rewritten this as follows:
ECMAScript 262 Edition 11:

var CompletableFuture = Java.type("java.util.concurrent.CompletableFuture");

function exampleFunction1() {
  var future1 = new CompletableFuture();
  // Simulate an asynchronous operation
  actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plusSeconds(2), function() {
    console.info("Completing Task 1");
    future1.complete("Result 1");
  });
  return future1;
}

function exampleFunction2() {
  var future2 = new CompletableFuture();
  // Simulate an asynchronous operation
  actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plusSeconds(3), function() {
    console.info("Completing Task 2");
    future2.complete("Result 2");
  });
  return future2;
}

var future1 = exampleFunction1();
var future2 = exampleFunction2();

CompletableFuture.allOf(future1, future2).join();

console.info("Both tasks completed");
console.info("Result of Task 1: " + future1.get());
console.info("Result of Task 2: " + future2.get());

Unfortunately this does not work, but blocks the rule.

How could I rewrite my old script to JSScripting?

Even CompletableFuture is way over complicated. Again, OH rules don’t run in an event loop. If you want something to run in the future, use createTimer or setTimeout. If you want the rule to block until the code has completed, don’t use anything. All that stuff is handled for you.

function exampleFunction1() {
    console.info("Completing Task 1");
}

function exampleFunction2() {
    console.info("Completing Task 2");
}

exampleFunction1();
exampleFunction2();
console.info("Both tasks completed");

That will call exampleFunction1 and exampleFunction2, blocking the rule while it’s running, meaning that if the rule gets triggered again it will queue up the subsequent triggers and work them off in series and in order.

That’s the use case you describe.

In the case where you really do want to run something in the background at a certain time, use setTimeout or createTimer to schedule execution of a function. This does not block the rule though. Usually subsequent triggers of the rule will look to see if the timer already exists and do something with it (cancel it, reschedule it, etc.).

function exampleFunction1() {
  console.info("Completing Task 1");
  cache.private.put('fun1Timer', null);
}

function exampleFunction2() {
  console.info("Completing Task 2");
  cache.private.put('fun2Timer', null);
}

if(cache.private.get('fun1Timer') !== null) {
  cache.private.get('fun1Timer').reschedule(time.toZDT('PT5M'));
}
else {
  cache.private.put('fun1Timer', actions.ScriptExecution.createTimer(time.toZDT('PT5M'), exampleFunction1());
}

if(cache.private.get('fun2Timer') !== null) {
  clearTimeout(cache.private.get('fun2Timer'));
  cache.private.put('fun2Timer', setTimeout(exampleFunction2, time.toZDT('PT5M').getMillisFromNow()));
}
else {
  cache.private.put('fun2Timer', setTimeout(exampleFunction2, time.toZDT('PT5M').getMillisFromNow()));
}

console.info("Both tasks scheduled");

All this work to schedule something into the background and block the rule until the background stuff is completed is overly complicated and completely unnecessary. This isn’t a node application or a browser where you have to deal with an event loop. This isn’t a J2EE application where access to Threads is available but tightly controlled.