Design Pattern: Rule Latching

Tags: #<Tag:0x00007faedaa25220>

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.

Concept 1

image

Use the existence of a Timer to indicate whether or not the Rule is locked. Unlike Concept 2, in this case the only choice is to exit the Rule if the Timer exists (i.e. debouncing).

Example

var Timer latchTimer = null

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

    latchTimer = createTimer(now.plusMinutes(1), [ | latchTimer = null ]) // set the Timer back to null after the given time

    // Rule's code
end

Theory of Operation

When the Rule triggers it checks to see if there is a Timer running. If so, the Rule exits ignoring the event. If there is no Timer running, create one that only needs to set the Timer variable back to null when it goes off. After that, do the actions that you are trying to keep from occurring too fast.

Concept 2

image

This part of the design pattern is not my own but I cannot find the original post that describes it to give credit to the right person.

Use a ReentrantLock and require the Rule to acquire the lock before running. You have two choices, you can have the Rule wait for the lock to be released before running or you can have the Rule exit if it can’t acquire the lock.

Exiting if it can’t acquire the lock is preferable where possible because if there are only five Rule execution threads available at a time. If the number of Rules waiting for a lock pile up then all Rules will stop executing until the backlog of events is worked down, if ever.

Example

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

val ReentrantLock latch = new ReentrantLock

rule "Latched Rule"
when
    Item MyGroup received update
then
    try {
        // Wait for lock and run example
        latch.lock 
        // Rule's code

        // Skip the rule if the lock is already locked example
        if(latch.tryLock(0, TimeUnit.SECONDS)){
            latch.lock
            // Rule's code
        }
    }
    catch(Exception e){
        logError("latch", "Error occured in Latched Rule! " + e.toString)
    }
    finally {
        latch.unlock
    }    
end

Theory of Operation

When the Rule triggers try to acquire the lock. Once acquired run the Rules code. If there is any error log that error and always make sure the lock gets unlocked, even if there is an error.

Related Design Patterns

None identified

3 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.