What is Hubitat's thread safety model?

I have some questions about thread safety.

  1. Will Hubitat execute all of script's methods on a single thread, or multiple threads? Methods such as installed(), updated(), various event handlers.
  2. If multiple threads are calling my script, is there a preferred synchronization mechanism? SmartThings documentation suggests to not use synchronized yet fails to mention if something needs to be used instead. According to this thread it seems like @Synchronized is not working in Hubitat.

I tried looking at the documentation (both Hubitat and SmartThings), but failed to find the answers. Perhaps someone could point me to it if it's there?

Also looks like script is called from multiple threads after all, but isn't this a big issue that this is not mentioned in the docs?

Thanks!

Is there something extra I need to do to get response from people developing Hubitat?

Maybe want to try emailing support.

The forums are really more for the community than official support responses (although they do a good job of that too).

Thanks! I've done that a couple of days ago and also did not get a response.

1 Like

Is something not working on your hub?

Nope, but I'm about to change a script that uses 'synchronized' statement in its code. And I wonder why is it there at all.
And as mentioned in the thread I link to above, someone did have something like race condition in his experience.

So I just need to know this fundamental property of the script environment, because it affects almost every script.

Maybe Hubitat doesn't know the answer themselves, and that is why there is no response. :smile:

1 Like

Let's hope it's not the case, and they're just too busy answering other questions :slight_smile:

we know the answer, I'm just not the right person to answer it...

3 Likes

I knew y'all did. I was trying to goad an answer out of you. Which didn't work. Lol.

Never a better way to coax a response out of an engineer (or any highly technical person) than to insinuate or say they don't know the answer. :smile:

I'll go back under my bridge now. :slight_smile:

6 Likes
  1. In general, apps and drivers may be multi-threaded.
  2. @Synchronized is a complex topic. It might be possible to use it to protect against race conditions, if one knows exactly what one is doing. It's a subject beyond the scope of what can be answered here.
1 Like

Thanks for the response! But I have some more followup questoins then :slight_smile:

  1. Does this mean that every script method, that, say, accesses app state (or any other shared state), must be synchronised (I mean 'synchronised' keyword here, not '@Synchronised')? Unless people know exactly what they're doing.
  2. I'm guessing this includes 'updated()' app method too?
  3. What about installed(), uninstalled()? I guess as soon as they subscribe to calllbacks and untill they unsubscribe, there could be callbacks coming in parallel to them too?
  4. If that's the case, you might want to bring this to developers attention. I have a feeling most script developers are not aware about this fundamental issue. Unless, of course they totally are, and are not even discussing this on forums because it's so obvious.
  1. In general, since apps and drivers can be multi-threaded, race conditions are possible between simultaneous instances of the same app or driver, and obviously, this can be a problem. App state (and driver state) is not protected in any way, and can certainly cause issues. Apps also have access to atomicState, which is mostly protected as a practical matter, although not actually locked per se.
  2. Any method that runs asynchronously could be the source of multi-threaded execution. This certainly includes installed, updated, et al, as well as callbacks, event handlers and scheduled methods.
  3. Ditto.
  4. Developers with any experience on this platform (or on SmartThings) are most likely familiar with the issues raised by multi-threading. Many end users are as well, because it is not difficult to run into problems with Rules in Rule Machine --> a not infrequent topic when an error is thrown by a rule caused by this. We assume that app and driver developers are familiar with this topic in general, or quickly become familiar with it when they encounter it.

Hubitat is an event driven system, with multiple CPUs and multi-threading. Developer beware. User beware. It is not a synchronous environment.

8 Likes

I think we as developers are aware, I guess as a developer I’m not sure the best way to handle it. HE doesn’t have a framework that I have found that lets us add the synchronization needed when we know we have a shared resource and possible race. Meaning, what libraries, guidelines, and techniques should we use to handle it? I don’t see a clean way to add locks or semaphores for example though I’ve found some techniques that work...

2 Likes

Are you talking about an app or a driver? -- it makes a difference. Also, it would be much better if you gave some specifics rather than abstract notions. For example, why does a potential race condition arise in your use case?

Thanks for the clarification, but then I have even more questions :slight_smile:

  1. Is app's state thread safe? Is it safe to add/access different elements of the state from multiple threads? Is it more like HashMap or ConcurrentHashMap?
  2. Is it at least safe to assume that there are no methods running in parallel to installed() untill first subscribe()/schedule() call? Like, if state is not thread safe, is it ok to add elements to it in installed(), before any subscruptions, then only be reading them from callbacks, without extra locking?

Thanks!

As Bruce mentioned, is there a specific issue that you are having that we can help with?
Otherwise we really don't have anything else to add to this topic.

Ask yourself this: who calls installed() and in what circumstance? It sounds to me that you are over-thinking a hypothetical problem. As mentioned above, state is not safe. But, with respect to installed(), so what? installed() is called exactly once (unless your UI code for some reason calls it).

An app's state is loaded from the database when an app instance is started, and the changed elements are written to the database when it exits. This reveals it's lack of safety.

During the execution of installed(), before there are subscriptions, callbacks, or schedules, how else would the app be instantiated? It is not committed to the database until Done is hit the first time (try it for yourself).

When you hit Done in an app for the first time, it is committed to the database, installed() is called, it's subscriptions and schedules may become active for the first time depending on what installed() does. It is normal for installed() and updated() to look like this:

def installed() {
    initialize()
}

def updated() {
    unsubscribe()
    unschedule()    //if relevant
    initialize()
}

Notice that by doing this, the inactive condition of the app exists when initialize() is called. It is very common to do all sorts of housekeeping and state initialization in initialize().

3 Likes

Sure, I have two real examples of what I was trying to do. In one case I have Kevo locks. You cannot send them commands to quickly or the API times out. I have 2 locks that I wanted an RM rule to lock. I didn't want the user (rule creator) to have to remember to put pauses in place, that's bad API design. So I wanted my integration to have a queue and it would run commands at the speed their API allowed. I used a list in state. Problem is, when the "lock" command was triggered which would add to the queue, the queue would all get screwed up. I'd end up with missed entries, duplicates, etc. I had to implement a semaphore model to work around this (hubitat-kevo/Kevo_Plus_Integration.groovy at master · dcmeglio/hubitat-kevo · GitHub)

Next one, I was using SSDP and UPnP discovery to find HEOS devices on my network. Because of the async http calls the list of discovered devices wouldn't update correctly because the threads were stomping on eachother (https://community.hubitat.com/t/correct-technique-to-deal-with-concurrency-in-async-calls). I wound up having to rewrite it to be synchronous. I would have rathered keep it async (which is usually more performant for network IO) but I couldn't get it to work unless there was a way to add synchronization around accessing the device list.

Those were two actual problems I encountered building integrations for Hubitat, not hypotheticals. It'd be great if there were cleaner methods (maybe there are and I can't find them?) of synchronizing multi-threading. In short, it really comes down to how do we make sure accesses to state/atomicState don't have multiple threads racing with eachother.

@bravenel

2 Likes

This is an artifact of the lock, right? Shouldn't the lock API be handling this issue?

There are not "cleaner" methods to do this. If you solved the problem with synchronous HTTP then you have a viable solution.

With respect to state, it is not safe, period. With respect to atomicState, it is probabilistic due to the "atomic" nature of the state access (race condition is remotely possible). It would be fair to say that this is a corner of the platform where 100% guarantees do not exist. We are not likely to address this topic with an engineering effort any time soon.

1 Like