Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#256 support redis zset #258

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/main/scala/play/api/cache/redis/CacheApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,15 @@ private[redis] trait AbstractCacheApi[Result[_]] {
* @return Scala wrapper
*/
def map[T: ClassTag](key: String): RedisMap[T, Result]

/**
* Scala wrapper around Redis sorted-set-related commands.
*
* @param key the key storing the map
* @tparam T type of elements within the sorted-set
* @return Scala wrapper
*/
def zset[T: ClassTag](key: String): RedisSortedSet[T, Result]
}

/** Synchronous and blocking implementation of the connection to the redis database */
Expand Down
51 changes: 51 additions & 0 deletions src/main/scala/play/api/cache/redis/RedisSortedSet.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package play.api.cache.redis

import scala.collection.immutable.TreeSet

trait RedisSortedSet[Elem, Result[_]] extends RedisCollection[TreeSet[Elem], Result] {
override type This = RedisSortedSet[Elem, Result]

/**
* Adds all the specified members with the specified scores to the sorted set stored at key.
* It is possible to specify multiple score / member pairs. If a specified member is already
* a member of the sorted set, the score is updated and the element reinserted at the right
* position to ensure the correct ordering.
*
* If key does not exist, a new sorted set with the specified members as sole members is created,
* like if the sorted set was empty.
*
* @note If the key exists but does not hold a sorted set, an error is returned.
* @note <strong>Time complexity:</strong> O(log(N)) for each item added, where N is the number of elements in the sorted set.
* @param scoreValues values and corresponding scores to be added
* @return the sorted set for chaining calls
*/
def add(scoreValues: (Double, Elem)*): Result[This]

/**
* <p>Tests if the element is contained in the sorted set. Returns true if exists, otherwise returns false</p>
*
* @note <strong>Time complexity:</strong> O(1)
* @param element tested element
* @return true if exists in the set, otherwise false
*/
def contains(element: Elem): Result[Boolean]

/**
* <p>Removes the specified members from the sorted set stored at key. Non existing members are ignored.
* An error is returned when key exists and does not hold a sorted set.</p>
*
* @note <strong>Time complexity:</strong> O(M*log(N)) with N being the number of elements in the sorted set and M the number of elements to be removed.
* @param element elements to be removed
* @return the sorted set for chaining calls
*/
def remove(element: Elem*): Result[This]

/**
* Returns the specified range of elements in the sorted set stored at key which sorted in order specified by param `isReverse`.
* @param start the start index of the range
* @param stop the stop index of the range
* @param isReverse whether sorted in descending order or not
* @return
*/
def range(start: Long, stop: Long, isReverse: Boolean = false): Result[Seq[Elem]]
}
90 changes: 90 additions & 0 deletions src/main/scala/play/api/cache/redis/connector/RedisConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,95 @@ private[redis] trait SetCommands {
def setRemove(key: String, value: Any*): Future[Long]
}

/**
* Internal non-blocking Redis API implementing REDIS protocol
*
* Subset of REDIS commands, sorted set related commands.
*
* @see https://redis.io/commands
*/
private[redis] trait SortedSetCommands {

/**
* Adds all the specified members with the specified scores to the sorted set stored at key.
* It is possible to specify multiple score / member pairs. If a specified member is already
* a member of the sorted set, the score is updated and the element reinserted at the right
* position to ensure the correct ordering.
*
* If key does not exist, a new sorted set with the specified members as sole members is created,
* like if the sorted set was empty. If the key exists but does not hold a sorted set, an error
* is returned.
*
* @note Time complexity: O(log(N)) for each item added, where N is the number of elements in the sorted set.
* @param key cache storage key
* @param scoreValues values and corresponding scores to be added
* @return number of inserted elements ignoring already existing
*/
def zsetAdd(key: String, scoreValues: (Double, Any)*): Future[Long]

/**
* Returns the sorted set cardinality (number of elements) of the sorted set stored at key.
*
* Time complexity: O(1)
*
* @param key cache storage key
* @return the cardinality (number of elements) of the set, or 0 if key does not exist.
*/
def zsetSize(key: String): Future[Long]

/**
* Returns the score of member in the sorted set at key.
*
* If member does not exist in the sorted set, or key does not exist, nil is returned.
*
* Time complexity: O(1)
*
* @param key cache storage key
* @param value tested element
* @return the score of member (a double precision floating point number).
*/
def zscore(key: String, value: Any): Future[Option[Double]]

/**
* Removes the specified members from the sorted set stored at key. Non existing members are ignored.
*
* An error is returned when key exists and does not hold a sorted set.
*
* Time complexity: O(M*log(N)) with N being the number of elements in the sorted set and M the number of elements to be removed.
*
* @param key cache storage key
* @param value values to be removed
* @return total number of removed values, non existing are ignored
*/
def zsetRemove(key: String, value: Any*): Future[Long]

/**
* Returns the specified range of elements in the sorted set stored at key.
*
* An error is returned when key exists and does not hold a sorted set.
* @param key cache storage key
* @param start the start index of the range
* @param stop the stop index of the range
* @note The start and stop arguments represent zero-based indexes, where 0 is the first element,
* 1 is the next element, and so on. These arguments specify an inclusive range.
* @return list of elements in the specified range
*/
def zrange[T: ClassTag](key: String, start: Long, stop: Long): Future[Seq[T]]

/**
* Returns the specified range of elements in the sorted set stored at key.
* The elements are considered to be ordered from the highest to the lowest score.
* Descending lexicographical order is used for elements with equal score.
*
* @param key cache storage key
* @param start the start index of the range
* @param stop the stop index of the range
* @note Apart from the reversed ordering, the zrevRange is similar to zrange.
* @return list of elements in the specified range
*/
def zrevRange[T: ClassTag](key: String, start: Long, stop: Long): Future[Seq[T]]
}

/**
* Internal non-blocking Redis API implementing REDIS protocol
*
Expand All @@ -473,3 +562,4 @@ trait RedisConnector extends AnyRef
with ListCommands
with SetCommands
with HashCommands
with SortedSetCommands
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,57 @@ private[connector] class RedisConnectorImpl(serializer: AkkaSerializer, redis: R
}
}

def zsetAdd(key: String, scoreValues: (Double, Any)*) = {
// encodes the value
def toEncoded(value: Any) = encode(key, value)
Future.sequence(scoreValues.map(scoreValue => toEncoded(scoreValue._2).map(encodedString => (scoreValue._1, encodedString)))).flatMap(redis.zadd(key, _: _*)) executing "ZADD" withKey key andParameters scoreValues expects {
case inserted =>
log.debug(s"Inserted $inserted elements into the zset at '$key'.")
inserted
} recover {
case ExecutionFailedException(_, _, _, ex) if ex.getMessage startsWith "WRONGTYPE" =>
log.warn(s"Value at '$key' is not a zset.")
throw new IllegalArgumentException(s"Value at '$key' is not a zset.")
}
}

def zsetSize(key: String) =
redis.zcard(key) executing "ZCARD" withKey key logging {
case length => log.debug(s"The zset at '$key' has $length items.")
}

def zscore(key: String, value: Any) = {
encode(key, value) flatMap (redis.zscore(key, _)) executing "ZSCORE" withKey key andParameter value logging {
case Some(score) => log.debug(s"The score of item: $value is $score in the collection at '$key'.")
case None => log.debug(s"Item $value does not exist in the collection at '$key'")
}
}

def zsetRemove(key: String, values: Any*) = {
// encodes the value
def toEncoded(value: Any) = encode(key, value)

Future.sequence(values map toEncoded).flatMap(redis.zrem(key, _: _*)) executing "ZREM" withKey key andParameters values logging {
case removed => log.debug(s"Removed $removed elements from the zset at '$key'.")
}
}

def zrange[T: ClassTag](key: String, start: Long, stop: Long) = {
redis.zrange[String](key, start, stop) executing "ZRANGE" withKey key andParameter s"$start $stop" expects {
case encodedSeq =>
log.debug(s"Got range from $start to $stop in the zset at '$key'.")
encodedSeq.map(encoded => decode[T](key, encoded))
}
}

def zrevRange[T: ClassTag](key: String, start: Long, stop: Long) = {
redis.zrevrange[String](key, start, stop) executing "ZREVRANGE" withKey key andParameter s"$start $stop" expects {
case encodedSeq =>
log.debug(s"Got reverse range from $start to $stop in the zset at '$key'.")
encodedSeq.map(encoded => decode[T](key, encoded))
}
}

def hashRemove(key: String, fields: String*) =
redis.hdel(key, fields: _*) executing "HDEL" withKey key andParameters fields logging {
case removed => log.debug(s"Removed $removed elements from the collection at '$key'.")
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/play/api/cache/redis/impl/RedisCache.scala
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ private[impl] class RedisCache[Result[_]](redis: RedisConnector, builder: Builde
new RedisMapImpl(key, redis)
}

def zset[T: ClassTag](key: String): RedisSortedSet[T, Result] = key.prefixed { key =>
new RedisSortedSetImpl(key, redis)
}

// $COVERAGE-OFF$
override def toString = s"RedisCache(name=${runtime.name})"
// $COVERAGE-ON$
Expand Down
48 changes: 48 additions & 0 deletions src/main/scala/play/api/cache/redis/impl/RedisSortedSetImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package play.api.cache.redis.impl

import play.api.cache.redis._

import scala.language.{higherKinds, implicitConversions}
import scala.reflect.ClassTag

/** <p>Implementation of Set API using redis-server cache implementation.</p> */
private[impl] class RedisSortedSetImpl[Elem: ClassTag, Result[_]](key: String, redis: RedisConnector)(implicit builder: Builders.ResultBuilder[Result], runtime: RedisRuntime) extends RedisSortedSet[Elem, Result] {

// implicit ask timeout and execution context
import dsl._

@inline
private def This: This = this

def add(scoreValues: (Double, Elem)*) = {
redis.zsetAdd(key, scoreValues: _*).map(_ => This).recoverWithDefault(This)
}

def contains(element: Elem) = {
redis.zscore(key, element).map(_.isDefined).recoverWithDefault(false)
}

def remove(element: Elem*) = {
redis.zsetRemove(key, element: _*).map(_ => This).recoverWithDefault(This)
}

def range(start: Long, stop: Long, isReverse: Boolean = false) = {
if (isReverse) {
redis.zrevRange[Elem](key, start, stop).recoverWithDefault(Seq.empty)
} else {
redis.zrange[Elem](key, start, stop).recoverWithDefault(Seq.empty)
}
}

def size = {
redis.zsetSize(key).recoverWithDefault(0)
}

def isEmpty = {
redis.zsetSize(key).map(_ == 0).recoverWithDefault(true)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
redis.zsetSize(key).map(_ == 0).recoverWithDefault(true)
size.map(_ == 0)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried with the compiler, it turned out that can't use map here cause the impact of recoverWithDefault . I checked out the implementation for set and hash, the code style is the same.

  def size = {
    redis.setSize(key).recoverWithDefault(0)
  }

  def isEmpty = {
    redis.setSize(key).map(_ == 0).recoverWithDefault(true)
  }

  def nonEmpty = {
    redis.setSize(key).map(_ > 0).recoverWithDefault(false)
  }

}

def nonEmpty = {
redis.zsetSize(key).map(_ > 0).recoverWithDefault(false)
}
}
2 changes: 1 addition & 1 deletion version.sbt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version in ThisBuild := "2.6.2-SNAPSHOT"
version in ThisBuild := "2.6.3-SNAPSHOT"