Rule Error "null"

From my limited testing, it seems the same rule can be running at the same time if there are 2 events that trigger it? Especially using Member of rules. That would imply more than one thread?

Yes it would, 1 Thread for each rule running. If 1 rule is running twice then 2 threads… And so on

Are the variables in each running rule different ie thread safe or not?

Depends on how it’s declared but for the most part no.

is there way other than var / val to declare them to make them thread safe? what about iterators in a foreach loop?

Alternatively, I used a lock as per this thread. That solved the error.

1 Like

Global Val’s can be used with near impunity since you can’t change them. Global vars are not thread safe. Local vars will only exist in the installer if the Rule running so from that perspective they are thread safe in so far as each thread gets it’s own variable.

Be carful work locks. It is really easy to run into troubke with locks since it is possible for errors to occur that prevent finally from being called and your lock never unlocked.

Only Val’s are available to a forEach loop. If you want to create it summarize some data from all the members of the collection you should use a map reduce.

Hard to provide useful concrete help when answering general questions without code and items to look at.

1 Like

What about triggeringItem in a rule? If you are using the “member of” Group trigger and 2 members of that group change at nearly the same time what happens to the triggeringItem in the rule? Does each running version of the rule reference the correct triggeringItem or if the rule fires again does the triggeringItem change the one running in the rule that fired first if that rule is still running?

triggeringItem gets populated only for one rule’s context. So one instance of the rule will run with triggeringItem set to one of the items and another instance with triggeringItem set to the other item.

I’ve not looked in the code to verify this is the case but that is what I’ve observed.

Rick,

Can the code below be optimized? In short, I have an item that is a member of several groups and I want to get one of these groups for further use. The group I want is a member of another group and that is how I identify the group I want…

        var GroupItem area = null
        triggeringItem.getGroupNames.forEach [ i | // iterate over groups the triggering item is a member of 
            val b = gArea.members.findFirst [ a | a.name == i ] as GroupItem // find if the group is a member of gArea
            if (b !== null) {
                area = b // set area when we find the right group
            }
        ]

A lot more reading and trial and error … this seems to work :slight_smile:

val GroupItem area = gArea.members.findFirst [ a | a.name == triggeringItem.getGroupNames.findFirst [ i | a.name == i ] ]

More interesting, is the log error of

2018-09-21 10:13:31.368 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule ‘React on OccupancyControl change/update’: null

Is no longer present in the logger after shortening the above code to a single line. All the other code in the rule is the same. Makes me think that something in the rules DSL is not thread safe?

For the optimizations, often I’ve found the best optimization is a whole other approach (e.g. not using two sets of Groups at all) but it’s impossible to tell without more context.

It’s frustrating when users ask how to do something to solve a particular problem they have the way they have decided it needs to be solved without giving us the opportunity to explore other approaches that may sidestep the root cause of the problem in the first place.

null errors like that usually have something to do with type but it’s hard to say if that it’s there case here.

I can say this is one of the few case where the full collection of set operators would be useful because all you are doing here is taking the intersection of the two Sets.

I can say that the first approach won’t work because you can’t access or assign a var from within a forEach loop. Though I’m pretty sure that you would get asyntax error for that. Did you watch the logs when the .rules file was loaded?

I’m not at a computer so can’t test this out, but I think using something like this would work as well:

val GroupItem area = gArea.members.reduce[ result, area | result = if(triggeringItem.getGroupNames.contains(area.name)) area else result ]

Another error I have seen with this rule in the logger is

2018-09-24 10:58:01.751 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'React on OccupancyState changed': The name 'logInfo' cannot be resolved to an item or type; line 14, column 5, length 106

This seems to only happen on the first time the rule is executed. Otherwise the logging command works.

That is definitely the known startup timing bug. The Rules start executing before everything is ready. There are all sorts of work arounds that people apply like setting a timer and only moving the .rules files over to the rules folder after enough time has passed for everything else to be loaded and the like.

I try to avoid relying too much on System started Rules until this bug gets fixed.

These errors happen at systemstartup and also when editing and saving - ie on first load after changing the rule.

In addition, it seems the use group iterators are not thread safe. The only way to get completely rids of these errors was using locks in the rule.

Are theses in a lambda or in a rule proper?

in the rule proper.

this is one rule that will trigger it



import java.util.concurrent.locks.ReentrantLock

var ReentrantLock lock = new ReentrantLock()



// do actions when an area becomes occupied or vacant
// propagate state to parent or child areas 
 

rule "React on OccupancyState changed"
when
    Member of gOccupancyState changed    // your condition here
then

    try {
        lock.lock ()

        logInfo("Occupancy State Changed","Changed:  "+triggeringItem.name+"     State:    "+triggeringItem.state)

        // get the area that the triggering item belongs too
        val GroupItem area = gArea.members.findFirst [ a | a.name == triggeringItem.getGroupNames.findFirst [ i | a.name == i ] ]
        //logInfo("Occupancy State Changed","Area Name:  "+area)     

        // find the occupancy locking item
        val occupancylocking = area.members.findFirst [ i | (i.name.indexOf ("OL_") == 0 ) ]
        logInfo("Occupancy State Update","Occupancy Locking:  "+occupancylocking)

        if (occupancylocking != ON) {
            // Process any actions based on the tags for that area for change from OFF to ON ie vacant to occupited
            if (triggeringItem.state == ON) {
                // perform any actions
                triggeringItem.getTags.forEach [ i |
                    val np = i.toString.split (":")
                    if (np.get (0) == "OccupiedAction")
                        switch (np.get (1)) {
                            case "LightsOn": {
                                area.members.forEach [ i |
                                    i.getTags.forEach [ t | 
                                        if (t=="Lighting") {
                                            i.sendCommand (ON)
                                        }
                                    ]
                                ]
                            }

                        }
                ]
            }

            // OFF = vacant, do any vacant actions
            if (triggeringItem.state == OFF) {
                // Process any actions based on the tags for that area
                triggeringItem.getTags.forEach [ i |
                    val np = i.toString.split (":")
                    if (np.get (0) == "VacantAction")
                        switch (np.get (1)) {
                            case "LightsOff": {
                                area.members.forEach [ i |
                                    i.getTags.forEach [ t | 
                                        if (t=="Lighting") {
                                            i.sendCommand (OFF)
                                        }
                                    ]
                                ]
                            }

                            case "SceneOff": {
                                area.members.forEach [ i |
                                    i.getTags.forEach [ t | 
                                        if (t=="AreaAll") {
                                            i.sendCommand (OFF)
                                        }
                                    ]
                                ]
                            }
                            
                            case "ExhaustFansOff": {
                                area.members.forEach [ i |
                                    i.getTags.forEach [ t | 
                                        if (t=="Exhaust Fan") {
                                            i.sendCommand (OFF)
                                        }
                                    ]
                                ]
                            }

                            case "AVOff": {
                                area.members.forEach [ i |
                                    if (gAVPower.members.findFirst [ a | a.name == i.name] !== null)
                                        i.sendCommand (OFF)
                                ]
                            }

                        }
                ]
            }   
        }

        //update parent area group
        val GroupItem parentarea = gArea.members.findFirst [ a | a.name == area.getGroupNames.findFirst [ i | a.name == i ] ]
        //logInfo("Occupancy Status Changed","parent area  "+parentarea)     
        
        if (parentarea !== null) {
            val GenericItem poccupancystate = parentarea.members.findFirst [ i | (i.name.indexOf ("OS_") == 0 ) ]

            if (poccupancystate !== null) {
                // need to iterate all child areas and see if any are on
                var Boolean anyon = false 
                parentarea.members.forEach [ i | // iterate all members of the parent area
                    //logInfo("Occupancy Status Changed","parent area members  "+i)     
                    val childarea = gArea.members.findFirst [ a | a.name == i.name ] as GroupItem // find if the member is a member of gArea

                    if (childarea !== null) { // we have a child area
                        val GenericItem childoccupancystate = childarea.members.findFirst [ i | (i.name.indexOf ("OS_") == 0 ) ]
                        if (childoccupancystate !==null)
                            if (childoccupancystate.state == ON)
                                anyon = true
                    }
                ]

                if (anyon == true)
                    poccupancystate.postUpdate (ON)
                else
                    poccupancystate.postUpdate (OFF)
            }
        }     


        // if area is vacant, force child areas to vacant
        if (triggeringItem.state == OFF) {
            area.members.forEach [ i | // iterate all members of the area
                //logInfo("Occupancy Status Changed","parent area members  "+i)     
                val childarea = gArea.members.findFirst [ a | a.name == i.name ] as GroupItem // find if the member is a member of gArea

                if (childarea !== null) { // we have a child area
                    val GenericItem childoccupancystate = childarea.members.findFirst [ i | (i.name.indexOf ("OS_") == 0 ) ]
                    if (childoccupancystate !==null)
                        childoccupancystate.postUpdate (OFF)
                }
            ]
        }

        //gOccupancyState.members.forEach [ i | 
        //  logInfo("Occupancy States"," Name:  "+i.name+"     State:    "+i.state)
        //]
    }

    finally {
        lock.unlock ()
    }    
end  




1 Like

All I can guess is that gOccupancyState is received so many events so rapidly that the Rule is being triggered before the even the Rule itself is done being loaded.

If this rule truly is being triggered that rapidly and constantly I can almost guarantee that you will run out of Threads at some point and will see high latency in the rest of your Rules running or a complete halt of the rest of your Rules.

I would strongly recommend looking for an approach that doesn’t require the lock, moving the logic to JSR223 rules, or moving the logic completely outside of OH entirely.

It is likely all the events that happen at startup. I am trying the solution where rules are moved out of the rule folder during startup and then added back in 5 minutes later.

I understand that OH has these limitations - but the way it deals with these problems is not satisfactory. Error messages are cryptic, often don’t reference the offending file or the line number that caused the problem or no error at all, and sometimes stuff that doesn’t work.