Design Pattern: Gate Keeper

The design is the same. That’s the nice thing with design patterns. The overall approach works the same no matter the version of OH and no matter the language.

Both the Rules code and the Python code above needs to be updated to work with OH 3. And I’ve not ported the library to JavaScript yet. It’s on my ever growing list of things to do but it’s far below completing the getting started tutorial so it will be some time before I get to it.

It should not be hard to update either the Python or the Rules DSL versions though. The main problem will be adjusting to work with ZonedDateTime. I’ll gladly accept a PR if someone gets to converting it before I do.

1 Like

Hello Rich,

I spent some time on a rather trivial problem: I wanted to detect multiple identical update events on an item within a short timespan (600ms) to interpret these event series as single, double or triple button presses. (The source of this event is a zigbee remote via zigbee2mqtt.)

I worked through your more than helpful design patterns and the Gate Keeper was just perfect for this.

During my tests I came to the conclusion that there is still a missing piece in your code, at least for being able to reliably work with short bursts of events: I think that the creation of the timer has to be protected by some kind of lock to make sure that one and only one instance of the timer is created.

Without this I regularly observed multiple timers being created at the first burst of events after I had changed and saved the “.rules”-File. This makes sense as the timer-null-check and the timer-creation are not one atomic operation.

I am not sure if this is a perfect solution but I used a ReentrantLock and wanted to share this approach:

val ConcurrentLinkedQueue<DateTime> recall1Queue = new ConcurrentLinkedQueue()
var Timer recall1QueueTimer = null
var int recall1PressCount = 0
var DateTime recall1FirstPressedAt = null
var timerCreateLock = new ReentrantLock()

rule "recall_1 pressed"
when 
    Item EG_TINT_REMOTE_ACTION received update "recall_1"
then
    recall1Queue.add(now)
    if(recall1QueueTimer === null && timerCreateLock.tryLock()) {
        recall1QueueTimer = createTimer(now) [|
            while(recall1Queue.peek !== null) { 
                val entry = recall1Queue.poll
                if (entry !==null) {
                    recall1PressCount = recall1PressCount + 1
                    if (recall1PressCount==1) {
                        recall1FirstPressedAt = now 
                    }         
                }
            } 
            if (recall1PressCount>0) {
                if ( new Duration(recall1FirstPressedAt, now).getMillis()>600) { 
                    logInfo("queue",recall1PressCount+"x pressed") 
                    if (recall1PressCount==1) {
                       //Single Click Action
                    } else if (recall1PressCount==2) {
                       //Double Click Action
                    }
                    recall1PressCount = 0
                }
            }
            recall1QueueTimer.reschedule(now.plusMillis(250))  
        ]
    }
end  

So far, it seems to be working reliably.

Take care,
Christian

P.S. Thanks again for your great contributions!

Thanks for posting!

That’s actually the opposite that the GK DP was written to address so I’m interested in how you used it. The DP was written to limit how quickly commands go out from openHAB to devices, not to detect how quickly the commands from devices are coming in. For that use case, a simple Timer approach might be easier to implement.

Are you running in OH 3 or OH 2? In OH 3 a reentrant lock is basically pointless because no more than one copy of a given rule can run at a given time. In OH 2 a reentrant lock is very dangerous so it is very much worth pursuing other options first. Though in your case you are using it in perhaps the safest manner possible.

One thing to note, OH is really poor at dealing with events in the sub-second time differences. It’s not a real time system and there are a number of limitations and parallel processing issues that can add many hundreds of msec or more latency to the processing of the events. So the general advice is to push this sort of detection as close to the device as possible. Ideally it should be on the device itself.

Anyway, looking at the code…

It looks like the first time the rule runs it acquires a lock. If the lock is free it creates a timer to run now, process off the queue, if the number of presses is over 0 do some other stuff and then reschedule the timer to run again in a quarter of a second. Subsequent triggers of the rule will fail to acquire the lock and just add to the queue. The Timer will work that off in time.

This is a pretty clever approach but it’s more complicated than it needs to be for OH 3 and it’s basically creating a busy wait loop.

In OH 3, as I mentioned, no two instances of a rule can run at the same time. So you don’t need the lock and can just set your timer without fear of more than one being created.

An alternative approach I would take would be to use timers instead of a busy wait loop. When the command is received, increment recall1PressCount. Then set a timer to decrement recall1PressCount after the amount of time you care about (600 msec?). You don’t need to keep track of these timers so no book keeping there is required. Finally, create a short timer to allow subsequent events to come in before acting on the button press counts.

Something like:

var recall1PressCount = 0 // avoid the use of primitives in Rules DSL as much as possible
var Timer recall1Timer = null

rule "recall_1 pressed"
when
    Item EG_TINT_REMOTE_ACTION received update "recall_1"
then
    // increment the count
    recall1PressCount = recall1PressCount + 1

    // decrement the count after 600 msec
    createTimer(now.plusMillis(650), [ | recall1PressCount = recall1PressCount - 1]) // In OH 3 you have to use now.plusNanos

    // Schedule a timer to allow the events to accumulate for 600 msec
    if(recall1Timer != null) {
        recall1Timer = createTimer(now.plusMillis(600), [ |
            logInfo("queue",recall1PressCount+"x pressed") 
            if (recall1PressCount==1) {
               //Single Click Action
            } else if (recall1PressCount==2) {
               //Double Click Action
            }
            recall1Timer = null
        ]
    }
end

You’ll have to decide what to do in cases where the event comes in three or more times in the 600 msec, but you still have to do that in your version as well (three clicks in 600 msec will be ignored as currently implemented in both versions). In my version there is a race condition where the decrement timer may run before the recall1Timer runs which is why I have it decrementing after 650 msec instead of 600. You’ll have to experiment with the difference to figure out which works most reliably. Again, OH is not realtime so what works for you may not work for others in terms of timing.

The advantage of this approach is that even with OH 2.5 you don’t need the lock and most importantly there is no busy wait loop consuming CPU and power doing nothing most of the time. It should also be a little more reactive as it strips out that 250 msec latency caused by the busy wait loop.

Thanks for posting this approach!

Thank you for answering. You brought up many good points.

I am still using OH 2.5, so race conditions within one rule are an issue.
I didn’t know that OH 3 serializes executions of the same rule - it’s good to know.

You are right: It would be ideal if the device had this capability. The remote itself doesn’t. The zigbee2mqtt-service might be able to offer this functionality, but I don’t know how. So I went this path, knowing that OH is not meant for real time processing.

and it’s basically creating a busy wait loop.

Yup, absolutely correct: This was bugging me, too.

This is an elegant solution: Every event initiates its own disposal through an anonymous timer.
There is still the race condition on the recall1Timer-creation, though. It probably will be created multiple times.

I built on your idea of a simple counter, got rid of the Queue and the ReentrantLock and used an AtomicInteger as a counter which also doubles as a poor man’s lock. I liked the idea of a cool down period and added one (of 400ms).

var atomicRecall1PressCount = new AtomicInteger(0) 
var Timer recall1Timer  = null

rule "recall_1 pressed"
when 
    Item EG_TINT_REMOTE_ACTION received update "recall_1"
then
    val newPressCount = atomicRecall1PressCount.incrementAndGet()
    if(newPressCount == 1) {
        recall1Timer  = createTimer(now.plusMillis(600)) [|  
				val finalPressCount = atomicRecall1PressCount.get()
				switch (finalPressCount ) { 
					case 1 : logInfo("testRecall", "single")
					case 2 : logInfo("testRecall2", "double")
 					case 3 : logInfo("testRecall2", "triple")
				}
				recall1Timer = createTimer(now.plusMillis(400)) [|  
				  recall1Timer = null
				  atomicRecall1PressCount.set(0);
				]
        ] 
    }
end  

I think this a quite a compact and simple solution.
Of course, it all depends on the atomicRecall1PressCount being reset to zero at the end of every series of events. Should the timer be interrupted somehow, the rule would be defunct from that point on.

Indeed but you could put a lock around just that line (with a try/catch/finally) to deal with that in OH 2.5. I’ve been using OH 3 for so long I don’t even think about those things anymore. Using the AtomicInteger for this is even better as we never have to worry about the lock becoming unlocked due to some error.

Though wouldn’t you want to test for newPressCount >= 1? You don’t want to recreate the Timer for the second press and beyond, only the first press, right?

I’ve never seen that happen in practice except in cases where a rule is reloaded. And in that case your counter will be recreated an reinitialized to 0 anyway. Though you’ll get errors in your logs from those orphaned timers. There really is no way around that in Rules DSL. In Python and JS text based rules you can define a “scriptUnloaded” function to cancel the timers when the file becomes unloaded. But in that case you would have to keep track of all the decrement Timers.

Correct, the idea is that exactly one timer is created at each start of a (potential) series of events.
This timer is the arbiter of the active series and resets everything at its end. It is created by the first event of the series which is easily determined by the value of the atomic integer after incrementing it.

I agree: I wasn’t able to implement the idea of a single timer per event series with a ReentrantLock. The code to unlock the lock had to be within the timer’s closure and led to a series of exceptions - which I didn’t understand.

Looking into Python in general and into its use in OH3 is definitely on my list. As a veteran Java developer getting into the Rules DSL wasn’t much of an obstacle. Learning Python will probably take much more effort for me. But: Live and learn! :slight_smile:

Thanks for the invigorating discussion!
– Christian

So then the code you posted above is incorrect. It tests for newPressCount == 1. When it’s 2 or more the Timer will be created again.

No, you wouldn’t want to release the lock inside the timer’s lambda. You are only trying to prevent more than one Timer being created. So you’d do something like:

try {
    myLock.lock
    if(recall1Timer != null){
        recall1Timer = createTimer(now.plusMillis(600), [ |
            // timer code
            recall1Timer = null
        ])
    }
}
catch Exception {
    // log out the error
}
finally {
    myLock.unlock
}

The only part of the code that is locked is the check whether the timer needs to be created or not and the creation of the Timer. So the first time the rule runs it acquires the lock, sees the timer doesn’t exist so it creates it. The second time the rule runs it has to wait for the lock. Once it acquires it it sees that the timer already exists so it doesn’t do anything.

Note that this is somewhat dangerous as there are some types of errors that can occur inside the try that do not rise up to the rule but can cause the rule to exit, leaving your lock locked forever. For this reason I think the AtomicInteger approach is a better approach over all. You just need to check for >=, not ==.

I’m not sure I understand your reasoning: 2 == 1 should return false, shouldn’t it? Therefore only the event incrementing atomicRecall1PressCount from 0 to 1 will be the one that creates the timer. All the other events of the current series will produce values higher than 1. Only after the timer and its subsequent cooling down timer have finished atomicRecall1PressCount will be reset to 0 and the next series of events can start.

Please excuse me should I have misunderstood your answer or a basic concept of the rules DSL. (I have flooded my code with logging statements and the output is consistent with my explanation above: I never see an extra set of timers being created during one series of events.)

Yes, of course. I hadn’t thought of that. In my attempts I was holding the lock before the creation of the timer and only unlocked it after the lambda’s execution.That always led to exceptions. Thanks for explaining that to me.

You’re right. Not sure what I was thinking.

Hello everybody,
I tried to use the Gatekeeper DP for my 433-blinds and thought I’d give python a try so that the gatekeeper could be used from any rule.
I’m struggeling to make it run by a (probably) simple problem (but i really searched anywhere: docs, examples, I tried any combination): Could somebody be so kind and give me a hint how (and where) to define the logger that is needed for the gatekeeper?
The version is oh3.1 and the patched library-helper is installed

The log-error is: SyntaxError: no viable alternative at input ‘Gatekeeper’ in /etc/openhab/automation/jsr223/python/personal/Rollo2.py

My code is basically the original example

from core.rules import rule
from core.triggers import when
from org.slf4j import LoggerFactory
from community.gatekeeper import Gatekeeper

gk = None
rollolog = LoggerFactory.getLogger("org.openhab.core.automation.Rollo2")
@rule("Rollo2")
@when("Time cron cron 0 0 8 * * ?")
@when("Item RolloProxy2OG received command DOWN")
def rollo2og(event):
    if not gk:
        global gk
        gk = new Gatekeeper(rollolog)
    else:
        gk.cancel_all()
    rollo2og.logInfo("start rollo2")
    gk.add_command("2s", lambda: events.sendCommand("RolloSchlaf1", "DOWN"))
    gk.add_command("2s", lambda: events.sendCommand("ROLLOschlaf2", "DOWN"))

Many Regards and thanks for everybody who helped me with their examples, tutorials…
Camo

A logger is automatically created for you when you use @rule. The logger is attached to the rule’s function. You don’t have to define the logger yourself. See Logging — openHAB Helper Libraries documentation

Hello All,

runtimeInfo:
  version: 3.1.0
  buildString: Release Build
locale: default
systemInfo:
  configFolder: /etc/openhab
  userdataFolder: /var/lib/openhab
  logFolder: /var/log/openhab
  javaVersion: 11.0.12
  javaVendor: Azul Systems, Inc.
  javaVendorVersion: Zulu11.50+19-CA
  osName: Linux
  osVersion: 5.10.60-sunxi
  osArchitecture: arm
  availableProcessors: 4
  freeMemory: 68282840
  totalMemory: 253845504
bindings:
  - serial

  userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
    like Gecko) Chrome/94.0.4606.61 Safari/537.36
timestamp: 2021-09-24T08:15:43.999Z

ı want to use this script for my group commands,

https://community.openhab.org/t/design-pattern-gate-keeper/36483#oh-3x-javascript-6

but I couldn’t figure out how to install and prepare this tool.
i read this article,

https://github.com/rkoshak/openhab-rules-tools
and this
https://github.com/rkoshak/openhab-rules-tools/tree/main/gatekeeper

and this
https://www.openhab.org/docs/configuration/jsr223.html#script-locations

I can’t find the automation folder’s location in OH3.

Can you explain in detail how to install and use the libraries on OH3?

I’m new to the Openhab ecosystem, I don’t know about scripts and libraries. I’ve been reading many topics and manuals, but I haven’t been able to put the platform in my head yet.

-Which method is faster
-Which method uses fewer resources,
-which method is stable and trouble-free.

I am an embedded system designer and code developer.
I am trying to learn which one is closer to me or more efficient.

Thank you for your time

It might not be created automatically. You can create it by hand though. On Linux

sudo -u openhab mkdir /etc/openhab/automation

Or when you follow the instructions to copy the library to your config, the needed folders will be created for you. Just cp -r openhab-rules-tools/gatekeeper/python/automation /etc/openhab.

Note that gatekeeper depends on time_utils so you need to copy that over too.

So the steps are:

  1. clone openhab-rules-tools
  2. cp -r openhab-rules-tools/time_utils/javascript/automation /etc/openhab
  3. cp -r openhab-rules-tools/gatekeeper/javascript/automation /etc/openhab

Done, the libraries are now installed.

The example in the readme for gatekeeper should show how to use it. At a high level, import the library, instantiate a Gatekeeper Object and use it. The test code is often helpful for examples as well.

If you run into trouble let us know.

Faster in what way? What methods are you really referring to? Between Scripts and Libraries? If so the question doesn’t make sense. Scripts are what are run in response to events. These are basically synonymous withe “rules”. Libraries, more properly called modules, don’t run at all on their their own. They must be loaded and used by a Script.

But in a home automation context, faster almost never matters. It’s far far more important to worry about legibility, simplicity, and maintainability than it is that your rules run fast. Even a really poorly run rule will run to completion in <100 msec which is more than fast enough for users to feel that then system is responsive (e.g. flipping the switch turns on the light).

Again, if it’s between scripts and libraries the question is nonsense. And again, in a home automation context this just doesn’t matter. Even when running on an RPi 2 or 3 you have ample CPU and RAM to be incredibly wasteful and have zero impact on the performance of your automations. So again, it’s far more important to make your code simple and maintainable than it is to worry about the resources it consumes.

Again, if it’s between scripts and libraries the question doesn’t make sense. However, if the question is modified to ask which rules language and which way of writing rules (text files or the UI) is more stable than the answer is neither. They are both pretty much the same in terms of stability. Which is more trouble-free? That’s hard to say. Right now I’d lean towards UI rules being more trouble-free but that was because of some bugs and such with text based rules that have mostly been fixed.

The biggest things I can suggest are:

Thank you, I will do.

I mean,
-DSL
-JS
-Python
comparing

thank you, your explain was quite enough and enlightening for me.

Hello Rick,
After a long break, I was able to return to automation and Openhab.

I have been trying to understand and understand the subject of Gatekeeper for 3 days, but I’m confused!

I want to put a delay between the commands for collective group commands, like those made for 433 MHz.
Openhab sends too many commands in collective commands very quickly and in a series. I have to make analyzes in the meantime and I need to make transformations between data types. The MCU on the hardware side (communicates with Openhab via Serial Bridge) is insufficient.
As a result, I have to save time for MCU and I need to put delays between the commands.

“Openhab-Rules-Tools” came directly from Openhabian.
But I couldn’t see/find an example or template made with JS for Gatekeeper.
I could not solve the problem by looking at the source file.

As a result, I did not understand how to use the “Openhab-Rules-Tools” library.

Will I write a new script using the library?
Should he come under “settings/scripts/”? (I don’t have a template either)
and
Under “Settings/Addons/Automation” I could not find a template for Gatekeeper and so I couldn’t add it.

If you help, I would like to open a title that can be used as a reference for people who are new and do not have knowledge/experience, and make this title a step-by-step guide for Gatekeeper.

My system is as follows:

Runtimeınfo:
   Version: 3.3.0
   Buildstring: Release Build
Locale: TR-TR
Systemınfo:
   Configfolder: /Etc /Openhab
   Userdatafolder:/VAR/Lib/Openhab
   logfolder:/var/log/openhab
   Javaversion: 11.0.16
   Javavendor: Ubuntu
   Osname: Linux
   Osversion: 5.15.48-Sunxi
   OSARCHitecture: ARM
   AvailaLebleprocessors: 4
   Freememory: 32443112
   Totalmemory: 194904064
   Startlevel: 100
Bindings:
   - denonmarantz
   - LIRC
   - Serial
   - Telegram
.
.
...
Timestamp: 2022-12-06T10: 23: 37.286z

Yes. It’s a library that you import into your scripts/rules. It’s standard JavaScript so you import it in the standard JavaScript way.

No, libraries do not appear in the UI anywhere. Only on the file system.

You might do well to go through a few JavaScript tutorials because for the most part, the OH docs will not cover the basics like how to import a library in depth. Importing an using a library is one of those basic JavaScript things.

I do notice that this is one of the several design pattern tutorials I need to update for JS Scripting but the usage example in the OP is still useful.

The first part (find and install) was done for you by openHABian so we can skip to the usage example.

If you break down the usage example, the first two lines import the library, third line creates the GateKeeper and the last two lines show adding a command to run with a 1 second delay following it and cancelling all scheduled commands.

If you look at the JS Scripting add-on’s docs it says

Scripts may include standard NPM based libraries by using CommonJS require . The library search will look in the path automation/js/node_modules in the user configuration directory.

Searching further down the page reveals a few examples of require but if you need more to understand it, there are many tutorials on the internet (e.g. How to use require() in ECMAScript modules - DEV Community).

Additionally, the openhab_rules_tools readme states:

Be sure to look at the comments, the code, and the tests for further details on usage and details.

A perfect example can be found in the Gatekeeper tests for how to import exactly the Gatekeeper itself. From that we find the require statement that imports Gatekeeper.

var { gatekeeper } = require('openhab_rules_tools');

Since you are running a version of OH prior to this past week, you’ll have to use the cache to save your Gatekeeper between runs of the rule, if you are creating rules in the UI.

var gk = cache.get(ruleUID+'_gk', () => new gatekeeper.Gatekeeper());

In any version of OH past 3.4 M5 you’ll use the privateCache or sharedCache which I believe will look something like this:

var { privateCache } = require('@runtime');

var gk = privateCache.get(ruleUID+'_gk', () => new gatekeeper.Gatekeeper());

Then you just use it. The first argument to addCommand is the amount of time to wait (supports anything supported by time.toZDT(), see JavaScript Scripting - Automation | openHAB) after executing the passed in command before executing the next one .

You may need to install the latest openhab-js library for openhab_rules_tools to work on an version of OH that old though. You can do that from openhabian_config.

1 Like

Hi All,
I am trying the Gatekeeper Script and test example for a week but I am facing the below error, again by again.
my system configuration is this.

runtimeInfo:
  version: 3.4.0.M6
  buildString: Milestone Build
locale: tr-TR
systemInfo:
  configFolder: /etc/openhab
  userdataFolder: /var/lib/openhab
  logFolder: /var/log/openhab
  javaVersion: 11.0.17
  javaVendor: Ubuntu
  osName: Linux
  osVersion: 5.15.80-sunxi
  osArchitecture: arm
  availableProcessors: 4
  freeMemory: 113579352
  totalMemory: 258334720
  startLevel: 100
bindings:
  - denonmarantz
  - lirc
  - serial
  - telegram
clientInfo:
  device:
    ios: false
    android: false
    androidChrome: false
    desktop: true
    iphone: false
    ipod: false
    ipad: false
    edge: false
    ie: false
    firefox: false
    macos: false
    windows: true
    cordova: false
    phonegap: false
    electron: false
    nwjs: false
    webView: false
    webview: false
    standalone: false
    os: windows
    pixelRatio: 1
    prefersColorScheme: light
  isSecureContext: false
  locationbarVisible: true
  menubarVisible: true
  navigator:
    cookieEnabled: true
    deviceMemory: N/A
    hardwareConcurrency: 8
    language: en-US
    languages:
      - en-US
      - en
      - tr
    onLine: true
    platform: Win32
  screen:
    width: 1920
    height: 1080
    colorDepth: 24
  support:
    touch: false
    pointerEvents: true
    observer: true
    passiveListener: true
    gestures: false
    intersectionObserver: true
  themeOptions:
    dark: dark
    filled: true
    pageTransitionAnimation: default
    bars: light
    homeNavbar: default
    homeBackground: default
    expandableCardAnimation: default
  userAgent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML,
    like Gecko) Chrome/108.0.0.0 Safari/537.36
timestamp: 2022-12-25T20:30:22.184

and the Gatekeeper test code is;

var { time } = require('openhab');
var { gatekeeper } = require('openhab_rules_tools');
var logger = log('rules_tools.Gatekeeper Tests');

var testFunGen = (test, num) => {
  logger.debug('generating function for {} run{}', test, num);
  return () => {
    logger.debug('{}: Test {} ran', test, num);
    cache.put(test + '_run' + num, time.toZDT());
  };
}

logger.info('Starting Gatekeeper tests');

var reset = (test) => {
  logger.debug('resetting {}');
  cache.put(test, null);
  logger.debug('resetting {}', test + '_start');
  cache.put(test + '_start', null);
  for (var i = 1; i <= 4; i++) {
    logger.debug('resetting {}', test + '_run' + 1)
    cache.put(test + '_run' + i, null);
  }
}

// Test 1: Scheduling
var gk1 = new gatekeeper.Gatekeeper();
var TEST1 = ruleUID + '_test1';
reset(TEST1);
cache.put(TEST1 + '_start', time.ZonedDateTime.now());
gk1.addCommand('PT1s', testFunGen(TEST1, 1));
gk1.addCommand('PT2s', testFunGen(TEST1, 2));
gk1.addCommand('PT3s', testFunGen(TEST1, 3));
gk1.addCommand(500, testFunGen(TEST1, 4));

actions.ScriptExecution.createTimer(time.toZDT('PT6.51s'), () => {
  var success = true;
  const start = cache.get(TEST1 + '_start');
  const run1 = cache.get(TEST1 + '_run1');
  const run2 = cache.get(TEST1 + '_run2');
  const run3 = cache.get(TEST1 + '_run3');
  const run4 = cache.get(TEST1 + '_run4');
  if (start === null) {
    logger.error('{} Failed to get starting timestamp', TEST1);
    success = false;
  }
  if (success && run1 === null) {
    logger.error('{} run1 failed to run!', TEST1);
    success = false;
  }
  if (success && run2 === null) {
    logger.error('{} run2 failed to run!', TEST1);
    success = false;
  }
  if (success && run3 === null) {
    logger.error('{} run3 failed to run!', TEST1);
    success = false;
  }
  if (success && run4 === null) {
    logger.error('{} run4 failed to run!', TEST1);
    success = false;
  }

  if (success) {
    logger.info('\n{}\n{}\n{}\n{}\n{}', start.toString(), run1.toString(), run2.toString(), run3.toString(), run4.toString());
    const dur1 = time.Duration.between(run1, run2).seconds();
    const dur2 = time.Duration.between(run2, run3).seconds();
    const dur3 = time.Duration.between(run3, run4).seconds();

    if (start.isAfter(run1)) {
      logger.error('{} failed, run1 ran before start!', TEST1);
      success = false;
    }
    if (success && dur1 != 1) {
      logger.error('{} failed, time between run1 and run2 is {} seconds.', dur1);
      success = false;
    }
    if (success && dur2 != 2) {
      logger.error('{} failed, time between run2 and run3 is {} seconds', dur2);
      success = false;
    }
    if (success && dur3 != 3) {
      logger.error('{} failed, time between run3 and run4 is {} seconds', dur3);
    }
    if (success) {
      logger.info('Gatekeeper test 1 success!');
    }
  }
});

// Test 2: cancelAll
var gk2 = new gatekeeper.Gatekeeper();
var TEST2 = ruleUID + '_test2'
reset(TEST2);
gk2.addCommand('PT1.5s', testFunGen(TEST2, 1));
gk2.addCommand('PT2s', testFunGen(TEST2, 2));
gk2.addCommand('PT3s', testFunGen(TEST2, 3));
gk2.addCommand(500, testFunGen(TEST2, 4));

actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plus(2750, time.ChronoUnit.MILLIS), () => {
  var success = true;
  const run1 = cache.get(TEST2 + '_run1');
  const run2 = cache.get(TEST2 + '_run2');
  const run3 = cache.get(TEST2 + '_run3');
  const run4 = cache.get(TEST2 + '_run4');

  if (!run1) {
    logger.error('{} failed, run1 did not run', TEST2);
    success = false;
  }
  if (success && !run2) {
    logger.error('{} failed, run2 did not run', TEST2);
    success = false;
  }
  if (success && run3) {
    logger.error('{} failed, run3 ran too soon', TEST2);
    success = false;
  }
  if (success && run4) {
    logger.error('{} failed, run4 ran too soon', TEST2);
    success = false;
  }
  if (success) {
    gk2.cancelAll();
    actions.ScriptExecution.createTimer(time.toZDT('PT4s'), () => {
      var success = true;
      const run3 = cache.get(TEST2 + '_run3');
      const run4 = cache.get(TEST2 + '_run4');

      if (run3) {
        logger.error('{} failed, run3 ran after being cancelled');
        success = false;
      }
      if (success && run4) {
        logger.error('{} failed, run4 ran after being cancelled');
      }
      if (success) {
        logger.info('Gatekeeper test 2 success!')
      }
    });
  }
});

and the error is;

2022-12-21 00:26:03.560 [ERROR] [rg.apache.cxf.jaxrs.utils.JAXRSUtils] - No message body reader has been found for class java.util.Map, ContentType: application/octet-stream
2022-12-21 00:26:03.617 [WARN ] [s.impl.WebApplicationExceptionMapper] - javax.ws.rs.WebApplicationException: HTTP 415 Unsupported Media Type
	at org.apache.cxf.jaxrs.utils.JAXRSUtils.readFromMessageBody(JAXRSUtils.java:1473)
	at org.apache.cxf.jaxrs.utils.JAXRSUtils.processRequestBodyParameter(JAXRSUtils.java:950)
	at org.apache.cxf.jaxrs.utils.JAXRSUtils.processParameters(JAXRSUtils.java:881)
	at org.apache.cxf.jaxrs.interceptor.JAXRSInInterceptor.processRequest(JAXRSInInterceptor.java:215)
	at org.apache.cxf.jaxrs.interceptor.JAXRSInInterceptor.handleMessage(JAXRSInInterceptor.java:79)
	at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308)
	at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121)
	at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:265)
	at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234)
	at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208)
	at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160)
	at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:225)
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:298)
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doPost(AbstractHTTPServlet.java:217)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:273)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:799)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:550)
	at org.ops4j.pax.web.service.jetty.internal.HttpServiceServletHandler.doHandle(HttpServiceServletHandler.java:74)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:600)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1624)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1440)
	at org.ops4j.pax.web.service.jetty.internal.HttpServiceContext.doHandle(HttpServiceContext.java:294)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:501)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1355)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
	at org.ops4j.pax.web.service.jetty.internal.JettyServerHandlerCollection.handle(JettyServerHandlerCollection.java:90)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
	at org.eclipse.jetty.server.Server.handle(Server.java:516)
	at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:487)
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:732)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:479)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
	at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:338)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:315)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:173)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:409)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034)
	at java.base/java.lang.Thread.run(Thread.java:829)

Do you have any suggestions or help?

Thank you…

Please upgrade to the 3.4.0 release!

Hi Florian,

already upgraded it.