Dealing with concurrent users

Why worry about concurrent users?

If you’re not careful, one user can easily overwrite the data persisted by another user working on the same page at the same time.

Consider this sequence of events:
  • user A opens page X

  • user B opens page X

  • user A changes input on a form on page X, and clicks on a ButtonInput that submits it

  • the database is changed as a result of user A’s changes in such a way that page X would not render the same anymore

  • user B still has the old page X open, and now makes similar changes on that page and clicks on a ButtonInput that submits the info

Without intervention in the above scenario user B’s changes might overwrite those of user A or the application could break - depending on how the code was written.

Reahl prevents changes to outdated forms

When a user clicks on a ButtonInput, Reahl checks whether the Input values have been changed since it was rendered for this user. If a change is detected, an error is shown (with an option to refresh the Form).

The error shown when submitting an out of date |Form|\.

Customising the up-to-date-check

You don’t have do any coding to enable this check. You may want to customise what should be included in this up-to-date check, and what not.

You should also ensure (as with all Forms) that your form displays any errors that may occur as is done with add_alert_for_domain_exception() in:

    def __init__(self, view):
        super().__init__(view, 'simple_form')
        self.use_layout(FormLayout())
        if self.exception:
            self.layout.add_alert_for_domain_exception(self.exception)

        domain_object = self.get_or_create_domain_object()

        link = self.add_child(A(view, Url('/'), description='Open another tab...'))
        link.set_attribute('target', '_blank')
        self.add_child(P(view, text='...and increment the value there. Come back here and submit the value. A Concurrency error will be shown'))

        #Your own widget that tracks changes
        self.add_child(MyConcurrencyWidget(view, domain_object))

        self.layout.add_input(TextInput(self, domain_object.fields.some_field_value))
        self.define_event_handler(domain_object.events.submit)
        self.add_child(Button(self, domain_object.events.submit))
        self.define_event_handler(domain_object.events.increment)
        self.add_child(Button(self, domain_object.events.increment))

When a Form is submitted, the new user input is not immediately applied. First, the values of all of its PrimitiveInputs are computed as they would render at this point and compared with what they were when the Form was rendered initially.

If any differences are detected here it means that someone must have changed relevant data in the database since the Form was rendered.

To prevent a specific PrimitiveInput from participating in this check, pass ignore_concurrent_change=True when constructing the PrimitiveInput.

If you have a Widget that is not a PrimitiveInput, but that represents some data in the database which you want to partipate in this check, override get_concurrency_hash_strings() on your Widget:

class MyConcurrencyWidget(Widget):
    def __init__(self, view, domain_object):
        super().__init__(view)
        self.domain_object = domain_object
        self.add_child(P(view, text='Counter: %s' % domain_object.counter))

    def get_concurrency_hash_strings(self):
        yield '%s' % self.domain_object.counter

get_concurrency_hash_strings() yields one or more strings that represent the database state that should influence the up-to-date check.