/**
   * Session based requests accept a "close" operator in addition to "eval". A close will trigger
   * the session to be killed and any uncommitted transaction to be rolled-back.
   */
  @Override
  public Optional<ThrowingConsumer<Context>> selectOther(final RequestMessage requestMessage)
      throws OpProcessorException {
    if (requestMessage.getOp().equals(Tokens.OPS_CLOSE)) {
      // this must be an in-session request
      if (!requestMessage.optionalArgs(Tokens.ARGS_SESSION).isPresent()) {
        final String msg =
            String.format(
                "A message with an [%s] op code requires a [%s] argument",
                Tokens.OPS_CLOSE, Tokens.ARGS_SESSION);
        throw new OpProcessorException(
            msg,
            ResponseMessage.build(requestMessage)
                .code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS)
                .result(msg)
                .create());
      }

      return Optional.of(
          ctx -> {
            // validate the session is present and then remove it if it is.
            final Session sessionToClose =
                sessions.get(requestMessage.getArgs().get(Tokens.ARGS_SESSION).toString());
            if (null == sessionToClose) {
              final String msg =
                  String.format(
                      "There was no session named %s to close",
                      requestMessage.getArgs().get(Tokens.ARGS_SESSION).toString());
              throw new OpProcessorException(
                  msg,
                  ResponseMessage.build(requestMessage)
                      .code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS)
                      .result(msg)
                      .create());
            }

            sessionToClose.kill();
          });
    } else {
      return Optional.empty();
    }
  }
  /**
   * Examines the {@link RequestMessage} and extracts the session token. The session is then either
   * found or a new one is created.
   */
  protected static Session getSession(final Context context, final RequestMessage msg) {
    final String sessionId = (String) msg.getArgs().get(Tokens.ARGS_SESSION);

    logger.debug(
        "In-session request {} for eval for session {} in thread {}",
        msg.getRequestId(),
        sessionId,
        Thread.currentThread().getName());

    final Session session =
        sessions.computeIfAbsent(sessionId, k -> new Session(k, context, sessions));
    session.touch();
    return session;
  }
  @Override
  public ByteBuf serializeRequestAsBinary(
      final RequestMessage requestMessage, final ByteBufAllocator allocator)
      throws SerializationException {
    ByteBuf encodedMessage = null;
    try {
      final Kryo kryo = kryoThreadLocal.get();
      try (final OutputStream baos = new ByteArrayOutputStream()) {
        final Output output = new Output(baos);
        final String mimeType = serializeToString ? MIME_TYPE_STRINGD : MIME_TYPE;
        output.writeByte(mimeType.length());
        output.write(mimeType.getBytes(UTF8));

        kryo.writeObject(output, requestMessage.getRequestId());
        output.writeString(requestMessage.getProcessor());
        output.writeString(requestMessage.getOp());
        kryo.writeObject(output, requestMessage.getArgs());

        final long size = output.total();
        if (size > Integer.MAX_VALUE)
          throw new SerializationException(
              String.format("Message size of %s exceeds allocatable space", size));

        encodedMessage = allocator.buffer((int) size);
        encodedMessage.writeBytes(output.toBytes());
      }

      return encodedMessage;
    } catch (Exception ex) {
      if (encodedMessage != null) ReferenceCountUtil.release(encodedMessage);

      logger.warn(
          "Request [{}] could not be serialized by {}.",
          requestMessage.toString(),
          GryoMessageSerializerV1d0.class.getName());
      throw new SerializationException(ex);
    }
  }
  protected void evalOp(final Context context) throws OpProcessorException {
    final RequestMessage msg = context.getRequestMessage();
    final Session session = getSession(context, msg);

    // place the session on the channel context so that it can be used during serialization.  in
    // this way
    // the serialization can occur on the same thread used to execute the gremlin within the
    // session.  this
    // is important given the threadlocal nature of Graph implementation transactions.
    context.getChannelHandlerContext().channel().attr(StateKey.SESSION).set(session);

    evalOpInternal(
        context,
        session::getGremlinExecutor,
        () -> {
          final Bindings bindings = session.getBindings();

          // parameter bindings override session bindings if present
          Optional.ofNullable((Map<String, Object>) msg.getArgs().get(Tokens.ARGS_BINDINGS))
              .ifPresent(bindings::putAll);

          return bindings;
        });
  }
  /**
   * A useful method for those extending this class, where the means for binding construction can be
   * supplied to this class. This function is used in {@link #evalOp(Context)} to create the final
   * argument to {@link super#evalOpInternal(Context, Supplier, BindingSupplier)}. In this way an
   * extending class can use the default {@link BindingSupplier} which carries a lot of re-usable
   * functionality or provide a new one to override the existing approach.
   */
  protected Function<Context, BindingSupplier> getBindingMaker() {
    return context ->
        () -> {
          final RequestMessage msg = context.getRequestMessage();
          final Bindings bindings = new SimpleBindings();

          // don't allow both rebindings and aliases parameters as they are the same thing. aliases
          // were introduced
          // as of 3.1.0 as a replacement for rebindings. this check can be removed when rebindings
          // are completely
          // removed from the protocol
          final boolean hasRebindings = msg.getArgs().containsKey(Tokens.ARGS_REBINDINGS);
          final boolean hasAliases = msg.getArgs().containsKey(Tokens.ARGS_ALIASES);
          if (hasRebindings && hasAliases) {
            final String error =
                "Prefer use of the 'aliases' parameter over 'rebindings' and do not use both";
            throw new OpProcessorException(
                error,
                ResponseMessage.build(msg)
                    .code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS)
                    .result(error)
                    .create());
          }

          final String rebindingOrAliasParameter =
              hasRebindings ? Tokens.ARGS_REBINDINGS : Tokens.ARGS_ALIASES;

          // alias any global bindings to a different variable.
          if (msg.getArgs().containsKey(rebindingOrAliasParameter)) {
            final Map<String, String> rebinds =
                (Map<String, String>) msg.getArgs().get(rebindingOrAliasParameter);
            for (Map.Entry<String, String> kv : rebinds.entrySet()) {
              boolean found = false;
              final Map<String, Graph> graphs = context.getGraphManager().getGraphs();
              if (graphs.containsKey(kv.getValue())) {
                bindings.put(kv.getKey(), graphs.get(kv.getValue()));
                found = true;
              }

              if (!found) {
                final Map<String, TraversalSource> traversalSources =
                    context.getGraphManager().getTraversalSources();
                if (traversalSources.containsKey(kv.getValue())) {
                  bindings.put(kv.getKey(), traversalSources.get(kv.getValue()));
                  found = true;
                }
              }

              if (!found) {
                final String error =
                    String.format(
                        "Could not alias [%s] to [%s] as [%s] not in the Graph or TraversalSource global bindings",
                        kv.getKey(), kv.getValue(), kv.getValue());
                throw new OpProcessorException(
                    error,
                    ResponseMessage.build(msg)
                        .code(ResponseStatusCode.REQUEST_ERROR_INVALID_REQUEST_ARGUMENTS)
                        .result(error)
                        .create());
              }
            }
          }

          // add any bindings to override any other supplied
          Optional.ofNullable((Map<String, Object>) msg.getArgs().get(Tokens.ARGS_BINDINGS))
              .ifPresent(bindings::putAll);
          return bindings;
        };
  }