/**
  * Attempts to compile a script and cache it in the request {@link javax.script.ScriptEngine}.
  * This is only possible if the {@link javax.script.ScriptEngine} implementation implements {@link
  * javax.script.Compilable}. In the event that the requested {@link javax.script.ScriptEngine}
  * does not implement it, the method will return empty.
  */
 public Optional<CompiledScript> compile(final String script, final Optional<String> language)
     throws ScriptException {
   final String lang = language.orElse("gremlin-groovy");
   try {
     return Optional.of(scriptEngines.compile(script, lang));
   } catch (UnsupportedOperationException uoe) {
     return Optional.empty();
   }
 }
  /**
   * Executors are only closed if they were not supplied externally in the {@link
   * org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor.Builder}
   */
  public CompletableFuture<Void> closeAsync() throws Exception {
    final CompletableFuture<Void> future = new CompletableFuture<>();

    new Thread(
            () -> {
              // leave pools running if they are supplied externally.  let the sender be responsible
              // for shutting them down
              if (!suppliedExecutor) {
                executorService.shutdown();
                try {
                  if (!executorService.awaitTermination(180000, TimeUnit.MILLISECONDS))
                    logger.warn(
                        "Timeout while waiting for ExecutorService of GremlinExecutor to shutdown.");
                } catch (InterruptedException ie) {
                  logger.warn(
                      "ExecutorService on GremlinExecutor may not have shutdown properly as shutdown thread terminated early.");
                }
              }

              // calls to shutdown are idempotent so no problems calling it twice if the pool is
              // shared
              if (!suppliedScheduledExecutor) {
                scheduledExecutorService.shutdown();
                try {
                  if (!scheduledExecutorService.awaitTermination(180000, TimeUnit.MILLISECONDS))
                    logger.warn(
                        "Timeout while waiting for ScheduledExecutorService of GremlinExecutor to shutdown.");
                } catch (InterruptedException ie) {
                  logger.warn(
                      "ScheduledExecutorService on GremlinExecutor may not have shutdown properly as shutdown thread terminated early.");
                }
              }

              try {
                scriptEngines.close();
              } catch (Exception ex) {
                logger.warn(
                    "Error while shutting down the ScriptEngines in the GremlinExecutor", ex);
              }

              future.complete(null);
            },
            "gremlin-executor-close")
        .start();

    return future;
  }
  /**
   * 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;
  }