Friday, April 18, 2014

Solving the observer problem

Abstract

As most sophisticated web apps, Orbeon Forms has to deal with lots of asynchronous events in the browser, like Ajax requests, timers, and user interactions. We'll go over a real-life example of logic involving several asynchronous events, see the difficulty involved in implementing it with observers, and explore 3 alternatives: state machines, await, and Functional Reactive Programming (FRP). We'll see why, based on our experience, we don't consider state machines to be a good solution, and through an example explore the two remaining alternatives, await and FRP.

Example of the loading indicator

Orbeon Forms can show on the page a "loading indicator" to inform users that something is happening: data was sent to the server, and we're waiting for a response, maybe because we need to validate user input, save data, or call a service. Its logic works is as follows:
  1. In essence, when a request is in progress, we want the loading indicator to show.
  2. However, if the request takes very little time, we don't want to show the loading indicator.
  3. Also, when a request comes back, we don't want to hide the indicator if we already know we'll right away send another request to the server.
Stated in plain English, this is quite simple.

The problem with observers

The browser gives us events, and we can run code when those events happen. Code running upon some event is called an observer. For the loading indicator, we would have an observer when an Ajax request starts, and another one when it ends. Since we don't want to show the indicator right away, the observer on the "Ajax request started" starts a timer for say 200 ms. So we would have an observer on that timer, which on completion shows the indicator if the request is still in progress. But to know that, we need to have state (isRequestInProgress), shared between the two observers.
But a boolean isn't enough. Say you start a first request at t = 0 ms. At that point, you also start a timer for 200 ms. Say that first request ends at t = 100 ms. Now say a second request starts at t = 150 ms. At t = 200 ms, the first timer ends: a request is still in progress, so it shows the loading indicator. Bug! At that point the request has been going on for only 50 ms, so we shouldn't show the loading indicator just yet. One way to solve this problem is to use a counter, instead of a boolean, an increment/decrement that counter at the right time.
I won't go here through all the details of the implementation, but the end result is that the above logic is spread across a number of observers that share and update some global state. Logic written this way is surprisingly hard to maintain, and subtle bugs can lead to a loading indicator that stays "stuck", i.e. shown even if there no Ajax request in progress, or that doesn't show anymore.

Possible solutions

We're aware of 3 alternatives to observers:
  1. State machines
  2. await
  3. Functional reactive programming (FRP)

State machines

In Form Builder, we started using a state machine about 2 years ago, and had a mixed experience with that approach. A state machine solves part of the observer problem, by putting the high level logic in a single place, in a fairly declarative form. However, we found that complex state machines are hard to debug, as you can't debug the declaration of the state machine itself, but instead have to debug the generic implementation of the state machine.

Experimenting with await and FRP

Before going for await or FRP in production code, we wanted to do some experimentation. For this, we took the example described in the excellent paper Deprecating the Observer Pattern. The example uses the canvas and the mousedown, mousemove, and mouseup events to let the user draw on the canvas. You'll find below the CoffeeScript code for 3 implementations:
  1. With observers, using plain jQuery.
  2. With await, using IcedCoffeeScript.
  3. With FRP, using Bacon.js.
For each case, you can click on the link after the code to view the full source and run that example.

With observers

isDown = false

canvas.on 'mousedown', (e) -> 
    isDown = true
    startLine(e)

canvas.on 'mousemove', (e) -> 
    if isDown
        continueLine(e)

canvas.on 'mouseup', (e) -> 
    isDown = false
See and run the code

With await

while true

  await canvas.one('mousedown', defer(e))
  startLine(e)

  until e.type == 'mouseup'
      rv = new iced.Rendezvous
      canvas.one('mousemove', rv.id().defer(e))
      canvas.one('mouseup'  , rv.id().defer(e))
      await rv.wait(defer())
      continueLine(e)
See and run the code

With FRP

downStream         = canvas.asEventStream('mousedown')
upStream           = canvas.asEventStream('mouseup')
moveStream         = canvas.asEventStream('mousemove')

isDownProp         = downStream.map(true)
                               .merge(upStream.map(false))
                               .toProperty(false)
moveWhenDownStream = moveStream.filter(isDownProp)

downStream         .onValue(startLine)
moveWhenDownStream .onValue(continueLine)
See and run the code

Conclusion

We believe that both await and FRP are significant improvement over observers or a state machine. At this point, we have a slight preference for await, as we think that:
  • Most developers will find it easier to understand code using await than code using FRP, and we believe in Erik Meijer's mantra: "write baby code".
  • We think that code using await will be easier to debug than code using FRP, as it will be easier to put breakpoints in the right place, and follow the code execution.

No comments:

Post a Comment