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 thethrows
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 inthrows
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:
- 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.
- 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. - 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.
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.