Design Pattern: Rule Latching

Please see Design Pattern: What is a Design Pattern and How Do I Use Them for details on how to work with DPs.

Problem Statement

Sometimes one wants to make sure that only one instance of a Rule is running at a given time. For example, when a rule is triggered by a Group Item’s receiving updates, the Rule gets triggered more than once per event. Another use is if one needs to implement debouncing where events that take place within a certain time are ignored…

Concept 1: Debounce

image

Use the existence of a time stamp to indicate whether or not the Rule is locked. If the timestamp is in the future, ignore the rule trigger.

OH 3.x JavaScript

See the rate_limit library at GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules..

Usage:

var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
load(OPENHAB_CONF+'/automation/lib/javascript/community/rateLimit.js');

var runme = function() {
  // latched code goes here
}

this.rl = (rl === undefined) ? new RateLimit() : this.rl;

this.rl.run(runme, "24h"); // after the first run will wait 24 hours before runme is allowed to run again

OH 2.x Python

See the rate_limit library at GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules..

Usage:

import community.rate_limit import RateLimit

rl = RateLimit()

...

    # Only alert once a day
    if event.itemState < low:
        rl.run(lambda: send_alert("Remember to fill the humidifiers, {} is "
                                  "{}%".format(name, event.itemState),
                                  low_hum.log),
               days=1)

Rules DSL 3.x

var ZonedDateTime until = null

rule "Latched Rule"
when
    Item MyItem received update
then
    if(until !== null && until.isAfter(now)) return; // skip this event if the timer exists

    until = now.plusDays(1)

    // Rule's code
end

Rules DSL 2.5

var DateTime until = null

rule "Latched Rule"
when
    Item MyItem received update
then
    if(until !== null && until.isAfter(now)) return; // skip this event if the timer exists

    until = now.plusDays(1)

    // Rule's code
end

Theory of Operation

When the Rule triggers it checks to see if there is a time stamp and if so that the timestamp is for the future. If so exit the rule. If until is in the past or not yet set save the timestamp and run the code.

Concept 2: ReentrantLocks

Deprecated. Do not use ReentrantLocks in Rules DSL. See Why have my Rules stopped running? Why Thread::sleep is a bad idea for details. Use Design Pattern: Gate Keeper with a reasonable timeout instead.

If using JSR223 Rules in OH 2.x or any type of Rules in OH 3, it is safe to use a lock in the Rule because each rule has it’s own thread. However it’s pointless to do so as only one instance of a given rule can run at a time (i.e. the latching is done for you already).

Related Design Patterns

Design Pattern How Used
Design Pattern: Gate Keeper A way to prevent the same code

from executing more than once at a time.

Update: Added JavaScript implementation, updated for OH 3.

4 Likes

I had trouble in the past that the system did not try long enough to get a lock and the rule sometimes did not run. I modified this reentrant locks to try a little longer to get a lock and it seem to have helped. Now I may have been misunderstanding things, but could it be helpful to give more time to try to get a lock? Of course, there are other edge conditions to be considered for every application. But in case it helps anyone, here the code that worked for me

import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock

rule "My rule"
when
        System started or
        Item Date received update
then
var gotLock = lock.tryLock(2000, TimeUnit.MILLISECONDS)		
	try {
		if (gotLock) {
 // do stuff ( i cut out my rule here for brevity)	    
}		   
 		}
		else logInfo("My rule", "re-entry lock actived")
	}
	finally {
		if(gotLock){
			lock.unlock()
		}
	} 
end

Obviously the part I wanted to point out is this lock.tryLock(2000, TimeUnit.MILLISECONDS) (and the additionally imported library). I used this when I was on a raspberry and it seemed to make it more reliable;

This is actually something that is discussed in the ReentrantLock’s JavaDocs.

Part of the problem, as with everything involved with concurrency, is timing or requests and such. The tryLock method that doesn’t take the timeout says

Even when this lock has been set to use a fair ordering policy, a call to tryLock() will immediately acquire the lock if it is available, whether or not other threads are currently waiting for the lock. This “barging” behavior can be useful in certain circumstances, even though it breaks fairness. If you want to honor the fairness setting for this lock, then use tryLock(0, TimeUnit.SECONDS) which is almost equivalent (it also detects interruption).

The section discussing tryLock with the timeout says:

If this lock has been set to use a fair ordering policy then an available lock will not be acquired if any other threads are waiting for the lock.

So, given that, tryLock() causes the thread to jump to the front of the line which could theoretically starve out previous threads waiting to acquire the lock. By adding the timeout what you are doing is forcing the threads to behave nicely and go for the lock in order. I suspect this explains why adding it worked for you. I bet if you changed the 2000 to 0 it would still work as expected.

Thanks for posting! I’m going to change the OP to use the timeout version instead like your example.

Misses a closing brace :slight_smile:

if (latch.tryLock(0, TimeUnit.SECONDS)) {
1 Like

@rlkoshak Would you consider editing the example in the first post to:

import java.util.concurrent.locks.ReentrantLock

Copying the example as is gives an error.

Also, there’s a typo here

        logErrror("latch", "Error occured in Latched Rule! " + e.toString)

This whole DP is slated to be rewritten, but I’ve added the import for now.

1 Like

Thanks. I missed one.

import java.util.concurrent.TimeUnit

Alternative:

if ( !lock.isLocked )
{
    lock.lock()
    try {
        //TODO
        
        Thread::sleep(OneMinute*5)
    } finally {
        lock.unlock()
    }
}

Although I prefeer the timre variant …

You have to be very very careful using locks like this, and it’s one reason why I didn’t show that version above on purpose.

  1. If there is a type error inside the try block the finally block never runs and the lock will never be unlocked. After this rule gets triggered 5 more times all your rules will stop.

  2. If this rule triggers five times in five minutes for some reason, almost your other rules elm so until the first rule exits. And if your rule continues to trigger it will confine to starve all the rest of your rules.

It’s a really bad idea to block rules using a lock like this. Instead, use the Gate Keeper’s Queue example which will let you put the event on a queue that gradually gets worked off without consuming a rule runtime thread for each event awaiting processing.

OK. Got it. Between the isLocked() and lock() another lock can happen. In this case the second thread needs to wait the 5 minutes :face_with_raised_eyebrow:

Why is in the case of an type error the finally block with the unlock not called?

All I can say is that’s how the underlying Xbase/Xtend language libraries work. I can’t tell you why.

I studier it’s because the language is weakly typed running on a strongly typed language base (Java) when it cannot convert an object’s type to something usable it can’t proceed and execution must ends instead of the exception being thrown.

I have several rules modifing timers from different rules (see example below). To prevent race conditions I used a lock pattern. Sometimes I have the problem that the rules fail because lock is NULL. I think there is a problem with the initialization of the global var. On top of that I read here in the forum that it is no longer recommend to use reentrant locks. Is there a better pattern/solution available? Is there a difference in OH2 vs OH3.

import java.util.concurrent.locks.ReentrantLock

var ReentrantLock lock = new ReentrantLock()
var Timer timer1

rule "Rule 1"
when
    Item Item_1 changed from OFF to ON
then
    lock.lock()
  try {   
        if (timer1 !== null) {
            timer1.cancel()
        }
        timer1 = createTimer(now.plusSeconds(30), [|
            ...
        ])
    } finally {
        lock.unlock()
    }
end

rule "Rule 2"
when
   Item Item_1 changed from ON to OFF
then
    lock.lock()
    try {
        if (timer1 !== null) {
            timer1.cancel()
        }  

    } finally {
        lock.unlock()
    }
end

You might look at your real error message again. If it really mentions NULL it is to do with an Item state.

The error message is

Rule '...': cannot invoke method public void java.util.concurrent.locls.ReentrantLock.lock() on null

This happens both for Rule_1 and Rule_2.

Are you actually using OH3? It’s not clear.

Certainly edge-case timing will have changed in OH3, because you are operating in a different rule engine.

You don’t need any kind of lock for a single UI entered DSL rule - only one “copy” of a rule may execute at a time, further triggers queuing.
I’m not certain that also applies to file created DSL rules - but I expect it does.
So that’s one application for reentrant lock that is no longer needed.

That doesn’t help you much where you are trying to share a lock across multiple rules, though.

Further up this thread is a description of using tryLock with a timeout for better reliability.

There are usually lots of more or less complicated ways to avoid using locks. In the example you’ve given us, you’d simply combine those two rules into one.
Or post 1 gives a timer based approach.
We should probably know a bit more about what you are really doing and what problems it gives you to offer sensible suggestions.

However …

That would be a bad import, with a typo. locls ?

Yes typo, had only a screenshot. The error comes from a system not “under my control”, which is running OH2.

I do not see how this could help, if the root problem is that there is no lock instance at all (for a reason I don’t understand).

You asked for alternatives, you get alternatives.
But yes, you are looking at the correct problem. The import fails I reckon still.

I meant calling other methods on lock would also fail if the instance is null. What do you mean with “import fails I reckon still”. This is the original code:

import java.util.concurrent.locks.ReentrantLock

var ReentrantLock lock = new ReentrantLock()

var Timer einbruchTimer
var Timer alarmAktivierenTimer
...

There’s a limited number of ways for that to happen. You’re going to have to investigate when it happens to track it down further. We’re not told the circumstances; maybe you could test and log alerts if null at each rule run. That would change rule timings by itself of course, but that doesn’t matter in the example shown.