Attacking State
Chris Double is giving lots of detail on the continuation-based webserver experiments he's doing in SISC Scheme. It's interesting to see someone else working through the same problems I went through with Seaside, and neat to recognize bits of Seaside translated into Scheme: his block-href function, for example, looks like it was inspired by Seaside's callback system.
Chris points out that by serializing individual continuations to disk, they each end up hanging on to different copies of the same objects. This means that if, for example, he's filling in a data structure over multiple pages, each page view will serialize the current state of that data structure - and if the user uses the back button to go back to a previous page, that state will get rolled back.
Snapshotting and dealing with state is one of the most complex issues I've had to deal with in Seaside, and the way I've done it has changed quite a bit over time. Here are some of the different possibilities:
- Snapshot nothing. This is what Chris did at first, when he was using hash tables to store his continuations. The state of the session simply progresses chronologically forward, with no way to roll it back. This quickly breaks down if the user hits the back button: they are trying to interact with pages that are "in the past" of the program and whose current server side state may be inconsistent with what the user has in their browser cache. With the simple counter app Chris mentions, for example, you could increment to 10, backtrack to 5, hit the + link... and get to 11, because the "5" state is long gone.
- Snapshot everything. This is the current approach Chris is talking about, where the entire session state (referenced through the current continutation) is snapshotted after every response, and rolled back before every request. For the counter app, this works great, because the only session state is the value of that counter. So when you backtrack to 5 and hit the + link, the program says "oh, the state when that page was generated was that the counter was 5, I'm going to rollback to that", and you get 6 as you would expect. This doesn't scale, however. Once your application starts to actually do anything useful, you will almost certainly hit state that you don't want to roll back. Say you're building a shopping cart, for example - users don't expect items they have bought to disappear from their cart just because they hit the back button.
Chris: the way to avoid this and still serialize the continuations to disk is to serialize sessions rather than single continuations. Keep a hash table of all of the continuations captured in one user's session, and serialize that entire hash table at once. That way the individual continuations can share references to objects. It's much less common (and probably ill advised) to have two completely separate sessions holding onto the same object. I don't know if you keep session IDs already (there's a nice purity about only needing a single continuation ID), but you'll need to start if you do this.
- Snapshot only UI state. This is what Seaside did for a long while. To be precise, on every page view, it would make a shallow copy snapshot of the current page object. This means that the counter, for example, would backtrack properly, because the counter value would be, in Seaside, an instance variable of a page object, and so would be saved and restored on every page view. The shopping cart, however, would be unaffected, because it is in a model object rather than a UI object.
The problem is that real applications aren't this simple. In particular, the UI state might be too complex for a simple shallow copy of the page object to work. You might be using an MVC pattern where the page (which is roughly the View) is holding onto a separate Model object that represents, say, which items are selected in a list. This is definitely "UI state", in that it should be rolled back to track the back button, but it's not obvious to the framework that it is, because it's not explicitly part of some page.
- Snapshot any object the developer wants. This is what Seaside currently does. The session keeps a list of objects that are registered for backtracking; it does the shallow copy snapshot and restore trick with them and with them only. This puts the onus on the developer to know which objects need their state tracked and which don't, but in practice this flexibility turned out to be necessary.
The most common kind of object to get registered in Seaside are what in Smalltalk are called ValueHolders, also sometimes called "boxes" - objects that consist solely of an instance variable and a getter/setter for it. There's a special subclass called StateHolder that automatically registers itself for backtracking when it's created. So in the counter app, for example, you would wrap the count value in a StateHolder to make sure it got backtracked properly. In Scheme, I can imagine doing this with some special binding form:
(backtrackable-let ((counter 0)) ...)