Design Pattern: Rule Latching

It was never really recommended. The root problem is that errors can occur inside the try that cause exceptions that never filter back up unto the rule. As a result, the lock will never get unlocked, even if it’s in a finally clause. Furthermore, locks cause a rule to consume a runtime thread doing nothing but waiting for access to the lock. This runs the risk of running out of your runtime threads making it so no rules can run because all your threads are in use waiting for a lock, a lock that might not ever be released if there was an error.

That’s for OH 2. In OH 3 the problem is still there but a little less drastic. You can starve out individual rules instead of all your rules because each rule gets its own thread. Also, there is no need to use a lock in a single rule because in OH 3 only one instance of a given rule can run at a time anyway.

I realize your example is contrived, but I see two concerns with the presented approach:

  • The rule runs really fast so it is unlikely to ever happen that Item_1 changed again while the rule is still running processing the previous change. In short the lock here is adding danger and complexity to the system solving a problem that doesn’t exist. Before adding the lock did you ever actually see Rule 2 run while Rule 1 was still running?

  • Why split this into two rules in the first place? If the need for the lock is caused by the fact that it’s in two separate rules, it’s a far better approach to merge the rules and eliminate the lock than it is to add a lock. This is especially the case in OH 3 where you’ll get the locking for free for the one rule.

And this is also an example where contrived examples like this don’t really help us. As far as is written in this example, there is no reason at all for the lock in the first place. But presumably you have a set of rules where the lock was necessary for some reason. But because we don’t see that, we can’t offer other solutions which might work much better.

This is true for all rules in OH 3, regardless of how the rule is defined and what language the rule is defined in. It’s not just UI entered rules and not just Rules DSL. It’s a universal behavior.

Anyway, as rossko57 is stating, it’s really hard to offer much here in terms of debugging without more information. In the contrived example shown, there is no need for the lock at all so sidestep this problem and stop using the lock. Even with the rules separated as they are, as written there would be no need to use a lock.

One final note. Unfortunately that specific error is kind of a catchall error that Rules DSL throws when it can’t figure out how to coerce the arguments to compatible types. So it’s entirely possible that the problem is not that the lock is null at all. It might be something else weird going on. Try adding some logging. Log out what lock is. If it’s null it will log as null. If it’s a valid lock it will log out something else. The main thing we want to prove is whether or not lock is indeed null or something else is going on.

And for the record, a merged rule that doesn’t need the lock would be something like:

var Timer timer1

rule "The Rule"
when
    Item Item_1 changed
then
    timer1?.cancel()
    if(newState == ON) {
        timer1 = createTimer(now.plusSeconds(30), [ |
            ...
        ])
    }
end
1 Like

Thanks for your in detail explanation. I wasn’t aware that the threading model was changed between OH2 and OH3. This gives me a good starting point for reworking my rules and getting rid of the locks.
As you already guessed the example is a simplifaction. Currently I cannot share the real rules without approval of the owner of the system - sorry for that. Please find a slightly shortened version below (the debug log statements regarding the locks are new following your hints). The real rules cover multiple timers, so it is not as easy to combine them into one rule. But I will also take this into account as a possible way ahead.
Another good hint is that my conclusion that the lock must be NULL might be wrong. I will try to further investigate that.
Regarding the chance for race conditions for short running rules. I definitely had problems with multiple rules concurrently modifying those timers which lead to real errors. So I started to guard all timers with the lock pattern, wether there was a real problem or not. Maybe this is no longer necessary for OH3.

So thanks again.

import java.util.concurrent.locks.ReentrantLock

var ReentrantLock lock = new ReentrantLock()

var Timer einbruchTimer
var Timer alarmAktivierenTimer

/* Alarmanlage */

rule "Alarm mit Zeitverzögerung auslösen"
when
    Item Alarm_Ausloesen changed from OFF to ON
then
    if (lock == null) {
        logInfo("Debug", "Lock is null #1")
    }
    lock.lock()
    try {
        logInfo("Alarm", "Einbruch. Erzeuge Timer zur Alamierung.")            
        if (einbruchTimer !== null) {
            einbruchTimer.cancel()
        }
        einbruchTimer = createTimer(now.plusSeconds(30), [|
            if (Alarm_Aktiv.state == ON) {
                logInfo("Alarm", "Alarm ausgelöst.")
                sendBroadcastNotification("Alarm ausgelöst!")
                Alarmsirene_Flur_Keller_Schalter.sendCommand(ON)
... 
                createTimer(now.plusSeconds(300), [|
                    if (Alarm_Aktiv.state == ON) {
                        logInfo("Alarm", "Schalte Alarm nach 5min ab.")
                        sendBroadcastNotification("Alarm wird nach 5 Minuten automatisch beendet!")
                        Alarm_Ausloesen.sendCommand(OFF)
                    }
                ])
            }
        ])
    } finally {
        lock.unlock()
    }
end

rule "Alarm beenden"
when
    Item Alarm_Ausloesen changed to OFF
then
    if (Anwesend.state == ON && Alarmsirene_Flur_Keller_Schalter.state == ON) {
        sendBroadcastNotification("Alarm manuell beendet!")
    }
    Alarmsirene_Flur_Keller_Schalter.sendCommand(OFF)
...

end

rule "Alarm auslösen bei Einbruch"
when
    Item Kellertuer_Sensor changed to OPEN or
...
    Item Eye_OG_Lux_Alarm_OG changed to ON
then
    if (Alarm_Aktiv.state == ON) {
        Alarm_Ausloesen.sendCommand(ON)
    }
end

rule "Alarm EG/Keller sofort auslösen bei Einbruch"
when
    Item Kellertuer_Sensor changed to OPEN or
...
    Item Haustuer_Sensor changed to OPEN
then
    if (Alarm_Intern_EG_Aktiv.state == ON) {
        Alarm_Sofort_Ausloesen.sendCommand(ON)
    }
end

rule "Alarm OG sofort auslösen bei Einbruch"
when
    Item Fenster_Buero_Seite_Sensor changed to OPEN or
...
    Item Tuer_Schlafzimmer_Seite_Sensor changed to OPEN
then
    if (Alarm_Intern_OG_Aktiv.state == ON) {
        Alarm_Sofort_Ausloesen.sendCommand(ON)
    }
end

rule "Alarm intern EG/Keller/OG sofort auslösen"
when
    Item Alarm_Sofort_Ausloesen received command ON
then
    sendBroadcastNotification("Alarm intern ausgelöst!")
    Alarmsirene_Flur_Keller_Schalter.sendCommand(ON)
...
        createTimer(now.plusSeconds(300), [|
        logInfo("Alarm", "Schalte Alarm nach 5min ab.")
            if (Alarm_Intern_EG_Aktiv.state == ON || Alarm_Intern_OG_Aktiv.state == ON) {
                sendBroadcastNotification("Alarm intern wird nach 5 Minuten automatisch beendet!")
                Alarm_Sofort_Ausloesen.sendCommand(OFF)
            }
        ])
end

rule "Alarm intern EG/Keller beenden"
when
    Item Alarm_Intern_EG_Aktiv changed from ON to OFF
then
    if (Alarm_Sofort_Ausloesen.state == ON) {
        sendBroadcastNotification("Alarm intern manuell beendet!")
        Alarmsirene_Flur_Keller_Schalter.sendCommand(OFF)
...
    }
end

rule "Alarm intern OG beenden"
when
    Item Alarm_Intern_OG_Aktiv changed from ON to OFF
then
    if (Alarm_Sofort_Ausloesen.state == ON) {
        sendBroadcastNotification("Alarm intern manuell beendet!")
...
    }
end

...

rule "Alarmanlage mit Verzögerung aktivieren"
when
    Item Anwesend changed to OFF
then
    if (lock == null) {
        logInfo("Debug", "Lock is null #3")
    }
    lock.lock()
    try {
        logInfo("Alarm", "Erzeuge Timer zur Aktivierung der Alarmanlage.")

        if (echo_kueche.state == ON)    {
            if (Fenster_Gaestezimmer_Sensor.state == OPEN ||
...
                Kellertuer_Sensor.state == OPEN) {
                    echo_kueche_TTS_Volume.sendCommand(50)
                    echo_kueche_TTS.sendCommand('Die Alarmanlage wurde aktiviert, aber es sind noch Fenster oder Türen geöffnet')}
            if (Fenster_Gaestezimmer_Sensor.state == CLOSED &&
...
                Tueren_Fenster_OG.state == CLOSED &&
                Kellertuer_Sensor.state == CLOSED) {
                    echo_kueche_TTS_Volume.sendCommand(50)
                    echo_kueche_TTS.sendCommand('Die Alarmanlage wurde aktiviert')}
            }
       if (echo_wohnzimmer.state == ON)    {
            if (Fenster_Gaestezimmer_Sensor.state == OPEN ||
...
                Kellertuer_Sensor.state == OPEN) {
                    echo_wohnzimmer_TTS_Volume.sendCommand(65)
                    echo_wohnzimmer_TTS.sendCommand('Die Alarmanlage wurde aktiviert, aber es sind noch Fenster oder Türen geöffnet')}
            if (Fenster_Gaestezimmer_Sensor.state == CLOSED &&
...
                Kellertuer_Sensor.state == CLOSED) {
                    echo_wohnzimmer_TTS_Volume.sendCommand(65)
                    echo_wohnzimmer_TTS.sendCommand('Die Alarmanlage wurde aktiviert')}
            }

        if (alarmAktivierenTimer !== null) {
            alarmAktivierenTimer.cancel()
        }
        alarmAktivierenTimer = createTimer(now.plusSeconds(30), [|
            Alarm_Aktiv.sendCommand(ON)
        ])           
    } finally {
        lock.unlock()
    }
end


rule "Alarmanlage deaktivieren"
when
    Item Anwesend changed to ON
then
    if (lock == null) {
        logInfo("Debug", "Lock is null #2")
    }
    lock.lock()
    try {
        logInfo("Alarm", "Alarmanlage aus. Deaktivere Timer für Alamierung.")
        if (echo_kueche.state == ON)    {
            echo_kueche_TTS_Volume.sendCommand(50)
            echo_kueche_TTS.sendCommand('Die Alarmanlage wurde deaktiviert')}
        if (echo_wohnzimmer.state == ON)    {
            echo_wohnzimmer_TTS_Volume.sendCommand(65)
            echo_wohnzimmer_TTS.sendCommand('Die Alarmanlage wurde deaktiviert')}
        if (einbruchTimer !== null) {
            einbruchTimer.cancel()
        }  
        if (alarmAktivierenTimer !== null) {
            alarmAktivierenTimer.cancel()
        }
        Alarm_Aktiv.sendCommand(OFF)
        Alarm_Ausloesen.sendCommand(OFF)
    } finally {
        lock.unlock()
    }
end

Now with the log statements in place, the rules are working… strange. I can only see a warning about comparison with == vs ===. The null error is gone.
In the end it might be the best to get rid of the lock, for several reasons, I understood now.

Hi All

Please can you confirm that I am understanding correctly… Would the following rule work without causing an endless loop?

rule "Test"

when
    Item TestItem  changed
then {
    logWarn("testrule", "Rule Triggered")
    TestItem.postUpdate("")
    logWarn("testrule", "Test Item cleared")
}
end

As I recall with OH2.5 my system crashed when I tried to do this. Took me ages to recover - so too nervous to try again

If this will in fact work it will allow me to trigger my rule directly without the need for a “Proxy Switch” to trigger the rule once the Item changes.

Thanks as always
Mark

It depends. If TestItem changes to “Foo” the rule will trigger, then TestItem is updated to “” which causes TestItem to change which triggers the rule again. Then TestItem us updated to “” again which does not cause a change so the rule doesn’t trigger.

I don’t understand this at all and it smells of the XY Problem. But since the question nor this statement has nothing to do with rule latching I’ll leave that discussion to another thread, should you choose to open one. I’ll just note that in OH 3 there is a “play” button on each rule that lets you run the rule manually and it’s very much possible to have a rule call another rule.

Hi Rich.

Sorry. Did not mean to hijack your threat or cause offence.
I clearly misunderstand what Rule Latching implies… I just want my rule not to run twice if the item is cleared.

Will open another thread and see what info I can glean.

Cheers
Mark