How to design a multi-user ajax web application to be concurrently safe

Raynos picture Raynos · Jan 27, 2011 · Viewed 30.2k times · Source

I have a web page that shows a large amount of data from the server. The communication is done via ajax.

Every time the user interacts and changes this data (Say user A renames something) it tells the server to do the action and the server returns the new changed data.

If user B accesses the page at the same time and creates a new data object it will again tell the server via ajax and the server will return with the new object for the user.

On A's page we have the data with a renamed object. And on B's page we have the data with a new object. On the server the data has both a renamed object and a new object.

What are my options for keeping the page in sync with the server when multiple users are using it concurrently?

Such options as locking the entire page or dumping the entire state to the user on every change are rather avoided.

If it helps, in this specific example the webpage calls a static webmethod that runs a stored procedure on the database. The stored procedure will return any data it has changed and no more. The static webmethod then forwards the return of the stored procedure to the client.

Bounty Edit:

How do you design a multi-user web application which uses Ajax to communicate with the server but avoids problems with concurrency?

I.e. concurrent access to functionality and to data on a database without any risk of data or state corruption

Answer

Christoph Strasen picture Christoph Strasen · Feb 17, 2011

Overview:

  • Intro
  • Server architecture
  • Client architecture
  • Update case
  • Commit case
  • Conflict case
  • Performance & scalability

Hi Raynos,

I will not discuss any particular product here. What others mentioned is a good toolset to have a look at already (maybe add node.js to that list).

From an architectural viewpoint, you seem to have the same problem that can be seen in version control software. One user checks in a change to an object, another user wants to alter the same object in another way => conflict. You have to integrate users changes to objects while at the same time being able to deliver updates timely and efficiently, detecting and resolving conflicts like the one above.

If I was in your shoes I would develop something like this:

1. Server-Side:

  • Determine a reasonable level at which you would define what I'd call "atomic artifacts" (the page? Objects on the page? Values inside objects?). This will depend on your webservers, database & caching hardware, # of user, # of objects, etc. Not an easy decision to make.

  • For each atomic artifact have:

    • an application-wide unique-id
    • an incrementing version-id
    • a locking mechanism for write-access (mutex maybe)
    • a small history or "changelog" inside a ringbuffer (shared memory works well for those). A single key-value pair might be OK too though less extendable. see http://en.wikipedia.org/wiki/Circular_buffer
  • A server or pseudo-server component that is able to deliver relevant changelogs to a connected user efficiently. Observer-Pattern is your friend for this.

2. Client-Side:

  • A javascript client that is able to have a long-running HTTP-Connection to said server above, or uses lightweight polling.

  • A javascript artifact-updater component that refreshes the sites content when the connected javascript client notifies of changes in the watched artifacts-history. (again an observer pattern might be a good choice)

  • A javascript artifact-committer component that may request to change an atomic artifact, trying to acquire mutex lock. It will detect if the state of the artifact had been changed by another user just seconds before (latancy of javascript client and commit process factors in) by comparing known clientside artifact-version-id and current serverside artifact-version-id.

  • A javascript conflict-solver allowing for a human which-change-is-the-right decision. You may not want to just tell the user "Someone was faster than you. I deleted your change. Go cry.". Many options from rather technical diffs or more user-friendly solutions seem possible.

So how would it roll ...

Case 1: kind-of-sequence-diagram for updating:

  • Browser renders page
  • javascript "sees" artifacts which each having at least one value field, unique- and a version-id
  • javascript client gets started, requesting to "watch" the found artifacts history starting from their found versions (older changes are not interesting)
  • Server process notes the request and continuously checks and/or sends the history
  • History entries may contain simple notifications "artifact x has changed, client pls request data" allowing the client to poll independently or full datasets "artifact x has changed to value foo"
  • javascript artifact-updater does what it can to fetch new values as soon as they become known to have updated. It executes new ajax requests or gets feeded by the javascript client.
  • The pages DOM-content is updated, the user is optionally notified. History-watching continues.

Case 2: Now for committing:

  • artifact-committer knows the desired new value from user input and sends a change-request to the server
  • serverside mutex is acquired
  • Server receives "Hey, I know artifact x's state from version 123, let me set it to value foo pls."
  • If the Serverside version of artifact x is equal (can not be less) than 123 the new value is accepted, a new version id of 124 generated.
  • The new state-information "updated to version 124" and optionally new value foo are put at the beginning of the artifact x's ringbuffer (changelog/history)
  • serverside mutex is released
  • requesting artifact committer is happy to receive a commit-confirmation together with the new id.
  • meanwhile serverside server component keeps polling/pushing the ringbuffers to connected clients. All clients watching the buffer of artifact x will get the new state information and value within their usual latency (See case 1.)

Case 3: for conflicts:

  • artifact committer knows desired new value from user input and sends a change-request to the server
  • in the meanwhile another user updated the same artifact successfully (see case 2.) but due to various latencies this is yet unknown to our other user.
  • So a serverside mutex is acquired (or waited on until the "faster" user committed his change)
  • Server receives "Hey, I know artifact x's state from version 123, let me set it to value foo."
  • On the Serverside the version of artifact x now is 124 already. The requesting client can not know the value he would be overwriting.
  • Obviously the Server has to reject the change request (not counting in god-intervening overwrite priorities), releases the mutex and is kind enough to send back the new version-id and new value directly to the client.
  • confronted with a rejected commit request and a value the change-requesting user did not yet know, the javascript artifact committer refers to the conflict resolver which displays and explains the issue to the user.
  • The user, being presented with some options by the smart conflict-resolver JS, is allowed another attempt to change the value.
  • Once the user selected a value he deems right, the process starts over from case 2 (or case 3 if someone else was faster, again)

Some words on Performance & Scalability

HTTP Polling vs. HTTP "pushing"

  • Polling creates requests, one per second, 5 per second, whatever you regard as an acceptable latency. This can be rather cruel to your infrastructure if you do not configure your (Apache?) and (php?) well enough to be "lightweight" starters. It is desirable to optimize the polling request on the serverside so that it runs for far less time than the length of the polling interval. Splitting that runtime in half might well mean lowering your whole system load by up to 50%,
  • Pushing via HTTP (assuming webworkers are too far off to support them) will require you to have one apache/lighthttpd process available for each user all the time. The resident memory reserved for each of these processes and your systems total memory will be one very certain scaling limit that you will encounter. Reducing the memory footprint of the connection will be necessary, as well as limiting the amount continuous CPU and I/O work done in each of these (you want lots of sleep/idle time)

backend scaling

  • Forget database and filesystem, you will need some sort of shared memory based backend for the frequent polling (if the client does not poll directly then each running server process will)
  • if you go for memcache you can scale better, but its still expensive
  • The mutex for commits has to work globaly even if you want to have multiple frontend servers to loadbalance.

frontend scaling

  • regardless if you are polling or receiving "pushes", try to get information for all watched artifacts in one step.

"creative" tweaks

  • If clients are polling and many users tend to watch the same artifacts, you could try to publish the history of those artifacts as a static file, allowing apache to cache it, nevertheless refreshing it on the serverside when artifacts change. This takes PHP/memcache out of the game some for requests. Lighthttpd is verry efficent at serving static files.
  • use a content delivery network like cotendo.com to push artifact history there. The push-latency will be bigger but scalability's a dream
  • write a real server (not using HTTP) that users connect to using java or flash(?). You have to deal with serving many users in one server-thread. Cycling through open sockets, doing (or delegating) the work required. Can scale via forking processes or starting more servers. Mutexes have to remain globaly unique though.
  • Depending on load scenarios group your frontend- and backend-servers by artifact-id ranges. This will allow for better usage of persistent memory (no database has all the data) and makes it possible to scale the mutexing. Your javascript has to maintain connections to multiple servers at the same time though.

Well I hope this can be a start for your own ideas. I am sure there are plenty more possibilities. I am more than welcoming any criticism or enhancements to this post, wiki is enabled.

Christoph Strasen