All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twitter.finagle.redis.ScriptCommands.scala Maven / Gradle / Ivy

There is a newer version: 6.39.0
Show newest version
package com.twitter.finagle.redis

import com.twitter.finagle.redis.protocol._
import com.twitter.finagle.redis.util.ReplyFormat
import com.twitter.io.Buf
import com.twitter.util.Future

private[redis] trait ScriptCommands { self: BaseClient =>
  private[redis] val filterReply: PartialFunction[Reply, Future[Reply]] = {
    case ErrorReply(message) => Future.exception(new ServerError(message))
    case reply: Reply => Future.value(reply)
  }

  /**
   * Executes EVAL command returns the raw redis-server [[Reply]].
   *
   * User may convert this [[Reply]] to type T by calling their [[Scripts.castReply[T](reply)]],
   * or "reply.cast[T]" which is an implicit conversion through [[Scripts.CastableReply]] helper class.
   *
   * An idiomatic usage:
   * {{{
   *   import Scripts._
   *   client.eval(script, keys, argv).map { _.cast[Long] }
   * }}}
   * returns a Future[Long], and is equivalent to
   * {{{ client.eval(script, keys, argv).map(Scripts.castReply[Long]) }}}
   *
   * @param script the script to execute
   * @param keys the redis keys that the script may have access to
   * @param argv the array of arguments that the script takes
   * @return redis-server [[Reply]]
   */
  def eval(script: Buf, keys: Seq[Buf], argv: Seq[Buf]): Future[Reply] =
    doRequest(Eval(script, keys, argv))(filterReply)


  /**
   * Executes EVALSHA command and returns the raw redis-server [[Reply]].
   *
   * Similar to [[eval]], but takes "sha" (SHA-1 digest of a script) as the first parameter.
   * If the script cache on redis-server does not contain a script whose SHA-1 digest is "sha",
   * this will return [[ErrorReply(message)]] where message starts with "NOSCRIPT".
   *
   * @param sha SHA-1 digest of the script to execute
   * @param keys the redis keys that the script may have access to
   * @param argv the array of arguments that the script takes
   * @return redis-server [[Reply]]
   */
  def evalSha(sha: Buf, keys: Seq[Buf], argv: Seq[Buf]): Future[Reply] =
    doRequest(EvalSha(sha, keys, argv))(filterReply)

  /**
   * Executes EVALSHA command, and if redis-server complains about NOSCRIPT, execute EVAL with fallback "script" instead.
   *
   * Similar to [[evalSha]], but this takes a fallback "script" parameter in addition to "sha",
   * so that any [[ErrorReply("NOSCRIPT...")]] will be silently caught and a corresponding EVAL command will be retried.
   *
   * @param sha SHA-1 digest of the script to execute
   * @param script the fallback script. User must guarantee that {{{sha == SHA1(script)}}},
   *               where "SHA1" computes the SHA-1 digest (as a HEX string) of script.
   * @param keys the redis keys that the script may have access to
   * @param argv the array of arguments that the script takes
   * @return redis-server [[Reply]]
   */
  def evalSha(sha: Buf, script: Buf, keys: Seq[Buf], argv: Seq[Buf]): Future[Reply] = {
    doRequest(EvalSha(sha, keys, argv)) {
      case ErrorReply(message) =>
        if (message.startsWith("NOSCRIPT"))
          eval(script, keys, argv)
        else
          Future.exception(new ServerError(message))
      case reply: Reply =>
        Future.value(reply)
    }
  }

  /**
   * Returns whether each sha1 digest in "digests" indicates some valid script on redis-server script cache.
   */
  def scriptExists(digests: Buf*): Future[Seq[Boolean]] = {
    doRequest(ScriptExists(digests)) {
      case MBulkReply(message) => Future.value(message.map {
        case IntegerReply(n) => n != 0
        case _ => false
      })
    }
  }

  /**
   * Flushes the script cache on redis-server.
   */
  def scriptFlush(): Future[Unit] = {
    doRequest(ScriptFlush) {
      // SCRIPT FLUSH must return a simple string reply indicating OK status
      case StatusReply(_) => Future.Unit
    }
  }

  /**
   * Loads a script to redis-server script cache, and returns its SHA-1 digest as a HEX string.
   *
   * Note that the SHA-1 digest of "script" (as a HEX string) can also be calculated locally,
   * and it does equal what will be returned by redis-server,
   * but this call, once succeeds, has the side effect of loading the script to the cache on redis-server.
   */
  def scriptLoad(script: Buf): Future[Buf] = {
    doRequest(ScriptLoad(script)) {
      // SCRIPT LOAD must return a non-empty bulk string reply indicating the SHA1 digest of the loaded script
      case BulkReply(message) => Future.value(message)
    }
  }
}

object ScriptCommands {

  /**
   * Converts a redis-server [[Reply]] to type [[T]].
   *
   * The conversion is best-effort, and if it fails, a [[ReplyCastFailure]] will be thrown.
   *
   * Currently supported target types:
   *   Unit
   *   Long
   *   Boolean
   *   Buf
   *   Seq[Buf]
   */
  def castReply[T](reply: Reply)(implicit ev: ReplyConverter[T]): T =
    ev.cast(reply)

  implicit class CastableReply(val reply: Reply) extends AnyVal {
    def cast[T]()(implicit ev: ReplyConverter[T]): T =
      ev.cast(reply)
  }

  /**
   * type class to help converting [[Reply]] to specific type [[T]]
   */
  trait ReplyConverter[T] {
    def cast(reply: Reply): T
  }

  object ReplyConverter {

    implicit val unit = new ReplyConverter[Unit] {
      override def cast(reply: Reply): Unit = reply match {
        case StatusReply(_) | EmptyBulkReply() => ()
        case r => Future.exception(ReplyCastFailure("Error casting " + r + " to Unit"))
      }
    }

    implicit val long = new ReplyConverter[Long] {
      override def cast(reply: Reply): Long = reply match {
        case IntegerReply(n) => n
        case r => throw new ReplyCastFailure("Error casting " + r + " to Long")
      }
    }

    implicit val bool = new ReplyConverter[Boolean] {
      override def cast(reply: Reply): Boolean = reply match {
        case IntegerReply(n) => (n == 1)
        case EmptyBulkReply() => false
        case StatusReply(_) => true
        case r => throw new ReplyCastFailure("Error casting " + r + " to Boolean")
      }
    }

    implicit val buffer = new ReplyConverter[Buf] {
      override def cast(reply: Reply): Buf = reply match {
        case BulkReply(message) => message
        case EmptyBulkReply() => Buf.Empty
        case r => throw new ReplyCastFailure("Error casting " + r + " to Buf")
      }
    }

    implicit val buffers = new ReplyConverter[Seq[Buf]] {
      override def cast(reply: Reply): Seq[Buf] = reply match {
        case MBulkReply(messages) => ReplyFormat.toBuf(messages)
        case EmptyMBulkReply() => Nil
        case r => throw new ReplyCastFailure("Error casting " + r + " to Seq[Buf]")
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy