Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Impossible to write a script with async elements #137

Open
BreizHell opened this issue Oct 22, 2024 · 12 comments
Open

Impossible to write a script with async elements #137

BreizHell opened this issue Oct 22, 2024 · 12 comments
Assignees

Comments

@BreizHell
Copy link

BreizHell commented Oct 22, 2024

jArchi Version

1.7

Archi Version

5.4.2

Operating System

Linux Mint Cinnamon 64bit

Description

Async scenarios lead to the following checkmate :

  • Case 1: If a promise is pending, but the synchronous part of the code is finished, the entire script is ended, before the promise can finish
  • Case 2: If a top level await is written, org.graalvm.polyglot.PolyglotException is raised

Steps to reproduce

  • Case 1:

    console.log("starting synchronous part");
    
    new Promise((resolve) => {
      console.log("starting async part");
      setTimeout(resolve, 1000);
    }).then(() => console.log("async part ended"));
    
    console.log("synchronous part ended");

    In this case, the JArchi console only prints:

    • "starting synchronous part"
    • "starting async part"
    • "synchronous part ended"

    => "async part ended" is never printed

  • Case 2:

    console.log("starting synchronous part");
    
    await new Promise((resolve) => {
      console.log("starting async part");
      setTimeout(resolve, 1000);
    })
    console.log("async part ended");

    There, the console displays the following error:

    org.graalvm.polyglot.PolyglotException: SyntaxError: Test.ajs:3:6 Expected ; but found new
    await new Promise((resolve) => {
          ^
    Test.ajs:6:0 Expected eof but found }
    }).then(() => console.log("async part ended"));
    ^
    
      at <js>.:program(<eval>:1)
      at org.graalvm.polyglot.Context.eval(Context.java:399)
      at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:478)
      at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:446)
      at java.scripting/javax.script.AbstractScriptEngine.eval(Unknown Source)
    
@Phillipus
Copy link
Member

This might be a limitation in the GraalVM engine (jArchi uses version 22.3.0) or it might be something else. If a JS expert could help here that would be nice.

@BreizHell
Copy link
Author

BreizHell commented Oct 22, 2024

From what I understand, this may be solveable with a bit of inspiration from this article.

The gist of it would be :

  • Either create or modify one of JArchi's Java classes to include a then method :
    public class ThenableObject {
      void then(Value resolve, Value reject) {
        try {
          resolve.executeVoid();
        } catch (Throwable t) {
          reject.executeVoid(t);
        }
      }
    }
  • From the .ajs script, invoke said class, and delegate the promise to it:
    const javaExecutor = Java.type('ThenableObject').createNew();
    
    const interopPromise = new Promise(javaExecutor);
    
    // register some promise reactions
    interopPromise
      .then(
        (result) => console.log(`Java resolves: ${result}`),
        (error) => console.log(`Java rejects: ${error}`)
      )

@BreizHell
Copy link
Author

I'd like to take a go at my proposal above, but I have no experience in java, and I have no idea how to compile the JArchi project

@jbsarrodie
Copy link
Member

Hi,

I'll have a look at it.

@jbsarrodie jbsarrodie self-assigned this Oct 22, 2024
@BreizHell BreizHell changed the title Impossible to write a scripts with async elements Impossible to write a script with async elements Oct 22, 2024
@BreizHell
Copy link
Author

BreizHell commented Oct 25, 2024

Hello again!

With help from the documentation and Graal's own unit tests, I have attempted the following :

  • I have created a new file /com.archimatetool.script/src/com/archimatetool/script/commands/AsyncThing.java :
    package com.archimatetool.script.commands;
    import org.graalvm.polyglot.Value;
    
    import java.util.function.Consumer;
    
    import org.graalvm.polyglot.*;
    
    public class AsyncThing {
      @HostAccess.Export
      public void callFn(Value asyncFunction) {
      	Value jsPromise = asyncFunction.execute();
      	
      	Consumer<Object> javaThen = (v) -> System.out.println(v.getClass().toString() + "\n" + v.toString());
      	jsPromise.invokeMember("then", javaThen);
      	
      	Consumer<Object> javaCatch = (v) -> System.err.println(v.getClass().toString() + "\n" + v.toString());
      	jsPromise.invokeMember("catch", javaCatch);
      }
      
      @HostAccess.Export
      public void handlePromise(Value jsPromise) {
      	Consumer<Object> javaThen = (v) -> System.out.println(v.getClass().toString() + "\n" + v.toString());
      	jsPromise.invokeMember("then", javaThen);
      	
      	Consumer<Object> javaCatch = (v) -> System.err.println(v.getClass().toString() + "\n" + v.toString());
      	jsPromise.invokeMember("catch", javaCatch);
      }
    }
    • On the .ajs side, those two test scripts:

      Test Scripts
      const AsyncThing = Java.type("com.archimatetool.script.commands.AsyncThing");
      const asyncHandler = new AsyncThing();
      console.log("starting");
      asyncHandler.handlePromise(
        new Promise((r) => setTimeout(r, 1000))
          .then(() => console.log("done"));
      );
      asyncHandler.callFn(async () => {
        console.log("starting");
        await new Promise((r) => setTimeout(r, 1000))
        console.log("done");
      });
      Script Console Output starting ${\color{red}\textsf{class \ com.oracle.truffle.polyglot.PolyglotMap}}$ ${\color{red}\textsf{[object \ Error]}}$ starting ${\color{red}\textsf{class \ com.oracle.truffle.polyglot.PolyglotMap}}$ ${\color{red}\textsf{[object \ Error]}}$

As you can see, an error gets thrown uppon launching an async process. The error doesn't really tell me what's going on either ...

I'll try some other code ideas, but I have a feeling that this issue might be solved by switching to a newer version of Graal/GraalJS.
However, I'm not a Java dev, so I don't have the required skillset to attempt this. I've been struggling hard to even get the Eclipse project going ...

@jbsarrodie, have you had the time to take a look at this issue ? Do you have some leads ?

@Phillipus
Copy link
Member

One thing that concerns me is whether using asynchronous code will mess up the order of commands. This is important for undo/redo and getting things in the right order.

@BreizHell
Copy link
Author

In my case, I'm tasked with creating a JArchi plugin that generates a PowerPoint presentation from Archi's model.
So it's not really a command that interacts with archi, but something that reads Archi's data to do other things on the side.

@Phillipus
Copy link
Member

but I have a feeling that this issue might be solved by switching to a newer version of Graal/GraalJS.

I've pushed a new branch graalvm-latestversion with the latest version of GraalVM. I tested your Case 1 and 2 snippets and got the same result.

@BreizHell
Copy link
Author

BreizHell commented Oct 25, 2024

Okay, so uh ... The Java code I posted 4 hours ago actually works, It's just that GraalJS doesn't support setInterval.

With that in mind, @Phillipus, if you try the same .ajs snippets, but add this code at the very top, they should both work :

const JavaThread = Java.type("java.lang.Thread");
var setTimeout = (callback, ms, ...args) => {
    JavaThread.sleep(ms)
    if (args.length) callback(...args)
    else callback()
}

@jbsarrodie
Copy link
Member

Hi,

@jbsarrodie, have you had the time to take a look at this issue ? Do you have some leads ?

I've just had a look and:

  • As you noticed, setTimeout doesn't exist in a polyglot context.
  • So I updated an old Nashorn polyfill for it to work on graalvm
  • And I reproduced your case 1...
  • But this setTimeout polyfill implementation is based on java.util.Timer, which creates a new thread...
  • In the meantime you shared a polyfill based on JavaThread.sleep, so I tested the case 1 with it and it now works on my side with original (but old) graalvm version !

So it it seems that for case 1 it was only related to setTimeout

For case 2, I can reproduce the error, even with a slightly more recent version of graalvm (22.3.4) that I use for some other purposes.

I've pushed a new branch graalvm-latestversion with the latest version of GraalVM. I tested your Case 1 and 2 snippets and got the same result.

So it seems that a recent version of graalvm is not enough to solve case 2.

BTW, @Phillipus we should really upgrade graalvm, several issues that I faced in the past were related to it, so I used a patched version. Because I was using an old version of jArchi (1.5), I didn't noticed 1.7 was still using the same. In addition, we should also add chromeinspector and profiler (both part of graalvm) to make it possible for power users to enable the builtin debugger: I've managed to enable it through a script, the devtools URL is then printed on the terminal/cmd used to start Archi. This is more than enough for power users (and saved me recently to debug a tough issue).

One thing that concerns me is whether using asynchronous code will mess up the order of commands. This is important for undo/redo and getting things in the right order.

It shouldn't have any impact, because all changes are done inside the same thread and recorded in the undo stack in the order they were really done. I did some basic tests that tend to prove it.

@BreizHell
Copy link
Author

BreizHell commented Oct 28, 2024

Hi @Phillipus @jbsarrodie,

Do you think you'll implement a similar class in principle to what I have proposed Friday into a future release of JArchi ?

@Phillipus
Copy link
Member

Phillipus commented Dec 7, 2024

I can get Case 1 and Case 2 working by (a) providing the setTimeout function above and (b) wrapping case 2 in an async function:

const JavaThread = Java.type("java.lang.Thread");
var setTimeout = (callback, ms, ...args) => {
    JavaThread.sleep(ms)
    if (args.length) callback(...args)
    else callback()
}

console.log("case 1. starting synchronous part");

new Promise((resolve) => {
    console.log("case 1. starting async part");
    setTimeout(resolve, 1000);
}).then(() => console.log("case 1. async part ended"));

console.log("case 1. synchronous part ended");

async function start() {
    await new Promise((resolve) => {
        console.log("case 2. starting async part");
        setTimeout(resolve, 1000);
    })
    console.log("case 2. async part ended");
}

console.log("case 2. starting synchronous part");
start();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants