/** * Evaluate a script and allow for the submission of both a transform {@link Function} and {@link * Consumer}. The {@link Function} will transform the result after script evaluates but before * transaction commit and before the returned {@link CompletableFuture} is completed. The {@link * Consumer} will take the result for additional processing after the script evaluates and after * the {@link CompletableFuture} is completed, but before the transaction is committed. * * @param script the script to evaluate * @param language the language to evaluate it in * @param boundVars the bindings to evaluate in the context of the script * @param transformResult a {@link Function} that transforms the result - can be {@code null} * @param withResult a {@link Consumer} that accepts the result - can be {@code null} */ public CompletableFuture<Object> eval( final String script, final String language, final Bindings boundVars, final Function<Object, Object> transformResult, final Consumer<Object> withResult) { final String lang = Optional.ofNullable(language).orElse("gremlin-groovy"); logger.debug( "Preparing to evaluate script - {} - in thread [{}]", script, Thread.currentThread().getName()); final Bindings bindings = new SimpleBindings(); bindings.putAll(this.globalBindings); bindings.putAll(boundVars); beforeEval.accept(bindings); final CompletableFuture<Object> evaluationFuture = new CompletableFuture<>(); final FutureTask<Void> f = new FutureTask<>( () -> { try { logger.debug( "Evaluating script - {} - in thread [{}]", script, Thread.currentThread().getName()); final Object o = scriptEngines.eval(script, bindings, lang); // apply a transformation before sending back the result - useful when trying to // force serialization // in the same thread that the eval took place given ThreadLocal nature of graphs as // well as some // transactional constraints final Object result = null == transformResult ? o : transformResult.apply(o); evaluationFuture.complete(result); // a mechanism for taking the final result and doing something with it in the same // thread, but // AFTER the eval and transform are done and that future completed. this provides a // final means // for working with the result in the same thread as it was eval'd if (withResult != null) withResult.accept(result); afterSuccess.accept(bindings); } catch (Exception ex) { final Throwable root = null == ex.getCause() ? ex : ExceptionUtils.getRootCause(ex); // thread interruptions will typically come as the result of a timeout, so in those // cases, // check for that situation and convert to TimeoutException if (root instanceof InterruptedException) evaluationFuture.completeExceptionally( new TimeoutException( String.format( "Script evaluation exceeded the configured threshold of %s ms for request [%s]: %s", scriptEvaluationTimeout, script, root.getMessage()))); else { afterFailure.accept(bindings, root); evaluationFuture.completeExceptionally(root); } } return null; }); executorService.execute(f); if (scriptEvaluationTimeout > 0) { // Schedule a timeout in the thread pool for future execution final ScheduledFuture<?> sf = scheduledExecutorService.schedule( () -> { logger.info( "Timing out script - {} - in thread [{}]", script, Thread.currentThread().getName()); if (!f.isDone()) { afterTimeout.accept(bindings); f.cancel(true); } }, scriptEvaluationTimeout, TimeUnit.MILLISECONDS); // Cancel the scheduled timeout if the eval future is complete or the script evaluation failed // with exception evaluationFuture.handleAsync( (v, t) -> { logger.debug( "Killing scheduled timeout on script evaluation as the eval completed (possibly with exception)."); return sf.cancel(true); }); } return evaluationFuture; }