IanG on Tap

Ian Griffiths in Weblog Form (RSS 2.0)

Blog Navigation

April (2018)

(1 item)

August (2014)

(1 item)

July (2014)

(5 items)

April (2014)

(1 item)

March (2014)

(1 item)

January (2014)

(2 items)

November (2013)

(2 items)

July (2013)

(4 items)

April (2013)

(1 item)

February (2013)

(6 items)

September (2011)

(2 items)

November (2010)

(4 items)

September (2010)

(1 item)

August (2010)

(4 items)

July (2010)

(2 items)

September (2009)

(1 item)

June (2009)

(1 item)

April (2009)

(1 item)

November (2008)

(1 item)

October (2008)

(1 item)

September (2008)

(1 item)

July (2008)

(1 item)

June (2008)

(1 item)

May (2008)

(2 items)

April (2008)

(2 items)

March (2008)

(5 items)

January (2008)

(3 items)

December (2007)

(1 item)

November (2007)

(1 item)

October (2007)

(1 item)

September (2007)

(3 items)

August (2007)

(1 item)

July (2007)

(1 item)

June (2007)

(2 items)

May (2007)

(8 items)

April (2007)

(2 items)

March (2007)

(7 items)

February (2007)

(2 items)

January (2007)

(2 items)

November (2006)

(1 item)

October (2006)

(2 items)

September (2006)

(1 item)

June (2006)

(2 items)

May (2006)

(4 items)

April (2006)

(1 item)

March (2006)

(5 items)

January (2006)

(1 item)

December (2005)

(3 items)

November (2005)

(2 items)

October (2005)

(2 items)

September (2005)

(8 items)

August (2005)

(7 items)

June (2005)

(3 items)

May (2005)

(7 items)

April (2005)

(6 items)

March (2005)

(1 item)

February (2005)

(2 items)

January (2005)

(5 items)

December (2004)

(5 items)

November (2004)

(7 items)

October (2004)

(3 items)

September (2004)

(7 items)

August (2004)

(16 items)

July (2004)

(10 items)

June (2004)

(27 items)

May (2004)

(15 items)

April (2004)

(15 items)

March (2004)

(13 items)

February (2004)

(16 items)

January (2004)

(15 items)

Blog Home

RSS 2.0

Writing

Programming C# 5.0

Programming WPF

Other Sites

Interact Software

Continuations for User Journeys in Web Applications Considered Harmful

Sunday 21 May, 2006, 12:37 AM

Gilad Bracha recently wrote about why he thinks continuation support should not be added to the JVM. In it he said:

"By far the most compelling use case for continuations are continuation-based web servers."

The idea that continuations are a good fit for web applications is a popular one. (Bracha doesn’t even feel the need to offer evidence supporting his assertion that it’s the most compelling use case.) I want to take issue with this idea. Actually, so does Bracha, but for different reasons... His argument seems to be that current web application UI interaction designs are unfortunate throwbacks - unimaginative responses to the intrinsic constraints of HTTP - and that using continuations to deal with this simply makes life easier for developers without addressing the underlying usability problems.

I completely agree with Bracha that the UI model in question here is rubbish. However, I have another reason for rejecting this use case. Even if you ignore the archaic and unfortunate UI conventions ubiquitous on the web, I believe the use of continuations in this context is, on balance, a bad idea. It has some superficial attractions, but it also has serious shortcomings.

Fairly Short Introduction to Continuations

Those of you who know what continuations are can skip this bit.

The idea behind continuations is pretty simple: the flow of execution in your code can head off somewhere outside of the current function, but be able to continue from where it left off some time later. The canonical example is the humble function call. In mainstream languages like VB.NET, C#, Java, and C++, we’re accustomed to the idea that when we call a method, it will eventually return, at which point execution will carry on from the point directly after the function call.

Under the covers, this ability to continue after the function call completes is enabled by passing a bit of information to the called function. Every function call includes an implicit parameter indicating where to return once the subroutine has finished. On many CPUs, this is done by storing a return address on the stack, an operation which is often built directly into an instruction. For example, the x86 CALL instruction combines two functions: push a return address onto the stack, and then jump to a new location.

A continuation is really just a generalization of this ‘carry on from where we were’ functionality. The limitation with the ordinary function call model is that there’s only one scenario in which you can use it: function calls. But what if I wanted a little more flexibility? Functions are passed an implicit continuation parameter, but how about I have a function that also returns a continuation? Just as the caller says “Here’s where you should return to once you’re done,” the function could also say “Here’s where you should start from the next time you call me.”

Actually, we can already do something very like this in C# thanks to the iterator support added in 2.0. For example, this next example shows a function that returns a number of results. Every time it returns a value, it also generates information about where to start from next time (which actually ends up getting stored by the compiler-generated implementation of IEnumerable):

static IEnumerable<int> ReturnSeveralTimes()
{
  int i = 1; int j = 1;
  yield return 1;
  yield return 1;
  for (int steps = 0; steps < 10; ++steps="")
  {
    int next = i + j;
    yield return next;
    i = j;
    j = next;
  }
  yield return 2;
  yield return 3;
  yield return 5;
  yield return 7;
}

This returns the first 12 Fibonnaci numbers, and then just for fun, it returns the first 4 prime numbers. It really is returning from the function each time it hits one of those yield return statements, and yet it retains the ability to continue from where it left off. In other words, yield return does something very similar to a function call: it somehow concocts the ability to continue execution from a particular line of code at some point in the future. But unlike a function call, where the continuation is supplied by the caller and executed by the called function, here another continuation is also supplied by the callee, and is executed by the caller when they ask the iterator for the next item.

Some languages take this further. Rather than offering continuation in a limited set of specific scenarios, at any point in the code you can say “Build me a continuation from this point in my function.” You can then treat this continuation as a piece of data - store it in a variable, pass it as a value to a function, store it in a collection. And you can execute the continuation whenever you see fit, as often as you like.

This requires a certain amount of magic under the covers. Supporting continuations in special cases such as function calls or iterators is much easier than providing completely generalised support. Continuations do not fit all that well with the stack-oriented execution model offered by the JVM or CLR.

Continuations and the Web

Continuations can look attractive on the web, because they offer a tool that lets you capture the shape of a sequential user journey in the structure of your code. For example, an ecommerce site might feature a multi-page dialog with the user of the kind outlined in this pseudo-code:

OnClickedCheckout()
{
  ShowBasket(User.Basket);
  WaitForConfirmationOfContents();
  ShowPaymentOptions();
  PaymentMethod pm = GetPaymentOptionSelection();
  pm.GetPaymentDetails();
  if(ConfirmPayment(User.Basket, pm))
  {
    Order o = ProcessOrder(User.Basket, pm))
    if (o.Succeeded)
    {
      ShowOrderSuccess(o);
    }
    else
    {
      ShowOrderFailure(o);
    }
  }
}

The user journey is directly reflected by the structure of the code. And as I’ve said before, I’m a fan of code that does what it looks like it does.

Of course web code never looks like this. The problem is that most of the function calls in that pseudocode require us to send a page back to the user and then wait for the user’s next action. But web servers aren’t built that way. They typically call our code when a request comes in from the browser, and don’t generate a response until we return. So we can’t write one sequential piece of code that represents this path through the site. Instead, the path’s embodiment is often scattered throughout lots of little event handlers, or it might be the input to some data-driven controller.

But continuations seem to offer a solution. Whenever we get to the point in the code where we need to generate a page and feed it back to the user, we could create a continuation, store it somewhere, and then return control to the web server. When the next request comes in, a framework could retrieve that continuation, and execute it, allowing us to continue from where we left off.

You could probably cruft up a working example of this style of coding in a web app by writing an iterator function in C# which did a yield return each time it wanted to show a page to the user and wait for the results. Languages with comprehensive support for continuations could offer a more elegant example of the same idea.

However, I think this is a bad idea. Although the relationship between the code and the user navigation path is apparently simple, it hides subtle but significant details. This makes the same mistake as we did in the 1990s with distributed object models. We’d like to believe we’re hiding a lot of implementation details behind a simple abstraction. In practice we’re hiding important features behind an inappropriate abstraction. Our industry has more or less learned the lesson that procedure calls are an unworkably naive abstraction for request/response messaging over a network. I suspect that it’s equally naive to attempt to manage the complexities of user/web server interactions with simple sequential code.

Here are a few of the issues that make sequential code an unsuitable abstraction for user navigation through a web site.

Abandoned Sessions

Sometimes the user just walks away. I might get part way through the purchasing sequence on a web site and then decide to stop. The web server never gets any positive indication that I closed the browser window. It merely stops hearing from me.

What does this mean in a world where I’m using continuations to help model user journeys as sequential code? It means that sometimes my functions just stop part way through without reaching the end.

On the plus side, it is predictable where this will happen: it can only occur at boundaries where I choose to construct a continuation and then relinquish control for now. But at every such boundary, I need to be aware that sometimes, the continuation will never execute.

This is very much not analogous to the function returning or throwing an exception. In the world of our chosen abstraction - that of sequential execution of a method - it looks like our thread has hung.

The problem with this is that a lot of the techniques we have learned for resource management stop working. Resource cleanup code may never execute because the function is abandoned mid-flow.

Thread Affinity

With an ordinary sequentially executing function, I can safely assume one thread will run the function from start to finish. But if I’m using continuations to provide the illusion that I’ve got sequential execution spanning multiple user interactions, then I might get a thread switch every time I generate a web page.

Traditionally, any particular invocation of a function runs on a single thread from start to finish. We’re not accustomed to mid-function thread switches, and it will render some hitherto safe practices unworkable. Using objects with thread affinity will become particularly hazardous, for example - we will need to be mindful of the potential switch points and make sure we never use such objects across such a boundary.

Worse, problems probably won’t show up during development. It’s not uncommon for a web server that only has to process one request at a time to use the same thread for every request. It’s only when you put the server under load that it starts using multiple threads. This provides another way in which code that worked fine in development can fail in production. (And we have enough of those already.)

Web Farms

This is essentially the same problem as the thread affinity issue, but at the machine level: in a web farm, your sequential function might end up executing on a variety of machines over its lifetime. However, you’d probably avoid this problem in practice using sticky sessions. (And unless your continuations are serializable across machine boundaries you’ll have to do this.)

The Back Button and Branching

This one’s the killer.

Your web site may present linear user journeys, but that doesn’t mean your users necessarily follow them. I often don’t.

I habitually do two things that will confound any web site that expects the user to do things in a particular order. First, sometimes I use the back button. Second, sometimes I bifurcate my navigation - I’ll open a link in another tab. Both of these will confuse any web site that thinks it knows what my ‘current page’ is. The notion of a current page is not enshrined in either HTTP or HTML, and I enjoy the flexibility this offers me when browsing sites. Indeed, it’s one of the reasons I really like tabbed web browsers.

Let’s look at what these two actions mean for our continuation-based technique where we attempt to model user journeys through sequential execution of code in a function.

The Back button gives users the ability to wind execution back a bit. Of course from a user perspective, the Back button just gives the user the ability to go back. But if we’re choosing to make the structure of our code model how the user navigates the site, then that’s what Back has to mean.

Depending on exactly how your framework builds on top of the underlying continuation mechanism, this might actually break your framework completely. Bracha tacitly assumes a framework that won’t be broken by this - he assumes that the system will keep old continuations around, so if you do go back, it can execute them again. So the framework has well defined behaviour in terms of how it will execute your code. But what does your code see?

Your function could execute, say, the first 10 lines of code, and then when the user hits Back and then clicks on a link, your code is forced back to line 5. You have no control over this - your function can be rewound to any of the places where you spat out a web page. And this rewinding can happen as often as the user likes.

Normal functions don’t do that - they only jump back to earlier points if you use flow control constructs such as loops. Giving the user the option to inject goto statements at will is an unusual design choice, but anything that models user journeys as sequential execution of code will have to cope with this kind of rewinding, or it’ll break the back button. And I don’t know about you, but I hate sites that break the back button. (Yes, Windows Live Search, I’m looking at you.)

When a site breaks my Back button for me, I resort to bifurcation: if I’m on such a site, I just open each link in a new tab. I also do this for sites that configure caching so as to force a reload on Back. And sometimes I do this on sites where Back works just fine - maybe linear navigation simply doesn’t suit what I’m trying to do.

Let’s consider what this bifurcating model means in the world where we’ve chosen to model user journeys in the structure of a function. On sites where the developer (or framework) simply assumes that there is always a ‘current page’, this browsing style just doesn’t work properly. (An egregious bug, IMO.) But what about sites that keep all the old continuations around, so that they can go back to them if necessary, as discussed above?

This would mean that not only can users rewind execution, they can also fork it. When my supposedly sequential function chooses to yield another page to the user, if the user chooses to open it in a new tab or browser window, it is as though they’ve created a new thread, which proceeds to the next line while leaving the original thread parked where it was.

This means I have to write my function in such a way that it can cope not only being rewound, but also to being split so that multiple threads execute the function simultaneously, each taking different paths. But of course because I’m using continuations, each of these threads gets to use the same set of local variables. The fact that I enabled users to inject gotos into my code at will is now looking like a walk in the park - now they can add arbitrary concurrency!

Bracha characterises this as an advantage of this continuation-based abstraction. I suppose he has a point, in that it does offer a mechanism for managing multiple paths taken through the web site by one user. But I disagree that it’s a good thing: on the contrary, this is what makes this whole approach essentially unworkable. It imposes such a complex set of bizarre and unusual behaviours on the code that it’s a recipe for failure.

General Principles

My final objection is a bit more abstract: I think it’s a mistake to choose an abstraction that badly misrepresents the underlying reality. We made this mistake with various distributed object model technologies last decade. I think the attempt to model a user’s path through a web site as the sequential execution of code is at least as misguided. Users don’t necessarily take a single linear path through a web site, so code execution is completely the wrong shape of abstraction.

In Summary

You use continuations to build a framework that allows the execution path of your code to model the path a user takes through your web site. But the paths users take are not necessarily straightforward. Consequently, this approach requires you to write your code in such a way that it can tolerate sudden halts, thread switches, rewinding, and forking of execution.

This makes it a thoroughly uncompelling use case for continuations.

In short, believing that this approach will make things simpler is pure wishful thinking.

Copyright © 2002-2024, Interact Software Ltd. Content by Ian Griffiths. Please direct all Web site inquiries to webmaster@interact-sw.co.uk