This was in release notes but way down, so I figured it deserves a special mention here in Developers section. So... singleThreaded is a boolean option that belongs in app/driver definition, with default value of false, meaning apps/drivers behave the way they always did. If set to true, though, hub runs all methods for a particular instance of app/driver sequentially, one at a time. It will load app/driver instance data (including state) run a method, and save the data (including state) completely before moving on to the next method call. This applies to top level methods only and not to calls made by app/driver methods.
This feature has less overead than using atomicState. You can think of app/driver method as running in a single transaction, always committed at the end, even if method threw an exception.
What happens if multiple threads attempt to interact simultaneously with a singleThreaded app or driver?
This seems like a major simplifying feature for certain drivers with challenging concurrency issues, but I'm wondering how the callers might need to be modified to account for any changes in behavior (need to retry or wait longer and the like).
If calls originate from the hub core software, it would line those requests up in a queue and run them one at a time, first in first out.
Good point about the non-singleThreaded callers, those will have their own (non-singleThreaded) execution context, so single threaded logic will not apply to invoked app/driver. It will be just another call. Off the top of my head, I can quickly add a way to check whether something is running in single threaded context or not, but fully implementing a transactional approach for internal calls will require a more careful evaluation.
What about calls to other methods from the same driver? Imagine an async handler (I assume this originates from the hub core software) that uses a utility method which updates State. Can I assume that singleThreaded behaviors apply to that invocation of the utility method?
A runIn() scheduled method or async handler like parse() are called by the hub, and will run in a single threaded context at that level. Internal invocations of utility methods will occur in whatever context top level method called by the hub happens to be in. In other words, calling an app/driver method from app/driver code does not change the context it runs in.