6

To keep scope small I will talk about UI race conditions initiated by the same user in the same app sessions. The question is general and not specific to mobile, web or desktop UI.

The issue

Modern UI expected to be responsive i.e. commands triggered by user executed asyncronously while UI remains responsive and available. This is perfect breeding ground for all sorts of race conditions because async command can potentially take a lot of time (e.g. if connection is slow/unresponsive) and during that time user can initiate potentially conflicting command in parallel.

There are "bruteforce" workarounds such as make all command synchronous or disable screen / show blocking progress bar or ring for any command / blocking modal dialogs etc. however I don't consider them here.

In my experience it's a serious mental overhead to keep track of all UI elements that can potentially lead to race conditions / conflicting commands and remember to enable/disable them accordingly. The payoff (better UX) is not always worth it. Rx/Reactive programming makes it easier to automatically enable/disable UI elements but it doesn't relieve need to keep many dependencies in one's head. It's especially bad when many people work on a project and write different components/views separately.

Example

For example consider a simple app for editing orders. Order has lines. There are some asynchronous commands (e.g. they all eagerly write to database or other backend).

Order commands (with UI elements)

  • Delete (button)
  • Change customer (dropdown)
  • Change shipping date (calendar)
  • Confirm (button)

Line commands

  • Create line (new row in grid)
  • Edit line (row in grid)
  • Delete line (button)

It's easy to come up with examples of commands which are safe or not safe to run concurrently. E.g. it's OK to edit a line and delete another line at the same time as these commands don't interfere. It's OK to change order shipping date while lines are being saved (shipping date doesn't affect lines), but it's not not OK to change order's customer (e.g. it will refresh pricing for lines i.e. no line command should be in progress). Let's say we add rule that only order without lines can be deleted, now Delete Order button must be disabled if there's any line command (e.g. add line) in progress... And so on and so forth.

In large apps there are more entities and dependencies between them and hence risk of more subtle race conditions.

Example is for illustration only, the question is more general.

The question

Do you know any disciplined approach to maintain good balance between UX/responsiveness and risk of allowing conflicting commands/race conditions? I don't expect a silver bullet and suppose that answer won't be short and will accept relevant links to other resources or books as answers. Any platform or language will do but ideally it has to be in spirit of Rx / (functional) reactive programming.

UPDATE based on discussion, people wonder what kind of application may need to care about it because simple sales order app is not a motivating example. I agree: a better example would be Visual Studio or your favorite IDE: some commands disabled while build is running / while sources being fetched from git etc. We don't want to be locked out of IDE on every background command and those commands can last a while. It's very likely that projects like IDE use very systematic approach to avoid conflicting commands (and if it's simple and elegant enough then why can't a sales orders kind of app benefit from it)

10
  • How many instances of this app use the same database? It sounds like you want to solve concurrency in the (1) GUI. That's not how it's done.
    – H H
    Aug 13, 2019 at 8:28
  • 3
    If you allow concurrency in principle, but certain operations are not safe to perform concurrently, then there isn't any way to reduce the complexity inherent in having to analyse what can and can't be done concurrently, or what needs to be done when a partially completed command is superseded by another second command issued concurrently.
    – Steve
    Aug 13, 2019 at 9:18
  • 3
    In practice, it may be easier to just disable everything except specifically allowed concurrent operations. The number of concurrent operations which are both conceptually possible and useful enough to justify the effort of analysis and implementation are likely to be few if any. Imagine a paper order form - how often is it useful to allow the person to tear up the order form at the same time as adding an order line (rather than requiring him to do only one or the other)? How often do they add multiple order lines at once, using one hand for each?
    – Steve
    Aug 13, 2019 at 10:05
  • 2
    I should add as well, if you are looking for specific patterns that manage the problems of concurrency and avoid complex global reasoning, they include "transactional units" and "synchronous execution"! These patterns are not special to computer software - they are used by human clerks in paper bureaucracies, because the alternative is unmanageable complexity and unfathomable dynamical behaviour.
    – Steve
    Aug 13, 2019 at 10:12
  • 1
    But what if the saving fails? That must be at least a possibility otherwise we could just hive it all off into a background thread and forget all about it. If multiple lines are to be added at once, it may make more sense to allow them to be prepared in a batch before being saved - it is very unlikely that an order has any use in being saved, before being in a complete state. Again to use a paper analogy, you would not place a piece of paper in the filing cabinet after each order line - you complete one form in full, before submitting it as a whole for storage or further processing.
    – Steve
    Aug 13, 2019 at 10:25

2 Answers 2

1

In extreme cases, you may use the producer-consumer pattern, combined with the command pattern. Basically, put user actions in a queue, and execute them sequentially. This will solve thread races. You can expand on that and allow some operations to be executed in parallel to others. I.e reads can be executed in parallel to one another, but not in parallel to writes.

For databases, this would mean adding a new layer on top of it, so the app only interacts with said layer and not with the DB or ORM directly.

3
  • 2
    This is more tricky than it first appears. If you queue actions, what happens on failure? Is the whole queue reversed, is the queue submitted for user review (and potential resubmission), is the rest of the queue purged (thus how does the user know where he was up to?), or are all items executed best-effort? Also, how do you determine legitimate sequences of operations - for example, can you queue three deletes against a two-item list, or is this checked ahead of time? If the queue items are merely tried in turn, and an illegitimate sequence is found, then you're back to managing failure.
    – Steve
    Aug 13, 2019 at 13:21
  • This only applies to independent, background , long running operations, where failures can be easily contained. The type of actions which will cause UI responsiveness freezes. Several IDEs and productivity tools use this kind of approach. The point is to prevent 2 such operations from racing each other.
    – Ccm
    Aug 13, 2019 at 14:06
  • 1
    Indeed, but in the case of (say) spell checkers, allowing the user to proceed at their own pace is more important than the correctness of spelling in the meantime, and there is often a final round of document review. In IDEs, there is always a final compilation - you are not modifying the code of a live system in real-time, and if you overstep errors now they will be caught later. Within most systems of business administration, it is not usually acceptable to allow users to proceed past incorrect states - you very often are modifying the behaviour of a live administrative system in real-time.
    – Steve
    Aug 13, 2019 at 14:28
1

I'll describe my hand-made approach, I'm not 100% happy with it but it covers many cases and gives some choice between all or nothing. (projects were in XAML and I used Nito Calcualted Properties and Rx for MVVM but it can be repeated elsewhere)

There is a dynamic tree of nested command scopes, and every command has a scope or few scopes associated with it.

Rules are simple

  • Command can't be executed until its scope is free
  • Command exclusively locks the scope(s) while being executed
  • Scope released as soon as command finished (or failed)

UI controls that trigger command are disabled or enabled in reactive manner. Locking/unlocking of scopes happens synchronously on main UI thread.

Example scopes

  • App (global scope), can't execute command in this scope until app is completely idle, no other command can execute while App scope is locked
  • App/Orders - commands related to all orders (can't imagine any)
  • App/Orders/{OrderId} - commands related to one order. Command in this scope locks order's header and lines id.
  • App/Orders/{OrderId}/Lines - commands related to all lines (can't think of any)
  • App/Orders/{OrderId}/Lines/{LineId} - commands specific to a line
  • App/Orders/{OrderId}/Header - commands related to order header info e.g. address etc. (doesn't intersect with lines scope / can be edited concurrently)
  • App/Products/... etc. - scopes for other commands

Benefits

  • local reasoning: when adding or changing a command you need to think only about its own scope(s)
  • simple: number of scopes in hierarchy is smaller than number of commands and doesn't change often
  • it's usually easy to pinpoint scope associated with a command
  • some flexibility: if in doubt then simply choose more coarse-grained scope / refine later if needed
  • repeatable/domain-agnostic. Editing scopes hierarchy is trivial (and typesafe in my case)

"Command scope manager" can be a singleton detached from view models to keep track of locked scopes even after view is unloaded. This allows users to jump back and forth between screens (load/unload views) and still have controls disabled or enabled correctly.

13
  • 1
    I take your original point that you are using sales orders to illustrate a principle, but I'm very interested to know what the actual context is that will justify this degree of complexity. It is certainly not justified for sales order entry - you are introducing a railway signalling system, with its complex interlocks, for what is a simple everyday case of filling in and submitting an order form!
    – Steve
    Aug 13, 2019 at 14:09
  • 1
    Unless your commands can take multiple seconds to execute, I can't imagine this level of complexity is justified. Even if the system can handle this level of complexity, the user is not going to understand it nor take advantage of it.
    – 17 of 26
    Aug 13, 2019 at 14:25
  • @17-of-26, agreed, even if the commands do take multiple seconds, I would argue that this problem should be addressed directly by modifying the design which causes them, rather than taking such delays as a given and working around them. Like Alphonse X said when presented with a mechanical planetarium based on the geocentric principle, "if the Almighty had consulted me before embarking on creation, I would have recommended something simpler"!
    – Steve
    Aug 13, 2019 at 14:36
  • Well any I/O commands can take several seconds to execute subject to backend availability at given moment. I got your point that whenever possible stick with KISS (i.e. lock everything or make it synchronous) however sometimes customers don't like it believe it or not
    – KolA
    Aug 13, 2019 at 14:56
  • 1
    I can only trust that you have a handle on things then, KolA. I don't think the VS IDE/Build analogy adds much, because the solution in such cases is in some respects very simple - take a snapshot of the code files, start the build process, disable all conflicting commands, await a conclusion then re-enable the commands. But it is also very complex - to get something that behaves like VS, then hire multiple generations of world-class developers, invest thousands of man-years and billions in money, apply a Rolls-Royce test suite, tackle a million bug reports, and by version 16 you will be done!
    – Steve
    Aug 13, 2019 at 22:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.