Continuations in Java 8, Part I

This post is a first one in a series dedicated to continuations support in JDK 1.8 – namely, continuations usage with labmdas (anonymous functions of SAM interfaces) and Stream API (java.util.stream).

When Oracle (and formerly Sun) developing next Java version, the backward compatibility is the one of the primary concerns. API compatibility, bytecode compatibility, whatever. But for tool vendors, like ones who develop compilers, IDE, ad-hoc bytecode enhancers / generators, runtime utilities relying on reflection, almost every new Java release is always a wake-up call to keep their products up to date.

JDK 1.1 adds inner classes and anonymous classes – a pretty convenient way to declare a class in-place. However, this is a great example of the leaked abstraction – tool vendors had to cope with automatically generated class names, with automatically generated constructors of non-static inner classes, with hidden accessor methods generated to use private members (fields and methods) between inner/outer classes.

Next big move was JDK 1.5 – generics added a lot of fun to the daily routine of tools vendors. Generic signatures in class/method/field/parameter/variable declarations, type erasures, covariant method return types, automatically generated bridge methods… oh my! Overwhelming list of futures to support! What can be better than upgrading thousands lines of the code to support all of this! However, this Java release brings us annotations – the real impulse to revisit all our dynamic code generation techniques anyway. And the community responded promptly – AOP-specific libraries and tools, (revisited) dependency injection techniques, mappings for JPA and XML, fully refactored JEE and so on, and so on…

Then it was JDK 1.7 with INVOKEDYNAMIC. Not a ground-shaking change for the majority of existed Java tools, but a great value for the authors of JVM-compiled languages. Just recall: JRuby (Ruby on Java) beats Ruby (Ruby on C) in terms of performance! Isn’t this amazing?! But… At that time I had not expected how this dynamic invocation gun shot at Tascalate Javaflow library with JDK 1.8 release…

So, now we are running our applications on JDK 1.8. And the number of additional features that should be taken on account by tool vendors is overwhelming. Default methods in interfaces, static methods in interfaces – supporting this is not a complex task per se, but the fact, that method’s bodies are possible inside an interface, invalidates a lot of the logic inside many tools (Tascalate JavaFlow were affected, too). However, the most ground-shaking addition was lambdas, to be precise – the way lambdas are created by compiler and Java runtime.

The key point in the phrase above is “by the compiler AND Java runtime”. This means that a related bytecode is not only generated at compile-time BUT at run-time as well. The lesser-known to the general public LambdaMetafactory class is heavily involved in this magic. The API notes for the class describes a process pretty well: the compiler desugars lambda function’s body to the auto-generated method of the enclosing class, put an INVOKEDYNAMIC instruction whenever the SAM interface is used, and the LambdaMetafactory class links this dynamic invocation with an actual interface implementation generated at run-time. And in its’ own turn, this on-the-fly implementation delegates processing to the desugared lambda body (created at compile-time). A thrilling process!

So, we cannot rely on the compile-time-only bytecode modification any longer. But the lack of the labmdas support would be a serious omission. Fortunately, we, Java developers, have good old agents inside JVM that plays for us. A pun intended: this is namely JavaAgents technology that was introduced back in JDK 1.5 days. The technology provides a facility to intercept class loading mechanism and enhance a bytecode before it’s seen by the JVM. It works even with bytecode generated at run-time.

So, to support continuable anonymous lambdas and method references, you have to download first Tascalate JavaFlow Instrumentation Agent from the latest release on GitHub. Then you must please add the following arguments to Java command line:

java -javaagent:<path-to-jar>/javaflow.instrument-continuations.jar <rest-of arguments>

The agent JAR file includes all necessary dependencies and requires no additional CLASSPATH settings. It’s possible to use this agent in conjunction with either Maven or Ant build tools supplied. Moreover, it’s even a recommended option – it helps to minimize the associated overhead of the instrumentation during class-loading process at run-time. However, using just instrumentation agent has its’ own benefits when you are developing and debugging code within your IDE of choise. Just specify the same :javaagent option for your Run/Debug configuration (screen-shot below) – and you are ready to execute quick “debug-fix” loops relying on IDE-s incremental compilation + JavaAgent instrumentation – as opposed to time-consuming full project rebuild with Mavent/Ant.

JavaAgent IDE Debug Settings.png

Next time we will explore concrete examples with continuable Java 8 lambdas as well as additional Tascalate JavaFlow utility classes that simplifies related tasks.

Tascalate JavaFlow 2.0 is out

Tascalate JavaFlow continuations library version 2.0 is released. Now it’s published to the Maven Central Repository, so no need to build it from GitHub sources any longer. Plus, additional binary resources are uploaded to the GitHub release page:

  • Ant project templates (1 and 2) with all necessary libraries and sample build.xml
  • Command-line JAR rewrite tool
  • Java Agent to instrument classes at run-time (during class-loading) – javaflow.instrument-continuations.jar — a MUST-have if continuable code is invoked within Java 8 lambdas and a real time-saver if you are debugging continuable code from an IDE *
  • Java Agent to instrument proxies of popular CDI containers (JBoss Weld and Apache OpenWebBeans) – javaflow.instrument-cdi-proxy.jar – to correctly support continuable methods in CDI managed beans

Additional information may be found on the project’s front-page on GitHub

*In several next posts I will elaborate more about continuations with Java 8 lambdas

The case for @continuable @annotation (or @Continuable for that matter)

There is a well-defined naming convention in Java to start annotation names with an uppercase letter – same as for class/interface names. Though this naming convention is widely accepted, I think it’s a bit dogmatic. If you take a look at the list of Java-specific annotations in Scala you may see that both lowercase (like @cloneable or @throws) and uppercase (like @SerialVersionUID or @BeanProperty) forms are used. Personally, I tend to agree with decisions made by architects of the Scala’s standard library: some annotations are meta-data, for example javax.persistence.@Entity/javax.persistence.@Column or javax.xml.bind.annotation.@XmlElement/javax.xml.bind.annotation.@XmlAttribute; other ones look like a directive to processing tool or like a syntax extension. In addition, directive-like annotations typically have no extra parameters, so they are very similar to @-prefixed keywords. Hence, I think it would be more natural to write @override rather than @Override in Java — it’s a directive to compiler to check whether or not the method overrides/implements a method defined in a superclass/interface. Moreover, in some languages “override” is indeed a keyword, so it’s even more tempting to use all-lower-case variant.

The above should explain why lower-case annotation names where chosen for two annotations in Tascalate Javaflow: @continuable and @ccs. Both are directives to bytecode instrumentation tools, both have no extra parameters, and both looks like library-defined keywords:

public @continuable void execute() {
  ...
  final @ccs Runnable contRunnable = new MyContinuableRunnable(someArg);
  ...
}

What if you don’t agree with arguments above and would prefer to follow standard Java naming conventions for annotations? Not a problem at all with Tascalate Javaflow! There is a third annotation defined in the library – org.apache.commons.javaflow.api.ContinuableAnnotation – that allows you to define your own annotations instead of @continuable and @ccs. In fact, ContinuableAnnotation is a meta annotation and it may/should be applied only to other annotations. Here is how to define @ContinuableMethod annotation that you may use instead of @continuable in your code to strictly adhere to Java naming conventions:

package mycompany.myapp.annotations;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.apache.commons.javaflow.api.ContinuableAnnotation;

@Documented                       // optional
@Retention(RetentionPolicy.CLASS) // mandatory, may be RetentionPolicy.RUNTIME as well
@Target({ElementType.METHOD})     // mandatory, only methods are examined
@ContinuableAnnotation            // mandatory
public @interface ContinuableMethod {
}

The rules are:

  1. Your custom @ContinuableMethod annotation must be annotated with @ContinuableAnnotation (obviously)
  2. It must be an annotation applicable to methods – any other targets have no effect and will only confuse a user of your annotation class. However, sometimes you need to have other targets and it’s ok to define more – typical example is CDI interceptor binding annotations.
  3. It must have retention policy defined either as RetentionPolicy.CLASS or as RetentionPolicy.RUNTIME, SOURCE-level annotations are not saved into class bytes

Similar, you can re-define @ccs as @ContinuableTarget:

package mycompany.myapp.annotations;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.apache.commons.javaflow.api.ContinuableAnnotation;

// optional
@Documented                       
// mandatory, may be RetentionPolicy.RUNTIME as well
@Retention(RetentionPolicy.CLASS) 
// mandatory, use exact syntax
@Target({ElementType.LOCAL_VARIABLE, ElementType.PARAMETER, ElementType.TYPE_USE}) 
// mandatory
@ContinuableAnnotation            
public @interface ContinuableTarget {
}

The rules are similar to the method-level annotation except for @Target – please use exact syntax as above. In addition to regular variable/parameter targets you must declare ElementType.TYPE_USE (Java 8 feature) – otherwise annotation is not saved in a class bytecode.

No other customization is necessary: you may use your own annotations right away thanks to meta-annotation. By the way, there is another important use-case when ContinuableAnnotation may be useful in your code — stereotype annotations. Imaging, that your application has a method-level annotation @WorflowTask with it’s own duties. Moreover, all @WorkflowTask methods must be @continuable. To mark some business methods as workflow tasks you should use both annotations:

package mycompany.myapp.services;
import mycompany.myapp.annotations.WorkflowTask;
import org.apache.commons.javaflow.api.ContinuableAnnotation;

public class MyService {
  ...
  @WorkflowTask(timeout="3d",name="SomeTask") 
  @continuable
  public int myBusinessMethod() { ... }
  ...
}

However, if you mark your @WorkflowTask annotation as @ContinuableAnnotation then you may use just one annotation in your code:

package mycompany.myapp.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.apache.commons.javaflow.api.ContinuableAnnotation;

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD})    
@ContinuableAnnotation           
public @interface WorkflowTask{
  public String name();
  public String timeout();
  ...
}

//=====

package mycompany.myapp.services;
import mycompany.myapp.annotations.WorkflowTask;

public class MyService {
  ...
  @WorkflowTask(timeout="3d",name="SomeTask") 
  public int myBusinessMethod() { ... }
  ...
}

Now @WorkflowTask annotation is a stereotype that:

  • clearly defines specific business method role
  • captures this role at a conceptual level
  • encapsulates implementation details

Continuation rulezz or rules for Continuations

In previous article I said that original JavaFlow library instrumented each and every method and adds prologue/epilogue before every method invocation to support continuations. This leads to the terrible performance, however this option adds no extra responsibilities to developer. To overcome the original JavaFlow inefficiency in the Tascalate JavaFlow library the explicit marker annotation is added — @continuable. With a great power comes a great responsibility – now user MUST annotate every continuable method with this annotation and, moreover, must ensure that there are no non-continuable methods on a call stack i.e. avoid non-continuable-man-in-the-middle case.

Probably, this is a major weakness of the proposed solution, and later I’m planning to add the compile-time verifier to ensure there are no such broken call chains (via the Language Model API + JSR 269). If you are curious, other Java continuation libraries are applying different techniques to both mark method as continuable and enforce developer to call continuable methods only from within other continuable methods:

  • Kilim uses specific checked exception – Pausable, and instruments all methods that declare this exception in the throws clause. And it’s not easy to break a call chain while you have to do smth. about the checked exception, and “smth.” is just to keep re-declaring it in throws clause of the every method involved.
  • offbynull/coroutines uses explicit Continuation parameter, and instruments only methods with this parameter. Hence, you can’t create a continuable method without declaring (and later passing deeper) this parameter; therefore you can’t get a broken call chain.
  • Quasar uses a combination of what Kilim does – SuspendExecution, and what the Tascalate JavaFlow does – @Suspendable annotation – with all the usual caveats apply, see below.

There are 3 rules you must understand to avoid errors with the continuations library:

  1. Instrumentation analyzes call stack statically (at build-time via Maven plugin / Ant task, or at class loading time via JavaAgent or special class loader). No dynamic run-time checks are performed.
  2. Method must be either annotated @continuable or must override method annotated @continuable to be instrumented. Otherwise it’s ignored. (Lambdas are separate case, and I will discuss them later). “Override” in this case works both for overriding method from a superclass and implementing an interface method.
  3. While instrumented, only invocations to other @continuable methods are processed and surrounded with continuation-related call stack management code. If no such invocations exist then method is unchanged.
  4. Now let us review several examples and see why some code work and (more importantly) why some doesn’t work.

    Lets’ start with breaking rules in the most trivial way:

    package org.apache.commons.javaflow.examples.simple;
    
    import org.apache.commons.javaflow.api.Continuation;
    import org.apache.commons.javaflow.api.continuable;
    
    public class Execution implements Runnable {
    
        @Override
        public @continuable void run() {
            for (int i = 1; i <= 5; i++) {
                delegateCall(i);	        
            }
        }
        
        /*@continuable*/
        // commented out to show error
        void delegateCall(int i) {
            System.out.println("Exe before suspend");
            Object fromCaller = Continuation.suspend(i);
            System.out.println("Exe after suspend: " + fromCaller);         
        }
    }

    If you run the code (main class in all examples comes from the first post) you will see the following stack trace:

    Exe before suspend
    Exe after suspend: null
    Exe before suspend
    Exe after suspend: null
    Exe before suspend
    Exe after suspend: null
    Exe before suspend
    Exe after suspend: null
    Exe before suspend
    Exe after suspend: null
    May 26, 2016 3:11:31 PM org.apache.commons.javaflow.core.StackRecorder execute
    SEVERE: Stack corruption on suspend (empty stack). Is org.apache.commons.javaflow.examples.simple.Execution@340870931/sun.misc.Launcher$AppClassLoader@414493378 instrumented for javaflow?
    java.lang.IllegalStateException: Stack corruption on suspend (empty stack). Is org.apache.commons.javaflow.examples.simple.Execution@340870931/sun.misc.Launcher$AppClassLoader@414493378 instrumented for javaflow?
    	at org.apache.commons.javaflow.core.StackRecorder.execute(StackRecorder.java:119)
    	at org.apache.commons.javaflow.api.Continuation.resumeWith(Continuation.java:229)
    	at org.apache.commons.javaflow.api.Continuation.resume(Continuation.java:215)
    	at org.apache.commons.javaflow.api.Continuation.startWith(Continuation.java:136)
    	at org.apache.commons.javaflow.api.Continuation.startWith(Continuation.java:111)
    	at org.apache.commons.javaflow.examples.simple.SimpleExample.main(SimpleExample.java:11)

    This happens because delegateCall method is not annotated with @continuable. Hence neither it nor run methods are instrumented correctly. As a consequence, the invocation of Continuation.suspend() in the delegateCall goes south the hard way. The fix is trivial: add @continuable annotation to the delegateCall.

    Let us create a more complex case:

    package org.apache.commons.javaflow.examples.inheritance;
    
    interface IDemo {
        void call(int payload);
    }
    
    package org.apache.commons.javaflow.examples.inheritance;
    
    import org.apache.commons.javaflow.api.Continuation;
    import org.apache.commons.javaflow.api.continuable;
    
    public class DemoConcrete implements IDemo {
    
        public @continuable void call(int payload) {
            System.out.println("Exe before suspend");
            Object fromCaller = Continuation.suspend(payload);
            System.out.println("Exe after suspend: " + fromCaller); 
        }
    }
    

    package org.apache.commons.javaflow.examples.inheritance;
    
    import org.apache.commons.javaflow.api.continuable;
    
    public class Execution implements Runnable {
    
        @Override
        public @continuable void run() {
            DemoConcrete demo = new DemoConcrete();
            for (int i = 1; i <= 5; i++) {
                demo.call(i);
            }
        }
    }

    It works! Test passed with flying colors! But, hey, why we are working with the concrete implementation rather than with the interface?

    DemoConcrete demo = new DemoConcrete();

    Let’s make OOP gurus happy and change this line to:

    IDemo demo = new DemoConcrete();

    Ooops! We got an exception that looks quite familiar. But why??? Because we are violationg rules [1] and [3]: when JavaFlow tools modify code they see now INVOKE_INTERFACE call to the method IDemo.call, and it’s non-@continuable. Originally it was INVOKE_VIRTUAL invocation of DemoConcrete.call, and it’s @continuable. The fix is to apply the following modification to the interface declaration:

    package org.apache.commons.javaflow.examples.inheritance;
    import org.apache.commons.javaflow.api.continuable;
    
    interface IDemo {
        @continuable void call(int payload);
    }
    

    Now it works (walks and quacks) as expected. But what if an interface definition is out of your control? The recommended approach is to create a derived interface with necessary methods re-declared with @continuable annotation. The full example is below:

    package org.apache.commons.javaflow.examples.inheritance;
    // Original
    interface IDemo {
        void call(int payload);
    }
    
    package org.apache.commons.javaflow.examples.inheritance;
    import org.apache.commons.javaflow.api.continuable;
    // Continuation-specific extension
    interface IDemoContinuable extends IDemo {
        @continuable void call(int payload);
    }
    
    package org.apache.commons.javaflow.examples.inheritance;
    
    import org.apache.commons.javaflow.api.Continuation;
    import org.apache.commons.javaflow.api.continuable;
    
    // Implement continuation-specific extension
    // (and the original interface transitively)
    public class DemoConcrete implements IDemoContinuable {
    
        public void call(int payload) {
            System.out.println("Exe before suspend");
            Object fromCaller = Continuation.suspend(payload);
            System.out.println("Exe after suspend: " + fromCaller); 
        }
    }
    package org.apache.commons.javaflow.examples.inheritance;
    
    import org.apache.commons.javaflow.api.continuable;
    
    public class Execution implements Runnable {
    
        @Override
        public @continuable void run() {
            // Use continuation-specific extension
            IDemoContinuable demo = new DemoConcrete();
            for (int i = 1; i <= 5; i++) {
                demo.call(i);
            }
        }
    }

    Looks a bit verbose and complex? Probably. On other hand, this is necessary only for interfaces you have no control of. For your business interfaces/objects developed from ground up you should apply @continuable annotations directly.

    But wait. There is another option. And I would say not a “better option”. It’s a “worse” option, and you should use it very-very rarely. Only when it truly itches to do something quick. Like in the scenario above: you have ready hierarchy, you have only one call, and you are in hurry to get the job done. So, let us assume that you have a non@continuable IDemo.call, a @continuable DemoConcrete.call and we are back to our broken Execution with the following fix:

    package org.apache.commons.javaflow.examples.inheritance;
    
    import org.apache.commons.javaflow.api.ccs;
    import org.apache.commons.javaflow.api.continuable;
    
    public class Execution implements Runnable {
    
        @Override
        public @continuable void run() {
            @ccs IDemo demo = new DemoConcrete();
            for (int i = 1; i <= 5; i++) {
                demo.call(i);
            }
        }
    }

    In case you overlooked the change, we added small (3-letters only, afterall) annotation @ccs. This abbreviation stays for “Continuable Call Site” and may be applied only to local variables/parameters (and only when using Java 8 and above).

    The @ccs annotation says to instrumentation tools that if owner of a method (this) is an annotated variable/parameter then the method invocation should be considered continuable. Please be aware: any method invocation. Including toString() and equals(…). So use with care. Btw, it works with raw variables as well as with multidimensional arrays.

    I haven’t mentioned this before, but @continuable annotation may be applied to almost any method: instance, static, private, protected, package-private, public, abstract, concrete. Almost any… besides constructors. No one of continuation libraries available allows you to suspend in constructor. If you put @continuable annotation on constructor you will get a compilation error.

Let’s Continuation.resume()

Continuations support in Java is an old topic. Also Sun (and later Oracle) always declined to include first-class continuations into the JVM and the Java language, the community keep trying to add this functionality via libraries and/or tools.

One of the first implementations of continuations for Java was Apache Commons JavaFlow. It was started back in 2005 (if my memory serves me well) by Torsten Curdt and was a very promising library besides some serious drawbacks. Initially the project was actively maintained, and you may find among contributors such Java heroes like Kohsuke Kawaguchi (Jenkins, Hudson and numerous other famous projects) and Eugene Kuleshov (a core ASM developer).

However, pretty soon the project was abandoned and never got out from Commons Sandbox. Some users (me included) were contributing minor patches, but fundamental issues had been never addressed until recently. Here is a list of 3 major problems the project had:

  1. Unacceptable performance. JavaFlow works via transforming bytecode into state machine to support continuations. To make this state machine work it has to save/restore call stack before/after every continuable method invocation. Pay attention – every continuable method. But original JavaFlow does not provide any option to mark a method as a continuable one. Hence it saves and restores stack before/after every method call. What a waste of CPU cycles!
  2. Java the language and Java the VM evolved, slowly but constantly. InokeDynamic, Lambdas — all of these require proper changes to bytecode modifications.
  3. JavaFlow tooling outdated pretty fast. Actually, command-line utility and Ant task was a step back even at the time the project was started — because it uses Maven as a build tool from the beginning.

The community concentrates mostly on the issue #3 — tools, and you may find a lot of JavaFlow Maven plugins on GitHub. That’s why I decided to share my own work that tries to address all aforementioned problems. If you ready to give JavaFlow a (second) try, then here is a quick start guide for Tascalate JavaFlow:

  1. Clone Git repository locally:
    git clone https://github.com/vsilaev/tascalate-javaflow.git
    
  2. Run Mavne build into the parent directory of the project
    mvn clean install
    
  3. Create an initial Java project for corresponding Mavne archetype
  4. Add the following dependency:
    <dependency>
        <groupId>net.tascalate.javaflow</groupId>
        <artifactId>net.tascalate.javaflow.api</artifactId>
        <version>2.0-SNAPSHOT</version>
    </dependency>
    
  5. Add the following build plugin configuration:
    <plugin>
        <groupId>net.tascalate.javaflow</groupId>
        <artifactId>net.tascalate.javaflow.tools.maven</artifactId>
        <version>2.0-SNAPSHOT</version>
        <executions>
            <execution>
                <phase>process-classes</phase>
                <goals>
                    <goal>javaflow-enhance</goal>
                </goals>
            </execution>
        </executions>
    </plugin>

    This plugin will instrument bytecode of classes after compilation to enable continuations support

Now we are ready to add some code. First let us start from the continuable class:

package demo;

import org.apache.commons.javaflow.api.Continuation;
import org.apache.commons.javaflow.api.continuable;

public class Execution implements Runnable {
  @Override
  public @continuable void run() {
    for (int i = 1; i <= 5; i++) {
        System.out.println("Exe before suspend");
        Object arg = Continuation.suspend(i);
        System.out.println("Exe after suspend: " + arg);	        
      }
  }
}

… and the main class to invoke it:

package demo;

import org.apache.commons.javaflow.api.Continuation;

public class SimpleExample {
  public static void main(final String[] argv) {

    final String[] strings = {"A", "B", "C"};
    for (
        Continuation cc = Continuation.startWith(new Execution()); 
        null != cc; ) {

      final int retVal = (Integer)cc.value();
      System.out.println("Interrupted " + retVal); 
      // Let's continuation resume
      cc = cc.resume( strings[retVal % strings.length] );
    }

      System.out.println("ALL DONE");
  }
}

Build project with Maven and run demo.SimpleExample. The following output should be produced:

Exe before suspend
Interrupted 1
Exe after suspend: B
Exe before suspend
Interrupted 2
Exe after suspend: C
Exe before suspend
Interrupted 3
Exe after suspend: A
Exe before suspend
Interrupted 4
Exe after suspend: B
Exe before suspend
Interrupted 5
Exe after suspend: C
ALL DONE

Now we are ready to review the code

  1. Execution class is almost a regular implementation of the java.lang.Runnable interface. “Almost” because its method run is annotated with @continuable annotation. This is where the magic begins. This annotation tells JavaFlow tools to instrument a method body and rewrite it with continuations support. Namely, a state machine will be created to allow multiple method’s enter/exit points, and stack will be captured/restored around invocation of any other continuable methods, in this case — Continuation.suspend(...)
  2. Main method of the SimpleExample class creates new continuation from the Runnable object and starts it immediately: Continuation.startWith(new Execution())
  3. Execution.run method is started and executed till the Continuation.suspend call. At this point Execution.run method ends, and the value passed as an argument to Continuation.suspend is returned to caller (variable retVal). So method with a void return type produces an Integer result. The magic continues.
  4. After printing value received from the continuation, the main method resumes a continuation via the call to the Continuation.resume(...)
  5. Execution.run method starts again, however this time it’s not running from the beginning, but rather from the point where a call to the Continuation.suspend(...) returns, right insight the “for” loop. A value received here is exactly the string we passed outside via calling Continuation.resume(...). The magic runs in a loop.
  6. On the next iteration Execution.run again met a call to the Continuation.suspend. We are back to the main method again.
  7. The process repeats till we end looping in the Execution.run method. This time the method returns normally (without continuation) and we get a null continuation in the main method. We are done.

Next time I will explain in more details what options are possible to declare a method as continuable, but examples for this topic are readily available in Tascalate JavaFlow repository at GitHub.