Skip to content

Commit d5a0484

Browse files
[GR-54570] Add an example of using the continuation API with serialization.
PullRequest: graal/18875
2 parents 32fbc59 + 9182d7e commit d5a0484

File tree

3 files changed

+503
-36
lines changed

3 files changed

+503
-36
lines changed

espresso/docs/continuations.md

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
1-
# Espresso Continuations
1+
---
2+
layout: docs
3+
toc_group: espresso
4+
link_title: Continuation API
5+
permalink: /reference-manual/espresso/continuations/
6+
---
27

3-
The continuations feature of the Espresso VM allows you to control the program stack. When a continuation is _suspended_
4-
the stack is unwound and copied onto the heap as ordinary Java objects. When a continuation is _resumed_ those objects
5-
are put back onto the stack, along with all the needed metadata to resume execution at the pause point. The heap objects
6-
can be serialized to resume execution in a different JVM running the same code (e.g. after a restart).
8+
# Continuation API
9+
10+
The [Continuation API](https://mvnrepository.com/artifact/org.graalvm.espresso/continuations) enables you to control the program stack.
11+
When a continuation is _suspended_, the stack is unwound and copied onto the heap as ordinary Java objects.
12+
When a continuation is _resumed_, those objects are put back onto the stack, along with all the needed metadata to resume execution at the pause point.
13+
The heap objects can be serialized to resume execution in a different JVM running the same code (for example, after a restart).
714

815
## Usage
916

10-
See the JavaDoc of the `org.graalvm.continuations` package, and make sure to add the `continuations.jar` in the Espresso
11-
distribution to your classpath when compiling (but not at runtime).
17+
Add the `continuations.jar` to your classpath at compilation time (it will be automatically provided at runtime).
18+
The continuation feature is experimental and needs to be explicitly enabled by using these options: `--experimental-options --java.Continuum=true`.
1219

13-
Currently, only the Espresso VM supports the continuations feature. Since it is still experimental, the option needs to
14-
be enabled by using the flags `--experimental-options --java.Continuum=true`.
20+
See an [example usage](serialization.md) of the Continuation API with serialization.
1521

16-
### High level
22+
### High Level
1723

1824
If you can model your use case as code that emits (or _yields_) a stream of objects, you can use the `Generator<T>`
19-
class. This provides something similar to Python's generators with a convenient API. Just subclass it,
20-
implement `generate` and call `emit` from inside it.
25+
class. This provides something similar to Python's generators with a convenient API:
26+
subclass it, implement `generate`, and call `emit` from inside it.
27+
28+
[See an example of using the Generator API](generators.md).
2129

22-
### Low level
30+
### Low Level
2331

2432
You create a new `Continuation` by passing the constructor an object that implements the functional
2533
interface `ContinuationEntryPoint` (which can be a lambda). That object's `start` method receives
@@ -29,10 +37,10 @@ unwound and stored inside the `Continuation` object. You can then call `resume()
2937
time or to restart from the last suspend point.
3038

3139
Continuations are single-threaded constructs. There are no second threads involved, and the `resume()` method blocks
32-
until the continuation either finishes successfully, throws an exception or calls suspend. `isResumable()` can be called
33-
to determine if the continuation can be resumed (if the continuation has been freshly created or it has been previously
34-
suspended), and `isCompleted()` can be called to determine if the continuation has completed (either by returning
35-
normally, or if an exception escaped).
40+
until the continuation either finishes successfully, throws an exception or calls suspend.
41+
You can use `isResumable()` to check if the continuation can be resumed (for example, if the continuation has been
42+
freshly created or it has been previously suspended), and `isCompleted()` to verify whether the continuation has completed
43+
(either by returning normally, or if an exception escaped).
3644

3745
`Continuation` implements `Serializable` and can serialize to a backwards compatible format. Because frames can point to
3846
anything in their parameters and local variables, the class `ContinuationSerializable` provides static
@@ -57,7 +65,7 @@ prevent a VM crash. Examples of these checks are:
5765
Deserializing a continuation supplied by an attacker will allow complete takeover of the JVM. Only resume continuations
5866
you persisted yourself!
5967

60-
## Use cases
68+
## Use Cases
6169

6270
Serializing a continuation makes it _multi-shot_, meaning you can restart a continuation more than once and thus redo
6371
the same computation with different inputs. This ability to explore "parallel worlds" opens up many interesting use
@@ -84,7 +92,7 @@ cases.
8492
to migrate units of work.
8593
- **Parsing**: Implementing non-deterministic parsers or interpreters where multiple potential parsing paths are
8694
explored simultaneously.
87-
- **Custom Control Structures**: Creating new control structures (like loops, exception handling) that are not natively
95+
- **Custom Control Structures**: Creating new control structures (for example, loops, exception handling) that are not natively
8896
supported in the programming language.
8997
- **Time-travel Debugging**: Capturing continuations at various points in a program to enable "stepping back" in time
9098
during debugging sessions.
@@ -95,22 +103,23 @@ cases.
95103

96104
CPUs attempt to guess the direction of a branch when the data it depends on hasn't arrived yet. This is helpful because
97105
memory is slow. The same trick can be done at much higher levels using continuations when reading data from slow data
98-
sources like a far away server. If you have a computation where CPU intensive work is interleaved with long blocking
106+
sources such as a far away server. If you have a computation where CPU intensive work is interleaved with long blocking
99107
periods, this can speed things up.
100108

101-
When a continuation performs a slow operation that yields a value (e.g. an RPC), we can suspend, serialize, dispatch the
102-
request and then instead of waiting for the result and scheduling other work - as would be standard in a
103-
continuation-based async threads implementation like Loom - we can instead pick one or more values that we think the RPC
104-
_might_ return. Then we deserialize the continuation again for each value we want to try and resume it. This forks
105-
execution into multiple parallel universes. The new paths can then fork again, forming a tree of possibilities. If the
106-
continuation tries to do something that isn't controlled by the surrounding framework and can't be undone (a side
107-
effect), it suspends at that point and doesn't continue (this is as far as we can speculate).
109+
When a continuation performs a slow operation that yields a value (for example, an RPC), you can suspend, serialize, dispatch the request.
110+
Instead of waiting for the result and scheduling other work—as would be standard in continuation-based async threads implementations such as [Project Loom](https://wiki.openjdk.org/display/loom/Main)—you
111+
can instead pick one or more values that we think the RPC _might_ return.
112+
Then the continuation is deserialized for each value, resuming execution separately for each possibility.
113+
This approach forks the execution into multiple parallel paths. The new paths can then fork again, forming a tree of possibilities.
114+
115+
If the continuation encounters an operation that is not controlled by the surrounding framework and cannot be undone (for example, a side
116+
effect), it suspends at that point and doesn't continue speculatively.
108117

109-
As results arrive from the remote server we can steadily resolve our way down the tree discarding all the serialized
110-
continuations that exist in worlds that weren't taken. Eventually the final result is received and the continuation can
111-
finish, or continue past a side effecting point.
118+
When results arrive from the remote server, the speculative tree of serialized continuations can be resolved incrementally.
119+
Serialized continuations corresponding to paths that are no longer valid are discarded.
120+
Once the final result is received, the continuation either completes execution or proceeds beyond a point where side effects occur.
112121

113-
If the server protocol allows results to be chained together (e.g.
122+
If the server protocol allows results to be chained together (for example,
114123
as [Cap'n'Proto RPC does](https://capnproto.org/rpc.html#time-travel-promise-pipelining),
115124
or [FoundationDB](https://github.com/apple/foundationdb/wiki/Everything-about-GetMappedRange)), back-to-back speculative
116125
calls can be transmitted to the server for local processing, avoiding roundtrips.
@@ -127,11 +136,11 @@ There are special situations in which a call to `suspend` may fail with `Illegal
127136

128137
Furthermore, there is currently no support for continuation-in-continuation.
129138

130-
## Internal implementation notes
139+
## Internal Implementation Notes
131140

132141
*This section is only relevant for people working on Espresso itself.*
133142

134-
Continuations interact with the VM via private intrinsics registered on the `Continuation` and `ContinuationImpl` class.
143+
Continuations interact with the VM through private intrinsics registered for the `Continuation` and `ContinuationImpl` classes.
135144

136145
A continuation starts by calling into the VM. Execution resurfaces in the guest world at the private `run` method of
137146
`ContinuationImpl`, which then invokes the user's given entry point.
@@ -145,13 +154,18 @@ On resuming a `Continuation`, the entire call stack needs to be re-winded. This
145154
than for regular calls, and there is one such call target per encountered resume `bci`.
146155

147156
These call targets take a single argument: the `HostFrameRecord` that was stored into the `Continuation`. Using this
148-
record, we restore the frame for the current method, we unlink the current record from the rest (for GC purposes), and
149-
we pass the rest of the records to the next method. This is all done in a special invoke node, `InvokeContinuableNode`.
157+
record, the frame is restored for the current method, the current record is unlinked from the rest (for GC purposes), and
158+
the rest of the records is passed to the next method. This is all done in a special invoke node, `InvokeContinuableNode`.
150159

151160
The separation of the call targets has two advantages:
152161

153162
- It does not interfere with regular calls.
154163
- Resuming and suspending can be partial-evaluated, leading to fast suspend/resume cycles.
155164

156165
Serialization is done entirely in guest-side code, by having the `Continuation` class implement `Serializable`. The
157-
format is designed to enable backwards-compatible evolution of the format.
166+
format is designed to enable backwards-compatible evolution of the format.
167+
168+
### Further Reading
169+
* [Serialization of Continuations](serialization.md)
170+
* [Generator API](generators.md)
171+

espresso/docs/generators.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
---
2+
layout: docs
3+
toc_group: espresso
4+
link_title: Generator API
5+
permalink: /reference-manual/espresso/continuations/generators/
6+
---
7+
8+
# Generators
9+
10+
An example of how to use the included `Generator<E>` class below for Python-style generators. It prints out the numbers from 1-5.
11+
12+
```java
13+
import org.graalvm.continuations.Generator;
14+
15+
import java.io.*;
16+
17+
public class GeneratorTest {
18+
public static void main(String[] args) throws IOException, ClassNotFoundException {
19+
var generator = new Generator<Integer>() {
20+
@Override
21+
protected void generate() {
22+
for (int i = 1; i <= 5; i++) {
23+
doWork(i);
24+
}
25+
}
26+
27+
private void doWork(int i) {
28+
if (i % 2 == 0) {
29+
emit(i);
30+
}
31+
}
32+
};
33+
34+
while (generator.hasMoreElements()) {
35+
System.out.println(generator.nextElement());
36+
37+
// Round-trip the generator through Java object serialization.
38+
// In a real program you'd write to disk, or just use
39+
// generators alone without serialization.
40+
generator = deserialize(serialize(generator));
41+
}
42+
}
43+
44+
private static ByteArrayOutputStream serialize(Object obj) throws IOException {
45+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
46+
try (ObjectOutputStream oos = new ObjectOutputStream(bytes)) {
47+
oos.writeObject(obj);
48+
}
49+
return bytes;
50+
}
51+
52+
@SuppressWarnings("unchecked")
53+
private static <T> T deserialize(ByteArrayOutputStream firstSuspension) throws IOException, ClassNotFoundException {
54+
return (T) new ObjectInputStream(new ByteArrayInputStream(firstSuspension.toByteArray())).readObject();
55+
}
56+
}
57+
```
58+
59+
Here's what the implementation looks like using [the low level API](continuations.md#low-level).
60+
61+
```java
62+
package org.graalvm.continuations;
63+
64+
import java.io.Serial;
65+
import java.io.Serializable;
66+
import java.util.Enumeration;
67+
import java.util.NoSuchElementException;
68+
69+
/**
70+
* An {@link Enumeration} that emits an element any time {@link #emit(Object)} is called from inside
71+
* the {@link #generate()} method. Emit can be called anywhere in the call stack. This type of
72+
* enumeration is sometimes called a <i>generator</i>.
73+
*/
74+
public abstract class Generator<E> implements Enumeration<E>, Serializable {
75+
@Serial
76+
private static final long serialVersionUID = -5614372125614425080L;
77+
78+
private final Continuation continuation;
79+
private Continuation.SuspendCapability suspendCapability;
80+
private transient E currentElement;
81+
private transient boolean hasProduced;
82+
83+
84+
/**
85+
* This constructor exists only for deserialization purposes. Don't call it directly.
86+
*/
87+
@SuppressWarnings("this-escape")
88+
protected Generator() {
89+
continuation = new Continuation((Continuation.EntryPoint & Serializable) suspendCapability -> {
90+
this.suspendCapability = suspendCapability;
91+
generate();
92+
});
93+
}
94+
95+
/**
96+
* Runs the generator and returns true if it emitted an element. If it finished running, returns
97+
* false. If the generator throws an exception it will be propagated from this method.
98+
*/
99+
@Override
100+
public final boolean hasMoreElements() {
101+
if (hasProduced)
102+
return true;
103+
Continuation.State state = continuation.getState();
104+
boolean ready = state == Continuation.State.SUSPENDED || state == Continuation.State.NEW;
105+
if (!ready)
106+
return false;
107+
continuation.resume();
108+
return hasProduced;
109+
}
110+
111+
/**
112+
* Runs the generator if necessary, and returns the element it yielded.
113+
*
114+
* @throws NoSuchElementException if the generator has finished and no longer emits elements,
115+
* or if the generator has previously thrown an exception and failed.
116+
*/
117+
@Override
118+
public final E nextElement() {
119+
if (!hasMoreElements())
120+
throw new NoSuchElementException();
121+
E el = currentElement;
122+
currentElement = null;
123+
hasProduced = false;
124+
return el;
125+
}
126+
127+
/**
128+
* Call this method to emit an element from inside {@link #generate()}.
129+
*/
130+
protected final void emit(E element) {
131+
assert !hasProduced;
132+
currentElement = element;
133+
hasProduced = true;
134+
suspendCapability.suspend();
135+
}
136+
137+
/**
138+
* Implement this method to {@link #emit(Object)} elements from the enumeration.
139+
*/
140+
protected abstract void generate();
141+
142+
private transient boolean reentrancy = false;
143+
144+
@Override
145+
public String toString() {
146+
// Printing the continuation will invoke toString on everything reachable from the stack,
147+
// thus we need to cancel the re-entrancy here.
148+
if (reentrancy)
149+
return "this generator";
150+
reentrancy = true;
151+
String result = continuation.toString();
152+
reentrancy = false;
153+
return result;
154+
}
155+
}
156+
```

0 commit comments

Comments
 (0)