RIFE logo
This page last changed on Aug 06, 2006 by koreth.


What are continuations?

These constructs are inspired from Scheme and basically contain all information about a specific program location and the local method variables. Using this information, the application is able to create an interruption in program execution (this is RIFE-specific) continue later at the exact same location as if nothing happened.

A simple presentation that explains continuations clearly can be downloaded from here.

If wonder about how to debug continuations, look at this presentation.

[top]

How does this apply to web applications?

Imagine that you have a series of forms that need to be filled out as a sort of wizard. This really is one logical entity with multiple steps of user intervention. Traditional application frameworks solve this somewhat with sessions, however you still need to either split up the logic in multiple pages or start using conditionals to check which steps have been performed before (and optionally revalidate the data).

So you get something like this:

/* start page 1 */
output form 1
wait for input
process input
go to page 2
/* end page 1 */

/* start page 2 */
check for form 1 data
if not present go to page 1
output form 2
wait for input
process input
go to page 3
/* end page 2 */

/* start page 3 */
check for form 2 data
if not present go to page 2
output form 3
wait for input
process input
go to next page
/* end page 3 */

What you really want is this:

/* start page */
output form 1
wait for input
process input
output form 2
wait for input
process input
output form 3
wait for input
process input
go to next page
/* end page */

So you think about this as a continuous series of steps that all follow each-other with intermittant user interaction. If all steps have been processed you want to continue to the next element.

RIFE allows you to write elements now exactly as the pseudo example above. Just replace the 'wait for input' by the pause(); method call. The pause() method call will only be detected as a continuation if it's used inside the processElement() method. Anywhere else it will simply be stripped away. This is done because tracing the bytecode of the other methods in the element implies tracing all the methods of all the classes that are used inside the Element. Not only would this cause a general performance hit, this also would be impossible since it's not allowed to dynamically modify the classes of the JRE itself.

Simple example to illustrate the feature with code:

public void processElement()
{
    int total = 0;
    while (total < 50)
    {
        print(getHtmlTemplate("form"));
        pause();
        total += getParameterInt("answer", 0);
    }

    print("got a total of "+total);
}

This element just gets a series of answers and adds them together. When the total exceeds 50, the execution ends and the total is printed.

Since continuations always run inside the same element, you either have to do the user interaction through submissions or through exits that point back to the same element.

I rewrote the numberguess example with continuations as an example and it's included in the examples archive. You'll immediately notice how much more intuitive it is.

[top]

Different continuation handling models

Since RIFE tries its best to make the end-user's experience as comfortable as possible, continuations have changed to fully support back-button presses in a browser. So someone can take different execution paths in an application, go back, take another path and even later resume from earlier paused location. To make this work, RIFE needs to clone the context of the executing code each time a continuation is resumed, thus creating a totally independent context that doesn't interfere with any previous ones. The downside is that to be able to do this, all variables in the scope need to be cloned too. For most commonly used JDK types, this has been automated and a lot of RIFE's classes are implementing Cloneable now too. However, you need to be aware that this is a requirement for any of the classes that you write yourself or that you use from an external library. To make your life easier, you can use the genericClone(Object) and deepClone(Object) methods in the com.uwyn.rife.tools.ObjectUtils class to quickly implement clone() methods, but it might be possible that you're unable to provide a cloning functionality to certain classes or that it requires too much work.

There are several options to solve this. First, you have to be aware of the fact that the scope for the continuations context consists out of the local variables of the processElement() method and the member variables of the element that is being executed. So, the easiest option to use uncloneable types, is to move them outside of this scope, a separate method being a perfect choice.

For example:

public class Continuation extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("mytemplate");
        String result = getUncloneableResult();
        template.setValue("result", result);
        print(template);
        pause();
    }
    
    private String getUncloneableResult()
    {
        UncloneableType type = new UncloneableType();
        return type.getResult();
    }
}

Your other option is to indicate to RIFE that it shouldn't clone the continuation context at all, but always continue to use the same one. This largely removes any restrictions on the usable types, but it also prevents an end-user from back-tracking and taking another execution path. Only the last-used continuation will remain active and RIFE will enforce this by changing the continuation IDs at each request and invalidating old IDs. While this might seem like a nice quick solution if you run into trouble, you should think very carefully before using it since it results in making the back-button of the browser (or Safari's snapback function) throw the user back to the beginning of the element. A non-negligible benefit of this behaviour however, is that continuations will use much less resources. That being said, you have two options to indicate to RIFE that you want this behaviour for an element. Either you override the public boolean cloneContinuations() method and return false, or use setCloneContinuations(false) before using the first continuation. I prefer the first one which results in this, for example:

public class Continuation extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("mytemplate");
        UncloneableType type = new UncloneableType();
        template.setValue("result", type.getResult());
        print(template);
        pause();
    }
    
    public boolean cloneContinuations()
    {
        return false;
    }
}

[top]

Support for inter-element continuations (call/answer)

Continuations are not limited to one element. It's also possible to activate an exit and pause the execution of the calling element. When the target element calls the answer() method, the execution of the calling element will automatically resumed. You can do this very simply by using call("exitname") method instead of exit("exitname") method. All the behaviour of a regular exit is still available and the target element will be processed normally.

For example, consider the following element implementations that are linked together through a flowlink that starts at the "callexit" exit and points to the second element:

public class Call extends Element
{
    public void processElement()
    {
        print("before");
        call("callexit");
        print("after");
    }
}

public class CallTarget extends Element
{
    public void processElement()
    {
        print("-target-");
        answer();
    }
}

The resulting output will be:

before-target-after

While this may seem very trivial and look like a regular method call, that is exactly its power. Consider that it is completely decoupled and adaptable through the site structure, that it will be remembered even when the target element uses a pause() call to suspend the execution while gathering user info and that the target element element could be written in one of the scripting languages like Groovy.

One typical use for inter-element continuations is the display of intermediate pages like dialogs or help pages without losing track of the main execution flow. To make this even more comfortable, it's possible to return control to the calling element and to pass a value along by using the answer(Object) method. This makes it possible to gather user information and to provide the result to the caller.

For example, the following element implementations are again linked together through a flowlink that connects the "dialog" exit to the Dialog element. The Delete element will first display a page to the user and wait for input. If the user presses the button that corresponds to the delete submission, it will activate the "dialog" exit and hold the execution until the call is answered. This makes the execution flow go into the Dialog element which displays a confirmation dialog. Depending on which button the user presses, the "yes" or "no" submission is sent to the element and the call is answered with either true or false. The execution in the Delete element is now resumed and according to the user's answer, a different action is taken.

public class Delete extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("pub.home");
        print(template);
        pause();
        
        if (hasSubmission("delete"))
        {
            Boolean answer = (Boolean)call("dialog");
            if (answer.booleanValue())
            {
                print("deleted");
            }
            else
            {
                print("not deleted");
            }
        }
    }
}

public class Dialog extends Element
{
    public void processElement()
    {
        Template template = getHtmlTemplate("pub.dialog");
        print(template);
        pause();
        
        if (hasSubmission("yes"))
        {
            answer(new Boolean(true));
        }
        else if (hasSubmission("no"))
        {
            answer(new Boolean(false));
        }
    }
}

[top]

Handling multiple submissions

At present, pause() may only be called from the processElement() method of an element implementation. A future release may provide a way to pause other element methods such as handlers for particular submissions. Meanwhile, if your element handles multiple continuation-enabled submissions or has complex logic to execute between submission steps, you may want to structure it like this, to minimize the amount of your actual element logic that has to live in the processElement() method.

public void processElement()
{
    if (hasSubmission("submission1"))
    {
        submission1InitialLogic();
        pause();
        submission1FinalLogic();
    }
    else if (hasSubmission("submission2"))
    {
        submission2InitialLogic();
        do {
            pause();
        } while (! submission2Step1Done());
        submission2MiddleLogic();
        do {
            pause();
        } while (! submission2Step2Done());
        submission2FinalLogic();
    }
    else
    {
        defaultPageLogic();
    }
}

[top]

Use initialize() to run code on each submission

If you would like to execute code each time the user continues executing a continuation flow, put it in the element's initialize() method. RIFE calls initialize() before it returns control to the code after the pause() call.

One implication of this is that if you have initialization code you don't want executed multiple times as the user steps through a continuation, that code should not be placed in initialize(); it should be called from the start of processElement() instead.

[top]

Document generated by Confluence on Oct 19, 2010 14:56