Design Pattern: Recursive Timers

Tags: #<Tag:0x00007f745486ba30>

Problem Statement

Often one finds a situation where at the end of a triggered timer one wants to reschedule the timer to go off again. But if you do this in the normal way that means you need a full copy of the Timer’s body inside the Timer’s body inside the Timer’s body and so on and so on ad infinitum.

This DP is deprecated. Please see Design Pattern: Looping Timers for the current best way to handle this type of problem.

5 Likes

(I think there are still a few typos in above)

I’ve been playing with this recursive timer, to combine it with some countdown ideas stolen from here

My objective, to create a countdown timer for e.g. motion activated lights. Must be able to reschedule for that purpose. Cannot use “expire binding” methods as timer durations are variable depending on context – home/away etc.
Ordinary so far, but I also wanted to be able to inspect remaining time from rules for smarter behaviour - e.g. as my light zones may be triggered from different causes for different durations, I want a rule to able to see if an existing timer is longer or shorter than what is now proposed.

Using recursion, we create a one-minute timer that decrements a Number Item. Lambdas, closures, recursion - all gives me headaches! Eventually I figured out my major obstacle was that the actual timer in Rik’s version is anonymous, and so could not be cancelled or rescheduled from a rule. The uncancelled timer contains a snapshot of the ‘old’ counter, so trying to simply update counter value fails.
The trick is put both the recursive function in a hashmap (so it can call itself) and the timer it spawns into another (where rules can cancel it).

Here is my example for OH1;

test items

Number my_countdown "minutes counter [%s min]" 
Switch test_countdown "Simulate trigger or cancel"

you can put those on a sitemap to play

rules file

// an array of timers for each lighting zone
val Map<String, Timer> lightTimers = newHashMap
// an array of countdown functions, a workaround for rescheduling
val Map<String, Object> lightTickers = newHashMap

// lambda function for recursive countdown timers i.e.
// a timer that can re-call itself upon expiry for a periodic effect
// param1 = Number Item counter
// param2 = the timer map array (so it can be cancelled)
// param3 = the function array, needed for recursion
// param4 = recursive function itself
// We host it in a Map because the Timer will not have access to change the global var itself
// but can make a call on the Map if we pass it into the lambda to update the contents.

val org.eclipse.xtext.xbase.lib.Functions$Function4 doCountdown = [
	NumberItem myDelay,
	Map<String, Timer> timersMap,
	Map<String, Object> functionMap,
	org.eclipse.xtext.xbase.lib.Functions$Function4 makeTicker |
		val int ddelay = (myDelay.state as DecimalType).intValue
		timersMap.put(myDelay.name, null)  // destruct old
		if(ddelay > 0) {
			timersMap.put(myDelay.name, createTimer(now.plusMinutes(1)) [ |
				myDelay.postUpdate(ddelay-1)
				functionMap.put(myDelay.name, makeTicker.apply(myDelay, timersMap, functionMap, makeTicker))
                                ]
			)
		} else {
			functionMap.put(myDelay.name, null)
			logInfo("testing", "countdown complete")
                            // if you want to update Items here, you will need to pass those into the lambda as well
                            // I chose instead to have rules triggered from counter Item changing to 0
		}
]


rule "test trigger"
	when
		Item test_countdown changed
	then
		if (test_countdown.state == ON) {
			my_countdown.sendCommand(5)
		} else {
			my_countdown.sendCommand(0)
		}
end

// send an integer minute count for start or reschedule, or 0 for abort

rule "Timeout setting"
	when
		Item my_countdown received command
	then
		val oldTimer = lightTimers.get(my_countdown.name)
		if (oldTimer != null) {
			logInfo("testing", "cancel existing ticker") 
			oldTimer.cancel
			lightTimers.put(my_countdown.name, null)
			lightTickers.put(my_countdown.name, null)
		}
		if (my_countdown.state > 0) {
			logInfo("testing", "ticker startup")
			lightTickers.put(my_countdown.name, doCountdown.apply(my_countdown, lightTimers, lightTickers, doCountdown))
		} else {
			logInfo("testing", "countdown zeroed")
		} 
end

I do not think this is 100% bombproof - at the least there is a chance that testing for a null timer in the rule could happen in the instant where a previous function is between ‘ticks’ i.e. nulled the last timer and just about to set a new one. Not likely though; the result could be two countdown sequences fighting over one counter, but that should be a smaller problem than it sounds. Eventually it reaches zero!

1 Like

Yes, I think you are right. This whole DP needs a rewrite. I think there are better approaches overall that don’t require recursive timers.

This DP was written at a time when rescheduleTimer didn’t work.

When I rewrite it, I will put the Expire Binding example first. Remove the current example entirely or rewrite it as

var Timer timer = null

rule "Front Door changed"
then
    Item FrontDoor changed
then
    if(FrontDoor.state == OPEN) {
        timer = createTimer(now.plusHours(1), [ | 
            logWarn("doors", "The Front Door has been open for over an hour!")
            timer.reschedule(now.plusHours(1))
        ])
    }
    else timer?.cancel
end

great, didn’t think about this
thanks
I have to try it out right now. I ran out of lambda parameters (6) :slight_smile:

Work like a charm, thanks

@rlkoshak: I think there are a few mistakes (or missunderstandings on my side) in your timer based example:

  1. lambda function
timers.put(door.name, doorTimer.apply(door, timers, createDoorTimer))

shouldn’t that be

timers.put(door.name, createDoorTimer.apply(door, timers, createDoorTimer))
  1. Missing closing bracket at the end of createTimer (lambda function)

  2. rule “Front Door changed”

timers.put(FrontDoor.name, timers, createDoorTimer)

shouldn’t that be

timers.put(FrontDoor.name, createDoorTimer.apply(FrontDoor, timers, createDoorTimer))

to invoke your lambda function?

  1. rule “System started”
if(FrontDoor.state == OPEN) timers.put(doorTimers.apply(FrontDoor, timers, doorTimer))

shouldn’t that be

if(FrontDoor.state == OPEN) timers.put(FrontDoor.name, createDoorTimer.apply(FrontDoor, timers, createDoorTimer))

I’m picking up your timer example (instead of the Expire-Binding) because I’m trying to create an recursive timer with prolonged duration (first iteration time x, second 2x, third 3x…) - but for the time being I have no luck. Does someone have an idea what the problem is / a way to debug this code?

val Functions$Function5<ContactItem, Map<String, Timer>, Number, Number, Functions$Function5, Timer> createWindowTimer = [
  windowItem,
  timerMap,
  warningTime,
  repetitions,
  createWindowTimer |

    val String windowName = windowItem.name.toString
    val int warningTimeInt = warningTime.intValue * repetitions.intValue

    return createTimer(now.plusMinutes(warningTimeInt), [ |
        if (windowItem.state == CLOSED || repetitions > 3) {
            // reset the timer to null
            timerMap.put(windowName, null)
        } else {
            // notify me

            // Recreate the timer if the window is still OPEN
            timerMap.put(windowName, createWindowTimer.apply(windowItem, timerMap, warningTime, repetitions + 1, createWindowTimer))
        }
    ])
]

When the function is triggered I get the following error message:

Rule XXX: cannot invoke method public abstract java.lang.Object org.eclipse.xtext.xbase.lib.Functions$Function5.apply(java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object) on null

EDIT: Ok, found my mistake: I tried to call the function above from inside of another lambda function. Seems like that doesn’t work.

This DP is very old and due for a rewrite. There are better approaches (see Design Pattern: Cascading Timers and Design Pattern: Looping Timers). I would not recommend using this DP any longer, at least until I can rewrite it which will take a bit.

  1. That is correct, it should be createDoorTimer
  2. I’ll take your word for it. I don’t see it but have no doubt it is missing.
  3. I don’t remember any longer. Maybe. At one time I was storing the lambda itself in the map but that doesn’t seem to make sense here.
  4. Yes, same typo. At some point I decided on a new name (probably last time I edited the Rule) and wasn’t consistent.

Use Looping timers as I linked above. That will be a better approach I think.

Global vars and vals cannot see each other. If you have a lambda that needs to call another lambda, you must pass it as an argument.

Thx for your reply - I took time today to go through some of your DP’s - and they are great. Especially basics like Design Pattern: How to Structure a Rule will have me rework my rules.

Regarding my aimed for recursive & parameterized timer (variable duration multiplied by number of repetitions), I think that something building up on your example above is still a valid option.
The looping timer would have no (easy) way of counting the repetitions (to increase the duration of the next timer), the cascading timer doesn’t seem to fit my scenario.

On the other hand it might be possible to avoid a lambda function if I use a second map to keep count of the repetitions for each timer…:thinking:

Once again - thx for your help and your great DPs - and lets see what I can use from the different DPs to build a custom solution for my problem :grinning:

Yes there is:

var loopCount = 1
myTimer = createTimer(now.plusSeconds(loopCount), [ |
    // do stuff
    if(continue) {
        loopCount = loopCount + 1
        myTimer.reschedule(now.plusSeconds(loopCount))
    }
]

I’m 90% certain this will work as I have a memory of testing this at some point. The lambda get’s a copy of loopCount and because we are rescheduling it instead of recreating it loopCount + 1 get’s preserved until the next run of the Timer.

Don’t forget to combine DPs. They are not intended to be used in isolation. I have one Rule that uses five or six DPs in ten lines of code. :slight_smile:

You are completely right - I just tested your example and it works :grinning:
(By the way, it seems like you still dislike to close your timer-bracket :wink: (after the closing ‘]’ should be a ‘)’))

var Timer myTimer = null

rule "test01"
when
    Item Test received command ON
then
    var loopCount = 1
    myTimer = createTimer(now.plusSeconds(loopCount), [ |
        if(Test.state == OFF) {
            myTimer = null
        } else {
            logInfo("test", loopCount.toString)
            loopCount = loopCount + 1
            myTimer.reschedule(now.plusSeconds(loopCount))
        }
    ])
end
1 Like

Well, for the reply I’m typing from my phone. If a missing paren is all I’m missing I’m doing pretty good. :slight_smile:

For the original posting I have no excuse.

1 Like