/*
 * Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.oracle.truffle.js.test.threading;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.PolyglotAccess;
import org.graalvm.polyglot.Value;
import org.junit.Assert;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

public class AsyncTaskTests {

    
A JavaScript function can be executed asynchronously in another thread (using proper synchronization).
/** * A JavaScript function can be executed asynchronously in another thread (using proper * synchronization). */
@Test public void completableFuture() throws InterruptedException { final AtomicBoolean asyncTaskExecuted = new AtomicBoolean(false); final AtomicReference<Throwable> asyncException = new AtomicReference<>(); final AtomicReference<Object> asyncJsResult = new AtomicReference<>(); final ForkJoinPool testExecutor = new ForkJoinPool(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ByteArrayOutputStream err = new ByteArrayOutputStream(); try (Context cx = Context.newBuilder("js").allowHostAccess(HostAccess.ALL).allowPolyglotAccess(PolyglotAccess.ALL).out(out).err(err).build()) { // Expose a Java method as a JavaScript function. cx.getBindings("js").putMember("javaNextTick", (CallableInt) (jsLambda) -> { // Submit a JavaScript function for async execution in another thread. CompletableFuture.supplyAsync(() -> { asyncTaskExecuted.set(true); synchronized (cx) { // Re-enter context. cx.enter(); try { // Execute the JS callback function. return jsLambda.execute(); } finally { // Leave context. cx.leave(); } } }, testExecutor).whenComplete((r, ex) -> { asyncException.set(ex); asyncJsResult.set(r); }); }); // Create a JS function that will execute a Java callback asynchronously. Conceptually, // this is equivalent to `process.nextTick()` in Node.js Value jsFunction = cx.eval("js", "(function() {" + " return javaNextTick(()=>{console.log('something async'); return 42;});" + "})"); // The callback will execute a JS function in another concurrent thread. Synchronization // is needed to prevent data races. synchronized (cx) { // Execute the JS function. Context enter and leave are implicit. jsFunction.executeVoid(); } testExecutor.shutdown(); testExecutor.awaitTermination(1, TimeUnit.MINUTES); Assert.assertNull(asyncException.get()); Assert.assertTrue(asyncTaskExecuted.get()); Assert.assertTrue(new String(err.toByteArray()).isEmpty()); Assert.assertEquals("something async\n", new String(out.toByteArray())); Assert.assertEquals(42, ((Value) asyncJsResult.get()).asInt()); } }
A JavaScript function can be executed asynchronously in another thread (using proper synchronization). Asynchronous execution can be mapped to a JavaScript Promise.
/** * A JavaScript function can be executed asynchronously in another thread (using proper * synchronization). Asynchronous execution can be mapped to a JavaScript Promise. */
@Test public void completableFuturePromise() throws InterruptedException { final AtomicBoolean asyncTaskExecuted = new AtomicBoolean(false); final AtomicReference<Throwable> asyncException = new AtomicReference<>(); final AtomicReference<Object> asyncJsResult = new AtomicReference<>(); final ForkJoinPool testExecutor = new ForkJoinPool(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ByteArrayOutputStream err = new ByteArrayOutputStream(); try (Context cx = Context.newBuilder("js").allowHostAccess(HostAccess.ALL).allowPolyglotAccess(PolyglotAccess.ALL).out(out).err(err).build()) { // Expose a Java method as a JavaScript function. cx.getBindings("js").putMember("javaPromiseInstance", (ThenableInt) (onResolve, onReject) -> { // Submit a JavaScript function for async execution in another thread. CompletableFuture.supplyAsync(() -> { asyncTaskExecuted.set(true); synchronized (cx) { // Re-enter context. cx.enter(); try { // Resolve the JS Promise with completion value 'post'. // Execution flow will continue in the JS engine. onResolve.execute("post"); // Returned value from Java completable future will be dispatched to // Java's `whenComplete`. return 42; } catch (Throwable t) { onReject.executeVoid(t); return t; } finally { // Leave context. cx.leave(); } } }, testExecutor).whenComplete((r, ex) -> { asyncException.set(ex); asyncJsResult.set(r); }); }); // Create an async JS function that will wait for an async task executed in a Java async // executor. Value asyncJsFunction = cx.eval("js", "(async function() {" + " console.log('pre');" + " var post = await javaPromiseInstance;" + " console.log(post);" + "})"); // The callback will execute a JS function in another concurrent thread. Synchronization // is needed to prevent data races. synchronized (cx) { // Execute the JS function. Context enter and leave are implicit. asyncJsFunction.executeVoid(); } testExecutor.shutdown(); testExecutor.awaitTermination(1, TimeUnit.MINUTES); Assert.assertNull(asyncException.get()); Assert.assertTrue(asyncTaskExecuted.get()); Assert.assertTrue(new String(err.toByteArray()).isEmpty()); Assert.assertEquals("pre\npost\n", new String(out.toByteArray())); Assert.assertEquals(42, asyncJsResult.get()); } }
A running JavaScript thread can be suspended (blocked) while JS execution can continue in another thread.
/** * A running JavaScript thread can be suspended (blocked) while JS execution can continue in * another thread. */
@Test public void plainJavaThread() { final ByteArrayOutputStream out = new ByteArrayOutputStream(); final AtomicReference<Object> asyncJsResult = new AtomicReference<>(); try (Context cx = Context.newBuilder("js").allowHostAccess(HostAccess.ALL).allowPolyglotAccess(PolyglotAccess.ALL).out(out).err(out).build()) { // Enter the current context from the main thread. cx.enter(); // Expose a Java method as a JavaScript function. cx.getBindings("js").putMember("aJavaFunction", (CallableInt) (jsLambda) -> { // Create a new thread Thread thread = new Thread(() -> { // Enter the JS context from another thread. cx.enter(); // Execute a JavaScript function. Value jsResult = jsLambda.execute(); asyncJsResult.set(jsResult); // Leave the current context cx.leave(); }); // Leave the current context. cx.leave(); // Start thread and wait for completion. thread.start(); // Halt until thread completes. thread.join(); // Re-enter context from main thread. cx.enter(); }); // Create a JS function that will execute a Java callback. Value jsFunction = cx.eval("js", "(function() {" + " return aJavaFunction(()=>{ console.log('something'); return 42;});" + "});"); // Execute the JS function jsFunction.executeVoid(); // The context can be used again from the current thread cx.eval("js", "console.log('something else');"); } Assert.assertEquals(42, ((Value) asyncJsResult.get()).asInt()); Assert.assertEquals("something\nsomething else\n", new String(out.toByteArray())); } @FunctionalInterface public interface CallableInt { void execute(Value jsLambda) throws InterruptedException; } @FunctionalInterface public interface ThenableInt { void then(Value onResolve, Value onReject); } }