Using JVM libraries in node.js

Using JVM libraries in node.js

GraalVM promises seamless interoperability between multiple languages, all running on the JVM. It makes it trivial to call code implemented in JVM languages from node.js. However, given Node’s event loop model, blocking it is not a viable option. In this talk we’ll look into how we can use blocking JVM libraries in node.js as well as call back from JVM libraries into node.js

01b8149f78caac2977f68facd6f0e432?s=128

Laurynas Lubys

December 12, 2018
Tweet

Transcript

  1. Laurynas Lubys, backend developer Using JVM libraries in node.js laurynasl@wix.com

    linkedin.com/in/laurynaslubys github.com/laurynasl-wix
  2. AGENDA Interop 101 bcrypt rabbitmq hacks

  3. Interop 101 01

  4. The docs ▪ https://github.com/graalvm/graaljs/blob/master/docs/user/JavaInterop.md ▪ https://www.graalvm.org/docs/reference-manual/languages/js/ INTEROP 101

  5. TLDR; with regards to node.js ▪ Drop in replacement for

    node.js ▪ Mostly compatible with existing libs ▪ JVM interop enabled with --jvm ▪ Primitives are automatically converted ▪ JS objects can be converted to Map<> INTEROP 101
  6. hello-world.js node --jvm hello-world.js > Hello world! const System =

    Java.type("java.lang.System"); System.out.println("Hello world!");
  7. bcrypt 02

  8. bcrypt? bcrypt is a password hashing function designed by Niels

    Provos and David Mazières, based on the Blowfish cipher, and presented at USENIX in 1999.[1] Besides incorporating a salt to protect against rainbow table attacks, bcrypt is an adaptive function: over time, the iteration count can be increased to make it slower, so it remains resistant to brute-force search attacks even with increasing computation power. Blowfish is notable among block ciphers for its expensive key setup phase. It starts off with subkeys in a standard state, then uses this state to perform a block encryption using part of the key, and uses the result of that encryption (which is more accurately a hashing) to replace some of the subkeys. Then it uses this modified state to encrypt another part of the key, and uses the result to replace more of the subkeys. It proceeds in this fashion, using a progressively modified state to hash the key and replace bits of state, until all subkeys have been set. Provos and Mazières took advantage of this, and took it further. They developed a new key setup algorithm for Blowfish, dubbing the resulting cipher "Eksblowfish" ("expensive key schedule Blowfish"). The key setup begins with a modified form of the standard Blowfish key setup, in which both the salt and password are used to set all subkeys. There are then a number of rounds in which the standard Blowfish keying algorithm is applied, using alternatively the salt and the password as the key, each round starting with the subkey state from the previous round. In theory, this is no stronger than the standard Blowfish key schedule, but the number of rekeying rounds is configurable; this process can therefore be made arbitrarily slow, which helps deter brute-force attacks upon the hash or salt. - https://en.wikipedia.org/wiki/Bcrypt BCRYPT
  9. bcrypt. It’s a hashing function with a knob you can

    turn to change the computational resources needed to calculate the hash, so that it’s harder to brute-force. - Me BCRYPT
  10. The goal ▪ Use the JVM jbcrypt library in node.js

    ▪ Integrate it into our server to handle user logins BCRYPT
  11. The Plan 1. Tell the JVM where to find the

    jar: --jvm.cp=... 2. Call it from node 3. Use it in our application 4. Go home early BCRYPT
  12. Tell the JVM where to find the jar ▪ Many

    options ▪ None of them great ▪ Packaging a jar with an npm module is an issue ▪ We’ll dump the classpath from maven and feed it to --jvm.cp BCRYPT
  13. FLAGS="\ --jvm \ --jvm.cp=./jbcrypt-0.3m.jar" node $FLAGS bcrypt.js > $2a$10$sfMWTWRO... const

    BCrypt = Java.type("org.mindrot.jbcrypt.BCrypt"); const salt = "$2a$10$sfMWTWROyUu5JxTEwm413u"; const hash = BCrypt.hashpw("Hello", salt); console.log(hash) bcrypt.js
  14. const express = require('express'); const BCrypt = Java.type("org.mindrot.jbcrypt.BCrypt"); const salt

    = "$2a$10$sfMWTWROyUu5JxTEwm413u"; const app = express(); app.post('/login', (req, res) => { const pass = req.query.password; res.send(BCrypt.hashpw(pass, salt)); }); app.get('/', (req, res) => { res.send("Welcome!") }); module.exports = { app }; app.js
  15. Demo time!

  16. Cool, ship it!

  17. Go home early BCRYPT

  18. Go home early ▪ We blocked the event loop ▪

    The node community knows how to deal with blocking workloads ▪ We can create a pool of workers and do the work there BCRYPT
  19. rabbitmq 03

  20. The goal ▪ Consume messages from rabbitmq ▪ We want

    to use the rabbitmq “push API” RABBITMQ
  21. The Plan 1. Figure out how to do callbacks 2.

    Figure out the Java API 3. Implement a callback in node 4. Handle a rabbitmq message in node.js RABBITMQ
  22. node --jvm callback.js > In node: 1 > In node:

    2 const List = Java.type("java.util.ArrayList"); const aList = new List(); aList.add(1); aList.add(2); aList.forEach(v => console.log(`In node: ${v}`)); callback.js
  23. package examples; import com.rabbitmq.client.*; public class RabbitMQJava { public static

    void main(String[] args) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setUsername("rabbitmq"); factory.setPassword("rabbitmq"); factory.setHost("localhost"); factory.setPort(5672); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); String queueName = "hello-rabbit"; channel.queueDeclare(queueName, false, false, false, null); DeliverCallback deliverCallback = (consumerTag, message) -> System.out.println("Received message: " + new String(message.getBody())); CancelCallback cancelCallback = consumerTag -> { } channel.basicConsume(queueName, true, deliverCallback, cancelCallback); System.out.println("Listening for messages on " + queueName); } } Figure out the API
  24. const ConnectionFactory = Java.type("com.rabbitmq.client.ConnectionFactory"); const JString = Java.type("java.lang.String"); const ExtendDeliverCallback

    = Java.extend(Java.type("com.rabbitmq.client.DeliverCallback")); const ExtendCancelCallback = Java.extend(Java.type("com.rabbitmq.client.CancelCallback")); const factory = new ConnectionFactory(); factory.setUsername("rabbitmq"); factory.setPassword("rabbitmq"); factory.setHost("localhost"); factory.setPort(5672); const connection = factory.newConnection(); const channel = connection.createChannel(); const queueName = "hello-rabbit"; channel.queueDeclare(queueName, false, false, false, null); const deliverCallback = new ExtendDeliverCallback((consumerTag, message) => console.log("Received message: " + new JString(message.getBody()))); const cancelCallback = new ExtendCancelCallback(consumerTag => { }); channel.basicConsume(queueName, true, deliverCallback, cancelCallback); console.log("Listening for messages on " + queueName); (function wait() { setTimeout(wait, 1000); })(); Implement a callback
  25. package examples; import com.rabbitmq.client.*; public class RabbitMQJava { public static

    void main(String[] args) throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setUsername("rabbitmq"); factory.setPassword("rabbitmq"); factory.setHost("localhost"); factory.setPort(5672); Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); String queueName = "hello-rabbit"; channel.queueDeclare(queueName, false, false, false, null); DeliverCallback deliverCallback = (consumerTag, message) -> System.out.println("Received message: " + new String(message.getBody())); CancelCallback cancelCallback = consumerTag -> { } channel.basicConsume(queueName, true, deliverCallback, cancelCallback); System.out.println("Listening for messages on " + queueName); } } const ConnectionFactory = Java.type("com.rabbitmq.client.ConnectionFactory"); const JString = Java.type("java.lang.String"); const ExtendDeliverCallback = Java.extend(Java.type("com.rabbitmq.client.DeliverCallback")); const ExtendCancelCallback = Java.extend(Java.type("com.rabbitmq.client.CancelCallback")); const factory = new ConnectionFactory(); factory.setUsername("rabbitmq"); factory.setPassword("rabbitmq"); factory.setHost("localhost"); factory.setPort(5672); const connection = factory.newConnection(); const channel = connection.createChannel(); const queueName = "hello-rabbit"; channel.queueDeclare(queueName, false, false, false, null); const deliverCallback = new ExtendDeliverCallback((consumerTag, message) => console.log("Received message: " + new JString(message.getBody()))); const cancelCallback = new ExtendCancelCallback(consumerTag => { }); channel.basicConsume(queueName, true, deliverCallback, cancelCallback); console.log("Listening for messages on " + queueName); (function wait() { setTimeout(wait, 1000); })();
  26. Demo time!

  27. We can’t call JS from another thread

  28. So... ▪ No event handlers written in node.js? ▪ No

    using async Java IO libraries in node.js? ▪ No reactive streams? RABBITMQ
  29. Take away points ▪ Dependency management is not solved (but

    manageable) ▪ Calling synchronous code from node is simple (but blocks the event loop) ▪ Calling async Java code from node is not solved.
  30. hacks 04

  31. node ${FLAGS} async.js > Let's wait now > node.js: Hello

    from JVM! > JVM: Hey! const {bindCallback} = require('???'); const SomethingAsync = Java.type("examples.Async"); const bound = bindCallback((error, result) => { console.log(`node.js: ${result}`); return new Promise((resolve) => { setTimeout(() => resolve("Hey!"), 100); }) }); SomethingAsync.callMeBack(bound); console.log("Let's wait now"); async.js
  32. GraalJS does not provide that out of the box

  33. Let’s see what we can do about that

  34. InteropBus Async Java code pending ready complete( ) ( ,

    ) We want to call with async results from java. node.js worker node.js Java land bindCallback on(‘message’)
  35. Back to rabbitmq

  36. public class JSDeliverCallback implements DeliverCallback { private final BoundJSCallback<Delivery, Void>

    jsCallback; public JSDeliverCallback(BoundJSCallback<Delivery, Void> jsCallback) { this.jsCallback = jsCallback; } @Override public void handle(String consumerTag, Delivery message) { jsCallback.apply(message, null); } } JSDeliverCallback.java
  37. +const {bindCallback} = require("../../lib/interop/simplified"); -const ExtendDeliverCallback = Java.extend(Java.type("com.rabbitmq.client.DeliverCallback")); +const JSDeliverCallback

    = Java.type("examples.JSDeliverCallback"); -const deliverCallback = new ExtendDeliverCallback((consumerTag, message) => - console.log("Received message: " + new JString(message.getBody()))); +const deliverCallback = new JSDeliverCallback(bindCallback((error, message) => + console.log("Received message: " + new JString(message.getBody())))); patch
  38. Demo time!

  39. Take away points ▪ Dependency management is not solved (but

    manageable) ▪ Calling synchronous code from node is simple (but blocks the event loop) ▪ Calling async Java code from node is not solved (but hackable)
  40. Thank You laurynasl@wix.com linkedin.com/in/laurynaslubys github.com/laurynasl-wix github.com/laurynasl-wix/graal-jvm-interop-talk