Jython, threading, locking and debouncing

I’m just starting to convert my rules to the experimental rule engine/Jython, and have discovered something that surprised me, but is probably obvious to anyone who knows more about programming than I do.

With the rules DSL, say I have a rule triggered by a switch, and the rule takes one second to run. If the switch goes ON ten times very quickly (<1s), then there will be ten threads running at around the same time, often with undesirable effects (e.g. DSL running out of threads). So debouncing is required - e.g. checking a locking variable at the start of the rule.

Under Jython, I’m seeing completely different behaviour. The rule still fires ten times, but does so sequentially (with thread 2 only starting when thread 1 is completed, and so on).

Presumably this means I have to implement debouncing differently - e.g. using a timestamp as in this design pattern.

I’ll need to think about other impacts as I migrate my old DSL rules. Any thoughts/advice would be helpful. I can’t see any references to this difference in the documentation, but I may have missed something…

Dan

I don’t think I have put anything about this in the docs… I’ll get something in there… but I’ve definitely mentioned this in the forum. In the NGRE, each rule action will run sequentially. Meaning, if a rule triggers 100 times, there will be 100 actions that will be executed one after the other. If you need a rule action to take a while during execution or to run asynchronously, then just give it a new thread. I use thread.start_new_thread for situations like adding audio alerts to a Queue() while another audio alert is playing. I’ll get this into the docs too, but ask if you want some more detail.

2 Likes

Thank you!

I guess two questions:

  1. would be great to see an example of thread.start_new_thread, if you have one to hand.

  2. not sure how I should cope with debouncing for the extreme case where a switch receives a very large number of inputs - say 100 times due to a dodgy electrical contact. Is there any way for the rule to check the time of the original trigger? So, for example, the rule could exit immediately if it’s more than two seconds since the switch went ON.

Dan

I have an Item that I update with text, which then gets processed through TTS and played as an announcements on speakers around the house. There is a small delay while getting the speakers in and out of multiroom audio mode (Sony’s HomeShare “party mode”). If I did not use start_new_thread, this rule would sit and wait for the audio alert to complete, which would include starting and stopping the party for each alert. At the other end in the audio_alert function, I loop through the queue until it is empty. In this way, I can keep feeding things into the queue, while keeping the existing party looping in a separate thread. One party fed asynchronously! Bumpity bumpity bumpity…

from thread import start_new_thread
from personal.utils import audio_alert, ALERT_QUEUE

@rule("Alert: Audio notification")
@when("Item Audio_Notification received command")
def audio_notification(event):
    if event.itemCommand.toString() not in ALERT_QUEUE.queue:
        ALERT_QUEUE.put_nowait(event.itemCommand.toString())
        audio_notification.log.debug(u"added to queue\n{}".format(event.itemCommand))
        if ALERT_QUEUE.qsize() == 1:
            audio_notification.log.warn(u"started audio_alert\n{}".format(event.itemCommand))
            start_new_thread(audio_alert, ())
    else:
        audio_notification.log.warn(u"alert already in queue\n{}".format(event.itemCommand))

The important bit for you is…

start_new_thread(audio_alert, ())

The audio_alert is the function being called and the () is an empty set of arguments for the function.

To get the time of the last update, you could use persistence with an everyUpdate strategy, set a variable, or use a timer. I’ve never run into a need for something like this. If you post a specific example, then I’d put something together for you. It may be something useful to add to the helper libraries.

2 Likes

thank you!

re. debouncing - possibly an easier way is to rethinking locking/timestamps entirely

in the rules DSL, I would use timestamps - i.e. at the start of each rule I’d check how long it was since the rule was last run, and if last run recently I would exit the rule

in Jython, may make more sense to set a timestamp when a rule finishes. Then at the start of the rule I can check how long it is since the same rule finished. If <1s, that suggests there has been a queue of triggers building up, and I should exit the rule.

1 Like

I’ve had a go making a simple debouncing decorator function to stop rules running multiple times sequentially: here

1 Like

I hope my point does not seem disruptive but I firmly believe that debouncing / blocking / waiting patterns should be avoided for smooth interoperation in systems and that items must accept any command at anytime (and be wrap into virtual proxy items to do so if needed)

I would address your ring repeat issue using a virtual_ring item with 2 states IDLE and RINGING, and which accepts the command RING. (below pseudo-code written on the fly)

rule "Ring processor"
when 
   Item virtual_ring received command RING
then
    if(virtual_ring.state==IDLE) {
        virtual_ring.postUpdate(RINGING)
        real_ring.sendCommand(ON) //whatever needs to be done on the real item to trigger door ring
        ringTimer?.cancel()
        ringTimer=createTimer(ringDuration)[ virtual_ring.postUpdate(IDLE) ]
    }
end

My pseudo code is DSL. This also the reason of my posting: I just installed JSR/Jython and being “barely fluent” in Python and would like to catch the real architectural benefits of Jython over DSL before I reimplement all my stuff. (I insist on architectural benefit : I am aware of gain in speed, flexibility of rule programming… )

Is my implementation incorrect and raising other issues that make a debouncing method mandatory? Anyhow, I am sure this new decorator helps in other situations

that’s interesting - I’ll think about it!

Fundamentally I was confused by the serial nature of the way rules are now triggered, but your solution may be neater!