Skip to content

Continuations

Geert Bevin edited this page Jul 12, 2024 · 48 revisions

The original RIFE framework made a name for itself as the first Java web framework that provided continuations. RIFE2 revised the continuations engine to support invokedynamic and StackMapTable, offering support for the latest JVM and JDK versions.

To this day, RIFE2 remains the only Java framework that natively provides continuations in its web engine. With Project Loom researching how to provide continuations directly into the JVM, interest in this technology seems to be re-invigorated.

What are continuations?

Continuations basically contain all information about a specific program location and local variables, allowing you to stop execution and to later continue from a previous spot with the same local state available. It's like the active thread can complete shut down and not run anymore, then be started up again from the same spot with all the state available, as if nothing changed.

Continuations can be very useful in web application development because they leverage the natural flow of your code to express the state that needs to be captured and restored between HTTP requests.

A typical example is form entry where the first request displays the form, and the second request handles the form submission.

A traditional web application could structure that this way:

-> REQUEST GET https://host/yourpage
execute logic to determine state
use state to render form
<- RESPONSE

-> REQUEST POST https://host/yourpage
execute logic again the determine state
detect that a form was submitted
extract state from form
validate submitted state with previous state
handle form
<- RESPONSE

With continuations, this can be restructured in a more natural way:

-> REQUEST GET https://host/yourpage
execute logic to determine state
use state to render form
pause
<- RESPONSE

-> REQUEST POST https://host/yourpage
resume
handle form
<- RESPONSE

The pause() method

Whenever you want your element's execution to pause, you can use the c.pause() method. This will stop the processing of your element and send the HTTP response back.

For example:

public class Form implements Element {
    @Parameter String name;                 // field annotation captures form data
    public void process(Context c) {
        var t = c.template("HelloForm");    // create a new template instance
        t.setBlock("content", "form");      // display the form
        c.print(t);                         // print the template

        c.pause();                          // create continuation and send response
        // <--
        // this is where the form submission will automatically resume,
        // restoring the continuation, along with its state (the template)
        // none of the logic above this point will be executed when the
        // continuation resumes
        t.setValueEncoded("name", name);    // display name submitted through form
        t.setBlock("content", "greeting");  // display the greeting with the name
        c.print(t);                         // print the template
    }
}

Some template engine features are used in the example above that are explained later, but I hope it will bring the concept of continuations across. You can find the full example in the RIFE2 repository.

IMPORTANT : Continuations are only available in element classes and not in element lambdas since RIFE2 needs to be able to create new instances of the elements.

You can continue using all of Java's language features together with continuations.

For example this simple counter, uses a while loop and a local count variable:

public class Counter implements Element {
    public void process(Context c) {
        var count = 0;              // create and initialize the count variable
        while (count++ < 10) {      // continue looping until it has reached 10
            c.print(count);         // display the count and a link to increase it
            c.print(" <a href='" + c.urlFor(c.route()) + "'>add</a>");
            
            c.pause();              // create continuation and send response
            // <--
            // this is where the link will automatically resume,
            // restoring the continuation and iterating through the while
            // loop again
        }
        c.print("done");            // count is larger than 10, print done
    }
}

TIP : You can find this counter example in the RIFE2 GitHub repository.

The RIFE2 source repository also provides a more involved example, that implements a number guessing game with continuations in a single element.

NOTE : Continuations have a limited lifespan, when it takes too long to resume them, RIFE2 will automatically purge them. A continuation's maximum duration and the purge task frequency can be configured through RifeConfig.engine().

The continuation lifecycle

Only the process(Context) method of your element is instrumented for continuations. You can call other methods, but those will not be able to use the pause() method.

Each execution through the process method only has one active continuation, which gets a unique ID. This ID changes every time the continuation is resumed. Usage of previous IDs will start your element logic from the beginning again.

When you use RIFE2 c.urlFor(route) method or the route: filtered template value tag to generate your URLs, RIFE2 will automatically add a contId request parameter when necessary. You can also manually retrieve the active continuation ID through c.continuationId() or tell RIFE2 to generate a contID query parameter anywhere by using the context:contId template value tag.

Enable continuation instrumentation

Continuations are not possible without advanced bytecode manipulation. In order to enable continuations support, it's necessary to launch your JVM with the RIFE2 agent jar. This is distributed with the regular Maven artifact and uses the agent classifier.

If you use bld, activating the agent is easy, just call useRife2Agent(version) in your project“ file:

public class MyappBuild extends WebProject {
    public MyappBuild() {
        useRife2Agent(version(1,8,0));
        //...
    }
}

The RIFE2 Gradle plugin also has support for activating the agent, simply edit app/build.gradle.kts and enable the option:

//...
rife2 {
    version.set("1.8.0")
    uberMainClass.set("hello.AppUber")
    useAgent.set(true)
}
//...

NOTE : If RIFE2's instrumentation agent isn't present, your element will still be processed by the web engine, but instead of executing the c.pause() call as expected, it will trigger an exception that indicates instrumentation is missing.


Next learn more about Out-of-container Testing

Clone this wiki locally