I’ve never had a clear sense for how Button click and Timer tick events were handled during a server call, but I have now achieved a kind of enlightenment, thanks to @stucork.
This demo app helped clarify how things are sequenced for me:
Here’s how I understand it:
- Server calls from the client-side are queued up in order. If a second server function is called before the first one finishes, it waits and then starts after it is finished.
- On the client-side, the python function containing the server call waits for that server call to return before it can continue. But other client code triggered by click or tick events (etc.) will run in the meantime.
Someone else could surely explain it more clearly, using proper terminology. And I must admit I remain puzzled about how certain things are sequenced client-side.
If you don’t like it, that is if you want two server calls to run in parallel, then they should launch two background task (which is more complex than a simple server call, but it works).
That’s why timers can be used as runners. And using
time.sleep(0) gives the control back to the event loop so the next event can take the control (like
DoEvents in VB6/VBA), including any queued server call that is waiting for a result.
This is a total surprise to most “conventional” programmers (e.g., me). In a normal, single-threaded program, once a function is called, no other activity is possible. Selected program data structures can be left in an intermediate (inconsistent) state for an extended period (e.g., during a server call) with no issues, because it will be cleaned up before any other code can see it. The kind of cleanup may well depend on the result from the call, e.g., filing the result away in a global variable.
Now, that “other client code” can be triggered at any point in all those fleeting, intermediate states.
To prevent such hard-to-reproduce bugs, I’m increasingly inclined to just disable every visible code-triggering component that’s on the screen, until the cleanup is complete.
It takes a completely different mindset to sequence your operations such that your data structures are always in a consistent state. If “consistency” is spread across multiple variables, then I’m not sure it’s even possible to guarantee consistency, without some sort of in-memory “transaction” (all-or-nothing) mechanism.
One problem that I used to have was duplicated forms showing up one above the other, caused by clicking on a button that would clear a column panel and load a form on it. While the form was loading, the user clicked on another link that did something similar. The clear of the second click comes before the load of the first is finished because the first is waiting on a server call. The result is:
- click on button 1
- clear column panel
- start loading form1 on the column panel and wait for server call to return
- click on button 2
- clear column panel (which is already empty)
- start loading form2 on the column panel and wait for server call to return
- complete loading form1 or form2, whichever finishes its server call first, and add it to the column panel
- complete loading the other form and add it to the column panel
At this point I have both form1 and form2. @stucork just mentioned this problem a few days ago: Quick navigation results in multiple components(forms) being added to the top-level page - #4
I used to use a context manager that would show a semi-transparent div above the whole page and prevent the user from interacting with any input element, something like:
def button_click(self, **e):
Then I started working with the routing module and the problem disappeared (thanks Stu!).
The routing module helps only during the navigation. The problem you are mentioning still exists with possible inconsistent data during normal interaction with the form, but it doesn’t bother me much, in fact I don’t even remember when was the last time I used that context manager (which doesn’t mean the problem doesn’t exists, it means I’m lazy and my users don’t complain enough for me to work on it).
Now that the situation has been made explicit, we can reasonably program around it.
For example, to prevent serious mischief, whenever operations are in progress, I have started to explicitly disable my LogOut button. This tactic extends to any other situation where consistency is in flux: disable everything that could trigger unwanted activity, then start the operation, and re-enable them when the data are consistent again.
The most thoughtful designs will minimize the number of such places, or at least, put them in unsurprising places, e.g., after the user clicks the “Save” button. No one’s surprised if a spinner pops up after “Save”.
I’m thinking of making a feature request to make it easier to specify this behavior explicitly, but I’m not sure which direction to go:
- Ask that Anvil not yield the control flow unless I explicitly put an
await keyword in front of my server.call (or something equivalent).
- Ask for a client-side “transaction” decorator/context-manager that prevents the implicit “await” behavior, optionally disabling all visible UI components (and pausing Timers).
I think I might prefer #1, but #2 is probably easier to implement at this point without a breaking change.
I think #2 would be the way to go. I certainly wouldn’t want my UI to get all jammed up and disabled without explicitly telling Anvil to do so putting up a shield. Though to split the difference, perhaps there could be both a decorator to do this on a function by function basis and also an app level setting to change the default.
To prevent much pain and confusion among newbies, the default should be “safe” behavior, for any new projects. Experts should be able to turn that off, and use the decorator instead, where they feel it matters.
Anybody think we’re at the level of precision for a Feature Request?
This seems a helpful primer for those like me learning async concepts from scratch: