[No longer relevant on OH 3.0+] Design Pattern: Rule Latching

Edit: Updates for OH 4.

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.

No more than one instance of the rule running at a time

As of OH 3.0, the rule engine for all rules languages no longer allow this to happen. Each rule gets it’s own thread and if a rule that is actively running gets triggered again, the subsequent triggers will be queued up and worked off in order instead of there being two or more instances of the same rule running at the same time.

Therefore rule latching for this use case is no longer required.

Debounce

To handle this use case use Design Pattern: Rate Limit.

If you are on OH 2.5 or before, see the edit history for the old version of this post.

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.