├── project
└── build.properties
├── .gitignore
├── .travis.yml
└── src
├── test
├── resources
│ ├── setup_travis.sh
│ ├── logback.xml
│ └── application.conf
└── scala
│ └── com
│ └── redis
│ ├── api
│ ├── ServerOperationsSpec.scala
│ ├── HyperLogLogOperationsSpec.scala
│ ├── HashOperationsSpec.scala
│ ├── EvalOperationsSpec.scala
│ ├── PubSubOperationsSpec.scala
│ ├── ListOperationsSpec.scala
│ ├── KeyOperationsSpec.scala
│ ├── TransactionOperationsSpec.scala
│ ├── StringOperationsSpec.scala
│ └── SortedSetOperationsSpec.scala
│ ├── RedisSpecBase.scala
│ ├── IntegrationSpec.scala
│ ├── serialization
│ └── SerializationSpec.scala
│ └── ClientSpec.scala
└── main
└── scala
├── com
└── redis
│ ├── api
│ ├── RedisOps.scala
│ ├── EvalOperations.scala
│ ├── HyperLogLogOperations.scala
│ ├── ConnectionOperations.scala
│ ├── TransactionOperations.scala
│ ├── HashOperations.scala
│ ├── ServerOperations.scala
│ ├── PubSubOperations.scala
│ ├── KeyOperations.scala
│ ├── ListOperations.scala
│ ├── SetOperations.scala
│ ├── StringOperations.scala
│ └── SortedSetOperations.scala
│ ├── PubSubHandler.scala
│ ├── protocol
│ ├── ConnectionCommands.scala
│ ├── TransactionCommands.scala
│ ├── HyperLogLogCommands.scala
│ ├── EvalCommands.scala
│ ├── RedisCommand.scala
│ ├── package.scala
│ ├── PubSubCommands.scala
│ ├── ServerCommands.scala
│ ├── HashCommands.scala
│ ├── SetCommands.scala
│ ├── ListCommands.scala
│ ├── KeyCommands.scala
│ ├── StringCommands.scala
│ └── SortedSetCommands.scala
│ ├── pipeline
│ ├── DynamicPipePair.scala
│ ├── Serializing.scala
│ └── ResponseHandling.scala
│ ├── RedisClient.scala
│ ├── serialization
│ ├── Deserializer.scala
│ ├── Integration.scala
│ ├── Stringified.scala
│ ├── Format.scala
│ ├── PartialDeserializer.scala
│ └── RawReplyParser.scala
│ ├── RedisClientSettings.scala
│ └── RedisConnection.scala
└── akka
└── io
└── TcpPipelineHandler.scala
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.17
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | target
3 | project/boot
4 | *.swp
5 | *.iml
6 | .idea
7 | .idea_modules
8 | dump.rdb
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 |
3 | scala:
4 | - 2.10.7
5 | - 2.11.12
6 |
7 | jdk:
8 | - openjdk7
9 |
10 | before_script:
11 | - sh src/test/resources/setup_travis.sh
12 |
13 | matrix:
14 | include:
15 | - scala: 2.12.4
16 | jdk: oraclejdk8
17 |
--------------------------------------------------------------------------------
/src/test/resources/setup_travis.sh:
--------------------------------------------------------------------------------
1 | !/bin/sh
2 |
3 | # Install Redis
4 | wget http://download.redis.io/releases/redis-2.8.9.tar.gz
5 | tar -xzf redis-2.8.9.tar.gz
6 | cd redis-2.8.9
7 | make
8 | sudo make install
9 |
10 | # Start Redis Server
11 | /usr/local/bin/redis-server &
12 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/RedisOps.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import akka.actor.ActorRef
5 |
6 |
7 | private [redis] trait RedisOps extends StringOperations
8 | with ListOperations
9 | with SetOperations
10 | with SortedSetOperations
11 | with HashOperations
12 | with HyperLogLogOperations
13 | with KeyOperations
14 | with ServerOperations
15 | with EvalOperations
16 | with ConnectionOperations
17 | with TransactionOperations
18 | with PubSubOperations {
19 |
20 | def clientRef: ActorRef
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/PubSubHandler.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 |
3 | import akka.actor.{Props,ActorLogging, Actor}
4 | import akka.routing.Listeners
5 | import com.redis.protocol.PubSubCommands
6 |
7 | object PubSubHandler {
8 |
9 | def props = Props[PubSubHandler]
10 |
11 | }
12 |
13 | class PubSubHandler extends Actor with ActorLogging with Listeners {
14 |
15 | val handleEvents : Receive = {
16 | case e : PubSubCommands.PushedMessage =>
17 | log.debug( s"Event received $e" )
18 | gossip( e )
19 | }
20 |
21 | override def receive: Receive = listenerManagement orElse handleEvents
22 | }
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/ConnectionCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object ConnectionCommands {
7 | import DefaultWriters._
8 |
9 | case object Quit extends RedisCommand[Boolean]("QUIT") {
10 | def params = ANil
11 | }
12 |
13 | case class Auth(secret: String) extends RedisCommand[Boolean]("AUTH") {
14 | def params = secret +: ANil
15 | }
16 |
17 | case class Select(index: Int) extends RedisCommand[Boolean]("SELECT") {
18 | def params = index +: ANil
19 | }
20 |
21 | case object Ping extends RedisCommand[Boolean]("PING") {
22 | def params = ANil
23 | }
24 |
25 | case class Echo(message: String) extends RedisCommand[String]("ECHO") {
26 | def params = message +: ANil
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/pipeline/DynamicPipePair.scala:
--------------------------------------------------------------------------------
1 | package com.redis.pipeline
2 |
3 | import akka.io.PipePair
4 |
5 |
6 | abstract class DynamicPipePair[CmdAbove, CmdBelow, EvtAbove, EvtBelow]
7 | extends PipePair[CmdAbove, CmdBelow, EvtAbove, EvtBelow] {
8 |
9 | type CPL = CmdAbove => Iterable[Result]
10 | type EPL = EvtBelow => Iterable[Result]
11 |
12 | private[this] var _cpl: CPL = _
13 | private[this] var _epl: EPL = _
14 |
15 | override def commandPipeline = {
16 | if (_cpl eq null) _cpl = initialCommandPipeline
17 | _cpl
18 | }
19 |
20 | override def eventPipeline = {
21 | if (_epl eq null) _epl = initialEventPipeline
22 | _epl
23 | }
24 |
25 | def initialCommandPipeline: CPL
26 | def initialEventPipeline: EPL
27 |
28 | protected def become(pipes: SwitchablePipes): Unit = {
29 | _cpl = pipes.commandPipeline
30 | _epl = pipes.eventPipeline
31 | }
32 |
33 | case class SwitchablePipes(commandPipeline: CPL, eventPipeline: EPL)
34 | }
35 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/ServerOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import scala.concurrent.Future
4 |
5 | import org.scalatest.junit.JUnitRunner
6 | import org.junit.runner.RunWith
7 |
8 | import com.redis.RedisSpecBase
9 |
10 |
11 | @RunWith(classOf[JUnitRunner])
12 | class ServerOperationsSpec extends RedisSpecBase {
13 |
14 | describe("info") {
15 | it("should return information from the server") {
16 | client.info.futureValue.get.length should be > (0)
17 | }
18 | }
19 |
20 | describe("config") {
21 | it("should return appropriate config values with glob style params") {
22 | client.config.get("hash-max-ziplist-entries")
23 | .futureValue should equal(List("hash-max-ziplist-entries", "512"))
24 |
25 | client.config.get("*max-*-entries*")
26 | .futureValue should equal(List("hash-max-ziplist-entries", "512", "list-max-ziplist-entries", "512", "set-max-intset-entries", "512", "zset-max-ziplist-entries", "128"))
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/TransactionCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object TransactionCommands {
7 | import DefaultWriters._
8 |
9 | case object Multi extends RedisCommand[Boolean]("MULTI") {
10 | def params = ANil
11 | }
12 |
13 | // the type of RedisCommand is not used. It can be anything for which Format exists
14 | case object Exec extends RedisCommand[List[Array[Byte]]]("EXEC") {
15 | def params = ANil
16 | }
17 |
18 | case object Discard extends RedisCommand[Boolean]("DISCARD") {
19 | def params = ANil
20 | }
21 |
22 | case class Watch(keys: Seq[String]) extends RedisCommand[Boolean]("WATCH") {
23 | require(keys.nonEmpty, "Keys should not be empty")
24 | def params = keys.toArgs
25 | }
26 |
27 | case object Unwatch extends RedisCommand[Boolean]("UNWATCH") {
28 | def params = ANil
29 | }
30 |
31 | object Watch {
32 | def apply(key: String, keys: String*): Watch = Watch(key +: keys)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/HyperLogLogCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object HyperLogLogCommands {
7 | import DefaultWriters._
8 |
9 | case class PFAdd(key: String, elements: Seq[String]) extends RedisCommand[Int]("PFADD") {
10 | def params = key +: elements.toArgs
11 | }
12 |
13 | object PFAdd {
14 | def apply(key: String, element: String, elements: String*): PFAdd = PFAdd(key, element +: elements)
15 | }
16 |
17 | case class PFCount(keys: Seq[String]) extends RedisCommand[Long]("PFCOUNT") {
18 | def params = keys.toArgs
19 | }
20 |
21 | object PFCount {
22 | def apply(key: String, keys: String*): PFCount = PFCount(key +: keys)
23 | }
24 |
25 | case class PFMerge(destKey: String, sourceKeys: Seq[String]) extends RedisCommand[Boolean]("PFMERGE") {
26 | def params = destKey +: sourceKeys.toArgs
27 | }
28 |
29 | object PFMerge {
30 | def apply(destKey: String, sourceKey: String, sourceKeys: String*): PFMerge =
31 | PFMerge(destKey, sourceKey +: sourceKeys)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/EvalOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import serialization._
5 | import akka.pattern.ask
6 | import akka.util.Timeout
7 | import com.redis.protocol.EvalCommands
8 |
9 | trait EvalOperations { this: RedisOps =>
10 | import EvalCommands._
11 |
12 | def eval[A](script: String, keys: Seq[String] = Nil, args: Seq[Stringified] = Nil)
13 | (implicit timeout: Timeout, reader: Reader[A]) =
14 | clientRef.ask(Eval[A](script, keys, args)).mapTo[Eval[A]#Ret]
15 |
16 | def evalsha[A](shaHash: String, keys: Seq[String] = Nil, args: Seq[Stringified] = Nil)
17 | (implicit timeout: Timeout, reader: Reader[A]) =
18 | clientRef.ask(EvalSHA[A](shaHash, keys, args)).mapTo[EvalSHA[A]#Ret]
19 |
20 | // Sub-commands of SCRIPT
21 | object script {
22 | import Script._
23 |
24 | def load(script: String)(implicit timeout: Timeout) =
25 | clientRef.ask(Load(script)).mapTo[Load#Ret]
26 |
27 | def exists(shaHash: String)(implicit timeout: Timeout) =
28 | clientRef.ask(Exists(shaHash)).mapTo[Exists#Ret]
29 |
30 | def flush(implicit timeout: Timeout) =
31 | clientRef.ask(Flush).mapTo[Flush.Ret]
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/HyperLogLogOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import serialization._
5 | import akka.pattern.ask
6 | import akka.util.Timeout
7 | import com.redis.protocol.HyperLogLogCommands
8 |
9 | trait HyperLogLogOperations { this: RedisOps =>
10 | import HyperLogLogCommands._
11 |
12 | def pfadd(key: String, elements: Seq[String])(implicit timeout: Timeout) =
13 | clientRef.ask(PFAdd(key, elements)).mapTo[PFAdd#Ret]
14 |
15 | def pfadd(key: String, element: String, elements: String*)(implicit timeout: Timeout) =
16 | clientRef.ask(PFAdd(key, element, elements: _*)).mapTo[PFAdd#Ret]
17 |
18 | def pfcount(keys: Seq[String])(implicit timeout: Timeout) =
19 | clientRef.ask(PFCount(keys)).mapTo[PFCount#Ret]
20 |
21 | def pfcount(key: String, keys: String*)(implicit timeout: Timeout) =
22 | clientRef.ask(PFCount(key, keys: _*)).mapTo[PFCount#Ret]
23 |
24 | def pfmerge(destKey: String, sourceKeys: Seq[String])(implicit timeout: Timeout) =
25 | clientRef.ask(PFMerge(destKey, sourceKeys)).mapTo[PFMerge#Ret]
26 |
27 | def pfmerge(destKey: String, sourceKey: String, sourceKeys: String*)(implicit timeout: Timeout) =
28 | clientRef.ask(PFMerge(destKey, sourceKey, sourceKeys: _*)).mapTo[PFMerge#Ret]
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/pipeline/Serializing.scala:
--------------------------------------------------------------------------------
1 | package com.redis.pipeline
2 |
3 | import akka.io._
4 | import akka.util.{ByteString, ByteStringBuilder}
5 | import com.redis.protocol._
6 |
7 |
8 | class Serializing extends PipelineStage[HasLogging, Command, Command, Event, Event] {
9 |
10 | def apply(ctx: HasLogging) = new PipePair[Command, Command, Event, Event] {
11 | import ctx.{getLogger => log}
12 |
13 | val b = new ByteStringBuilder
14 |
15 | def render(req: RedisCommand[_]) = {
16 | def addBulk(bulk: ByteString) = {
17 | b += Bulk
18 | b ++= ByteString(bulk.size.toString)
19 | b ++= Newline
20 | b ++= bulk
21 | b ++= Newline
22 | }
23 |
24 | val args = req.params
25 |
26 | b.clear
27 | b += Multi
28 | b ++= ByteString((args.size + 1).toString)
29 | b ++= Newline
30 | addBulk(req.cmd)
31 | args.foreach(arg => addBulk(arg.value))
32 | b.result
33 | }
34 |
35 | val commandPipeline = (cmd: Command) => cmd match {
36 | case RedisRequest(_, redisCmd) => ctx.singleCommand(Tcp.Write(render(redisCmd)))
37 | case cmd: Tcp.Command => ctx.singleCommand(cmd)
38 | }
39 |
40 | val eventPipeline = (evt: Event) => ctx.singleEvent(evt)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/RedisClient.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 |
3 | import java.net.InetSocketAddress
4 | import akka.actor._
5 |
6 | // one RedisClient can have multiple RedisConnections to support pooling, clustering etc. Right now we have only
7 | // one RedsiConnection. In future we may offer APIs like RedisClient.single (single connection), RedisClient.pooled
8 | // (connection pool) etc.
9 |
10 | object RedisClient {
11 |
12 | def apply(host: String, port: Int = 6379, name: String = defaultName,
13 | settings: RedisClientSettings = RedisClientSettings())(implicit refFactory: ActorRefFactory): RedisClient =
14 | apply(new InetSocketAddress(host, port), name, settings)
15 |
16 | // More independent setting classes might be introduced for client, connection pool or cluster setup,
17 | // but not sure of actual interface yet
18 | def apply(remote: InetSocketAddress, name: String, settings: RedisClientSettings)
19 | (implicit refFactory: ActorRefFactory): RedisClient =
20 | new RedisClient(refFactory.actorOf(RedisConnection.props(remote, settings), name = name))
21 |
22 | private def defaultName = "redis-client-" + nameSeq.next
23 | private val nameSeq = Iterator from 0
24 | }
25 |
26 | class RedisClient(val clientRef: ActorRef) extends api.RedisOps
27 |
28 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/ConnectionOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import scala.concurrent.Future
5 | import scala.concurrent.duration._
6 | import serialization._
7 | import akka.pattern.ask
8 | import akka.actor._
9 | import akka.util.Timeout
10 | import akka.pattern.gracefulStop
11 | import com.redis.protocol._
12 |
13 |
14 | trait ConnectionOperations { this: RedisOps =>
15 | import ConnectionCommands._
16 |
17 | // QUIT
18 | // exits the server.
19 | def quit()(implicit timeout: Timeout): Future[Boolean] = {
20 | clientRef ! Quit
21 | gracefulStop(clientRef, 5 seconds)
22 | }
23 |
24 | // AUTH
25 | // auths with the server.
26 | def auth(secret: String)(implicit timeout: Timeout) =
27 | clientRef.ask(Auth(secret)).mapTo[Auth#Ret]
28 |
29 | // SELECT (index)
30 | // selects the DB to connect, defaults to 0 (zero).
31 | def select(index: Int)(implicit timeout: Timeout) =
32 | clientRef.ask(Select(index)).mapTo[Select#Ret]
33 |
34 | // ECHO (message)
35 | // echo the given string.
36 | def echo(message: String)(implicit timeout: Timeout) =
37 | clientRef.ask(Echo(message)).mapTo[Echo#Ret]
38 |
39 | // PING
40 | // pings redis
41 | def ping()(implicit timeout: Timeout) =
42 | clientRef.ask(Ping).mapTo[Ping.Ret]
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/EvalCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object EvalCommands {
7 | import DefaultWriters._
8 |
9 | case class Eval[A: Reader](script: String, keys: Seq[String] = Nil, args: Seq[Stringified] = Nil)
10 | extends RedisCommand[List[A]]("EVAL")(PartialDeserializer.ensureListPD) {
11 |
12 | def params = argsForEval(script, keys, args)
13 | }
14 |
15 | case class EvalSHA[A: Reader](shaHash: String, keys: Seq[String] = Nil, args: Seq[Stringified] = Nil)
16 | extends RedisCommand[List[A]]("EVALSHA")(PartialDeserializer.ensureListPD) {
17 |
18 | def params = argsForEval(shaHash, keys, args)
19 | }
20 |
21 | object Script {
22 |
23 | case class Load(script: String) extends RedisCommand[Option[String]]("SCRIPT") {
24 | def params = "LOAD" +: script +: ANil
25 | }
26 |
27 | case class Exists(shaHash: String) extends RedisCommand[List[Int]]("SCRIPT") {
28 | def params = "EXISTS" +: shaHash +: ANil
29 | }
30 |
31 | case object Flush extends RedisCommand[Boolean]("SCRIPT") {
32 | def params = "Flush" +: ANil
33 | }
34 |
35 | }
36 |
37 | private def argsForEval[A](luaCode: String, keys: Seq[String], args: Seq[Stringified]): Args =
38 | luaCode +: keys.length +: keys ++: args.toArgs
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/RedisCommand.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import akka.util.CompactByteString
4 | import com.redis.serialization._
5 |
6 |
7 | abstract class RedisCommand[A](_cmd: String)(implicit _des: PartialDeserializer[A]) {
8 | import RedisCommand._
9 |
10 | type Ret = A
11 |
12 | val cmd = CompactByteString(_cmd)
13 |
14 | def params: Args
15 |
16 | def des = _des
17 | }
18 |
19 |
20 | object RedisCommand {
21 |
22 | class Args (val values: Seq[Stringified]) extends AnyVal {
23 | import Stringified._
24 |
25 | def :+(x: Stringified): Args = new Args(values :+ x)
26 | def :+[A: Writer](x: A): Args = this :+ x.stringify
27 |
28 | def +:(x: Stringified): Args = new Args(x +: values)
29 | def +:[A: Writer](x: A): Args = x.stringify +: this
30 |
31 | def ++(xs: Seq[Stringified]): Args = new Args(values ++ xs)
32 | def ++[A: Writer](xs: Seq[A]): Args = this ++ (xs.map(_.stringify))
33 |
34 | def ++:(xs: Seq[Stringified]): Args = new Args(xs ++: values)
35 | def ++:[A: Writer](xs: Seq[A]): Args = xs.map(_.stringify) ++: this
36 |
37 | def size = values.size
38 |
39 | def foreach(f: Stringified => Unit) = values foreach f
40 | def map[A](f: Stringified => A) = values map f
41 | }
42 |
43 | object Args {
44 | val empty = new Args(Nil)
45 | }
46 |
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/TransactionOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import scala.concurrent.ExecutionContext
5 | import scala.util.{Success, Failure}
6 | import serialization._
7 | import akka.pattern.ask
8 | import akka.util.Timeout
9 | import com.redis.protocol.{TransactionCommands, Discarded}
10 | import ExecutionContext.Implicits.global
11 |
12 | trait TransactionOperations { this: RedisOps =>
13 | import TransactionCommands._
14 |
15 | def multi()(implicit timeout: Timeout) =
16 | clientRef.ask(Multi).mapTo[Multi.type#Ret]
17 |
18 | def exec()(implicit timeout: Timeout) =
19 | clientRef.ask(Exec).mapTo[Exec.type#Ret]
20 |
21 | def discard()(implicit timeout: Timeout) =
22 | clientRef.ask(Discard).mapTo[Discard.type#Ret].map(_ => Discarded)
23 |
24 | def watch(keys: Seq[String])(implicit timeout: Timeout) =
25 | clientRef.ask(Watch(keys)).mapTo[Watch#Ret]
26 |
27 | def watch(key: String, keys: String*)(implicit timeout: Timeout) =
28 | clientRef.ask(Watch(key, keys:_*)).mapTo[Watch#Ret]
29 |
30 | def unwatch()(implicit timeout: Timeout) =
31 | clientRef.ask(Unwatch).mapTo[Unwatch.type#Ret]
32 |
33 | def withTransaction(txn: RedisOps => Unit)(implicit timeout: Timeout) = {
34 | multi()
35 | try {
36 | txn(this)
37 | exec()
38 | } catch {
39 | case th: Throwable => discard()
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/serialization/Deserializer.scala:
--------------------------------------------------------------------------------
1 | package com.redis.serialization
2 |
3 | import akka.util.CompactByteString
4 | import scala.language.existentials
5 |
6 |
7 | class Deserializer {
8 | import Deserializer._
9 | import RawReplyParser.RawReply
10 | import PartialDeserializer._
11 |
12 | var parse: (CompactByteString, PartialFunction[RawReply, _]) => Result = parseSafe
13 |
14 | def parseSafe(input: CompactByteString, deserializerParts: PartialFunction[RawReply, _]): Result =
15 | try {
16 | val rawReply = new RawReply(input)
17 | val result = (deserializerParts orElse errorPD)(rawReply)
18 | parse = parseSafe
19 | Result.Ok(result, rawReply.remaining)
20 | } catch {
21 | case NotEnoughDataException =>
22 | parse = (i, d) => parseSafe((input ++ i).compact, d)
23 | Result.NeedMoreData
24 |
25 | case e: Throwable =>
26 | Result.Failed(e, input)
27 | }
28 | }
29 |
30 | object Deserializer {
31 | import com.redis.protocol._
32 |
33 | sealed trait Result
34 |
35 | object Result {
36 | case object NeedMoreData extends Result
37 | case class Ok[A](reply: A, remaining: CompactByteString) extends Result
38 | case class Failed(cause: Throwable, data: CompactByteString) extends Result
39 | }
40 |
41 | object NotEnoughDataException extends Exception
42 | object EmptyTxnResultException extends Exception
43 |
44 | def nullMultiBulk(data: CompactByteString) = data(1) match {
45 | case Err => true
46 | case _ => false
47 | }
48 | }
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/test/resources/application.conf:
--------------------------------------------------------------------------------
1 | akka {
2 |
3 | loggers = ["akka.event.slf4j.Slf4jLogger"]
4 |
5 | loglevel = "WARNING"
6 |
7 | actor {
8 | default-dispatcher {
9 | type = "Dispatcher"
10 |
11 | executor = "thread-pool-executor"
12 |
13 | thread-pool-executor {
14 | # Keep alive time for threads
15 | keep-alive-time = 60s
16 |
17 | # Min number of threads to cap factor-based core number to
18 | core-pool-size-min = 8
19 |
20 | # The core pool size factor is used to determine thread pool core size
21 | # using the following formula: ceil(available processors * factor).
22 | # Resulting size is then bounded by the core-pool-size-min and
23 | # core-pool-size-max values.
24 | core-pool-size-factor = 3.0
25 |
26 | # Max number of threads to cap factor-based number to
27 | core-pool-size-max = 64
28 |
29 | # Minimum number of threads to cap factor-based max number to
30 | # (if using a bounded task queue)
31 | max-pool-size-min = 8
32 |
33 | # Max no of threads (if using a bounded task queue) is determined by
34 | # calculating: ceil(available processors * factor)
35 | max-pool-size-factor = 3.0
36 |
37 | # Max number of threads to cap factor-based max number to
38 | # (if using a bounded task queue)
39 | max-pool-size-max = 64
40 |
41 | # Allow core threads to time out
42 | allow-core-timeout = on
43 | }
44 |
45 | # Throughput defines the number of messages that are processed in a batch
46 | # before the thread is returned to the pool. Set to 1 for as fair as possible.
47 | throughput = 5
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/package.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 |
3 | import akka.io.Tcp
4 | import akka.actor.ActorRef
5 | import akka.util.ByteString
6 | import scala.language.existentials
7 | import com.redis.serialization.{Writer, Stringified}
8 |
9 |
10 | package object protocol {
11 |
12 | type Command = Tcp.Command
13 |
14 | type CloseCommand = Tcp.CloseCommand
15 | val Close = Tcp.Close
16 | val ConfirmedClose = Tcp.ConfirmedClose
17 | val Abort = Tcp.Abort
18 |
19 | case class RedisRequest(sender: ActorRef, command: RedisCommand[_]) extends Command
20 |
21 |
22 | type Event = Tcp.Event
23 | type ConnectionClosed = Tcp.ConnectionClosed
24 | val Closed = Tcp.Closed
25 | val ConfirmedClosed = Tcp.ConfirmedClosed
26 | val Aborted = Tcp.Aborted
27 |
28 | case object RequestQueueEmpty extends Event
29 |
30 | case class RedisError(message: String) extends Throwable(message)
31 | object Queued extends RedisError("Queued") {
32 | def unapply(q: Queued.type) = Some("Queued")
33 | }
34 |
35 | case object Discarded
36 |
37 |
38 | type Args = RedisCommand.Args
39 |
40 | val ANil = RedisCommand.Args.empty
41 |
42 | implicit class StringifiedArgsOps(values: Seq[Stringified]) {
43 | def toArgs = new Args(values)
44 | }
45 |
46 | implicit class ArgsOps[A: Writer](values: Seq[A]) {
47 | def toArgs = new Args(values)
48 | }
49 |
50 | /**
51 | * Response codes from the Redis server
52 | */
53 | val Cr = '\r'.toByte
54 | val Lf = '\n'.toByte
55 | val Status = '+'.toByte
56 | val Integer = ':'.toByte
57 | val Bulk = '$'.toByte
58 | val Multi = '*'.toByte
59 | val Err = '-'.toByte
60 |
61 | val Newline = ByteString("\r\n")
62 |
63 | val NullBulkReplyCount = -1
64 | val NullMultiBulkReplyCount = -1
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/serialization/Integration.scala:
--------------------------------------------------------------------------------
1 | package com.redis.serialization
2 |
3 | import scala.language.implicitConversions
4 |
5 |
6 | trait SprayJsonSupport {
7 | import spray.json._
8 |
9 | implicit def sprayJsonStringReader[A](implicit reader: RootJsonReader[A]): Reader[A] =
10 | StringReader(s => reader.read(s.parseJson))
11 |
12 | implicit def sprayJsonStringWriter[A](implicit writer: RootJsonWriter[A]): Writer[A] =
13 | StringWriter(writer.write(_).toString)
14 | }
15 |
16 | object SprayJsonSupport extends SprayJsonSupport
17 |
18 |
19 | trait Json4sSupport {
20 | import org.json4s.{Serialization, Formats}
21 |
22 | def Serialization: Serialization
23 |
24 | implicit def json4sStringReader[A](implicit format: Formats, manifest: Manifest[A]): Reader[A] =
25 | StringReader(Serialization.read(_))
26 |
27 | implicit def json4sStringWriter[A <: AnyRef](implicit format: Formats): Writer[A] =
28 | StringWriter(Serialization.write(_))
29 | }
30 |
31 | trait Json4sNativeSupport extends Json4sSupport {
32 | val Serialization = org.json4s.native.Serialization
33 | }
34 |
35 | object Json4sNativeSupport extends Json4sNativeSupport
36 |
37 | trait Json4sJacksonSupport extends Json4sSupport {
38 | val Serialization = org.json4s.jackson.Serialization
39 | }
40 |
41 | object Json4sJacksonSupport extends Json4sJacksonSupport
42 |
43 |
44 | //trait LiftJsonSupport {
45 | // import net.liftweb.json._
46 |
47 | // implicit def liftJsonStringReader[A](implicit format: Formats, manifest: Manifest[A]): Reader[A] =
48 | // StringReader(parse(_).extract[A])
49 |
50 | // implicit def liftJsonStringWriter[A <: AnyRef](implicit format: Formats): Writer[A] =
51 | // StringWriter(Serialization.write(_))
52 | //}
53 |
54 | //object LiftJsonSupport extends LiftJsonSupport
55 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/HyperLogLogOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import org.scalatest.junit.JUnitRunner
4 | import org.junit.runner.RunWith
5 | import com.redis.RedisSpecBase
6 |
7 |
8 | @RunWith(classOf[JUnitRunner])
9 | class HyperLogLogOperationsSpec extends RedisSpecBase {
10 |
11 | describe("pfadd") {
12 | it("should add all the elements") {
13 | val key = "pfadd1"
14 | client.pfadd(key, "a", "b", "c", "d", "e").futureValue should be(1)
15 | client.pfcount(key).futureValue should be(5)
16 | }
17 |
18 | it("should ignore duplicated elements (with very low error rate)") {
19 | val key1 = "pfadd2"
20 | client.pfadd(key1, "foo", "bar", "zap").futureValue should be(1)
21 | client.pfadd(key1, "zap", "zap", "zap").futureValue should be(0)
22 | client.pfadd(key1, "foo", "bar").futureValue should be(0)
23 | }
24 | }
25 |
26 | describe("pfcount") {
27 | it("should return the approximated cardinality") {
28 | val key1 = "pfcount1"
29 | client.pfadd(key1, "foo", "bar", "zap").futureValue should be(1)
30 | client.pfcount(key1).futureValue should be(3)
31 |
32 | val key2 = "pfcount2"
33 | client.pfadd(key2, "1", "2", "3").futureValue should be(1)
34 | client.pfcount(key1, key2).futureValue should be(6)
35 | }
36 | }
37 |
38 | describe("pfmerge") {
39 | it("should merge multiple HyperLogLog values") {
40 | val key1 = "pfmerge1"
41 | val key2 = "pfmerge2"
42 | val key3 = "pfmerge3"
43 |
44 | client.pfadd(key1, "foo", "bar", "zap", "a").futureValue should be(1)
45 | client.pfadd(key2, "a", "b", "c", "foo").futureValue should be(1)
46 |
47 | client.pfmerge(key3, key1, key2).futureValue should be(true)
48 |
49 | client.pfcount(key3).futureValue should be (6)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/PubSubCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import akka.util.ByteString
4 |
5 | object PubSubCommands {
6 | import com.redis.serialization._
7 | import DefaultFormats._
8 |
9 | sealed trait PushedMessage {
10 | def destination: String
11 | }
12 | case class Subscribed(destination: String, isPattern: Boolean, numOfSubscriptions: Int) extends PushedMessage
13 | case class Unsubscribed(destination: String, isPattern: Boolean, numOfSubscriptions: Int) extends PushedMessage
14 | case class Message(destination: String, payload: ByteString) extends PushedMessage
15 | case class PMessage(pattern: String, destination: String, payload: ByteString) extends PushedMessage
16 |
17 | abstract class PubSubCommand(_cmd: String) extends RedisCommand[Unit](_cmd)(PartialDeserializer.UnitDeserializer)
18 | abstract class SubscribeCommand(_cmd: String) extends PubSubCommand(_cmd)
19 |
20 | case class Subscribe(channels: Seq[String]) extends SubscribeCommand("SUBSCRIBE") {
21 | require(channels.nonEmpty, "Channels should not be empty")
22 | def params = channels.toArgs
23 | }
24 |
25 | object Subscribe {
26 | def apply(channel: String, channels: String*) = new Subscribe( channel +: channels )
27 | }
28 |
29 | case class PSubscribe( patterns: Seq[String] ) extends SubscribeCommand("PSUBSCRIBE") {
30 | require(patterns.nonEmpty, "Patterns should not be empty")
31 | def params = patterns.toArgs
32 | }
33 |
34 | object PSubscribe {
35 | def apply(pattern: String, patterns: String*) : PSubscribe = PSubscribe( pattern +: patterns )
36 | }
37 |
38 | case class Unsubscribe(channels: Seq[String]) extends PubSubCommand("UNSUBSCRIBE") {
39 | override def params = channels.toArgs
40 | }
41 |
42 | case class PUnsubscribe(patterns: Seq[String]) extends PubSubCommand("PUNSUBSCRIBE") {
43 | override def params = patterns.toArgs
44 | }
45 |
46 | case class Publish(channel: String, value: Stringified) extends RedisCommand[Int]("PUBLISH") {
47 | override def params: Args = channel +: value +: ANil
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/serialization/Stringified.scala:
--------------------------------------------------------------------------------
1 | package com.redis.serialization
2 |
3 | import akka.util.ByteString
4 | import scala.language.implicitConversions
5 | import scala.collection.generic.CanBuildFrom
6 | import scala.collection.{SeqLike, GenTraversableOnce, GenTraversable}
7 |
8 |
9 | class Stringified(val value: ByteString) extends AnyVal {
10 | override def toString = value.utf8String
11 | }
12 |
13 |
14 | object Stringified {
15 | implicit def apply[A](v: A)(implicit writer: Writer[A]) = new Stringified(writer.toByteString(v))
16 |
17 | implicit def applySeq[A: Writer](vs: Seq[A]) = vs.map(apply[A])
18 |
19 | implicit class StringifyOps[A: Writer](x: A) {
20 | def stringify = Stringified(x)
21 | }
22 | }
23 |
24 |
25 | class KeyValuePair(val pair: Product2[String, Stringified]) extends AnyVal {
26 | def key: String = pair._1
27 | def value: Stringified = pair._2
28 | }
29 |
30 | object KeyValuePair {
31 | import Stringified._
32 |
33 | implicit def apply(pair: Product2[String, Stringified]): KeyValuePair =
34 | new KeyValuePair(pair)
35 |
36 | implicit def apply[A: Writer](pair: Product2[String, A]): KeyValuePair =
37 | new KeyValuePair((pair._1, pair._2.stringify))
38 |
39 | implicit def applySeq[A: Writer](pairs: Seq[Product2[String, A]]): Seq[KeyValuePair] =
40 | pairs.map(apply[A])
41 |
42 | implicit def applyIterable[A: Writer](pairs: Iterable[Product2[String, A]]): Iterable[KeyValuePair] =
43 | pairs.map(apply[A])
44 |
45 | def unapply(kvp: KeyValuePair) = Some(kvp.pair)
46 | }
47 |
48 |
49 | class ScoredValue(val pair: Product2[Double, Stringified]) extends AnyVal {
50 | def score: Double = pair._1
51 | def value: Stringified = pair._2
52 | }
53 |
54 | object ScoredValue {
55 | import Stringified._
56 |
57 | implicit def apply(pair: Product2[Double, Stringified]): ScoredValue =
58 | new ScoredValue(pair)
59 |
60 | implicit def apply[A, B](pair: Product2[A, B])(implicit num: Numeric[A], writer: Writer[B]): ScoredValue =
61 | new ScoredValue((num.toDouble(pair._1), pair._2.stringify))
62 |
63 | implicit def applySeq[A, B](pairs: Seq[Product2[A, B]])(implicit num: Numeric[A], writer: Writer[B]): Seq[ScoredValue] =
64 | pairs.map(apply[A, B])
65 |
66 | def unapply(sv: ScoredValue) = Some(sv.pair)
67 | }
68 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/RedisSpecBase.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 |
3 | import scala.concurrent.duration._
4 |
5 | import akka.actor._
6 | import akka.testkit.TestKit
7 | import akka.util.Timeout
8 | import com.redis.RedisClientSettings.ConstantReconnectionSettings
9 | import org.scalatest._
10 | import org.scalatest.concurrent.{Futures, ScalaFutures}
11 | import org.scalatest.time._
12 |
13 | class RedisSpecBase(_system: ActorSystem) extends TestKit(_system)
14 | with FunSpecLike
15 | with Matchers
16 | with Futures
17 | with ScalaFutures
18 | with BeforeAndAfterEach
19 | with BeforeAndAfterAll {
20 | // Akka setup
21 | def this() = this(ActorSystem("redis-test-"+ RedisSpecBase.iter.next))
22 | implicit val executionContext = system.dispatcher
23 | implicit val timeout = Timeout(2 seconds)
24 |
25 | // Scalatest setup
26 | implicit val defaultPatience = PatienceConfig(timeout = Span(5, Seconds), interval = Span(5, Millis))
27 |
28 | // Redis client setup
29 | val client = RedisClient("localhost", 6379)
30 |
31 | def withReconnectingClient(testCode: RedisClient => Any) = {
32 | val client = RedisClient("localhost", 6379, settings = RedisClientSettings(reconnectionSettings = ConstantReconnectionSettings(100)))
33 | testCode(client)
34 | client.quit().futureValue should equal (true)
35 | }
36 |
37 | def withFixedConstantReconnectingClient(testCode: RedisClient => Any) = {
38 | val client = RedisClient("localhost", 6379, settings = RedisClientSettings(reconnectionSettings = ConstantReconnectionSettings(100,2)))
39 | testCode(client)
40 | client.quit().futureValue should equal (true)
41 | }
42 |
43 | override def beforeEach = {
44 | client.flushdb()
45 | }
46 |
47 | override def afterEach = { }
48 |
49 | override def afterAll =
50 | try {
51 | client.flushdb()
52 | client.quit() onSuccess {
53 | case true => system.shutdown()
54 | case false => throw new Exception("client actor didn't stop properly")
55 | }
56 | } catch {
57 | // the actor wasn't stopped within 5 seconds
58 | case e: akka.pattern.AskTimeoutException => throw e
59 | }
60 |
61 | }
62 |
63 | object RedisSpecBase {
64 |
65 | private val iter = Iterator from 0
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/HashOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import serialization._
5 | import akka.pattern.ask
6 | import akka.util.Timeout
7 | import com.redis.protocol.HashCommands
8 |
9 | trait HashOperations { this: RedisOps =>
10 | import HashCommands._
11 |
12 | def hset(key: String, field: String, value: Stringified)(implicit timeout: Timeout) =
13 | clientRef.ask(HSet(key, field, value)).mapTo[HSet#Ret]
14 |
15 | def hsetnx(key: String, field: String, value: Stringified)(implicit timeout: Timeout) =
16 | clientRef.ask(HSetNx(key, field, value)).mapTo[HSetNx#Ret]
17 |
18 | def hget[A](key: String, field: String)(implicit timeout: Timeout, reader: Reader[A]) =
19 | clientRef.ask(HGet[A](key, field)).mapTo[HGet[A]#Ret]
20 |
21 | def hmset(key: String, mapLike: Iterable[KeyValuePair])(implicit timeout: Timeout) =
22 | clientRef.ask(HMSet(key, mapLike)).mapTo[HMSet#Ret]
23 |
24 |
25 | def hmget[A](key: String, fields: Seq[String])(implicit timeout: Timeout, reader: Reader[A]) =
26 | clientRef.ask(HMGet[A](key, fields)).mapTo[HMGet[A]#Ret]
27 |
28 | def hmget[A](key: String, field: String, fields: String*)(implicit timeout: Timeout, reader: Reader[A]) =
29 | clientRef.ask(HMGet[A](key, field, fields:_*)).mapTo[HMGet[A]#Ret]
30 |
31 |
32 | def hincrby(key: String, field: String, value: Int)(implicit timeout: Timeout) =
33 | clientRef.ask(HIncrby(key, field, value)).mapTo[HIncrby#Ret]
34 |
35 | def hexists(key: String, field: String)(implicit timeout: Timeout) =
36 | clientRef.ask(HExists(key, field)).mapTo[HExists#Ret]
37 |
38 |
39 | def hdel(key: String, fields: Seq[String])(implicit timeout: Timeout) =
40 | clientRef.ask(HDel(key, fields)).mapTo[HDel#Ret]
41 |
42 | def hdel(key: String, field: String, fields: String*)(implicit timeout: Timeout) =
43 | clientRef.ask(HDel(key, field, fields:_*)).mapTo[HDel#Ret]
44 |
45 |
46 | def hlen(key: String)(implicit timeout: Timeout) =
47 | clientRef.ask(HLen(key)).mapTo[HLen#Ret]
48 |
49 | def hkeys(key: String)(implicit timeout: Timeout) =
50 | clientRef.ask(HKeys(key)).mapTo[HKeys#Ret]
51 |
52 | def hvals[A](key: String)(implicit timeout: Timeout, reader: Reader[A]) =
53 | clientRef.ask(HVals(key)).mapTo[HVals[A]#Ret]
54 |
55 | def hgetall[A](key: String)(implicit timeout: Timeout, reader: Reader[A]) =
56 | clientRef.ask(HGetall(key)).mapTo[HGetall[A]#Ret]
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/ServerCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object ServerCommands {
7 | import DefaultWriters._
8 |
9 | case object Save extends RedisCommand[Boolean]("SAVE") {
10 | def params = ANil
11 | }
12 |
13 | case object BgSave extends RedisCommand[Boolean]("BGSAVE") {
14 | def params = ANil
15 | }
16 |
17 | case object LastSave extends RedisCommand[Long]("LASTSAVE") {
18 | def params = ANil
19 | }
20 |
21 | case object Shutdown extends RedisCommand[Boolean]("SHUTDOWN") {
22 | def params = ANil
23 | }
24 |
25 | case object BGRewriteAOF extends RedisCommand[Boolean]("BGREWRITEAOF") {
26 | def params = ANil
27 | }
28 |
29 | case object Info extends RedisCommand[Option[String]]("INFO") {
30 | def params = ANil
31 | }
32 |
33 | case object Monitor extends RedisCommand[Boolean]("MONITOR") {
34 | def params = ANil
35 | }
36 |
37 | case class SlaveOf(node: Option[(String, Int)]) extends RedisCommand[Boolean]("SLAVEOF") {
38 | def params = node match {
39 | case Some((h: String, p: Int)) => h +: p +: ANil
40 | case _ => "NO" +: "ONE" +: ANil
41 | }
42 | }
43 |
44 |
45 | object Client {
46 |
47 | case object GetName extends RedisCommand[Option[String]]("CLIENT") {
48 | def params = "GETNAME" +: ANil
49 | }
50 |
51 | case class SetName(name: String) extends RedisCommand[Boolean]("CLIENT") {
52 | def params = "SETNAME" +: name +: ANil
53 | }
54 |
55 | case class Kill(ipPort: String) extends RedisCommand[Boolean]("CLIENT") {
56 | def params = "KILL" +: ipPort +: ANil
57 | }
58 |
59 | case object List extends RedisCommand[Option[String]]("CLIENT") {
60 | def params = "LIST" +: ANil
61 | }
62 |
63 | }
64 |
65 |
66 | object Config {
67 |
68 | case class Get[A: Reader](globStyleParam: String) extends RedisCommand[List[A]]("CONFIG") {
69 | def params = "GET" +: globStyleParam +: ANil
70 | }
71 |
72 | case class Set(param: String, value: Stringified) extends RedisCommand[Boolean]("CONFIG") {
73 | def params = "SET" +: param +: value +: ANil
74 | }
75 |
76 | case object ResetStat extends RedisCommand[Boolean]("CONFIG") {
77 | def params = "RESETSTAT" +: ANil
78 | }
79 |
80 | case object Rewrite extends RedisCommand[Boolean]("CONFIG") {
81 | def params = "REWRITE" +: ANil
82 | }
83 |
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/HashCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object HashCommands {
7 | import DefaultWriters._
8 |
9 | case class HSet(key: String, field: String, value: Stringified) extends RedisCommand[Boolean]("HSET") {
10 | def params = key +: field +: value +: ANil
11 | }
12 |
13 | case class HSetNx(key: String, field: String, value: Stringified) extends RedisCommand[Boolean]("HSETNX") {
14 | def params = key +: field +: value +: ANil
15 | }
16 |
17 | case class HGet[A: Reader](key: String, field: String) extends RedisCommand[Option[A]]("HGET") {
18 | def params = key +: field +: ANil
19 | }
20 |
21 | case class HMSet(key: String, mapLike: Iterable[KeyValuePair]) extends RedisCommand[Boolean]("HMSET") {
22 | def params = key +: mapLike.foldRight(ANil) { (x, acc) => x.key +: x.value +: acc }
23 | }
24 |
25 | case class HMGet[A: Reader](key: String, fields: Seq[String])
26 | extends RedisCommand[Map[String, A]]("HMGET")(PartialDeserializer.keyedMapPD(fields)) {
27 | require(fields.nonEmpty, "Fields should not be empty")
28 | def params = key +: fields.toArgs
29 | }
30 |
31 | object HMGet {
32 | def apply[A: Reader](key: String, field: String, fields: String*): HMGet[A] = HMGet[A](key, field +: fields)
33 | }
34 |
35 | case class HIncrby(key: String, field: String, value: Int) extends RedisCommand[Long]("HINCRBY") {
36 | def params = key +: field +: value +: ANil
37 | }
38 |
39 | case class HExists(key: String, field: String) extends RedisCommand[Boolean]("HEXISTS") {
40 | def params = key +: field +: ANil
41 | }
42 |
43 |
44 | case class HDel(key: String, fields: Seq[String]) extends RedisCommand[Long]("HDEL") {
45 | require(fields.nonEmpty, "Fields should not be empty")
46 | def params = key +: fields.toArgs
47 | }
48 |
49 | object HDel {
50 | def apply(key: String, field: String, fields: String*): HDel = HDel(key, field +: fields)
51 | }
52 |
53 |
54 | case class HLen(key: String) extends RedisCommand[Long]("HLEN") {
55 | def params = key +: ANil
56 | }
57 |
58 | case class HKeys(key: String) extends RedisCommand[List[String]]("HKEYS") {
59 | def params = key +: ANil
60 | }
61 |
62 | case class HVals[A: Reader](key: String) extends RedisCommand[List[A]]("HVALS") {
63 | def params = key +: ANil
64 | }
65 |
66 | case class HGetall[A: Reader](key: String) extends RedisCommand[Map[String, A]]("HGETALL") {
67 | def params = key +: ANil
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/ServerOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import serialization._
5 | import akka.pattern.ask
6 | import akka.util.Timeout
7 | import com.redis.protocol.ServerCommands
8 |
9 | trait ServerOperations { this: RedisOps =>
10 | import ServerCommands._
11 |
12 | // SAVE
13 | // save the DB on disk now.
14 | def save()(implicit timeout: Timeout) =
15 | clientRef.ask(Save).mapTo[Save.Ret]
16 |
17 | // BGSAVE
18 | // save the DB in the background.
19 | def bgsave()(implicit timeout: Timeout) =
20 | clientRef.ask(BgSave).mapTo[BgSave.Ret]
21 |
22 | // LASTSAVE
23 | // return the UNIX TIME of the last DB SAVE executed with success.
24 | def lastsave()(implicit timeout: Timeout) =
25 | clientRef.ask(LastSave).mapTo[LastSave.Ret]
26 |
27 | // SHUTDOWN
28 | // Stop all the clients, save the DB, then quit the server.
29 | def shutdown()(implicit timeout: Timeout) = clientRef.ask(Shutdown).mapTo[Shutdown.Ret]
30 |
31 | // BGREWRITEAOF
32 | def bgrewriteaof()(implicit timeout: Timeout) = clientRef.ask(BGRewriteAOF).mapTo[BGRewriteAOF.Ret]
33 |
34 | // INFO
35 | // the info command returns different information and statistics about the server.
36 | def info()(implicit timeout: Timeout) = clientRef.ask(Info).mapTo[Info.Ret]
37 |
38 | // MONITOR
39 | // is a debugging command that outputs the whole sequence of commands received by the Redis server.
40 | def monitor()(implicit timeout: Timeout) =
41 | clientRef.ask(Monitor).mapTo[Monitor.Ret]
42 |
43 | // SLAVEOF
44 | // The SLAVEOF command can change the replication settings of a slave on the fly.
45 | def slaveof(node: Option[(String, Int)])(implicit timeout: Timeout) =
46 | clientRef.ask(SlaveOf(node)).mapTo[SlaveOf#Ret]
47 |
48 |
49 | object client {
50 | import Client._
51 |
52 | def getname()(implicit timeout: Timeout) =
53 | clientRef.ask(GetName).mapTo[GetName.Ret]
54 |
55 | def setname(name: String)(implicit timeout: Timeout) =
56 | clientRef.ask(SetName(name)).mapTo[SetName#Ret]
57 |
58 | def kill(ipPort: String)(implicit timeout: Timeout) =
59 | clientRef.ask(Kill(ipPort)).mapTo[Kill#Ret]
60 |
61 | def list()(implicit timeout: Timeout) =
62 | clientRef.ask(List).mapTo[List.Ret]
63 | }
64 |
65 |
66 | object config {
67 | import Config._
68 |
69 | def get[A](param: String)(implicit timeout: Timeout, reader: Reader[A]) =
70 | clientRef.ask(Get[A](param)).mapTo[Get[A]#Ret]
71 |
72 | def set(param: String, value: Stringified)(implicit timeout: Timeout) =
73 | clientRef.ask(Set(param, value)).mapTo[Set#Ret]
74 |
75 | def resetstat()(implicit timeout: Timeout) =
76 | clientRef.ask(ResetStat).mapTo[ResetStat.Ret]
77 |
78 | def rewrite()(implicit timeout: Timeout) =
79 | clientRef.ask(Rewrite).mapTo[Rewrite.Ret]
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/HashOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import org.scalatest.junit.JUnitRunner
4 | import org.junit.runner.RunWith
5 | import com.redis.RedisSpecBase
6 |
7 |
8 | @RunWith(classOf[JUnitRunner])
9 | class HashOperationsSpec extends RedisSpecBase {
10 |
11 | describe("hset") {
12 | it("should set and get fields") {
13 | val key = "hset1"
14 | client.hset(key, "field1", "val")
15 | client.hget(key, "field1").futureValue should be(Some("val"))
16 | }
17 | }
18 |
19 | describe("hmget") {
20 | it("should set and get maps") {
21 | val key = "hmget1"
22 | client.hmset(key, Map("field1" -> "val1", "field2" -> "val2"))
23 |
24 | client
25 | .hmget(key, "field1")
26 | .futureValue should equal (Map("field1" -> "val1"))
27 |
28 | client
29 | .hmget(key, "field1", "field2")
30 | .futureValue should equal (Map("field1" -> "val1", "field2" -> "val2"))
31 |
32 | client
33 | .hmget(key, "field1", "field2", "field3")
34 | .futureValue should equal (Map("field1" -> "val1", "field2" -> "val2"))
35 | }
36 | }
37 |
38 | describe("hincrby") {
39 | it("should increment map values") {
40 | val key = "hincrby1"
41 | client.hincrby(key, "field1", 1).futureValue should equal (1)
42 | client.hget(key, "field1").futureValue should equal (Some("1"))
43 | }
44 | }
45 |
46 | describe("hexists") {
47 | it("should check existence") {
48 | val key = "hexists1"
49 | client.hset(key, "field1", "val").futureValue should be (true)
50 | client.hexists(key, "field1").futureValue should be (true)
51 | client.hexists(key, "field2").futureValue should be (false)
52 | }
53 | }
54 |
55 | describe("hdel") {
56 | it("should delete fields") {
57 | val key = "hdel1"
58 | client.hset(key, "field1", "val")
59 | client.hexists(key, "field1").futureValue should be (true)
60 | client.hdel(key, "field1").futureValue should equal (1)
61 | client.hexists(key, "field1").futureValue should be (false)
62 | val kvs = Map("field1" -> "val1", "field2" -> "val2")
63 | client.hmset(key, kvs)
64 | client.hdel(key, "field1", "field2").futureValue should equal (2)
65 | }
66 | }
67 |
68 | describe("hlen") {
69 | it("should turn the length of the fields") {
70 | val key = "hlen1"
71 | client.hmset(key, Map("field1" -> "val1", "field2" -> "val2"))
72 | client.hlen(key).futureValue should be (2)
73 | }
74 | }
75 |
76 | describe("hkeys, hvals and client.hgetall") {
77 | it("should turn the aggregates") {
78 | val key = "hash_aggregates"
79 | client.hmset(key, Map("field1" -> "val1", "field2" -> "val2"))
80 | client.hkeys(key).futureValue should equal (List("field1", "field2"))
81 | client.hvals(key).futureValue should equal (List("val1", "val2"))
82 | client.hgetall(key).futureValue should equal (Map("field1" -> "val1", "field2" -> "val2"))
83 | }
84 | }
85 |
86 | }
87 |
88 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/PubSubOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import akka.actor.ActorRef
4 | import akka.util.Timeout
5 | import com.redis.serialization.Stringified
6 |
7 | trait PubSubOperations { this: RedisOps =>
8 | import com.redis.protocol.PubSubCommands._
9 | import akka.pattern.ask
10 |
11 | /**
12 | * SUBSCRIBE
13 | * Subscribes the client to the specified channels.
14 | * Once the client enters the subscribed state it is not supposed to issue any other commands,
15 | * except for additional SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE and PUNSUBSCRIBE commands.
16 | *
17 | * Any command, except of QUIT, SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE and PUNSUBSCRIBE will fail
18 | * with [[com.redis.RedisConnection.CommandRejected]] cause.
19 | *
20 | * The actor passed in the listener parameter will receive messages from all channels
21 | * the underlying client is subscribed to. It means, every listeners registered with the
22 | * client will receive messages from all channels, no matter which combinations of
23 | * listeners and channels were passed to the call(s).
24 | *
25 | * The result of the command is not available directly. The listener will receive one or
26 | * more [[Subscribed]] events in the case of success.
27 | */
28 | def subscribe( listener: ActorRef, channels: Seq[String] )(implicit timeout: Timeout) = {
29 | clientRef.tell( Subscribe(channels), listener )
30 | }
31 |
32 | def subscribe( listener: ActorRef, channel: String, channels: String* )(implicit timeout: Timeout) = {
33 | clientRef.tell( Subscribe(channel, channels: _*), listener )
34 | }
35 |
36 | /**
37 | * UNSUBSCRIBE
38 | * Unsubscribes the client from the given channels, or from all of them if none is given.
39 | * When no channels are specified, the client is unsubscribed from all the previously subscribed channels.
40 | * In this case, a message for every unsubscribed channel will be sent to the client.
41 | *
42 | * The operation affects all listeners registered with the client. A result of the operation will be
43 | * reported to listeners via [[Unsubscribed]] messages.
44 | */
45 | def unsubscribe(channels: String* )(implicit timeout: Timeout) : Unit = {
46 | clientRef ! Unsubscribe(channels)
47 | }
48 |
49 | /**
50 | * PUBLISH
51 | * Posts a message to the given channel.
52 | */
53 | def publish(channel: String, message: Stringified)(implicit timeout: Timeout) = {
54 | clientRef.ask( Publish(channel, message) ).mapTo[Publish#Ret]
55 | }
56 |
57 | /**
58 | * PSUBSCRIBE
59 | * Subscribes the client to the given patterns.
60 | *
61 | * See [[subscribe()]] for more details.
62 | */
63 | def psubscribe(listener: ActorRef, patterns: Seq[String])(implicit timeout: Timeout) = {
64 | clientRef.tell( PSubscribe(patterns), listener )
65 | }
66 |
67 | def psubscribe( listener: ActorRef, pattern: String, patterns: String*)(implicit timeout: Timeout) = {
68 | clientRef.tell( PSubscribe(pattern, patterns:_*), listener )
69 | }
70 |
71 | /**
72 | * PUNSUBSCRIBE
73 | * Unsubscribes the client from the given patterns, or from all of them if none is given.
74 | */
75 | def punsubscribe( patterns: String*)(implicit timeout: Timeout) = {
76 | clientRef ! PUnsubscribe(patterns)
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/EvalOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import org.scalatest.junit.JUnitRunner
4 | import org.junit.runner.RunWith
5 | import com.redis.RedisSpecBase
6 |
7 |
8 | @RunWith(classOf[JUnitRunner])
9 | class EvalOperationsSpec extends RedisSpecBase {
10 |
11 | import com.redis.serialization.DefaultFormats._
12 |
13 | describe("eval") {
14 | it("should eval lua code and get a string reply") {
15 | client
16 | .eval("return 'val1';")
17 | .futureValue should equal (List("val1"))
18 | }
19 |
20 | it("should eval lua code and get a string array reply") {
21 | client
22 | .eval("return { 'val1','val2' };")
23 | .futureValue should equal (List("val1", "val2"))
24 | }
25 |
26 | it("should eval lua code and get a string array reply from its arguments") {
27 | client
28 | .eval("return { ARGV[1],ARGV[2] };", args = List("a", "b"))
29 | .futureValue should equal (List("a", "b"))
30 | }
31 |
32 | it("should eval lua code and get a string array reply from its arguments & keys") {
33 | client
34 | .eval("return { KEYS[1],KEYS[2],ARGV[1],ARGV[2] };", List("a", "b"), List("a", "b"))
35 | .futureValue should equal (List("a", "b", "a", "b"))
36 | }
37 |
38 | it("should eval lua code and get a string reply when passing keys") {
39 | client.set("a", "b")
40 |
41 | client
42 | .eval("return redis.call('get', KEYS[1]);", List("a"))
43 | .futureValue should equal (List("b"))
44 | }
45 |
46 | it("should eval lua code and get a string array reply when passing keys") {
47 | client.lpush("z", "a")
48 | client.lpush("z", "b")
49 |
50 | client
51 | .eval("return redis.call('lrange', KEYS[1], 0, 1);", keys = List("z"))
52 | .futureValue should equal (List("b", "a"))
53 | }
54 |
55 | it("should evalsha lua code hash and execute script when passing keys") {
56 | val setname = "records";
57 |
58 | val luaCode = """
59 | local res = redis.call('ZRANGEBYSCORE', KEYS[1], 0, 100, 'WITHSCORES')
60 | return res
61 | """
62 | val shahash = client.script.load(luaCode).futureValue
63 |
64 | client.zadd(setname, 10, "mmd")
65 | client.zadd(setname, 22, "mmc")
66 | client.zadd(setname, 12.5, "mma")
67 | client.zadd(setname, 14, "mem")
68 |
69 | val rs = client.evalsha(shahash.get, List("records")).futureValue
70 | rs should equal (List("mmd", "10", "mma", "12.5", "mem", "14", "mmc", "22"))
71 | }
72 |
73 | it("should check if script exists when passing its sha hash code") {
74 | val luaCode = """
75 | local res = redis.call('ZRANGEBYSCORE', KEYS[1], 0, 100, 'WITHSCORES')
76 | return res
77 | """
78 | val shahash = client.script.load(luaCode).futureValue
79 |
80 | val rs = client.script.exists(shahash.get).futureValue
81 | rs should equal (List(1))
82 | }
83 |
84 | it("should remove script cache") {
85 | val luaCode = """
86 | local res = redis.call('ZRANGEBYSCORE', KEYS[1], 0, 100, 'WITHSCORES')
87 | return res
88 | """
89 | val shahash = client.script.load(luaCode).futureValue
90 |
91 | client.script.flush.futureValue should be (true)
92 |
93 | client.script.exists(shahash.get).futureValue should equal (List(0))
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/RedisClientSettings.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 |
3 | import java.lang.{Long => JLong}
4 |
5 | import RedisClientSettings._
6 |
7 | case class RedisClientSettings(
8 | backpressureBufferSettings: Option[BackpressureBufferSettings] = None,
9 | reconnectionSettings: ReconnectionSettings = NoReconnectionSettings
10 | )
11 |
12 | object RedisClientSettings {
13 |
14 | case class BackpressureBufferSettings(lowBytes: Long, highBytes: Long, maxBytes: Long) {
15 | require(lowBytes >= 0, "lowWatermark needs to be non-negative")
16 | require(highBytes >= lowBytes, "highWatermark needs to be at least as large as lowWatermark")
17 | require(maxBytes >= highBytes, "maxCapacity needs to be at least as large as highWatermark")
18 | }
19 |
20 | object BackpressureBufferSettings {
21 | val default = BackpressureBufferSettings(lowBytes = 100, highBytes = 3000, maxBytes = 5000)
22 | }
23 |
24 | trait ReconnectionSettings {
25 | def newSchedule: ReconnectionSchedule
26 |
27 | trait ReconnectionSchedule {
28 | val maxAttempts: Long
29 | var attempts: Long = 0
30 |
31 | /**
32 | * Gets the number of milliseconds until the next reconnection attempt.
33 | *
34 | * This method is expected to increment attempts like an iterator
35 | *
36 | * @return milliseconds until the next attempt
37 | */
38 | def nextDelayMs: Long
39 | }
40 | }
41 |
42 | case object NoReconnectionSettings extends ReconnectionSettings{
43 | def newSchedule: ReconnectionSchedule = new ReconnectionSchedule {
44 | val maxAttempts: Long = 0
45 | def nextDelayMs: Long = throw new NoSuchElementException("No delay available")
46 | }
47 | }
48 |
49 | case class ConstantReconnectionSettings(constantDelayMs: Long, maximumAttempts: Long = Long.MaxValue) extends ReconnectionSettings {
50 | require(constantDelayMs >= 0, s"Invalid negative reconnection delay (received $constantDelayMs)")
51 | require(maximumAttempts >= 0, s"Invalid negative maximum attempts (received $maximumAttempts)")
52 |
53 | def newSchedule: ReconnectionSchedule = new ConstantSchedule
54 |
55 | class ConstantSchedule extends ReconnectionSchedule {
56 | val maxAttempts = maximumAttempts
57 | def nextDelayMs = {
58 | attempts += 1
59 | constantDelayMs
60 | }
61 | }
62 | }
63 |
64 | case class ExponentialReconnectionSettings(baseDelayMs: Long, maxDelayMs: Long, maximumAttempts: Long = Long.MaxValue) extends ReconnectionSettings {
65 | require(baseDelayMs > 0, s"Base reconnection delay must be greater than 0. Received $baseDelayMs")
66 | require(maxDelayMs > 0, s"Maximum reconnection delay must be greater than 0. Received $maxDelayMs")
67 | require(maxDelayMs >= baseDelayMs, "Maximum reconnection delay cannot be smaller than base reconnection delay")
68 |
69 | def newSchedule = new ExponentialSchedule
70 |
71 | private val ceil = if ((baseDelayMs & (baseDelayMs - 1)) == 0) 0 else 1
72 | private val attemptCeiling = JLong.SIZE - JLong.numberOfLeadingZeros(Long.MaxValue / baseDelayMs) - ceil
73 |
74 | class ExponentialSchedule extends ReconnectionSchedule {
75 | val maxAttempts = maximumAttempts
76 | def nextDelayMs = {
77 | attempts += 1
78 | if (attempts > attemptCeiling) {
79 | maxDelayMs
80 | } else {
81 | val factor = 1L << (attempts - 1)
82 | Math.min(baseDelayMs * factor, maxDelayMs)
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/PubSubOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import akka.util.ByteString
4 | import com.redis.RedisConnection.CommandRejected
5 | import com.redis.{RedisClient, RedisSpecBase}
6 |
7 | import scala.concurrent.Await
8 |
9 | class PubSubOperationsSpec extends RedisSpecBase {
10 | import com.redis.protocol.PubSubCommands._
11 |
12 | import scala.concurrent.duration._
13 |
14 | def withClientPerTest( testCode : RedisClient => Any ): Unit = {
15 | val redisClient = RedisClient("localhost", 6379)
16 | try { testCode(redisClient) } finally redisClient.quit()
17 | }
18 |
19 | def sendReceive(channel: String, message: String): Unit = {
20 | client.publish(channel, message).futureValue should equal(1)
21 | val msg = expectMsgType[Message]
22 | msg.payload should equal( ByteString(message) )
23 | }
24 |
25 | def psendReceive(channel: String, message: String): Unit = {
26 | client.publish(channel, message).futureValue should equal(1)
27 | val msg = expectMsgType[PMessage]
28 | msg.payload should equal( ByteString(message) )
29 | }
30 |
31 | describe("Pub/Sub") {
32 |
33 | it("should subscribe to some channels and deliver subscription notifications") {
34 | withClientPerTest { subClient =>
35 | subClient.subscribe(testActor, "first", "second")
36 | expectMsgAllOf( Subscribed("first", false, 1), Subscribed("second", false, 2) )
37 | }
38 | }
39 |
40 | it("accepts only QUIT, SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE and PUNSUBSCRIBE while in the subscribed state") {
41 | withClientPerTest { subClient =>
42 | subClient.subscribe( testActor, "first" )
43 | expectMsgType[Subscribed]
44 | val future = subClient.set("key", "value")
45 | Await.result( future.failed, 1.second ) shouldBe a [CommandRejected]
46 | }
47 | }
48 |
49 | it("subscription can be canceled") {
50 | withClientPerTest { subClient =>
51 | subClient.subscribe(testActor, "first", "second")
52 | expectMsgType[Subscribed]
53 | expectMsgType[Subscribed]
54 |
55 | sendReceive( "first", "1" )
56 |
57 | subClient.unsubscribe("second")
58 | expectMsgType[Unsubscribed]
59 | sendReceive("first", "2")
60 | client.publish("second", "3").futureValue should equal(0)
61 | expectNoMsg()
62 | }
63 | }
64 |
65 | it("Should be able publish messages") {
66 | withClientPerTest { subClient =>
67 | subClient.clientRef tell (Subscribe("first"), testActor)
68 | expectMsg( Subscribed("first", false, 1) )
69 |
70 | sendReceive("first", "something")
71 | }
72 | }
73 |
74 | it("allows to subscribe to the further channels") {
75 | withClientPerTest { subClient =>
76 | subClient.subscribe( testActor, "1")
77 | expectMsgType[Subscribed]
78 | client.publish("2", "msg").futureValue should equal(0)
79 | expectNoMsg(1.second)
80 | subClient.subscribe( testActor, "2" )
81 | expectMsgType[Subscribed]
82 | sendReceive("2", "msg")
83 | }
84 | }
85 |
86 | it("allows to subscribe and unsubscribe using patterns") {
87 | withClientPerTest { subClient =>
88 | val pattern = "h?llo"
89 | subClient.psubscribe( testActor, pattern)
90 | expectMsgType[Subscribed]
91 |
92 | psendReceive("hello", "hellomsg")
93 | psendReceive("hallo", "hallomsg")
94 |
95 | subClient.punsubscribe( "hell?" )
96 | client.publish("hillo", "hellomsg")
97 | client.publish("hallo", "hallomsg")
98 | expectMsgType[Unsubscribed]
99 | // still receive messages
100 | expectMsgType[PMessage]
101 | expectMsgType[PMessage]
102 |
103 | subClient.punsubscribe(pattern)
104 | expectMsgType[Unsubscribed]
105 | client.publish("hillo", "hellomsg")
106 | client.publish("hallo", "hallomsg")
107 | expectNoMsg()
108 |
109 | }
110 | }
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/SetCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object SetCommands {
7 | import DefaultWriters._
8 |
9 | case class SAdd(key: String, values: Seq[Stringified]) extends RedisCommand[Long]("SADD") {
10 | require(values.nonEmpty, "Values should not be empty")
11 | def params = key +: values.toArgs
12 | }
13 |
14 | object SAdd {
15 | def apply(key: String, value: Stringified, values: Stringified*): SAdd = SAdd(key, value +: values)
16 | }
17 |
18 |
19 | case class SRem(key: String, values: Seq[Stringified]) extends RedisCommand[Long]("SREM") {
20 | require(values.nonEmpty, "Values should not be empty")
21 | def params = key +: values.toArgs
22 | }
23 |
24 | object SRem {
25 | def apply(key: String, value: Stringified, values: Stringified*): SRem = SRem(key, value +: values)
26 | }
27 |
28 |
29 | case class SPop[A: Reader](key: String) extends RedisCommand[Option[A]]("SPOP") {
30 | def params = key +: ANil
31 | }
32 |
33 | case class SMove(srcKey: String, destKey: String, value: Stringified) extends RedisCommand[Long]("SMOVE") {
34 | def params = srcKey +: destKey +: value +: ANil
35 | }
36 |
37 | case class SCard(key: String) extends RedisCommand[Long]("SCARD") {
38 | def params = key +: ANil
39 | }
40 |
41 | case class SIsMember(key: String, value: Stringified) extends RedisCommand[Boolean]("SISMEMBER") {
42 | def params = key +: value +: ANil
43 | }
44 |
45 |
46 | case class SInter[A: Reader](keys: Seq[String]) extends RedisCommand[Set[A]]("SINTER") {
47 | require(keys.nonEmpty, "Keys should not be empty")
48 | def params = keys.toArgs
49 | }
50 |
51 | object SInter {
52 | def apply[A: Reader](key: String, keys: String*): SInter[A] = SInter(key +: keys)
53 | }
54 |
55 |
56 | case class SUnion[A: Reader](keys: Seq[String]) extends RedisCommand[Set[A]]("SUNION") {
57 | require(keys.nonEmpty, "Keys should not be empty")
58 | def params = keys.toArgs
59 | }
60 |
61 | object SUnion {
62 | def apply[A: Reader](key: String, keys: String*): SUnion[A] = SUnion(key +: keys)
63 | }
64 |
65 |
66 | case class SDiff[A: Reader](keys: Seq[String]) extends RedisCommand[Set[A]]("SDIFF") {
67 | require(keys.nonEmpty, "Keys should not be empty")
68 | def params = keys.toArgs
69 | }
70 |
71 | object SDiff {
72 | def apply[A: Reader](key: String, keys: String*): SDiff[A] = SDiff(key +: keys)
73 | }
74 |
75 |
76 | case class SInterStore(destKey: String, keys: Seq[String]) extends RedisCommand[Long]("SINTERSTORE") {
77 | require(keys.nonEmpty, "Keys should not be empty")
78 | def params = destKey +: keys.toArgs
79 | }
80 |
81 | object SInterStore {
82 | def apply(destKey: String, key: String, keys: String*): SInterStore =
83 | SInterStore(destKey, key +: keys)
84 | }
85 |
86 |
87 | case class SUnionStore(destKey: String, keys: Seq[String]) extends RedisCommand[Long]("SUNIONSTORE") {
88 | require(keys.nonEmpty, "Keys should not be empty")
89 | def params = destKey +: keys.toArgs
90 | }
91 |
92 | object SUnionStore {
93 | def apply(destKey: String, key: String, keys: String*): SUnionStore =
94 | SUnionStore(destKey, key +: keys)
95 | }
96 |
97 |
98 | case class SDiffStore(destKey: String, keys: Seq[String]) extends RedisCommand[Long]("SDIFFSTORE") {
99 | require(keys.nonEmpty, "Keys should not be empty")
100 | def params = destKey +: keys.toArgs
101 | }
102 |
103 | object SDiffStore {
104 | def apply(destKey: String, key: String, keys: String*): SDiffStore =
105 | SDiffStore(destKey, key +: keys)
106 | }
107 |
108 |
109 | case class SMembers[A: Reader](key: String) extends RedisCommand[Set[A]]("SMEMBERS") {
110 | def params = key +: ANil
111 | }
112 |
113 | case class SRandMember[A: Reader](key: String) extends RedisCommand[Option[A]]("SRANDMEMBER") {
114 | def params = key +: ANil
115 | }
116 |
117 | case class SRandMembers[A: Reader](key: String, count: Int) extends RedisCommand[List[A]]("SRANDMEMBER") {
118 | def params = key +: count +: ANil
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/ListCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object ListCommands {
7 | import DefaultWriters._
8 |
9 | case class LPush(key: String, values: Seq[Stringified]) extends RedisCommand[Long]("LPUSH") {
10 | require(values.nonEmpty, "Values should not be empty")
11 | def params = key +: values.toArgs
12 | }
13 |
14 | object LPush {
15 | def apply(key: String, value: Stringified, values: Stringified*): LPush = LPush(key, value +: values)
16 | }
17 |
18 |
19 | case class LPushX(key: String, value: Stringified) extends RedisCommand[Long]("LPUSHX") {
20 | def params = key +: value +: ANil
21 | }
22 |
23 |
24 | case class RPush(key: String, values: Seq[Stringified]) extends RedisCommand[Long]("RPUSH") {
25 | require(values.nonEmpty, "Values should not be empty")
26 | def params = key +: values.toArgs
27 | }
28 |
29 | object RPush {
30 | def apply(key: String, value: Stringified, values: Stringified*): RPush = RPush(key, value +: values)
31 | }
32 |
33 |
34 | case class RPushX(key: String, value: Stringified) extends RedisCommand[Long]("RPUSHX") {
35 | def params = key +: value +: ANil
36 | }
37 |
38 | case class LRange[A: Reader](key: String, start: Int, stop: Int) extends RedisCommand[List[A]]("LRANGE") {
39 | def params = key +: start +: stop +: ANil
40 | }
41 |
42 | case class LLen(key: String) extends RedisCommand[Long]("LLEN") {
43 | def params = key +: ANil
44 | }
45 |
46 | case class LTrim(key: String, start: Int, end: Int) extends RedisCommand[Boolean]("LTRIM") {
47 | def params = key +: start +: end +: ANil
48 | }
49 |
50 | case class LIndex[A: Reader](key: String, index: Int) extends RedisCommand[Option[A]]("LINDEX") {
51 | def params = key +: index +: ANil
52 | }
53 |
54 | case class LSet(key: String, index: Int, value: Stringified) extends RedisCommand[Boolean]("LSET") {
55 | def params = key +: index +: value +: ANil
56 | }
57 |
58 | case class LRem(key: String, count: Int, value: Stringified) extends RedisCommand[Long]("LREM") {
59 | def params = key +: count +: value +: ANil
60 | }
61 |
62 | case class LPop[A: Reader](key: String) extends RedisCommand[Option[A]]("LPOP") {
63 | def params = key +: ANil
64 | }
65 |
66 | case class RPop[A: Reader](key: String) extends RedisCommand[Option[A]]("RPOP") {
67 | def params = key +: ANil
68 | }
69 |
70 | case class RPopLPush[A: Reader](srcKey: String, dstKey: String) extends RedisCommand[Option[A]]("RPOPLPUSH") {
71 |
72 | def params = srcKey +: dstKey +: ANil
73 | }
74 |
75 | case class BRPopLPush[A: Reader](srcKey: String, dstKey: String, timeoutInSeconds: Int)
76 | extends RedisCommand[Option[A]]("BRPOPLPUSH") {
77 |
78 | def params = srcKey +: dstKey +: timeoutInSeconds +: ANil
79 | }
80 |
81 | case class BLPop[A: Reader](timeoutInSeconds: Int, keys: Seq[String]) extends RedisCommand[Option[(String, A)]]("BLPOP") {
82 |
83 | require(keys.nonEmpty, "Keys should not be empty")
84 | def params = keys.toArgs :+ timeoutInSeconds
85 | }
86 |
87 | object BLPop {
88 | def apply[A](timeoutInSeconds: Int, key: String, keys: String*)(implicit reader: Reader[A]): BLPop[A] =
89 | BLPop(timeoutInSeconds, key +: keys)
90 | }
91 |
92 |
93 | case class BRPop[A: Reader](timeoutInSeconds: Int, keys: Seq[String])
94 | extends RedisCommand[Option[(String, A)]]("BRPOP") {
95 | require(keys.nonEmpty, "Keys should not be empty")
96 | def params = keys.toArgs :+ timeoutInSeconds
97 | }
98 |
99 | object BRPop {
100 | def apply[A](timeoutInSeconds: Int, key: String, keys: String*)(implicit reader: Reader[A]): BRPop[A] =
101 | BRPop(timeoutInSeconds, key +: keys)
102 | }
103 |
104 | case class LInsert(key: String, position: InsertPosition, pivot: Stringified, value: Stringified) extends RedisCommand[Long]("LINSERT") {
105 | def params = key +: position.label +: pivot +: value +: ANil
106 | }
107 |
108 | sealed abstract class InsertPosition(val label: String)
109 | case object Before extends InsertPosition("BEFORE")
110 | case object After extends InsertPosition("AFTER")
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/KeyCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object KeyCommands {
7 | import DefaultWriters._
8 |
9 | case class Keys(pattern: String = "*") extends RedisCommand[List[String]]("KEYS") {
10 | def params = pattern +: ANil
11 | }
12 |
13 | case object RandomKey extends RedisCommand[Option[String]]("RANDOMKEY") {
14 | def params = ANil
15 | }
16 |
17 | case class Rename(oldKey: String, newKey: String) extends RedisCommand[Boolean]("RENAME") {
18 | def params = oldKey +: newKey +: ANil
19 | }
20 |
21 | case class RenameNx(oldKey: String, newKey: String) extends RedisCommand[Boolean]("RENAMENX") {
22 | def params = oldKey +: newKey +: ANil
23 | }
24 |
25 | case object DBSize extends RedisCommand[Long]("DBSIZE") {
26 | def params = ANil
27 | }
28 |
29 | case class Exists(key: String) extends RedisCommand[Boolean]("EXISTS") {
30 | def params = key +: ANil
31 | }
32 |
33 |
34 | case class Del(keys: Seq[String]) extends RedisCommand[Long]("DEL") {
35 | require(keys.nonEmpty, "Keys should not be empty")
36 | def params = keys.toArgs
37 | }
38 |
39 | object Del {
40 | def apply(key: String, keys: String*): Del = Del(key +: keys)
41 | }
42 |
43 |
44 | case class Type(key: String) extends RedisCommand[String]("TYPE") {
45 | def params = key +: ANil
46 | }
47 |
48 | case class Expire(key: String, ttl: Int) extends RedisCommand[Boolean]("EXPIRE") {
49 | def params = key +: ttl +: ANil
50 | }
51 |
52 | case class PExpire(key: String, ttl: Int) extends RedisCommand[Boolean]("PEXPIRE") {
53 | def params = key +: ttl +: ANil
54 | }
55 |
56 | case class ExpireAt(key: String, timestamp: Long) extends RedisCommand[Boolean]("EXPIREAT") {
57 | def params = key +: timestamp +: ANil
58 | }
59 |
60 | case class PExpireAt(key: String, timestamp: Long) extends RedisCommand[Boolean]("PEXPIREAT") {
61 | def params = key +: timestamp +: ANil
62 | }
63 |
64 | case class TTL(key: String) extends RedisCommand[Long]("TTL") {
65 | def params = key +: ANil
66 | }
67 |
68 | case class PTTL(key: String) extends RedisCommand[Long]("PTTL") {
69 | def params = key +: ANil
70 | }
71 |
72 | case object FlushDB extends RedisCommand[Boolean]("FLUSHDB") {
73 | def params = ANil
74 | }
75 |
76 | case object FlushAll extends RedisCommand[Boolean]("FLUSHALL") {
77 | def params = ANil
78 | }
79 |
80 | case class Move(key: String, db: Int) extends RedisCommand[Boolean]("MOVE") {
81 | def params = key +: db +: ANil
82 | }
83 |
84 | case class Persist(key: String) extends RedisCommand[Boolean]("PERSIST") {
85 | def params = key +: ANil
86 | }
87 |
88 | case class Sort[A: Reader](key: String,
89 | limit: Option[Tuple2[Int, Int]] = None,
90 | desc: Boolean = false,
91 | alpha: Boolean = false,
92 | by: Option[String] = None,
93 | get: Seq[String] = Nil) extends RedisCommand[List[A]]("SORT") {
94 |
95 | def params = makeSortArgs(key, limit, desc, alpha, by, get)
96 | }
97 |
98 | case class SortNStore(key: String,
99 | limit: Option[Tuple2[Int, Int]] = None,
100 | desc: Boolean = false,
101 | alpha: Boolean = false,
102 | by: Option[String] = None,
103 | get: Seq[String] = Nil,
104 | storeAt: String) extends RedisCommand[Long]("SORT") {
105 |
106 | def params = makeSortArgs(key, limit, desc, alpha, by, get) ++ Seq("STORE", storeAt)
107 | }
108 |
109 | private def makeSortArgs(key: String,
110 | limit: Option[Tuple2[Int, Int]] = None,
111 | desc: Boolean = false,
112 | alpha: Boolean = false,
113 | by: Option[String] = None,
114 | get: Seq[String] = Nil): Args = {
115 |
116 | Seq(Seq(key)
117 | , limit.fold(Seq.empty[String]) { case (from, to) => "LIMIT" +: Seq(from, to).map(_.toString) }
118 | , if (desc) Seq("DESC") else Nil
119 | , if (alpha) Seq("ALPHA") else Nil
120 | , by.fold(Seq.empty[String])("BY" +: _ +: Nil)
121 | , get.map("GET" +: _ +: Nil).flatten
122 | ).flatten.toArgs
123 | }
124 |
125 | case class Select(index: Int) extends RedisCommand[Boolean]("SELECT") {
126 | def params = index +: ANil
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/serialization/Format.scala:
--------------------------------------------------------------------------------
1 | package com.redis.serialization
2 |
3 | import akka.util.{ByteString, CompactByteString}
4 | import scala.annotation.implicitNotFound
5 | import scala.language.implicitConversions
6 |
7 |
8 | @implicitNotFound(msg = "Cannot find implicit Read or Format type class for ${A}")
9 | trait Reader[A] { self =>
10 | def fromByteString(in: ByteString): A
11 |
12 | def map[B](f: A => B): Reader[B] =
13 | new Reader[B] {
14 | def fromByteString(in: ByteString) =
15 | f(self.fromByteString(in))
16 | }
17 | }
18 |
19 | trait ReaderLowPriorityImplicits {
20 | implicit object bypassingReader extends Reader[ByteString] {
21 | def fromByteString(in: ByteString) = in
22 | }
23 |
24 | implicit object byteArrayReader extends Reader[Array[Byte]] {
25 | def fromByteString(in: ByteString) = in.toArray[Byte]
26 | }
27 | }
28 |
29 | object Reader extends ReaderLowPriorityImplicits {
30 | implicit def default: Reader[String] = DefaultFormats.stringFormat
31 | }
32 |
33 |
34 | @implicitNotFound(msg = "Cannot find implicit Write or Format type class for ${A}")
35 | trait Writer[A] { self =>
36 | def toByteString(in: A): ByteString
37 |
38 | def contramap[B](f: B => A): Writer[B] =
39 | new Writer[B] {
40 | def toByteString(in: B) =
41 | self.toByteString(f(in))
42 | }
43 | }
44 |
45 | trait WriterLowPriorityImplicits {
46 | implicit object bypassingWriter extends Writer[ByteString] {
47 | def toByteString(in: ByteString) = in
48 | }
49 |
50 | implicit object byteArrayWriter extends Writer[Array[Byte]] {
51 | def toByteString(in: Array[Byte]) = CompactByteString(in)
52 | }
53 | }
54 |
55 | object Writer extends WriterLowPriorityImplicits {
56 | implicit def default: Writer[String] = DefaultFormats.stringFormat
57 | }
58 |
59 |
60 |
61 | trait StringReader[A] extends Reader[A] { self =>
62 | def read(in: String): A
63 |
64 | def fromByteString(in: ByteString): A = read(in.utf8String)
65 |
66 | override def map[B](f: A => B): StringReader[B] =
67 | new StringReader[B] {
68 | def read(in: String) =
69 | f(self.read(in))
70 | }
71 | }
72 |
73 | object StringReader {
74 | def apply[A](f: String => A) = new StringReader[A] { def read(in: String) = f(in) }
75 | }
76 |
77 | trait DefaultReaders {
78 | import java.{lang => J}
79 | implicit val intReader = StringReader[Int] (J.Integer.parseInt)
80 | implicit val shortReader = StringReader[Short] (J.Short.parseShort)
81 | implicit val longReader = StringReader[Long] (J.Long.parseLong)
82 | implicit val floatReader = StringReader[Float] (J.Float.parseFloat)
83 | implicit val doubleReader = StringReader[Double](J.Double.parseDouble)
84 | implicit val anyReader = StringReader[Any] (identity)
85 | }
86 | object DefaultReaders extends DefaultReaders
87 |
88 |
89 | trait StringWriter[A] extends Writer[A] { self =>
90 | def write(in: A): String
91 |
92 | def toByteString(in: A): ByteString = ByteString(write(in))
93 |
94 | override def contramap[B](f: B => A): StringWriter[B] =
95 | new StringWriter[B] {
96 | def write(in: B) =
97 | self.write(f(in))
98 | }
99 | }
100 |
101 | object StringWriter {
102 | def apply[A](f: A => String) = new StringWriter[A] { def write(in: A) = f(in) }
103 | }
104 |
105 | trait DefaultWriters {
106 | implicit val intWriter = StringWriter[Int] (_.toString)
107 | implicit val shortWriter = StringWriter[Short] (_.toString)
108 | implicit val longWriter = StringWriter[Long] (_.toString)
109 | implicit val floatWriter = StringWriter[Float] (_.toString)
110 | implicit val doubleWriter = StringWriter[Double](_.toString)
111 | implicit val anyWriter = StringWriter[Any] (_.toString)
112 | }
113 | object DefaultWriters extends DefaultWriters
114 |
115 |
116 | trait Format[A] extends StringReader[A] with StringWriter[A]
117 |
118 | object Format {
119 |
120 | def apply[A](_read: String => A, _write: A => String) = new Format[A] {
121 | def read(str: String) = _read(str)
122 |
123 | def write(obj: A) = _write(obj)
124 | }
125 |
126 | implicit def default = DefaultFormats.stringFormat
127 | }
128 |
129 |
130 | private[serialization] trait LowPriorityFormats extends DefaultReaders with DefaultWriters
131 |
132 | trait DefaultFormats extends LowPriorityFormats {
133 | implicit val stringFormat = Format[String](identity, identity)
134 | }
135 |
136 | object DefaultFormats extends DefaultFormats
137 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/ListOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import scala.concurrent.Future
4 |
5 | import org.scalatest.junit.JUnitRunner
6 | import org.junit.runner.RunWith
7 |
8 | import com.redis.RedisSpecBase
9 | import com.redis.protocol.ListCommands.{After, Before}
10 |
11 |
12 | @RunWith(classOf[JUnitRunner])
13 | class ListOperationsSpec extends RedisSpecBase {
14 |
15 | describe("lpush") {
16 | it("should do an lpush and retrieve the values using lrange") {
17 | val forpush = List.fill(10)("listk") zip (1 to 10).map(_.toString)
18 | val writeList = forpush map { case (key, value) => client.lpush(key, value) }
19 | val writeListResults = Future.sequence(writeList).futureValue
20 |
21 | writeListResults foreach {
22 | case someLong: Long if someLong > 0 =>
23 | someLong should (be > (0L) and be <= (10L))
24 | case _ => fail("lpush must return a positive number")
25 | }
26 | writeListResults should equal (1 to 10)
27 |
28 | // do an lrange to check if they were inserted correctly & in proper order
29 | val readList = client.lrange[String]("listk", 0, -1)
30 | readList.futureValue should equal ((1 to 10).reverse.map(_.toString))
31 | }
32 | }
33 |
34 | describe("rpush") {
35 | it("should do an rpush and retrieve the values using lrange") {
36 | val key = "listr"
37 | val forrpush = List.fill(10)(key) zip (1 to 10).map(_.toString)
38 | val writeList = forrpush map { case (key, value) => client.rpush(key, value) }
39 | val writeListResults = Future.sequence(writeList).futureValue
40 | writeListResults should equal (1 to 10)
41 |
42 | // do an lrange to check if they were inserted correctly & in proper order
43 | val readList = client.lrange[String](key, 0, -1)
44 | readList.futureValue.reverse should equal ((1 to 10).reverse.map(_.toString))
45 | }
46 | }
47 |
48 | import com.redis.serialization.DefaultFormats._
49 |
50 | describe("lrange") {
51 | it("should get the elements at specified offsets") {
52 | val key = "listlr1"
53 | val forrpush = List.fill(10)(key) zip (1 to 10).map(_.toString)
54 | val writeList = forrpush map { case (key, value) => client.rpush(key, value) }
55 | val writeListRes = Future.sequence(writeList).futureValue
56 |
57 | val readList = client.lrange[Int](key, 3, 5)
58 | readList.futureValue should equal (4 to 6)
59 | }
60 |
61 | it("should give an empty list when given key does not exist") {
62 | val readList = client.lrange[Int]("list_not_existing_key", 3, 5)
63 | readList.futureValue should equal (Nil)
64 | }
65 | }
66 |
67 | describe("linsert") {
68 | it("should insert the value before pivot") {
69 | val key = "linsert1"
70 | client.rpush(key, "foo", "bar", "baz").futureValue should equal (3)
71 | client.linsert(key, Before, "bar", "mofu").futureValue should equal (4)
72 | client.lrange[String](key, 0, -1).futureValue should equal ("foo" :: "mofu" :: "bar" :: "baz" :: Nil)
73 | }
74 |
75 | it("should insert the value after pivot") {
76 | val key = "linsert2"
77 | client.rpush(key, "foo", "bar", "baz").futureValue should equal (3)
78 | client.linsert(key, After, "bar", "mofu").futureValue should equal (4)
79 | client.lrange[String](key, 0, -1).futureValue should equal ("foo" :: "bar" :: "mofu" :: "baz" :: Nil)
80 | }
81 |
82 | it("should not insert any value and return -1 when pivot is not found") {
83 | val key = "linsert3"
84 | client.rpush(key, "foo", "bar", "baz").futureValue should equal (3)
85 | client.linsert(key, Before, "mofu", "value").futureValue should equal (-1)
86 | client.lrange[String](key, 0, -1).futureValue should equal ("foo" :: "bar" :: "baz" :: Nil)
87 | }
88 |
89 | it("should not insert any value and return 0 when key does not exist") {
90 | val key = "linsert4"
91 | client.linsert(key, Before, "mofu", "value").futureValue should equal (0)
92 | client.lrange[String](key, 0, -1).futureValue should equal (Nil)
93 | }
94 |
95 | it("should fail if the key points to a non-list") {
96 | val key = "linsert5"
97 | client.set(key, "string").futureValue should equal (true)
98 | val thrown = intercept[Exception] { client.linsert(key, Before, "mofu", "value").futureValue }
99 | thrown.getCause.getMessage should include ("Operation against a key holding the wrong kind of value")
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/serialization/PartialDeserializer.scala:
--------------------------------------------------------------------------------
1 | package com.redis.serialization
2 |
3 | import akka.util.ByteString
4 | import scala.collection.generic.CanBuildFrom
5 | import scala.collection.{Iterator, GenTraversable}
6 | import scala.language.higherKinds
7 | import scala.annotation.implicitNotFound
8 | import com.redis.protocol._
9 | import RawReplyParser._
10 |
11 |
12 | @implicitNotFound(msg = "Cannot find implicit PartialDeserializer for ${A}")
13 | trait PartialDeserializer[A] extends PartialFunction[RawReply, A] {
14 | def orElse(pf: PartialFunction[RawReply, A]) = PartialDeserializer(super.orElse(pf))
15 |
16 | override def andThen[B](f: A => B): PartialDeserializer[B] = PartialDeserializer(super.andThen(f))
17 | }
18 |
19 | object PartialDeserializer extends LowPriorityPD {
20 |
21 | def apply[A](pf: PartialFunction[RawReply, A]): PartialDeserializer[A] =
22 | new PartialDeserializer[A] {
23 | def isDefinedAt(x: RawReply) = pf.isDefinedAt(x)
24 | def apply(x: RawReply) = pf.apply(x)
25 | }
26 |
27 | import PrefixDeserializer._
28 |
29 | implicit val intPD = _intPD
30 | implicit val longPD = _longPD
31 | implicit val bulkPD = _rawBulkPD
32 | implicit val booleanPD = _booleanPD
33 | implicit val byteStringPD = bulkPD.andThen {
34 | _.getOrElse { throw new Error("Non-empty bulk reply expected, but got nil") }
35 | } orElse _statusStringPD
36 |
37 | implicit def multiBulkPD[A, B[_] <: GenTraversable[_]](implicit cbf: CanBuildFrom[_, A, B[A]], pd: PartialDeserializer[A]) = _multiBulkPD(cbf, pd)
38 | implicit def listPD[A](implicit pd: PartialDeserializer[A]) = multiBulkPD[A, List]
39 |
40 | val errorPD = _errorPD
41 |
42 | implicit val pubSubMessagePD = _pubSubMessagePD
43 |
44 | // Is needed to implement Pub/Sub messages, since they have no direct result.
45 | implicit val UnitDeserializer = new PartialDeserializer[Unit] {
46 | override def isDefinedAt(x: RawReply): Boolean = true
47 |
48 | override def apply(v1: RawReply): Unit = ()
49 | }
50 | }
51 |
52 | private[serialization] trait LowPriorityPD extends CommandSpecificPD {
53 | import PartialDeserializer._
54 |
55 | implicit def parsedPD[A](implicit reader: Reader[A]): PartialDeserializer[A] =
56 | byteStringPD andThen reader.fromByteString
57 |
58 | implicit def parsedOptionPD[A](implicit reader: Reader[A]): PartialDeserializer[Option[A]] =
59 | bulkPD andThen (_ map reader.fromByteString)
60 |
61 | implicit def setPD[A](implicit parse: Reader[A]): PartialDeserializer[Set[A]] =
62 | multiBulkPD[A, Set]
63 |
64 | implicit def pairOptionPD[A, B](implicit parseA: Reader[A], parseB: Reader[B]): PartialDeserializer[Option[(A, B)]] =
65 | pairIteratorPD[A, B] andThen {
66 | case iterator if iterator.hasNext => Some(iterator.next())
67 | case _ => None
68 | }
69 |
70 | implicit def mapPD[K, V](implicit parseA: Reader[K], parseB: Reader[V]): PartialDeserializer[Map[K, V]] =
71 | pairIteratorPD[K, V] andThen (_.toMap)
72 |
73 | protected def pairIteratorPD[A, B](implicit readA: Reader[A], readB: Reader[B]): PartialDeserializer[Iterator[(A, B)]] =
74 | multiBulkPD[ByteString, Iterable] andThen {
75 | _.grouped(2).map {
76 | case Seq(a, b) => (readA.fromByteString(a), readB.fromByteString(b))
77 | }
78 | }
79 | }
80 |
81 | private[serialization] trait CommandSpecificPD { this: LowPriorityPD =>
82 | import PartialDeserializer._
83 | import DefaultFormats._
84 |
85 | // special deserializer for Eval
86 | implicit val intListPD: PartialDeserializer[List[Int]] = multiBulkPD[Int, List]
87 |
88 | // special deserializers for Sorted Set
89 | implicit def doubleOptionPD: PartialDeserializer[Option[Double]] = parsedOptionPD[Double]
90 |
91 | implicit def scoredListPD[A](implicit reader: Reader[A]): PartialDeserializer[List[(A, Double)]] =
92 | pairIteratorPD[A, Double] andThen (_.toList)
93 |
94 | // lift non-bulk reply to `Option`
95 | def liftOptionPD[A](implicit pd: PartialDeserializer[A]): PartialDeserializer[Option[A]] =
96 | pd.andThen(Option(_)) orElse { case x: RawReply if x.head != Err => None }
97 |
98 |
99 | // special deserializer for (H)MGET
100 | def keyedMapPD[A](fields: Seq[String])(implicit reader: Reader[A]): PartialDeserializer[Map[String, A]] =
101 | multiBulkPD[Option[A], Iterable] andThen {
102 | _.view.zip(fields).collect {
103 | case (Some(value), field) => (field, value)
104 | }.toMap
105 | }
106 |
107 | // special deserializer for EVAL(SHA)
108 | def ensureListPD[A](implicit reader: Reader[A]): PartialDeserializer[List[A]] =
109 | multiBulkPD[A, List].orElse(parsedOptionPD[A].andThen(_.toList))
110 | }
111 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/IntegrationSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 |
3 | import org.scalatest.junit.JUnitRunner
4 | import org.junit.runner.RunWith
5 | import scala.concurrent.Future
6 | import com.redis.serialization._
7 |
8 |
9 | object IntegrationSpec {
10 | // should not be path dependent for equality test
11 | case class Person(id: Int, name: String)
12 | }
13 |
14 | @RunWith(classOf[JUnitRunner])
15 | class IntegrationSpec extends RedisSpecBase {
16 | import IntegrationSpec._
17 |
18 |
19 | describe("DefaultFormats integration") {
20 | import com.redis.serialization.DefaultFormats._
21 |
22 | it("should set values to keys") {
23 | client.set("key100", 200).futureValue should be (true)
24 | client.get[Long]("key100").futureValue should be (Some(200))
25 | }
26 |
27 | it("should not conflict when using all built in parsers") {
28 | client
29 | .hmset("hash", Map("field1" -> "1", "field2" -> 2))
30 | .futureValue should be (true)
31 |
32 | client
33 | .hmget[String]("hash", "field1", "field2")
34 | .futureValue should equal (Map("field1" -> "1", "field2" -> "2"))
35 |
36 | client
37 | .hmget[Int]("hash", "field1", "field2")
38 | .futureValue should equal (Map("field1" -> 1, "field2" -> 2))
39 |
40 | client
41 | .hmget[Int]("hash", "field1", "field2", "field3")
42 | .futureValue should equal (Map("field1" -> 1, "field2" -> 2))
43 | }
44 |
45 | it("should use a provided implicit parser") {
46 | client
47 | .hmset("hash", Map("field1" -> "1", "field2" -> 2))
48 | .futureValue should be (true)
49 |
50 | client
51 | .hmget("hash", "field1", "field2")
52 | .futureValue should equal (Map("field1" -> "1", "field2" -> "2"))
53 |
54 | implicit val intFormat = Format[Int](java.lang.Integer.parseInt, _.toString)
55 |
56 | client
57 | .hmget[Int]("hash", "field1", "field2")
58 | .futureValue should equal (Map("field1" -> 1, "field2" -> 2))
59 |
60 | client
61 | .hmget[String]("hash", "field1", "field2")
62 | .futureValue should equal (Map("field1" -> "1", "field2" -> "2"))
63 |
64 | client
65 | .hmget[Int]("hash", "field1", "field2", "field3")
66 | .futureValue should equal (Map("field1" -> 1, "field2" -> 2))
67 | }
68 |
69 | it("should use a provided implicit string parser") {
70 | implicit val stringFormat = Format[String](new String(_).toInt.toBinaryString, identity)
71 |
72 | client
73 | .hmset("hash", Map("field1" -> "1", "field2" -> 2))
74 | .futureValue should be (true)
75 |
76 | client
77 | .hmget[Int]("hash", "field1", "field2")
78 | .futureValue should equal (Map("field1" -> 1, "field2" -> 2))
79 |
80 | client
81 | .hmget[String]("hash", "field1", "field2")
82 | .futureValue should equal (Map("field1" -> "1", "field2" -> "10"))
83 | }
84 |
85 | it("should parse string as a bytearray with an implicit parser") {
86 | val x = "debasish".getBytes("UTF-8")
87 | client.set("key", x).futureValue should be (true)
88 |
89 | val s = client.get[Array[Byte]]("key").futureValue
90 | new String(s.get, "UTF-8") should equal ("debasish")
91 |
92 | client.get[Array[Byte]]("keey").futureValue should equal (None)
93 | }
94 | }
95 |
96 | describe("Custom format support") {
97 |
98 | val debasish = Person(1, "Debasish Gosh")
99 |
100 | it("should be easy to customize") {
101 |
102 | implicit val customPersonFormat =
103 | new Format[Person] {
104 | def read(str: String) = {
105 | val head :: rest = str.split('|').toList
106 | val id = head.toInt
107 | val name = rest.mkString("|")
108 |
109 | Person(id, name)
110 | }
111 |
112 | def write(person: Person) = {
113 | import person._
114 | s"$id|$name"
115 | }
116 | }
117 |
118 | // val _ = client.set("debasish", debasish).futureValue
119 |
120 | // client.get[Person]("debasish").futureValue should equal (Some(debasish))
121 | }
122 | }
123 |
124 | describe("Third-party library integration") {
125 |
126 | val debasish = Person(1, "Debasish Gosh")
127 | val jisoo = Person(2, "Jisoo Park")
128 |
129 | it("should support out-of-box (un)marshalling") {
130 | import spray.json.DefaultJsonProtocol._
131 | import SprayJsonSupport._
132 |
133 | implicit val personFormat = jsonFormat2(Person)
134 |
135 | val _ = Future.sequence(
136 | client.set("debasish", debasish) ::
137 | client.set("people", List(debasish, jisoo)) ::
138 | Nil
139 | ).futureValue
140 |
141 | // client.get[Person]("debasish").futureValue should equal (Some(debasish))
142 | // client.get[List[Person]]("people").futureValue should equal (Some(List(debasish, jisoo)))
143 | }
144 |
145 | }
146 |
147 | }
148 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/serialization/SerializationSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.serialization
2 |
3 | import org.scalatest._
4 | import org.scalatest.junit.JUnitRunner
5 | import org.junit.runner.RunWith
6 |
7 |
8 | object SerializationSpec {
9 | // should not be path dependent for equality test
10 | case class Person(id: Int, name: String)
11 | }
12 |
13 | @RunWith(classOf[JUnitRunner])
14 | class SerializationSpec extends FunSpec with Matchers {
15 | import SerializationSpec._
16 |
17 | val debasish = Person(1, "Debasish Gosh")
18 | val jisoo = Person(2, "Jisoo Park")
19 | val people = List(debasish, jisoo)
20 |
21 | describe("Format") {
22 | it("should be easy to customize") {
23 |
24 | implicit val customPersonFormat =
25 | new Format[Person] {
26 | def read(str: String) = {
27 | val head :: rest = str.split('|').toList
28 | val id = head.toInt
29 | val name = rest.mkString("|")
30 |
31 | Person(id, name)
32 | }
33 |
34 | def write(person: Person) = {
35 | import person._
36 | s"$id|$name"
37 | }
38 | }
39 |
40 | val write = implicitly[Writer[Person]].toByteString _
41 | val read = implicitly[Reader[Person]].fromByteString _
42 |
43 | read(write(debasish)) should equal (debasish)
44 | }
45 | }
46 |
47 |
48 | describe("Stringified") {
49 |
50 | it("should serialize String by default") {
51 | implicitly[String => Stringified].apply("string") should equal (Stringified("string"))
52 | }
53 |
54 | it("should not conflict with other implicit formats") {
55 | import DefaultFormats._
56 | implicitly[String => Stringified].apply("string") should equal (Stringified("string"))
57 | }
58 |
59 | it("should prioritize closer implicits") {
60 | @volatile var localWriterUsed = false
61 |
62 | implicit val localWriter: Writer[String] = StringWriter { string =>
63 | localWriterUsed = true
64 | string
65 | }
66 |
67 | implicitly[String => Stringified].apply("string")
68 | localWriterUsed should be (true)
69 | }
70 |
71 | it("should not encode a byte array to a UTF-8 string") {
72 | val nonUtf8Bytes = Array(0x85.toByte)
73 |
74 | new String(nonUtf8Bytes, "UTF-8").getBytes("UTF-8") should not equal (nonUtf8Bytes)
75 |
76 | Stringified(nonUtf8Bytes).value.toArray[Byte] should equal (nonUtf8Bytes)
77 | }
78 | }
79 |
80 |
81 | describe("Integration") {
82 |
83 | describe("SprayJsonSupport") {
84 | it("should encode/decode json objects") {
85 | import spray.json.DefaultJsonProtocol._
86 | import SprayJsonSupport._
87 |
88 | implicit val personFormat = jsonFormat2(Person)
89 |
90 | val write = implicitly[Writer[Person]].toByteString _
91 | val read = implicitly[Reader[Person]].fromByteString _
92 |
93 | val writeL = implicitly[Writer[List[Person]]].toByteString _
94 | val readL = implicitly[Reader[List[Person]]].fromByteString _
95 |
96 | read(write(debasish)) should equal (debasish)
97 | readL(writeL(people)) should equal (people)
98 | }
99 | }
100 |
101 | describe("Json4sNativeSupport") {
102 | it("should encode/decode json objects") {
103 | import Json4sNativeSupport._
104 |
105 | implicit val format = org.json4s.DefaultFormats
106 |
107 | val write = implicitly[Writer[Person]].toByteString _
108 | val read = implicitly[Reader[Person]].fromByteString _
109 |
110 | val writeL = implicitly[Writer[List[Person]]].toByteString _
111 | val readL = implicitly[Reader[List[Person]]].fromByteString _
112 |
113 | read(write(debasish)) should equal (debasish)
114 | readL(writeL(people)) should equal (people)
115 | }
116 | }
117 |
118 | describe("Json4sJacksonSupport") {
119 | it("should encode/decode json objects") {
120 | import Json4sJacksonSupport._
121 |
122 | implicit val format = org.json4s.DefaultFormats
123 |
124 | val write = implicitly[Writer[Person]].toByteString _
125 | val read = implicitly[Reader[Person]].fromByteString _
126 |
127 | val writeL = implicitly[Writer[List[Person]]].toByteString _
128 | val readL = implicitly[Reader[List[Person]]].fromByteString _
129 |
130 | read(write(debasish)) should equal (debasish)
131 | readL(writeL(people)) should equal (people)
132 | }
133 | }
134 |
135 | describe("LiftJsonSupport") {
136 | pending
137 | //it("should encode/decode json objects") {
138 | // import LiftJsonSupport._
139 |
140 | // implicit val format = net.liftweb.json.DefaultFormats
141 |
142 | // val write = implicitly[Writer[Person]].toByteString _
143 | // val read = implicitly[Reader[Person]].fromByteString _
144 |
145 | // val writeL = implicitly[Writer[List[Person]]].toByteString _
146 | // val readL = implicitly[Reader[List[Person]]].fromByteString _
147 |
148 | // read(write(debasish)) should equal (debasish)
149 | // readL(writeL(people)) should equal (people)
150 | //}
151 | }
152 | }
153 |
154 |
155 | }
156 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/ClientSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 |
3 | import scala.concurrent.Future
4 |
5 | import akka.testkit.TestProbe
6 | import org.junit.runner.RunWith
7 | import org.scalatest.exceptions.TestFailedException
8 | import org.scalatest.junit.JUnitRunner
9 | import serialization._
10 | import akka.io.Tcp.{Connected, CommandFailed}
11 | import scala.reflect.ClassTag
12 | import scala.concurrent.duration._
13 | import com.redis.RedisClientSettings.ConstantReconnectionSettings
14 | import com.redis.protocol.ServerCommands.Client.Kill
15 |
16 | @RunWith(classOf[JUnitRunner])
17 | class ClientSpec extends RedisSpecBase {
18 |
19 | import DefaultFormats._
20 |
21 | describe("non blocking apis using futures") {
22 | it("get and set should be non blocking") {
23 | @volatile var callbackExecuted = false
24 |
25 | val ks = (1 to 10).map(i => s"client_key_$i")
26 | val kvs = ks.zip(1 to 10)
27 |
28 | val sets: Seq[Future[Boolean]] = kvs map {
29 | case (k, v) => client.set(k, v)
30 | }
31 |
32 | val setResult = Future.sequence(sets) map { r: Seq[Boolean] =>
33 | callbackExecuted = true
34 | r
35 | }
36 |
37 | callbackExecuted should be (false)
38 | setResult.futureValue should contain only (true)
39 | callbackExecuted should be (true)
40 |
41 | callbackExecuted = false
42 | val gets: Seq[Future[Option[Long]]] = ks.map { k => client.get[Long](k) }
43 | val getResult = Future.sequence(gets).map { rs =>
44 | callbackExecuted = true
45 | rs.flatten.sum
46 | }
47 |
48 | callbackExecuted should be (false)
49 | getResult.futureValue should equal (55)
50 | callbackExecuted should be (true)
51 | }
52 |
53 | it("should compose with sequential combinator") {
54 | val key = "client_key_seq"
55 |
56 | val res = for {
57 | p <- client.lpush(key, 0 to 100)
58 | if p > 0
59 | r <- client.lrange[Long](key, 0, -1)
60 | } yield (p, r)
61 |
62 | val (count, list) = res.futureValue
63 | count should equal (101)
64 | list.reverse should equal (0 to 100)
65 | }
66 | }
67 |
68 | describe("error handling using promise failure") {
69 | it("should give error trying to lpush on a key that has a non list value") {
70 | val key = "client_err"
71 | client.set(key, "value200").futureValue should be (true)
72 |
73 | val thrown = intercept[TestFailedException] {
74 | client.lpush(key, 1200).futureValue
75 | }
76 |
77 | thrown.getCause.getMessage should include ("Operation against a key holding the wrong kind of value")
78 | }
79 | }
80 |
81 | describe("reconnections based on policy") {
82 |
83 | def killClientsNamed(rc: RedisClient, name: String): Future[List[Boolean]] = {
84 | // Clients are a list of lines similar to
85 | // addr=127.0.0.1:65227 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
86 | // We'll split them up and make a map
87 | val clients = rc.client.list().futureValue.get.toString
88 | .split('\n')
89 | .map(_.trim)
90 | .filterNot(_.isEmpty)
91 | .map(
92 | _.split(" ").map(
93 | _.split("=").padTo(2, "")
94 | ).map(
95 | item => (item(0), item(1))
96 | )
97 | ).map(_.toMap)
98 | Future.sequence(clients.filter(_("name") == name).map(_("addr")).map(rc.client.kill).toList)
99 | }
100 |
101 | it("should not reconnect by default") {
102 | val name = "test-client-1"
103 | client.client.setname(name).futureValue should equal (true)
104 |
105 | val probe = TestProbe()
106 | probe watch client.clientRef
107 | killClientsNamed(client, name).futureValue.reduce(_ && _) should equal (true)
108 | probe.expectTerminated(client.clientRef)
109 | }
110 |
111 | it("should reconnect with settings") {
112 | withReconnectingClient { client =>
113 | val name = "test-client-2"
114 | client.client.setname(name).futureValue should equal (true)
115 |
116 | val key = "reconnect_test"
117 | client.lpush(key, 0)
118 |
119 | killClientsNamed(client, name).futureValue.reduce(_ && _) should equal (true)
120 |
121 | client.lpush(key, 1 to 100).futureValue should equal(101)
122 | val list = client.lrange[Long](key, 0, -1).futureValue
123 |
124 | list.size should equal(101)
125 | list.reverse should equal(0 to 100)
126 | }
127 | }
128 |
129 | it("should reconnect when maxattempts is set in reconnect settings ") {
130 | withFixedConstantReconnectingClient { client =>
131 | val name = "test-client-2"
132 | client.client.setname(name).futureValue should equal (true)
133 |
134 | val key = "reconnect_test_with_maxattempts"
135 | client.lpush(key, 0)
136 |
137 | killClientsNamed(client, name).futureValue.reduce(_ && _) should equal (true)
138 | Thread.sleep(100) //ensure enough time has passed to reconnect
139 |
140 | client.lpush(key, 1 to 100).futureValue should equal(101)
141 | val list = client.lrange[Long](key, 0, -1).futureValue
142 |
143 | list.size should equal(101)
144 | list.reverse should equal(0 to 100)
145 |
146 | }
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/KeyOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import scala.concurrent.Future
5 | import scala.concurrent.duration._
6 | import serialization._
7 | import akka.pattern.ask
8 | import akka.actor._
9 | import akka.util.Timeout
10 | import akka.pattern.gracefulStop
11 | import com.redis.protocol._
12 |
13 |
14 | trait KeyOperations { this: RedisOps =>
15 | import KeyCommands._
16 |
17 | // KEYS
18 | // returns all the keys matching the glob-style pattern.
19 | def keys(pattern: String = "*")(implicit timeout: Timeout) =
20 | clientRef.ask(Keys(pattern)).mapTo[Keys#Ret]
21 |
22 | // RANDOMKEY
23 | // return a randomly selected key from the currently selected DB.
24 | def randomkey(implicit timeout: Timeout) =
25 | clientRef.ask(RandomKey).mapTo[RandomKey.Ret]
26 |
27 | // RENAME (oldkey, newkey)
28 | // atomically renames the key oldkey to newkey.
29 | def rename(oldkey: String, newkey: String)(implicit timeout: Timeout) =
30 | clientRef.ask(Rename(oldkey, newkey)).mapTo[Rename#Ret]
31 |
32 | // RENAMENX (oldkey, newkey)
33 | // rename oldkey into newkey but fails if the destination key newkey already exists.
34 | def renamenx(oldkey: String, newkey: String)(implicit timeout: Timeout) =
35 | clientRef.ask(RenameNx(oldkey, newkey)).mapTo[RenameNx#Ret]
36 |
37 | // DBSIZE
38 | // return the size of the db.
39 | def dbsize()(implicit timeout: Timeout) =
40 | clientRef.ask(DBSize).mapTo[DBSize.Ret]
41 |
42 | // EXISTS (key)
43 | // test if the specified key exists.
44 | def exists(key: String)(implicit timeout: Timeout) =
45 | clientRef.ask(Exists(key)).mapTo[Exists#Ret]
46 |
47 |
48 | // DELETE (key1 key2 ..)
49 | // deletes the specified keys.
50 | def del(keys: Seq[String])(implicit timeout: Timeout) =
51 | clientRef.ask(Del(keys)).mapTo[Del#Ret]
52 |
53 | def del(key: String, keys: String*)(implicit timeout: Timeout) =
54 | clientRef.ask(Del(key, keys:_*)).mapTo[Del#Ret]
55 |
56 |
57 | // TYPE (key)
58 | // return the type of the value stored at key in form of a string.
59 | def `type`(key: String)(implicit timeout: Timeout) =
60 | clientRef.ask(Type(key)).mapTo[Type#Ret]
61 |
62 | def tpe(key: String)(implicit timeout: Timeout) = `type`(key)
63 |
64 |
65 | // EXPIRE (key, expiry)
66 | // sets the expire time (in sec.) for the specified key.
67 | def expire(key: String, ttl: Int)(implicit timeout: Timeout) =
68 | clientRef.ask(Expire(key, ttl)).mapTo[Expire#Ret]
69 |
70 | // PEXPIRE (key, expiry)
71 | // sets the expire time (in milli sec.) for the specified key.
72 | def pexpire(key: String, ttlInMillis: Int)(implicit timeout: Timeout) =
73 | clientRef.ask(PExpire(key, ttlInMillis)).mapTo[PExpire#Ret]
74 |
75 | // EXPIREAT (key, unix timestamp)
76 | // sets the expire time for the specified key.
77 | def expireat(key: String, timestamp: Long)(implicit timeout: Timeout) =
78 | clientRef.ask(ExpireAt(key, timestamp)).mapTo[ExpireAt#Ret]
79 |
80 | // PEXPIREAT (key, unix timestamp)
81 | // sets the expire timestamp in millis for the specified key.
82 | def pexpireat(key: String, timestampInMillis: Long)(implicit timeout: Timeout) =
83 | clientRef.ask(PExpireAt(key, timestampInMillis)).mapTo[PExpireAt#Ret]
84 |
85 | // TTL (key)
86 | // returns the remaining time to live of a key that has a timeout
87 | def ttl(key: String)(implicit timeout: Timeout) =
88 | clientRef.ask(TTL(key)).mapTo[TTL#Ret]
89 |
90 | // PTTL (key)
91 | // returns the remaining time to live of a key that has a timeout in millis
92 | def pttl(key: String)(implicit timeout: Timeout) =
93 | clientRef.ask(PTTL(key)).mapTo[PTTL#Ret]
94 |
95 | // FLUSHDB the DB
96 | // removes all the DB data.
97 | def flushdb()(implicit timeout: Timeout) =
98 | clientRef.ask(FlushDB).mapTo[FlushDB.Ret]
99 |
100 | // FLUSHALL the DB's
101 | // removes data from all the DB's.
102 | def flushall()(implicit timeout: Timeout) =
103 | clientRef.ask(FlushAll).mapTo[FlushAll.Ret]
104 |
105 | // MOVE
106 | // Move the specified key from the currently selected DB to the specified destination DB.
107 | def move(key: String, db: Int)(implicit timeout: Timeout) =
108 | clientRef.ask(Move(key, db)).mapTo[Move#Ret]
109 |
110 | // PERSIST (key)
111 | // Remove the existing timeout on key, turning the key from volatile (a key with an expire set)
112 | // to persistent (a key that will never expire as no timeout is associated).
113 | def persist(key: String)(implicit timeout: Timeout) =
114 | clientRef.ask(Persist(key)).mapTo[Persist#Ret]
115 |
116 | // SORT
117 | // sort keys in a set, and optionally pull values for them
118 | def sort[A](key: String,
119 | limit: Option[Tuple2[Int, Int]] = None,
120 | desc: Boolean = false,
121 | alpha: Boolean = false,
122 | by: Option[String] = None,
123 | get: Seq[String] = Nil)(implicit timeout: Timeout, reader: Reader[A]) =
124 | clientRef.ask(Sort(key, limit, desc, alpha, by, get)).mapTo[Sort[A]#Ret]
125 |
126 | // SORT with STORE
127 | // sort keys in a set, and store result in the supplied key
128 | def sortNStore(key: String,
129 | limit: Option[Tuple2[Int, Int]] = None,
130 | desc: Boolean = false,
131 | alpha: Boolean = false,
132 | by: Option[String] = None,
133 | get: Seq[String] = Nil,
134 | storeAt: String)(implicit timeout: Timeout) =
135 | clientRef.ask(SortNStore(key, limit, desc, alpha, by, get, storeAt)).mapTo[SortNStore#Ret]
136 | }
137 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/ListOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import serialization._
5 | import akka.pattern.ask
6 | import akka.util.Timeout
7 | import com.redis.protocol.ListCommands
8 |
9 | trait ListOperations { this: RedisOps =>
10 | import ListCommands._
11 |
12 | // LPUSH (Variadic: >= 2.4)
13 | // add values to the head of the list stored at key
14 | def lpush(key: String, values: Seq[Stringified])(implicit timeout: Timeout) =
15 | clientRef.ask(LPush(key, values)).mapTo[LPush#Ret]
16 |
17 | def lpush(key: String, value: Stringified, values: Stringified*)(implicit timeout: Timeout) =
18 | clientRef.ask(LPush(key, value, values:_*)).mapTo[LPush#Ret]
19 |
20 |
21 | // LPUSHX (Variadic: >= 2.4)
22 | // add value to the tail of the list stored at key
23 | def lpushx(key: String, value: Stringified)(implicit timeout: Timeout) =
24 | clientRef.ask(LPushX(key, value)).mapTo[LPushX#Ret]
25 |
26 |
27 | // RPUSH (Variadic: >= 2.4)
28 | // add values to the head of the list stored at key
29 | def rpush(key: String, values: Stringified)(implicit timeout: Timeout) =
30 | clientRef.ask(RPush(key, values)).mapTo[RPush#Ret]
31 |
32 | def rpush(key: String, value: Stringified, values: Stringified*)(implicit timeout: Timeout) =
33 | clientRef.ask(RPush(key, value, values:_*)).mapTo[RPush#Ret]
34 |
35 |
36 | // RPUSHX (Variadic: >= 2.4)
37 | // add value to the tail of the list stored at key
38 | def rpushx(key: String, value: Stringified)(implicit timeout: Timeout) =
39 | clientRef.ask(RPushX(key, value)).mapTo[RPushX#Ret]
40 |
41 | // LLEN
42 | // return the length of the list stored at the specified key.
43 | // If the key does not exist zero is returned (the same behaviour as for empty lists).
44 | // If the value stored at key is not a list an error is returned.
45 | def llen(key: String)(implicit timeout: Timeout) =
46 | clientRef.ask(LLen(key)).mapTo[LLen#Ret]
47 |
48 | // LRANGE
49 | // return the specified elements of the list stored at the specified key.
50 | // Start and end are zero-based indexes.
51 | def lrange[A](key: String, start: Int, end: Int)(implicit timeout: Timeout, reader: Reader[A]) =
52 | clientRef.ask(LRange(key, start, end)).mapTo[LRange[A]#Ret]
53 |
54 | // LTRIM
55 | // Trim an existing list so that it will contain only the specified range of elements specified.
56 | def ltrim(key: String, start: Int, end: Int)(implicit timeout: Timeout) =
57 | clientRef.ask(LTrim(key, start, end)).mapTo[LTrim#Ret]
58 |
59 | // LINDEX
60 | // return the especified element of the list stored at the specified key.
61 | // Negative indexes are supported, for example -1 is the last element, -2 the penultimate and so on.
62 | def lindex[A](key: String, index: Int)(implicit timeout: Timeout, reader: Reader[A]) =
63 | clientRef.ask(LIndex[A](key, index)).mapTo[LIndex[A]#Ret]
64 |
65 | // LSET
66 | // set the list element at index with the new value. Out of range indexes will generate an error
67 | def lset(key: String, index: Int, value: Stringified)(implicit timeout: Timeout) =
68 | clientRef.ask(LSet(key, index, value)).mapTo[LSet#Ret]
69 |
70 | // LREM
71 | // Remove the first count occurrences of the value element from the list.
72 | def lrem(key: String, count: Int, value: Stringified)(implicit timeout: Timeout) =
73 | clientRef.ask(LRem(key, count, value)).mapTo[LRem#Ret]
74 |
75 | // LPOP
76 | // atomically return and remove the first (LPOP) or last (RPOP) element of the list
77 | def lpop[A](key: String)(implicit timeout: Timeout, reader: Reader[A]) =
78 | clientRef.ask(LPop[A](key)).mapTo[LPop[A]#Ret]
79 |
80 | // RPOP
81 | // atomically return and remove the first (LPOP) or last (RPOP) element of the list
82 | def rpop[A](key: String)(implicit timeout: Timeout, reader: Reader[A]) =
83 | clientRef.ask(RPop[A](key)).mapTo[RPop[A]#Ret]
84 |
85 | // RPOPLPUSH
86 | // Remove the first count occurrences of the value element from the list.
87 | def rpoplpush[A](srcKey: String, dstKey: String)
88 | (implicit timeout: Timeout, reader: Reader[A]) =
89 | clientRef.ask(RPopLPush[A](srcKey, dstKey)).mapTo[RPopLPush[A]#Ret]
90 |
91 | def brpoplpush[A](srcKey: String, dstKey: String, timeoutInSeconds: Int)
92 | (implicit timeout: Timeout, reader: Reader[A]) =
93 | clientRef.ask(BRPopLPush[A](srcKey, dstKey, timeoutInSeconds)).mapTo[BRPopLPush[A]#Ret]
94 |
95 |
96 | def blpop[A](timeoutInSeconds: Int, keys: Seq[String])
97 | (implicit timeout: Timeout, reader: Reader[A]) =
98 | clientRef.ask(BLPop[A](timeoutInSeconds, keys)).mapTo[BLPop[A]#Ret]
99 |
100 | def blpop[A](timeoutInSeconds: Int, key: String, keys: String*)
101 | (implicit timeout: Timeout, reader: Reader[A]) =
102 | clientRef.ask(BLPop[A](timeoutInSeconds, key, keys:_*)).mapTo[BLPop[A]#Ret]
103 |
104 |
105 | def brpop[A](timeoutInSeconds: Int, keys: Seq[String])
106 | (implicit timeout: Timeout, reader: Reader[A]) =
107 | clientRef.ask(BRPop[A](timeoutInSeconds, keys)).mapTo[BRPop[A]#Ret]
108 |
109 | def brpop[A](timeoutInSeconds: Int, key: String, keys: String*)
110 | (implicit timeout: Timeout, reader: Reader[A]) =
111 | clientRef.ask(BRPop[A](timeoutInSeconds, key, keys:_*)).mapTo[BRPop[A]#Ret]
112 |
113 | // LINSERT
114 | // Inserts value in the list stored at key either before or after the reference value pivot
115 | def linsert(key: String, position: InsertPosition, pivot: Stringified, value: Stringified)
116 | (implicit timeout: Timeout) =
117 | clientRef.ask(LInsert(key, position, pivot, value)).mapTo[LInsert#Ret]
118 | }
119 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/StringCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import scala.language.existentials
4 | import com.redis.serialization._
5 |
6 |
7 | object StringCommands {
8 | import DefaultWriters._
9 |
10 | case class Get[A: Reader](key: String) extends RedisCommand[Option[A]]("GET") {
11 | def params = key +: ANil
12 | }
13 |
14 | sealed trait SetOption { def toArgs: Args }
15 |
16 | sealed abstract class SetExpiryOption(label: String, n: Long) extends SetOption { def toArgs = label +: n +: ANil }
17 | case class EX(expiryInSeconds: Long) extends SetExpiryOption("EX", expiryInSeconds)
18 | case class PX(expiryInMillis: Long) extends SetExpiryOption("PX", expiryInMillis)
19 |
20 | sealed abstract class SetConditionOption(label: String) extends SetOption { def toArgs = label +: ANil }
21 | case object NX extends SetConditionOption("NX")
22 | case object XX extends SetConditionOption("XX")
23 |
24 | case class Set(key: String, value: Stringified,
25 | exORpx: Option[SetExpiryOption] = None,
26 | nxORxx: Option[SetConditionOption] = None) extends RedisCommand[Boolean]("SET") {
27 |
28 | def params = key +: value +: exORpx.fold[Seq[Stringified]](Nil)(_.toArgs.values) ++: nxORxx.fold(ANil)(_.toArgs)
29 | }
30 |
31 | object Set {
32 |
33 | def apply(key: String, value: Stringified, setOption: SetOption): Set =
34 | setOption match {
35 | case e: SetExpiryOption => Set(key, value, exORpx = Some(e))
36 | case c: SetConditionOption => Set(key, value, nxORxx = Some(c))
37 | }
38 |
39 | def apply(key: String, value: Stringified, exORpx: SetExpiryOption, nxORxx: SetConditionOption): Set =
40 | Set(key, value, Some(exORpx), Some(nxORxx))
41 | }
42 |
43 |
44 | case class GetSet[A: Reader](key: String, value: Stringified) extends RedisCommand[Option[A]]("GETSET") {
45 | def params = key +: value +: ANil
46 | }
47 |
48 | case class SetNx(key: String, value: Stringified) extends RedisCommand[Boolean]("SETNX") {
49 | def params = key +: value +: ANil
50 | }
51 |
52 | case class SetEx(key: String, expiry: Long, value: Stringified) extends RedisCommand[Boolean]("SETEX") {
53 | def params = key +: expiry +: value +: ANil
54 | }
55 |
56 | case class PSetEx(key: String, expiryInMillis: Long, value: Stringified) extends RedisCommand[Boolean]("PSETEX") {
57 | def params = key +: expiryInMillis +: value +: ANil
58 | }
59 |
60 |
61 | case class Incr(key: String) extends RedisCommand[Long]("INCR") {
62 | def params = key +: ANil
63 | }
64 |
65 | case class IncrBy(key: String, amount: Int) extends RedisCommand[Long]("INCRBY") {
66 | def params = key +: amount +: ANil
67 | }
68 |
69 |
70 | case class Decr(key: String) extends RedisCommand[Long]("DECR") {
71 | def params = key +: ANil
72 | }
73 |
74 | case class DecrBy(key: String, amount: Int) extends RedisCommand[Long]("DECRBY") {
75 | def params = key +: amount +: ANil
76 | }
77 |
78 |
79 | case class MGet[A: Reader](keys: Seq[String])
80 | extends RedisCommand[Map[String, A]]("MGET")(PartialDeserializer.keyedMapPD(keys)) {
81 | require(keys.nonEmpty, "Keys should not be empty")
82 | def params = keys.toArgs
83 | }
84 |
85 | object MGet {
86 | def apply[A: Reader](key: String, keys: String*): MGet[A] = MGet(key +: keys)
87 | }
88 |
89 |
90 | case class MSet(kvs: KeyValuePair*) extends RedisCommand[Boolean]("MSET") {
91 | def params = kvs.foldRight(ANil) { case (KeyValuePair(k, v), l) => k +: v +: l }
92 | }
93 |
94 | case class MSetNx(kvs: KeyValuePair*) extends RedisCommand[Boolean]("MSETNX") {
95 | def params = kvs.foldRight(ANil) { case (KeyValuePair(k, v), l) => k +: v +: l }
96 | }
97 |
98 | case class SetRange(key: String, offset: Int, value: Stringified) extends RedisCommand[Long]("SETRANGE") {
99 | def params = key +: offset +: value +: ANil
100 | }
101 |
102 | case class GetRange[A: Reader](key: String, start: Int, end: Int) extends RedisCommand[Option[A]]("GETRANGE") {
103 | def params = key +: start +: end +: ANil
104 | }
105 |
106 | case class Strlen(key: String) extends RedisCommand[Long]("STRLEN") {
107 | def params = key +: ANil
108 | }
109 |
110 | case class Append(key: String, value: Stringified) extends RedisCommand[Long]("APPEND") {
111 | def params = key +: value +: ANil
112 | }
113 |
114 | case class GetBit(key: String, offset: Int) extends RedisCommand[Boolean]("GETBIT") {
115 | def params = key +: offset +: ANil
116 | }
117 |
118 | case class SetBit(key: String, offset: Int, value: Boolean) extends RedisCommand[Long]("SETBIT") {
119 | def params = key +: offset +: (if (value) "1" else "0") +: ANil
120 | }
121 |
122 |
123 | case class BitOp(op: String, destKey: String, srcKeys: Seq[String]) extends RedisCommand[Long]("BITOP") {
124 | require(srcKeys.nonEmpty, "Src keys should not be empty")
125 | def params = op +: destKey +: srcKeys.toArgs
126 | }
127 |
128 | object BitOp {
129 | def apply(op: String, destKey: String, srcKey: String, srcKeys: String*): BitOp = BitOp(op, destKey, srcKey +: srcKeys)
130 | }
131 |
132 |
133 | case class BitCount(key: String, range: Option[(Int, Int)]) extends RedisCommand[Long]("BITCOUNT") {
134 | def params = key +: range.fold(ANil) { case (from, to) => from +: to +: ANil }
135 | }
136 |
137 | case class BitPos(key: String, bit: Boolean, start: Option[Int], end: Option[Int]) extends RedisCommand[Long]("BITPOS") {
138 | require(start.isDefined || end.isEmpty, "Start should be defined or end should be empty")
139 | def params = key +: (if (bit) "1" else "0") +: start.toSeq ++: end.toSeq ++: ANil
140 | }
141 | }
142 |
143 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/SetOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import serialization._
5 | import akka.pattern.ask
6 | import akka.util.Timeout
7 | import com.redis.protocol.SetCommands
8 |
9 | trait SetOperations { this: RedisOps =>
10 | import SetCommands._
11 |
12 | // SADD (VARIADIC: >= 2.4)
13 | // Add the specified members to the set value stored at key.
14 | def sadd(key: String, values: Seq[Stringified])(implicit timeout: Timeout) =
15 | clientRef.ask(SAdd(key, values)).mapTo[SAdd#Ret]
16 |
17 | def sadd(key: String, value: Stringified, values: Stringified*)(implicit timeout: Timeout) =
18 | clientRef.ask(SAdd(key, value, values:_*)).mapTo[SAdd#Ret]
19 |
20 |
21 | // SREM (VARIADIC: >= 2.4)
22 | // Remove the specified members from the set value stored at key.
23 | def srem(key: String, values: Seq[Stringified])(implicit timeout: Timeout) =
24 | clientRef.ask(SRem(key, values)).mapTo[SRem#Ret]
25 |
26 | def srem(key: String, value: Stringified, values: Stringified*)(implicit timeout: Timeout) =
27 | clientRef.ask(SRem(key, value, values:_*)).mapTo[SRem#Ret]
28 |
29 |
30 | // SPOP
31 | // Remove and return (pop) a random element from the Set value at key.
32 | def spop[A](key: String)(implicit timeout: Timeout, reader: Reader[A]) =
33 | clientRef.ask(SPop[A](key)).mapTo[SPop[A]#Ret]
34 |
35 | // SMOVE
36 | // Move the specified member from one Set to another atomically.
37 | def smove(sourceKey: String, destKey: String, value: Stringified)(implicit timeout: Timeout) =
38 | clientRef.ask(SMove(sourceKey, destKey, value)).mapTo[SMove#Ret]
39 |
40 | // SCARD
41 | // Return the number of elements (the cardinality) of the Set at key.
42 | def scard(key: String)(implicit timeout: Timeout) =
43 | clientRef.ask(SCard(key)).mapTo[SCard#Ret]
44 |
45 | // SISMEMBER
46 | // Test if the specified value is a member of the Set at key.
47 | def sismember(key: String, value: Stringified)(implicit timeout: Timeout) =
48 | clientRef.ask(SIsMember(key, value)).mapTo[SIsMember#Ret]
49 |
50 | // SINTER
51 | // Return the intersection between the Sets stored at key1, key2, ..., keyN.
52 | def sinter[A](keys: Seq[String])(implicit timeout: Timeout, reader: Reader[A]) =
53 | clientRef.ask(SInter[A](keys)).mapTo[SInter[A]#Ret]
54 |
55 | def sinter[A](key: String, keys: String*)(implicit timeout: Timeout, reader: Reader[A]) =
56 | clientRef.ask(SInter[A](key, keys:_*)).mapTo[SInter[A]#Ret]
57 |
58 | // SINTERSTORE
59 | // Compute the intersection between the Sets stored at key1, key2, ..., keyN,
60 | // and store the resulting Set at dstkey.
61 | // SINTERSTORE returns the size of the intersection, unlike what the documentation says
62 | // refer http://code.google.com/p/redis/issues/detail?id=121
63 | def sinterstore(destKey: String, keys: Seq[String])(implicit timeout: Timeout) =
64 | clientRef.ask(SInterStore(destKey, keys)).mapTo[SInterStore#Ret]
65 |
66 | def sinterstore(destKey: String, key: String, keys: String*)(implicit timeout: Timeout) =
67 | clientRef.ask(SInterStore(destKey, key, keys:_*)).mapTo[SInterStore#Ret]
68 |
69 |
70 | // SUNION
71 | // Return the union between the Sets stored at key1, key2, ..., keyN.
72 | def sunion[A](keys: Seq[String])(implicit timeout: Timeout, reader: Reader[A]) =
73 | clientRef.ask(SUnion[A](keys)).mapTo[SUnion[A]#Ret]
74 |
75 | def sunion[A](key: String, keys: String*)(implicit timeout: Timeout, reader: Reader[A]) =
76 | clientRef.ask(SUnion[A](key, keys:_*)).mapTo[SUnion[A]#Ret]
77 |
78 |
79 | // SUNIONSTORE
80 | // Compute the union between the Sets stored at key1, key2, ..., keyN,
81 | // and store the resulting Set at dstkey.
82 | // SUNIONSTORE returns the size of the union, unlike what the documentation says
83 | // refer http://code.google.com/p/redis/issues/detail?id=121
84 | def sunionstore(destKey: String, keys: Seq[String])(implicit timeout: Timeout) =
85 | clientRef.ask(SUnionStore(destKey, keys)).mapTo[SUnionStore#Ret]
86 |
87 | def sunionstore(destKey: String, key: String, keys: String*)(implicit timeout: Timeout) =
88 | clientRef.ask(SUnionStore(destKey, key, keys:_*)).mapTo[SUnionStore#Ret]
89 |
90 |
91 | // SDIFF
92 | // Return the difference between the Set stored at key1 and all the Sets key2, ..., keyN.
93 | def sdiff[A](keys: Seq[String])(implicit timeout: Timeout, reader: Reader[A]) =
94 | clientRef.ask(SDiff(keys)).mapTo[SDiff[A]#Ret]
95 |
96 | def sdiff[A](key: String, keys: String*)(implicit timeout: Timeout, reader: Reader[A]) =
97 | clientRef.ask(SDiff(key, keys:_*)).mapTo[SDiff[A]#Ret]
98 |
99 |
100 | // SDIFFSTORE
101 | // Compute the difference between the Set key1 and all the Sets key2, ..., keyN,
102 | // and store the resulting Set at dstkey.
103 | def sdiffstore(destKey: String, keys: Seq[String])(implicit timeout: Timeout) =
104 | clientRef.ask(SDiffStore(destKey, keys)).mapTo[SDiffStore#Ret]
105 |
106 | def sdiffstore(destKey: String, key: String, keys: String*)(implicit timeout: Timeout) =
107 | clientRef.ask(SDiffStore(destKey, key, keys:_*)).mapTo[SDiffStore#Ret]
108 |
109 |
110 | // SMEMBERS
111 | // Return all the members of the Set value at key.
112 | def smembers[A](key: String)(implicit timeout: Timeout, reader: Reader[A]) =
113 | clientRef.ask(SMembers(key)).mapTo[SMembers[A]#Ret]
114 |
115 | // SRANDMEMBER
116 | // Return a random element from a Set
117 | def srandmember[A](key: String)(implicit timeout: Timeout, reader: Reader[A]) =
118 | clientRef.ask(SRandMember(key)).mapTo[SRandMember[A]#Ret]
119 |
120 | // SRANDMEMBER
121 | // Return multiple random elements from a Set (since 2.6)
122 | def srandmember[A](key: String, count: Int)(implicit timeout: Timeout, reader: Reader[A]) =
123 | clientRef.ask(SRandMembers(key, count)).mapTo[SRandMembers[A]#Ret]
124 | }
125 |
126 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/serialization/RawReplyParser.scala:
--------------------------------------------------------------------------------
1 | package com.redis.serialization
2 |
3 | import com.redis.protocol.PubSubCommands.PMessage
4 |
5 | import scala.annotation.tailrec
6 |
7 | import akka.util.{ByteString, CompactByteString, ByteStringBuilder}
8 | import scala.collection.GenTraversable
9 | import scala.collection.generic.CanBuildFrom
10 | import scala.language.higherKinds
11 |
12 |
13 | private[serialization] object RawReplyParser {
14 | import com.redis.protocol._
15 |
16 | def parseInt(input: RawReply): Int = {
17 | @tailrec def loop(acc: Int, isMinus: Boolean): Int =
18 | input.nextByte().toChar match {
19 | case '\r' => input.jump(1); if (isMinus) -acc else acc
20 | case '-' => loop(acc, true)
21 | case c => loop((acc * 10) + c - '0', isMinus)
22 | }
23 | loop(0, false)
24 | }
25 |
26 | def parseLong(input: RawReply) = {
27 | @tailrec def loop(acc: Long, isMinus: Boolean): Long =
28 | input.nextByte().toChar match {
29 | case '\r' => input.jump(1); if (isMinus) -acc else acc
30 | case '-' => loop(acc, true)
31 | case c => loop((acc * 10) + c - '0', isMinus)
32 | }
33 |
34 | loop(0, false)
35 | }
36 |
37 | // Parse a string of undetermined length
38 | def parseSingle(input: RawReply): ByteString = {
39 | val builder = new ByteStringBuilder
40 |
41 | @tailrec def loop(): Unit =
42 | input.nextByte() match {
43 | case Cr => // stop
44 | case b: Byte =>
45 | builder += b
46 | loop()
47 | }
48 |
49 | loop()
50 | input.jump(1)
51 | builder.result
52 | }
53 |
54 | // Parse a string with known length
55 | def parseBulk(input: RawReply): Option[ByteString] = {
56 | val length = parseInt(input)
57 |
58 | if (length == NullBulkReplyCount) None
59 | else {
60 | val res = Some(input.take(length))
61 | input.jump(2)
62 | res
63 | }
64 | }
65 |
66 | def parseError(input: RawReply): RedisError = {
67 | val msg = parseSingle(input).utf8String
68 | msg match {
69 | case "QUEUED" => Queued
70 | case _ => new RedisError(msg)
71 | }
72 | }
73 |
74 | def parseMultiBulk[A, B[_] <: GenTraversable[_]](input: RawReply)(implicit cbf: CanBuildFrom[_, A, B[A]], pd: PartialDeserializer[A]): B[A] = {
75 | val builder = cbf.apply()
76 | val len = parseInt(input)
77 |
78 | @tailrec def inner(i: Int): Unit = if (i > 0) {
79 | builder += pd(input)
80 | inner(i - 1)
81 | }
82 |
83 | builder.sizeHint(len)
84 | inner(len)
85 | builder.result
86 | }
87 |
88 | import PubSubCommands.{PushedMessage, Subscribed, Unsubscribed, Message}
89 |
90 | val Subscribe = ByteString("subscribe")
91 | val PSubscribe = ByteString("psubscribe")
92 | val Unsubscribe = ByteString("unsubscribe")
93 | val PUnsubscribe = ByteString("punsubscribe")
94 | val Msg = ByteString("message")
95 | val PMsg = ByteString("pmessage")
96 |
97 | def parsePubSubMsg(input: RawReply): PushedMessage = {
98 | val len = parseInt( input )
99 | input.jump(1)
100 | val msgType = parseBulk( input )
101 | input.jump(1)
102 | val destination = parseBulk( input ).map( _.utf8String ).get
103 | input.jump(1)
104 | (msgType: @unchecked) match {
105 | case Some(Subscribe) => Subscribed( destination, isPattern = false, parseInt( input ) )
106 | case Some(PSubscribe) => Subscribed( destination, isPattern = true, parseInt( input ) )
107 | case Some(Unsubscribe) => Unsubscribed( destination, isPattern = false, parseInt( input ) )
108 | case Some(PUnsubscribe) => Unsubscribed( destination, isPattern = true, parseInt( input ) )
109 | case Some(Msg) => Message( destination, parseBulk(input).getOrElse(ByteString.empty) )
110 | case Some(PMsg) =>
111 | val channel = parseBulk(input).get.utf8String
112 | input.jump(1)
113 | PMessage( destination, channel, parseBulk(input).getOrElse(ByteString.empty) )
114 | }
115 | }
116 |
117 | class RawReply(val data: CompactByteString, private[this] var cursor: Int = 0) {
118 | import com.redis.serialization.Deserializer._
119 |
120 | def ++(other: CompactByteString) = new RawReply((data ++ other).compact, cursor)
121 |
122 | def hasNext = cursor < data.length
123 |
124 | def head =
125 | if (!hasNext) throw NotEnoughDataException
126 | else data(cursor)
127 |
128 | private[RawReplyParser] def nextByte() =
129 | if (!hasNext) throw NotEnoughDataException
130 | else {
131 | val res = data(cursor)
132 | cursor += 1
133 | res
134 | }
135 |
136 | private[RawReplyParser] def jump(amount: Int): Unit = {
137 | if (cursor + amount > data.length) throw NotEnoughDataException
138 | else cursor += amount
139 | }
140 |
141 | private[RawReplyParser] def take(amount: Int) =
142 | if (cursor + amount >= data.length) throw NotEnoughDataException
143 | else {
144 | val res = data.slice(cursor, cursor + amount)
145 | cursor += amount
146 | res
147 | }
148 |
149 | def remaining() = data.drop(cursor).compact
150 | }
151 |
152 | class PrefixDeserializer[A](prefix: Byte, read: RawReply => A) extends PartialDeserializer[A] {
153 | def isDefinedAt(x: RawReply) = x.head == prefix
154 | def apply(r: RawReply) = { r.jump(1); read(r) }
155 | }
156 |
157 | object PrefixDeserializer {
158 | val _intPD = new PrefixDeserializer[Int] (Integer, parseInt _)
159 | val _longPD = new PrefixDeserializer[Long] (Integer, parseLong _)
160 | val _statusStringPD = new PrefixDeserializer[ByteString] (Status, parseSingle _)
161 | val _rawBulkPD = new PrefixDeserializer[Option[ByteString]] (Bulk, parseBulk _)
162 | val _errorPD = new PrefixDeserializer[RedisError] (Err, parseError _)
163 |
164 | val _booleanPD =
165 | new PrefixDeserializer[Boolean](Status, (x: RawReply) => {parseSingle(x); true }) orElse
166 | (_longPD andThen (_ > 0)) orElse
167 | (_rawBulkPD andThen (_.isDefined))
168 |
169 | def _multiBulkPD[A, B[_] <: GenTraversable[_]](implicit cbf: CanBuildFrom[_, A, B[A]], pd: PartialDeserializer[A]) =
170 | new PrefixDeserializer[B[A]](Multi, parseMultiBulk(_)(cbf, pd))
171 |
172 | def _pubSubMessagePD = new PrefixDeserializer(Multi, parsePubSubMsg _)
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/KeyOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import scala.concurrent.Future
4 |
5 | import org.scalatest.junit.JUnitRunner
6 | import org.junit.runner.RunWith
7 | import com.redis.RedisSpecBase
8 |
9 |
10 | @RunWith(classOf[JUnitRunner])
11 | class KeyOperationsSpec extends RedisSpecBase {
12 |
13 | import com.redis.serialization.DefaultFormats._
14 |
15 | describe("keys") {
16 | it("should fetch keys") {
17 | val prepare = Seq(client.set("anshin-1", "debasish"), client.set("anshin-2", "maulindu"))
18 | val prepareRes = Future.sequence(prepare).futureValue
19 |
20 | val res = client.keys("anshin*")
21 | res.futureValue should have length (2)
22 | }
23 |
24 | it("should fetch keys with spaces") {
25 | val prepare = Seq(client.set("anshin 1", "debasish"), client.set("anshin 2", "maulindu"))
26 | val prepareRes = Future.sequence(prepare).futureValue
27 |
28 | val res = client.keys("anshin*")
29 | res.futureValue should have length (2)
30 | }
31 | }
32 |
33 | describe("randomkey") {
34 | it("should give") {
35 | val prepare = Seq(client.set("anshin-1", "debasish"), client.set("anshin-2", "maulindu"))
36 | val prepareRes = Future.sequence(prepare).futureValue
37 |
38 | val res: Future[Option[String]] = client.randomkey
39 | res.futureValue.get should startWith ("anshin")
40 | }
41 | }
42 |
43 | describe("rename") {
44 | it("should give") {
45 | val prepare = Seq(client.set("anshin-1", "debasish"), client.set("anshin-2", "maulindu"))
46 | val prepareRes = Future.sequence(prepare).futureValue
47 |
48 | client.rename("anshin-2", "anshin-2-new").futureValue should be (true)
49 | val thrown = intercept[Exception] { client.rename("anshin-2", "anshin-2-new").futureValue }
50 | thrown.getCause.getMessage should equal ("ERR no such key")
51 | }
52 | }
53 |
54 | describe("renamenx") {
55 | it("should give") {
56 | val prepare = Seq(client.set("anshin-1", "debasish"), client.set("anshin-2", "maulindu"))
57 | val prepareRes = Future.sequence(prepare).futureValue
58 |
59 | client.renamenx("anshin-2", "anshin-2-new").futureValue should be (true)
60 | client.renamenx("anshin-1", "anshin-2-new").futureValue should be (false)
61 | }
62 | }
63 |
64 | describe("dbsize") {
65 | it("should give") {
66 | val prepare = Seq(client.set("anshin-1", "debasish"), client.set("anshin-2", "maulindu"))
67 | val prepareRes = Future.sequence(prepare).futureValue
68 |
69 | client.dbsize.futureValue should equal (2)
70 | }
71 | }
72 |
73 | describe("exists") {
74 | it("should give") {
75 | val prepare = Seq(client.set("anshin-1", "debasish"), client.set("anshin-2", "maulindu"))
76 | val prepareRes = Future.sequence(prepare).futureValue
77 |
78 | client.exists("anshin-2").futureValue should be (true)
79 | client.exists("anshin-1").futureValue should be (true)
80 | client.exists("anshin-3").futureValue should be (false)
81 | }
82 | }
83 |
84 | describe("del") {
85 | it("should give") {
86 | val prepare = Seq(client.set("anshin-1", "debasish"), client.set("anshin-2", "maulindu"))
87 | val prepareRes = Future.sequence(prepare).futureValue
88 |
89 | client.del("anshin-2", "anshin-1").futureValue should equal (2)
90 | client.del("anshin-2", "anshin-1").futureValue should equal (0)
91 | }
92 | }
93 |
94 | describe("type") {
95 | it("should return data type") {
96 | // prepare
97 | val _ = Future.sequence(
98 | client.set("string", "value") ::
99 | client.lpush("list", "value") ::
100 | client.sadd("set", "value") ::
101 | client.hset("hash", "field", "value") ::
102 | client.zadd("zset", 1, "field") ::
103 | Nil
104 | ).futureValue
105 |
106 | // escaped api
107 | client.`type`("string").futureValue should equal ("string")
108 | client.`type`("list").futureValue should equal ("list")
109 | client.`type`("set").futureValue should equal("set")
110 |
111 | // alternative for convenience
112 | client.tpe("hash").futureValue should equal ("hash")
113 | client.tpe("zset").futureValue should equal ("zset")
114 | client.tpe("notexist").futureValue should equal ("none")
115 | }
116 | }
117 |
118 | describe("sort") {
119 | it("should give") {
120 | val prepare = Seq(
121 | client.hset("hash-1", "description", "one"),
122 | client.hset("hash-1", "order", "100"),
123 | client.hset("hash-2", "description", "two"),
124 | client.hset("hash-2", "order", "25"),
125 | client.hset("hash-3", "description", "three"),
126 | client.hset("hash-3", "order", "50"),
127 | client.sadd("alltest", 1),
128 | client.sadd("alltest", 2),
129 | client.sadd("alltest", 3)
130 | )
131 | val prepareRes = Future.sequence(prepare).futureValue
132 |
133 | client.sort("alltest").futureValue should equal(List("1", "2", "3"))
134 | client.sort("alltest", Some(Tuple2(0, 1))).futureValue should equal(List("1"))
135 | client.sort("alltest", None, true).futureValue should equal(List("3", "2", "1"))
136 | client.sort("alltest", None, false, false, Some("hash-*->order")).futureValue should equal(List("2", "3", "1"))
137 | client.sort("alltest", None, false, false, None, List("hash-*->description")).futureValue should equal(List("one", "two", "three"))
138 | client.sort("alltest", None, false, false, None, List("hash-*->description", "hash-*->order")).futureValue should equal(List("one", "100", "two", "25", "three", "50"))
139 | }
140 | }
141 |
142 | describe("sortNStore") {
143 | it("should give") {
144 | val prepare = Seq(
145 | client.sadd("alltest", 10),
146 | client.sadd("alltest", 30),
147 | client.sadd("alltest", 3),
148 | client.sadd("alltest", 1)
149 | )
150 | val prepareRes = Future.sequence(prepare).futureValue
151 |
152 | // default serialization : return String
153 | client.sortNStore("alltest", storeAt = "skey").futureValue should equal(4)
154 | client.lrange("skey", 0, 10).futureValue should equal(List("1", "3", "10", "30"))
155 |
156 | // Long serialization : return Long
157 | client.sortNStore("alltest", storeAt = "skey").futureValue should equal(4)
158 | client.lrange[Long]("skey", 0, 10).futureValue should equal(List(1, 3, 10, 30))
159 | }
160 | }
161 | }
162 |
163 |
164 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/TransactionOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import scala.concurrent.Future
4 |
5 | import org.scalatest.junit.JUnitRunner
6 | import org.junit.runner.RunWith
7 | import com.redis.RedisSpecBase
8 | import com.redis.protocol.Discarded
9 | import akka.actor.Status.Failure
10 | import com.redis.serialization.Deserializer._
11 |
12 |
13 | @RunWith(classOf[JUnitRunner])
14 | class TransactionOperationsSpec extends RedisSpecBase {
15 |
16 | import com.redis.serialization.DefaultFormats._
17 |
18 | describe("transactions with API") {
19 | it("should use API") {
20 | val result = client.withTransaction {c =>
21 | c.set("anshin-1", "debasish")
22 | c.exists("anshin-1")
23 | c.get("anshin-1")
24 | c.set("anshin-2", "debasish")
25 | c.lpush("anshin-3", "debasish")
26 | c.lpush("anshin-3", "maulindu")
27 | c.lrange("anshin-3", 0, -1)
28 | }
29 | result.futureValue should equal (List(true, true, Some("debasish"), true, 1, 2, List("maulindu", "debasish")))
30 | client.get("anshin-1").futureValue should equal(Some("debasish"))
31 | }
32 |
33 | it("should execute partial set and report failure on exec") {
34 | val result = client.withTransaction {c =>
35 | c.set("anshin-1", "debasish")
36 | c.exists("anshin-1")
37 | c.get("anshin-1")
38 | c.set("anshin-2", "debasish")
39 | c.lpush("anshin-2", "debasish")
40 | c.lpush("anshin-3", "maulindu")
41 | c.lrange("anshin-3", 0, -1)
42 | }
43 | val r = result.futureValue.asInstanceOf[List[_]].toVector
44 | r(0) should equal(true)
45 | r(1) should equal(true)
46 | r(2) should equal(Some("debasish"))
47 | r(3) should equal(true)
48 | r(4).asInstanceOf[akka.actor.Status.Failure].cause.isInstanceOf[Throwable] should equal(true)
49 | r(5) should equal(1)
50 | r(6) should equal(List("maulindu"))
51 | client.get("anshin-1").futureValue should equal(Some("debasish"))
52 | }
53 |
54 | it("should convert individual commands using serializers") {
55 | val result = client.withTransaction {c =>
56 | c.set("anshin-1", 10)
57 | c.exists("anshin-1")
58 | c.get[Long]("anshin-1")
59 | c.set("anshin-2", "debasish")
60 | c.lpush("anshin-3", 200)
61 | c.lpush("anshin-3", 100)
62 | c.lrange[Long]("anshin-3", 0, -1)
63 | }
64 | result.futureValue should equal (List(true, true, Some(10), true, 1, 2, List(100, 200)))
65 | }
66 |
67 | it("should not convert a byte array to an UTF-8 string") {
68 | val bytes = Array(0x85.toByte)
69 | val result = client.withTransaction {c =>
70 | c.set("anshin-1", 10)
71 | c.exists("anshin-1")
72 | c.get[Long]("anshin-1")
73 | c.set("bytes", bytes)
74 | c.get[Array[Byte]]("bytes")
75 | }
76 | result.futureValue
77 | .asInstanceOf[List[Any]]
78 | .last
79 | .asInstanceOf[Option[Array[Byte]]].get.toList should equal (bytes.toList)
80 | }
81 |
82 | it("should discard the txn queue since user tries to get the future value without exec") {
83 | val result = client.withTransaction {c =>
84 | c.set("anshin-1", "debasish").futureValue
85 | c.exists("anshin-1")
86 | c.get("anshin-1")
87 | c.set("anshin-2", "debasish")
88 | c.lpush("anshin-3", "debasish")
89 | c.lpush("anshin-3", "maulindu")
90 | c.lrange("anshin-3", 0, -1)
91 | }
92 | result.futureValue should equal (Discarded)
93 | client.get("anshin-1").futureValue should equal(None)
94 | }
95 |
96 | it("should execute a mix of transactional and non transactional operations") {
97 | client.set("k1", "v1").futureValue
98 | client.get("k1").futureValue should equal(Some("v1"))
99 | val result = client.withTransaction {c =>
100 | c.set("nri-1", "debasish")
101 | c.exists("nri-1")
102 | c.get("nri-1")
103 | c.set("nri-2", "debasish")
104 | c.lpush("nri-3", "debasish")
105 | c.lpush("nri-3", "maulindu")
106 | c.lrange("nri-3", 0, -1)
107 | }
108 | result.futureValue should equal (List(true, true, Some("debasish"), true, 1, 2, List("maulindu", "debasish")))
109 | }
110 |
111 | it("should execute transactional operations along with business logic") {
112 | val l = List(1,2,3,4) // just to have some business logic
113 | val result = client.withTransaction {c =>
114 | c.set("nri-1", "debasish")
115 | c.set("nri-2", "maulindu")
116 | val x = if (l.size == 4) c.get("nri-1") else c.get("nri-2")
117 | c.lpush("nri-3", "debasish")
118 | c.lpush("nri-3", "maulindu")
119 | c.lrange("nri-3", 0, -1)
120 | }
121 | result.futureValue should equal (List(true, true, Some("debasish"), 1, 2, List("maulindu", "debasish")))
122 | }
123 | }
124 |
125 | describe("Transactions with watch") {
126 | it("should fail if key is changed outside transaction") {
127 | client.watch("key1").futureValue should equal(true)
128 | client.set("key1", 1).futureValue should equal(true)
129 | val v = client.get[Long]("key1").futureValue.get + 20L
130 | val result = client.withTransaction {c =>
131 | c.set("key1", 100)
132 | c.get[Long]("key1")
133 | }
134 | result onComplete {
135 | case util.Success(_) =>
136 | case util.Failure(th) => th should equal(EmptyTxnResultException)
137 | }
138 | }
139 |
140 | it("should set key value") {
141 | client.watch("key1").futureValue should equal(true)
142 | val result = client.withTransaction {c =>
143 | c.set("key1", 100)
144 | c.get[Long]("key1")
145 | }
146 | result.futureValue should equal(List(true, Some(100)))
147 | }
148 |
149 | it("should not fail if key is unwatched") {
150 | client.watch("key1").futureValue should equal(true)
151 | client.set("key1", 1).futureValue should equal(true)
152 | val result = client.withTransaction {c =>
153 | c.set("key1", 100)
154 | c.get[Long]("key1")
155 | }
156 | result onComplete {
157 | case util.Success(_) =>
158 | case util.Failure(th) => th should equal(EmptyTxnResultException)
159 | }
160 | client.unwatch().futureValue should equal(true)
161 | client.set("key1", 5).futureValue should equal(true)
162 | val res = client.withTransaction {c =>
163 | c.set("key1", 100)
164 | c.get[Long]("key1")
165 | }
166 | res.futureValue should equal(List(true, Some(100)))
167 | }
168 | }
169 |
170 | }
171 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/StringOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import akka.pattern.ask
5 | import akka.util.Timeout
6 | import scala.language.existentials
7 | import protocol.StringCommands
8 | import serialization._
9 | import scala.concurrent.Future
10 |
11 |
12 | trait StringOperations { this: RedisOps =>
13 | import StringCommands._
14 |
15 | // GET (key)
16 | // gets the value for the specified key.
17 | def get[A](key: String)(implicit timeout: Timeout, reader: Reader[A]) =
18 | clientRef.ask(Get[A](key)).mapTo[Get[A]#Ret]
19 |
20 | // SET KEY (key, value)
21 | // sets the key with the specified value.
22 | def set(key: String, value: Stringified, exORpx: Option[SetExpiryOption] = None, nxORxx: Option[SetConditionOption] = None)
23 | (implicit timeout: Timeout) =
24 | clientRef.ask(Set(key, value, exORpx, nxORxx)).mapTo[Set#Ret]
25 |
26 | def set(key: String, value: Stringified, setOption: SetOption)(implicit timeout: Timeout) =
27 | clientRef.ask(Set(key, value, setOption)).mapTo[Set#Ret]
28 |
29 | def set(key: String, value: Stringified, exORpx: SetExpiryOption, nxORxx: SetConditionOption)
30 | (implicit timeout: Timeout) =
31 | clientRef.ask(Set(key, value, exORpx, nxORxx)).mapTo[Set#Ret]
32 |
33 |
34 | // GETSET (key, value)
35 | // is an atomic set this value and return the old value command.
36 | def getset[A](key: String, value: Stringified)(implicit timeout: Timeout, reader: Reader[A]) =
37 | clientRef.ask(GetSet[A](key, value)).mapTo[GetSet[A]#Ret]
38 |
39 | // SETNX (key, value)
40 | // sets the value for the specified key, only if the key is not there.
41 | def setnx(key: String, value: Stringified)(implicit timeout: Timeout) =
42 | clientRef.ask(SetNx(key, value)).mapTo[SetNx#Ret]
43 |
44 | // SETEX (key, expiry, value)
45 | // sets the value for the specified key, with an expiry
46 | def setex(key: String, expiry: Int, value: Stringified)(implicit timeout: Timeout) =
47 | clientRef.ask(SetEx(key, expiry, value)).mapTo[SetEx#Ret]
48 |
49 | // SETPX (key, expiry, value)
50 | // sets the value for the specified key, with an expiry in millis
51 | def psetex(key: String, expiryInMillis: Int, value: Stringified)(implicit timeout: Timeout) =
52 | clientRef.ask(PSetEx(key, expiryInMillis, value)).mapTo[PSetEx#Ret]
53 |
54 | // INCR (key)
55 | // increments the specified key by 1
56 | def incr(key: String)(implicit timeout: Timeout) =
57 | clientRef.ask(Incr(key)).mapTo[Incr#Ret]
58 |
59 | // INCRBY (key, by)
60 | // increments the specified key by increment
61 | def incrby(key: String, amount: Int)(implicit timeout: Timeout) =
62 | clientRef.ask(IncrBy(key, amount)).mapTo[IncrBy#Ret]
63 |
64 | // DECR (key)
65 | // decrements the specified key by 1
66 | def decr(key: String)(implicit timeout: Timeout) =
67 | clientRef.ask(Decr(key)).mapTo[Decr#Ret]
68 |
69 | // DECR (key, by)
70 | // decrements the specified key by increment
71 | def decrby(key: String, amount: Int)(implicit timeout: Timeout) =
72 | clientRef.ask(DecrBy(key, amount)).mapTo[DecrBy#Ret]
73 |
74 |
75 | // MGET (key, key, key, ...)
76 | // get the values of all the specified keys.
77 | def mget[A](keys: Seq[String])(implicit timeout: Timeout, reader: Reader[A]) =
78 | clientRef.ask(MGet[A](keys)).mapTo[MGet[A]#Ret]
79 |
80 | def mget[A](key: String, keys: String*)(implicit timeout: Timeout, reader: Reader[A]) =
81 | clientRef.ask(MGet[A](key, keys:_*)).mapTo[MGet[A]#Ret]
82 |
83 |
84 | // MSET (key1 value1 key2 value2 ..)
85 | // set the respective key value pairs. Overwrite value if key exists
86 | def mset(kvs: KeyValuePair*)(implicit timeout: Timeout) =
87 | clientRef.ask(MSet(kvs:_*)).mapTo[MSet#Ret]
88 |
89 | // MSETNX (key1 value1 key2 value2 ..)
90 | // set the respective key value pairs. Noop if any key exists
91 | def msetnx(kvs: KeyValuePair*)(implicit timeout: Timeout) =
92 | clientRef.ask(MSetNx(kvs:_*)).mapTo[MSetNx#Ret]
93 |
94 | // SETRANGE key offset value
95 | // Overwrites part of the string stored at key, starting at the specified offset,
96 | // for the entire length of value.
97 | def setrange(key: String, offset: Int, value: Stringified)(implicit timeout: Timeout) =
98 | clientRef.ask(SetRange(key, offset, value)).mapTo[SetRange#Ret]
99 |
100 | // GETRANGE key start end
101 | // Returns the substring of the string value stored at key, determined by the offsets
102 | // start and end (both are inclusive).
103 | def getrange[A](key: String, start: Int, end: Int)(implicit timeout: Timeout, reader: Reader[A]) =
104 | clientRef.ask(GetRange[A](key, start, end)).mapTo[GetRange[A]#Ret]
105 |
106 | // STRLEN key
107 | // gets the length of the value associated with the key
108 | def strlen(key: String)(implicit timeout: Timeout) =
109 | clientRef.ask(Strlen(key)).mapTo[Strlen#Ret]
110 |
111 | // APPEND KEY (key, value)
112 | // appends the key value with the specified value.
113 | def append(key: String, value: Stringified)(implicit timeout: Timeout) =
114 | clientRef.ask(Append(key, value)).mapTo[Append#Ret]
115 |
116 | // GETBIT key offset
117 | // Returns the bit value at offset in the string value stored at key
118 | def getbit(key: String, offset: Int)(implicit timeout: Timeout) =
119 | clientRef.ask(GetBit(key, offset)).mapTo[GetBit#Ret]
120 |
121 | // SETBIT key offset value
122 | // Sets or clears the bit at offset in the string value stored at key
123 | def setbit(key: String, offset: Int, value: Boolean)(implicit timeout: Timeout) =
124 | clientRef.ask(SetBit(key, offset, value)).mapTo[SetBit#Ret]
125 |
126 | // BITOP op destKey srcKey...
127 | // Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key.
128 | def bitop(op: String, destKey: String, srcKeys: Seq[String])(implicit timeout: Timeout) =
129 | clientRef.ask(BitOp(op, destKey, srcKeys)).mapTo[BitOp#Ret]
130 |
131 | def bitop(op: String, destKey: String, srcKey: String, srcKeys: String*)(implicit timeout: Timeout) =
132 | clientRef.ask(BitOp(op, destKey, srcKey, srcKeys: _*)).mapTo[BitOp#Ret]
133 |
134 | // BITCOUNT key range
135 | // Count the number of set bits in the given key within the optional range
136 | def bitcount(key: String, range: Option[(Int, Int)] = None)(implicit timeout: Timeout) =
137 | clientRef.ask(BitCount(key, range)).mapTo[BitCount#Ret]
138 |
139 | // BITPOS key bit start end
140 | // Return the position of the first bit set to 1 or 0 in a string
141 | def bitpos(key: String, bit: Boolean, start: Option[Int] = None, end: Option[Int] = None)(implicit timeout: Timeout) =
142 | clientRef.ask(BitPos(key, bit, start, end)).mapTo[BitPos#Ret]
143 |
144 | def bitpos(key: String, bit: Boolean, start: Int)(implicit timeout: Timeout): Future[BitPos#Ret] =
145 | bitpos(key, bit, Some(start))
146 |
147 | def bitpos(key: String, bit: Boolean, start: Int, end: Int)(implicit timeout: Timeout): Future[BitPos#Ret] =
148 | bitpos(key, bit, Some(start), Some(end))
149 | }
150 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/pipeline/ResponseHandling.scala:
--------------------------------------------------------------------------------
1 | package com.redis.pipeline
2 |
3 | import akka.io._
4 | import akka.io.TcpPipelineHandler.WithinActorContext
5 | import akka.actor.ActorRef
6 | import akka.actor.Status.Failure
7 | import akka.util.CompactByteString
8 | import com.redis.protocol._
9 | import com.redis.serialization.Deserializer
10 | import com.redis.serialization.Deserializer.Result
11 | import scala.collection.immutable.Queue
12 | import scala.annotation.tailrec
13 | import scala.language.existentials
14 |
15 |
16 | class ResponseHandling extends PipelineStage[WithinActorContext, Command, Command, Event, Event] {
17 |
18 | def apply(ctx: WithinActorContext) = new PipePair[Command, Command, Event, Event] {
19 | import ctx.{getLogger => log}
20 |
21 | private[this] val parser = new Deserializer()
22 | private[this] val redisClientRef: ActorRef = ctx.getContext.self
23 | private[this] var sentRequests = Queue.empty[RedisRequest]
24 | private[this] var txnMode = false
25 | private[this] var txnRequests = Queue.empty[RedisRequest]
26 |
27 | type ResponseHandler = CompactByteString => Iterable[Result]
28 |
29 | def pubSubHandler(handlerActor: ActorRef) : ResponseHandler = data => {
30 | def parsePushedEvents(data: CompactByteString) : Iterable[Result] = {
31 | import com.redis.serialization.PartialDeserializer.pubSubMessagePD
32 | parser.parse(data, pubSubMessagePD) match {
33 | case Result.Ok(reply, remaining) =>
34 | val result = reply match {
35 | case err: RedisError => Failure(err)
36 | case _ => reply
37 | }
38 | handlerActor ! result
39 | parsePushedEvents( remaining )
40 | case Result.NeedMoreData =>
41 | ctx.nothing
42 | case Result.Failed(err, data) =>
43 | log.error(err, "Response parsing failed: {}", data.utf8String.replace("\r\n", "\\r\\n"))
44 | ctx.singleCommand(Close)
45 | }
46 | }
47 | if (sentRequests.isEmpty) {
48 | log.debug("Received data from server: {}", data.utf8String.replace("\r\n", "\\r\\n"))
49 | parsePushedEvents(data)
50 | }
51 | else defaultHandler(data)
52 | }
53 |
54 | val defaultHandler : CompactByteString => Iterable[Result] = {
55 |
56 | @tailrec
57 | def parseExecResponse(data: CompactByteString, acc: Iterable[Any]): Iterable[Any] = {
58 | if (txnRequests isEmpty) acc
59 | else {
60 | // process every response with the appropriate de-serializer that we have accumulated in txnRequests
61 | parser.parse(data, txnRequests.head.command.des) match {
62 | case Result.Ok(reply, remaining) =>
63 | val result = reply match {
64 | case err: RedisError => Failure(err)
65 | case _ => reply
66 | }
67 | val RedisRequest(commander, cmd) = txnRequests.head
68 | commander.tell(result, redisClientRef)
69 | txnRequests = txnRequests.tail
70 | parseExecResponse(remaining, acc ++ List(result))
71 |
72 | case Result.NeedMoreData =>
73 | if (data isEmpty) ctx.singleEvent(RequestQueueEmpty)
74 | else parseExecResponse(data, acc)
75 |
76 | case Result.Failed(err, data) =>
77 | log.error(err, "Response parsing failed: {}", data.utf8String.replace("\r\n", "\\r\\n"))
78 | ctx.singleCommand(Close)
79 | }
80 | }
81 | }
82 |
83 | (data: CompactByteString) => {
84 | @tailrec
85 | def parseAndDispatch(data: CompactByteString): Iterable[Result] =
86 | if (sentRequests.isEmpty) ctx.singleEvent(RequestQueueEmpty)
87 | else {
88 | val RedisRequest(commander, cmd) = sentRequests.head
89 |
90 | // we have an Exec : need to parse the response which will be a collection of
91 | // MultiBulk and then end transaction mode
92 | if (cmd == TransactionCommands.Exec) {
93 | if (data isEmpty) ctx.singleEvent(RequestQueueEmpty)
94 | else {
95 | val result =
96 | if (Deserializer.nullMultiBulk(data)) {
97 | txnRequests = txnRequests.drop(txnRequests.size)
98 | Failure(Deserializer.EmptyTxnResultException)
99 | } else parseExecResponse(data.splitAt(data.indexOf(Lf) + 1)._2.compact, List.empty[Result])
100 |
101 | commander.tell(result, redisClientRef)
102 | txnMode = false
103 | }
104 | }
105 | parser.parse(data, cmd.des) match {
106 | case Result.Ok(reply, remaining) =>
107 | val result = reply match {
108 | case err: RedisError => Failure(err)
109 | case _ => reply
110 | }
111 | log.debug("RESULT: {}", result)
112 | if (reply != Queued) commander.tell(result, redisClientRef)
113 | sentRequests = sentRequests.tail
114 | parseAndDispatch(remaining)
115 |
116 | case Result.NeedMoreData => ctx.singleEvent(RequestQueueEmpty)
117 |
118 | case Result.Failed(err, data) =>
119 | log.error(err, "Response parsing failed: {}", data.utf8String.replace("\r\n", "\\r\\n"))
120 | commander.tell(Failure(err), redisClientRef)
121 | ctx.singleCommand(Close)
122 | }
123 | }
124 |
125 | log.debug("Received data from server: {}", data.utf8String.replace("\r\n", "\\r\\n"))
126 | parseAndDispatch(data)
127 | }
128 | }
129 |
130 | private[this] var handleResponse : ResponseHandler = defaultHandler
131 |
132 |
133 | val commandPipeline = (cmd: Command) => cmd match {
134 | case req @ RedisRequest(commander, cmd ) if cmd.isInstanceOf[PubSubCommands.PubSubCommand] =>
135 | handleResponse = pubSubHandler( commander )
136 | ctx singleCommand req
137 | case req: RedisRequest =>
138 |
139 | // Multi begins a txn mode & Discard ends a txn mode
140 | if (req.command == TransactionCommands.Multi) txnMode = true
141 | else if (req.command == TransactionCommands.Discard) txnMode = false
142 |
143 | // queue up all commands between multi & exec
144 | // this is an optimization that sends commands in bulk for transaction mode
145 | if (txnMode && req.command != TransactionCommands.Multi && req.command != TransactionCommands.Exec)
146 | txnRequests :+= req
147 |
148 | log.debug("Sending {}, previous head: {}", req.command, sentRequests.headOption.map(_.command))
149 | sentRequests :+= req
150 | ctx singleCommand req
151 |
152 | case _ => ctx singleCommand cmd
153 | }
154 |
155 | val eventPipeline = (evt: Event) => evt match {
156 |
157 | case Tcp.Received(data: CompactByteString) =>
158 | // println("Received data from server: {}", data.utf8String.replace("\r\n", "\\r\\n"))
159 | handleResponse(data)
160 |
161 | case _ => ctx singleEvent evt
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/RedisConnection.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 |
3 | import java.net.InetSocketAddress
4 | import java.util.concurrent.TimeUnit
5 |
6 | import akka.actor.Status.Failure
7 | import akka.routing.Listen
8 | import com.redis.RedisConnection.CommandRejected
9 |
10 | import scala.annotation.tailrec
11 | import scala.collection.immutable.Queue
12 | import scala.concurrent.duration.Duration
13 | import scala.language.existentials
14 |
15 | import akka.actor._
16 | import akka.io.{BackpressureBuffer, IO, Tcp, TcpPipelineHandler}
17 | import com.redis.RedisClientSettings.ReconnectionSettings
18 | import pipeline._
19 | import protocol._
20 |
21 | object RedisConnection {
22 | case class CommandRejected(msg: String, cmd: Any) extends Throwable(msg)
23 |
24 | def props(remote: InetSocketAddress, settings: RedisClientSettings) = Props(classOf[RedisConnection], remote, settings)
25 | }
26 |
27 | private [redis] class RedisConnection(remote: InetSocketAddress, settings: RedisClientSettings)
28 | extends Actor with ActorLogging {
29 | import Tcp._
30 | import context.system
31 |
32 | private[this] var pendingRequests = Queue.empty[RedisRequest]
33 | private[this] var txnRequests = Queue.empty[RedisRequest]
34 | private[this] var reconnectionSchedule = settings.reconnectionSettings.newSchedule
35 |
36 | IO(Tcp) ! Connect(remote)
37 |
38 | def receive = unconnected
39 |
40 | def unconnected: Receive = {
41 | case cmd: RedisCommand[_] =>
42 | log.info("Attempting to send command before connected: {}", cmd)
43 | addPendingRequest(cmd)
44 |
45 | case Connected(remote, _) =>
46 | log.info("Connected to redis server {}:{}.", remote.getHostName, remote.getPort)
47 | val connection = sender
48 | val pipe = context.actorOf(TcpPipelineHandler.props(init, connection, self), name = "pipeline")
49 | connection ! Register(pipe)
50 | sendAllPendingRequests(pipe)
51 | context become (running(pipe))
52 | context watch pipe
53 | reconnectionSchedule = settings.reconnectionSettings.newSchedule
54 |
55 | case CommandFailed(c: Connect) =>
56 | if (reconnectionSchedule.maxAttempts > 0 && reconnectionSchedule.attempts < reconnectionSchedule.maxAttempts ) {
57 | val delayMs = reconnectionSchedule.nextDelayMs
58 | log.error("Connect failed for {} with {}. Reconnecting in {} ms with attempt {} ... ", c.remoteAddress, c.failureMessage, delayMs,reconnectionSchedule.attempts)
59 | context become unconnected
60 | context.system.scheduler.scheduleOnce(Duration(delayMs, TimeUnit.MILLISECONDS), IO(Tcp), Connect(remote))(context.dispatcher, self)
61 | } else {
62 | log.error("Connect failed for {} with {}. Stopping... ", c.remoteAddress, c.failureMessage)
63 | context stop self
64 | }
65 | }
66 |
67 | def transactional(pipe: ActorRef): Receive = withTerminationManagement {
68 | case TransactionCommands.Exec =>
69 | sendAllTxnRequests(pipe)
70 | sendRequest(pipe, RedisRequest(sender, TransactionCommands.Exec))
71 | context become (running(pipe))
72 |
73 | case TransactionCommands.Discard =>
74 | txnRequests = txnRequests.drop(txnRequests.size)
75 | sendRequest(pipe, RedisRequest(sender, TransactionCommands.Discard))
76 | context become (running(pipe))
77 |
78 | // this should not happen
79 | // if it comes allow to flow through and Redis will report an error
80 | case TransactionCommands.Multi =>
81 | sendRequest(pipe, RedisRequest(sender, TransactionCommands.Multi))
82 |
83 | case cmd: RedisCommand[_] =>
84 | log.debug("Received a command in Multi: {}", cmd)
85 | addTxnRequest(cmd)
86 |
87 | }
88 |
89 | def subscription(pipe: ActorRef, handler: ActorRef): Receive = withTerminationManagement {
90 | case command: PubSubCommands.SubscribeCommand =>
91 | handler ! Listen( sender() )
92 | sendRequest(pipe, RedisRequest(handler, command))
93 | case command: PubSubCommands.PubSubCommand =>
94 | sendRequest(pipe, RedisRequest(handler, command))
95 | case ConnectionCommands.Quit =>
96 | sendRequest(pipe, RedisRequest(sender, ConnectionCommands.Quit))
97 | case cmd =>
98 | val message = s"Command '$cmd' is not allowed in subscribed state"
99 | log.warning( message )
100 | sender ! Failure( CommandRejected( message, cmd) )
101 | }
102 |
103 | def running(pipe: ActorRef): Receive = withTerminationManagement {
104 | case TransactionCommands.Multi =>
105 | sendRequest(pipe, RedisRequest(sender, TransactionCommands.Multi))
106 | context become (transactional(pipe))
107 |
108 | case command: PubSubCommands.SubscribeCommand =>
109 | log.debug( "Switching to subscription state." )
110 | val handler = context.actorOf( PubSubHandler.props, "pub-sub")
111 | context become subscription(pipe, handler)
112 | handler ! Listen( sender() )
113 | sendRequest(pipe, RedisRequest(handler, command))
114 |
115 | case command: RedisCommand[_] =>
116 | sendRequest(pipe, RedisRequest(sender, command))
117 |
118 | case init.Event(BackpressureBuffer.HighWatermarkReached) =>
119 | log.info("Backpressure is too high. Start buffering...")
120 | context become (buffering(pipe))
121 |
122 | case c: CloseCommand =>
123 | log.info("Got to close ..")
124 | sendAllPendingRequests(pipe)
125 | context become (closing(pipe))
126 | }
127 |
128 | def buffering(pipe: ActorRef): Receive = withTerminationManagement {
129 | case cmd: RedisCommand[_] =>
130 | log.debug("Received a command while buffering: {}", cmd)
131 | addPendingRequest(cmd)
132 |
133 | case init.Event(BackpressureBuffer.LowWatermarkReached) =>
134 | log.info("Client backpressure became lower, resuming...")
135 | context become running(pipe)
136 | sendAllPendingRequests(pipe)
137 | }
138 |
139 | def closing(pipe: ActorRef): Receive = withTerminationManagement {
140 | case init.Event(RequestQueueEmpty) =>
141 | log.debug("All done.")
142 | context stop self
143 |
144 | case init.Event(Closed) =>
145 | log.debug("Closed")
146 | context stop self
147 | }
148 |
149 | def withTerminationManagement(handler: Receive): Receive = handler orElse {
150 | case Terminated(x) => {
151 | if (reconnectionSchedule.attempts < reconnectionSchedule.maxAttempts) {
152 | val delayMs = reconnectionSchedule.nextDelayMs
153 | log.error("Child termination detected: {}. Reconnecting in {} ms... ", x, delayMs)
154 | context become unconnected
155 | context.system.scheduler.scheduleOnce(Duration(delayMs, TimeUnit.MILLISECONDS), IO(Tcp), Connect(remote))(context.dispatcher, self)
156 | } else {
157 | log.error("Child termination detected: {}", x)
158 | context stop self
159 | }
160 | }
161 | }
162 |
163 | def addPendingRequest(cmd: RedisCommand[_]): Unit =
164 | pendingRequests :+= RedisRequest(sender, cmd)
165 |
166 | def addTxnRequest(cmd: RedisCommand[_]): Unit =
167 | txnRequests :+= RedisRequest(sender, cmd)
168 |
169 | def sendRequest(pipe: ActorRef, req: RedisRequest): Unit = {
170 | pipe ! init.Command(req)
171 | }
172 |
173 | @tailrec
174 | final def sendAllPendingRequests(pipe: ActorRef): Unit =
175 | if (pendingRequests.nonEmpty) {
176 | val request = pendingRequests.head
177 | self.tell( request.command, request.sender )
178 | pendingRequests = pendingRequests.tail
179 | sendAllPendingRequests(pipe)
180 | }
181 |
182 | @tailrec
183 | final def sendAllTxnRequests(pipe: ActorRef): Unit =
184 | if (txnRequests.nonEmpty) {
185 | sendRequest(pipe, txnRequests.head)
186 | txnRequests = txnRequests.tail
187 | sendAllTxnRequests(pipe)
188 | }
189 |
190 | val init = {
191 | import RedisClientSettings._
192 | import settings._
193 |
194 | val stages = Seq(
195 | Some(new ResponseHandling),
196 | Some(new Serializing),
197 | backpressureBufferSettings map {
198 | case BackpressureBufferSettings(lowBytes, highBytes, maxBytes) =>
199 | new BackpressureBuffer(lowBytes, highBytes, maxBytes)
200 | }
201 | ).flatten.reduceLeft(_ >> _)
202 |
203 | TcpPipelineHandler.withLogger(log, stages)
204 | }
205 |
206 | }
207 |
--------------------------------------------------------------------------------
/src/main/scala/akka/io/TcpPipelineHandler.scala:
--------------------------------------------------------------------------------
1 | /**
2 | * Borrowed from Akka 2.2.4 due to the removal of pipeline in Akka 2.3
3 | * TODO Remove after migrating to Akka 2.4 reactive stream
4 | * https://github.com/debasishg/scala-redis-nb/issues/65
5 | * https://github.com/debasishg/scala-redis-nb/issues/66
6 | *
7 | * Copyright (C) 2009-2013 Typesafe Inc.
8 | */
9 |
10 | package akka.io
11 |
12 | import scala.beans.BeanProperty
13 | import scala.util.{ Failure, Success }
14 | import akka.actor._
15 | import akka.dispatch.{ RequiresMessageQueue, UnboundedMessageQueueSemantics }
16 | import akka.util.ByteString
17 | import akka.event.Logging
18 | import akka.event.LoggingAdapter
19 |
20 | object TcpPipelineHandler {
21 |
22 | /**
23 | * This class wraps up a pipeline with its external (i.e. “top”) command and
24 | * event types and providing unique wrappers for sending commands and
25 | * receiving events (nested and non-static classes which are specific to each
26 | * instance of [[Init]]). All events emitted by the pipeline will be sent to
27 | * the registered handler wrapped in an Event.
28 | */
29 | abstract class Init[Ctx <: PipelineContext, Cmd, Evt](
30 | val stages: PipelineStage[_ >: Ctx <: PipelineContext, Cmd, Tcp.Command, Evt, Tcp.Event]) {
31 |
32 | /**
33 | * This method must be implemented to return the [[PipelineContext]]
34 | * necessary for the operation of the given [[PipelineStage]].
35 | */
36 | def makeContext(actorContext: ActorContext): Ctx
37 |
38 | /**
39 | * Java API: construct a command to be sent to the [[TcpPipelineHandler]]
40 | * actor.
41 | */
42 | def command(cmd: Cmd): Command = Command(cmd)
43 |
44 | /**
45 | * Java API: extract a wrapped event received from the [[TcpPipelineHandler]]
46 | * actor.
47 | *
48 | * @throws MatchError if the given object is not an Event matching this
49 | * specific Init instance.
50 | */
51 | def event(evt: AnyRef): Evt = evt match {
52 | case Event(evt) ⇒ evt
53 | }
54 |
55 | /**
56 | * Wrapper class for commands to be sent to the [[TcpPipelineHandler]] actor.
57 | */
58 | case class Command(@BeanProperty cmd: Cmd) extends NoSerializationVerificationNeeded
59 |
60 | /**
61 | * Wrapper class for events emitted by the [[TcpPipelineHandler]] actor.
62 | */
63 | case class Event(@BeanProperty evt: Evt) extends NoSerializationVerificationNeeded
64 | }
65 |
66 | /**
67 | * This interface bundles logging and ActorContext for Java.
68 | */
69 | trait WithinActorContext extends HasLogging with HasActorContext
70 |
71 | def withLogger[Cmd, Evt](log: LoggingAdapter,
72 | stages: PipelineStage[_ >: WithinActorContext <: PipelineContext, Cmd, Tcp.Command, Evt, Tcp.Event]): Init[WithinActorContext, Cmd, Evt] =
73 | new Init[WithinActorContext, Cmd, Evt](stages) {
74 | override def makeContext(ctx: ActorContext): WithinActorContext = new WithinActorContext {
75 | override def getLogger = log
76 | override def getContext = ctx
77 | }
78 | }
79 |
80 | /**
81 | * Wrapper class for management commands sent to the [[TcpPipelineHandler]] actor.
82 | */
83 | case class Management(@BeanProperty cmd: AnyRef)
84 |
85 | /**
86 | * This is a new Tcp.Command which the pipeline can emit to effect the
87 | * sending a message to another actor. Using this instead of doing the send
88 | * directly has the advantage that other pipeline stages can also see and
89 | * possibly transform the send.
90 | */
91 | case class Tell(receiver: ActorRef, msg: Any, sender: ActorRef) extends Tcp.Command
92 |
93 | /**
94 | * The pipeline may want to emit a [[Tcp.Event]] to the registered handler
95 | * actor, which is enabled by emitting this [[Tcp.Command]] wrapping an event
96 | * instead. The [[TcpPipelineHandler]] actor will upon reception of this command
97 | * forward the wrapped event to the handler.
98 | */
99 | case class TcpEvent(@BeanProperty evt: Tcp.Event) extends Tcp.Command
100 |
101 | /**
102 | * create [[akka.actor.Props]] for a pipeline handler
103 | */
104 | def props[Ctx <: PipelineContext, Cmd, Evt](init: TcpPipelineHandler.Init[Ctx, Cmd, Evt], connection: ActorRef, handler: ActorRef) =
105 | Props(classOf[TcpPipelineHandler[_, _, _]], init, connection, handler)
106 |
107 | }
108 |
109 | /**
110 | * This actor wraps a pipeline and forwards commands and events between that
111 | * one and a [[Tcp]] connection actor. In order to inject commands into the
112 | * pipeline send an [[TcpPipelineHandler.Init.Command]] message to this actor; events will be sent
113 | * to the designated handler wrapped in [[TcpPipelineHandler.Init.Event]] messages.
114 | *
115 | * When the designated handler terminates the TCP connection is aborted. When
116 | * the connection actor terminates this actor terminates as well; the designated
117 | * handler may want to watch this actor’s lifecycle.
118 | *
119 | * IMPORTANT:
120 | *
121 | * Proper function of this actor (and of other pipeline stages like [[TcpReadWriteAdapter]]
122 | * depends on the fact that stages handling TCP commands and events pass unknown
123 | * subtypes through unaltered. There are more commands and events than are declared
124 | * within the [[Tcp]] object and you can even define your own.
125 | */
126 | class TcpPipelineHandler[Ctx <: PipelineContext, Cmd, Evt](
127 | init: TcpPipelineHandler.Init[Ctx, Cmd, Evt],
128 | connection: ActorRef,
129 | handler: ActorRef)
130 | extends Actor with RequiresMessageQueue[UnboundedMessageQueueSemantics] {
131 |
132 | import init._
133 | import TcpPipelineHandler._
134 |
135 | // sign death pact
136 | context watch connection
137 | // watch so we can Close
138 | context watch handler
139 |
140 | val ctx = init.makeContext(context)
141 |
142 | val pipes = PipelineFactory.buildWithSinkFunctions(ctx, init.stages)({
143 | case Success(cmd) ⇒
144 | cmd match {
145 | case Tell(receiver, msg, sender) ⇒ receiver.tell(msg, sender)
146 | case TcpEvent(ev) ⇒ handler ! ev
147 | case _ ⇒ connection ! cmd
148 | }
149 | case Failure(ex) ⇒ throw ex
150 | }, {
151 | case Success(evt) ⇒ handler ! Event(evt)
152 | case Failure(ex) ⇒ throw ex
153 | })
154 |
155 | def receive = {
156 | case Command(cmd) ⇒ pipes.injectCommand(cmd)
157 | case evt: Tcp.Event ⇒ pipes.injectEvent(evt)
158 | case Management(cmd) ⇒ pipes.managementCommand(cmd)
159 | case Terminated(`handler`) ⇒ connection ! Tcp.Abort
160 | case Terminated(`connection`) ⇒ context.stop(self)
161 | }
162 |
163 | }
164 |
165 | /**
166 | * Adapts a ByteString oriented pipeline stage to a stage that communicates via Tcp Commands and Events. Every ByteString
167 | * passed down to this stage will be converted to Tcp.Write commands, while incoming Tcp.Receive events will be unwrapped
168 | * and their contents passed up as raw ByteStrings. This adapter should be used together with TcpPipelineHandler.
169 | *
170 | * While this adapter communicates to the stage above it via raw ByteStrings, it is possible to inject Tcp Command
171 | * by sending them to the management port, and the adapter will simply pass them down to the stage below. Incoming Tcp Events
172 | * that are not Receive events will be passed downwards wrapped in a [[TcpPipelineHandler.TcpEvent]]; the [[TcpPipelineHandler]] will
173 | * send these notifications to the registered event handler actor.
174 | */
175 | class TcpReadWriteAdapter extends PipelineStage[PipelineContext, ByteString, Tcp.Command, ByteString, Tcp.Event] {
176 | import TcpPipelineHandler.TcpEvent
177 |
178 | override def apply(ctx: PipelineContext) = new PipePair[ByteString, Tcp.Command, ByteString, Tcp.Event] {
179 |
180 | override val commandPipeline = {
181 | data: ByteString ⇒ ctx.singleCommand(Tcp.Write(data))
182 | }
183 |
184 | override val eventPipeline = (evt: Tcp.Event) ⇒ evt match {
185 | case Tcp.Received(data) ⇒ ctx.singleEvent(data)
186 | case ev: Tcp.Event ⇒ ctx.singleCommand(TcpEvent(ev))
187 | }
188 |
189 | override val managementPort: Mgmt = {
190 | case cmd: Tcp.Command ⇒ ctx.singleCommand(cmd)
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/api/SortedSetOperations.scala:
--------------------------------------------------------------------------------
1 | package com.redis
2 | package api
3 |
4 | import serialization._
5 | import akka.pattern.ask
6 | import akka.util.Timeout
7 | import com.redis.protocol.SortedSetCommands
8 |
9 |
10 | trait SortedSetOperations { this: RedisOps =>
11 | import SortedSetCommands._
12 |
13 | // ZADD (Variadic: >= 2.4)
14 | // Add the specified members having the specified score to the sorted set stored at key.
15 | def zadd(key: String, scoreMembers: Seq[ScoredValue])
16 | (implicit timeout: Timeout) =
17 | clientRef.ask(ZAdd(key, scoreMembers)).mapTo[ZAdd#Ret]
18 |
19 | def zadd(key: String, score: Double, member: Stringified)
20 | (implicit timeout: Timeout) =
21 | clientRef.ask(ZAdd(key, score, member)).mapTo[ZAdd#Ret]
22 |
23 | def zadd(key: String, scoreMember: ScoredValue, scoreMembers: ScoredValue*)
24 | (implicit timeout: Timeout) =
25 | clientRef.ask(ZAdd(key, scoreMember +: scoreMembers)).mapTo[ZAdd#Ret]
26 |
27 |
28 | // ZREM (Variadic: >= 2.4)
29 | // Remove the specified members from the sorted set value stored at key.
30 | def zrem(key: String, members: Seq[Stringified])(implicit timeout: Timeout) =
31 | clientRef.ask(ZRem(key, members)).mapTo[ZRem#Ret]
32 |
33 | def zrem(key: String, member: Stringified, members: Stringified*)(implicit timeout: Timeout) =
34 | clientRef.ask(ZRem(key, member, members:_*)).mapTo[ZRem#Ret]
35 |
36 |
37 | // ZINCRBY
38 | //
39 | def zincrby(key: String, incr: Double, member: Stringified)(implicit timeout: Timeout) =
40 | clientRef.ask(ZIncrby(key, incr, member)).mapTo[ZIncrby#Ret]
41 |
42 | // ZCARD
43 | //
44 | def zcard(key: String)(implicit timeout: Timeout) =
45 | clientRef.ask(ZCard(key)).mapTo[ZCard#Ret]
46 |
47 | // ZSCORE
48 | //
49 | def zscore(key: String, element: Stringified)(implicit timeout: Timeout) =
50 | clientRef.ask(ZScore(key, element)).mapTo[ZScore#Ret]
51 |
52 | // ZRANGE
53 | //
54 | def zrange[A](key: String, start: Int = 0, end: Int = -1)
55 | (implicit timeout: Timeout, reader: Reader[A]) =
56 | clientRef.ask(ZRange[A](key, start, end)).mapTo[ZRange[A]#Ret]
57 |
58 | def zrevrange[A](key: String, start: Int = 0, end: Int = -1)
59 | (implicit timeout: Timeout, reader: Reader[A]) =
60 | clientRef.ask(ZRevRange[A](key, start, end)).mapTo[ZRevRange[A]#Ret]
61 |
62 | def zrangeWithScores[A](key: String, start: Int = 0, end: Int = -1)
63 | (implicit timeout: Timeout, reader: Reader[A]) =
64 | clientRef.ask(ZRangeWithScores[A](key, start, end)).mapTo[ZRangeWithScores[A]#Ret]
65 |
66 | def zrevrangeWithScores[A](key: String, start: Int = 0, end: Int = -1)
67 | (implicit timeout: Timeout, reader: Reader[A]) =
68 | clientRef.ask(ZRevRangeWithScores[A](key, start, end)).mapTo[ZRevRangeWithScores[A]#Ret]
69 |
70 | // ZRANGEBYSCORE
71 | //
72 | def zrangeByScore[A](key: String,
73 | min: Double = `-Inf`, minInclusive: Boolean = true,
74 | max: Double = `+Inf`, maxInclusive: Boolean = true,
75 | limit: Option[(Int, Int)] = None)
76 | (implicit timeout: Timeout, reader: Reader[A]) =
77 | clientRef
78 | .ask(ZRangeByScore[A](key, min, minInclusive, max, maxInclusive, limit))
79 | .mapTo[ZRangeByScore[A]#Ret]
80 |
81 | def zrevrangeByScore[A](key: String,
82 | max: Double = `+Inf`, maxInclusive: Boolean = true,
83 | min: Double = `-Inf`, minInclusive: Boolean = true,
84 | limit: Option[(Int, Int)] = None)
85 | (implicit timeout: Timeout, reader: Reader[A]) =
86 | clientRef
87 | .ask(ZRevRangeByScore[A](key, min, minInclusive, max, maxInclusive, limit))
88 | .mapTo[ZRevRangeByScore[A]#Ret]
89 |
90 | def zrangeByScoreWithScores[A](key: String,
91 | min: Double = `-Inf`, minInclusive: Boolean = true,
92 | max: Double = `+Inf`, maxInclusive: Boolean = true,
93 | limit: Option[(Int, Int)] = None)
94 | (implicit timeout: Timeout, reader: Reader[A]) =
95 | clientRef
96 | .ask(ZRangeByScoreWithScores[A](key, min, minInclusive, max, maxInclusive, limit))
97 | .mapTo[ZRangeByScoreWithScores[A]#Ret]
98 |
99 | def zrevrangeByScoreWithScores[A](key: String,
100 | max: Double = `+Inf`, maxInclusive: Boolean = true,
101 | min: Double = `-Inf`, minInclusive: Boolean = true,
102 | limit: Option[(Int, Int)] = None)
103 | (implicit timeout: Timeout, reader: Reader[A]) =
104 | clientRef
105 | .ask(ZRevRangeByScoreWithScores[A](key, min, minInclusive, max, maxInclusive, limit))
106 | .mapTo[ZRevRangeByScoreWithScores[A]#Ret]
107 |
108 | // ZRANK
109 | // ZREVRANK
110 | //
111 | def zrank(key: String, member: Stringified)(implicit timeout: Timeout) =
112 | clientRef.ask(ZRank(key, member)).mapTo[ZRank#Ret]
113 |
114 | def zrevrank(key: String, member: Stringified)(implicit timeout: Timeout) =
115 | clientRef.ask(ZRevRank(key, member)).mapTo[ZRank#Ret]
116 |
117 |
118 | // ZREMRANGEBYRANK
119 | //
120 | def zremrangebyrank(key: String, start: Int = 0, end: Int = -1)(implicit timeout: Timeout) =
121 | clientRef.ask(ZRemRangeByRank(key, start, end)).mapTo[ZRemRangeByRank#Ret]
122 |
123 | // ZREMRANGEBYSCORE
124 | //
125 | def zremrangebyscore(key: String, start: Double = `-Inf`, end: Double = `+Inf`)
126 | (implicit timeout: Timeout) =
127 | clientRef.ask(ZRemRangeByScore(key, start, end)).mapTo[ZRemRangeByScore#Ret]
128 |
129 | // ZUNION
130 | //
131 | def zunionstore(dstKey: String, keys: Iterable[String], aggregate: Aggregate = SUM)(implicit timeout: Timeout) =
132 | clientRef.ask(ZUnionStore(dstKey, keys, aggregate)).mapTo[ZInterStore#Ret]
133 |
134 | // ZINTERSTORE
135 | //
136 | def zinterstore(dstKey: String, keys: Iterable[String], aggregate: Aggregate = SUM)(implicit timeout: Timeout) =
137 | clientRef.ask(ZInterStore(dstKey, keys, aggregate)).mapTo[ZInterStore#Ret]
138 |
139 | def zunionstoreweighted(dstKey: String, kws: Iterable[Product2[String, Double]], aggregate: Aggregate = SUM)
140 | (implicit timeout: Timeout) =
141 | clientRef.ask(ZUnionStoreWeighted(dstKey, kws, aggregate)).mapTo[ZUnionStoreWeighted#Ret]
142 |
143 | def zinterstoreweighted(dstKey: String, kws: Iterable[Product2[String, Double]], aggregate: Aggregate = SUM)
144 | (implicit timeout: Timeout) =
145 | clientRef.ask(ZInterStoreWeighted(dstKey, kws, aggregate)).mapTo[ZInterStoreWeighted#Ret]
146 |
147 | // ZCOUNT
148 | //
149 | def zcount(key: String,
150 | min: Double = `-Inf`, minInclusive: Boolean = true,
151 | max: Double = `+Inf`, maxInclusive: Boolean = true)
152 | (implicit timeout: Timeout) =
153 | clientRef.ask(ZCount(key, min, minInclusive, max, maxInclusive)).mapTo[ZCount#Ret]
154 |
155 | // ZLEXCOUNT
156 | def zlexcount(key: String,
157 | minKey: String = `-LexInf`, minInclusive: Boolean = true,
158 | maxKey: String = `+LexInf`, maxInclusive: Boolean = true)(implicit timeout: Timeout) =
159 | clientRef.ask(ZLexCount(key, minKey, minInclusive, maxKey, maxInclusive)).mapTo[ZLexCount#Ret]
160 |
161 | // ZRANGEBYLEX
162 | //
163 | def zrangebylex[A](key: String,
164 | minKey: String = `-LexInf`, minInclusive: Boolean = true,
165 | maxKey: String = `+LexInf`, maxInclusive: Boolean = true,
166 | limit: Option[(Int, Int)] = None)
167 | (implicit timeout: Timeout, reader: Reader[A]) =
168 | clientRef
169 | .ask(ZRangeByLex[A](key, minKey, minInclusive, maxKey, maxInclusive, limit))
170 | .mapTo[ZRangeByLex[A]#Ret]
171 |
172 | // ZREMRANGEBYLEX
173 | //
174 | def zremrangebylex[A](key: String,
175 | minKey: String = `-LexInf`, minInclusive: Boolean = true,
176 | maxKey: String = `+LexInf`, maxInclusive: Boolean = true)
177 | (implicit timeout: Timeout) =
178 | clientRef
179 | .ask(ZRemRangeByLex[A](key, minKey, minInclusive, maxKey, maxInclusive))
180 | .mapTo[ZRemRangeByLex[A]#Ret]
181 |
182 |
183 | }
184 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/StringOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import scala.concurrent.Future
4 |
5 | import org.scalatest.junit.JUnitRunner
6 | import org.junit.runner.RunWith
7 |
8 | import akka.util.ByteString
9 | import com.redis.protocol.StringCommands
10 | import com.redis.RedisSpecBase
11 | import com.redis.serialization.Stringified
12 |
13 |
14 | @RunWith(classOf[JUnitRunner])
15 | class StringOperationsSpec extends RedisSpecBase {
16 | import StringCommands._
17 |
18 | describe("set") {
19 | it("should set values to keys") {
20 | val numKeys = 3
21 | val (keys, values) = (1 to numKeys map { num => ("key" + num, "value" + num) }).unzip
22 | val writes = keys zip values map { case (key, value) => client.set(key, value) }
23 |
24 | Future.sequence(writes).futureValue should contain only (true)
25 | }
26 |
27 | it("should not set values to keys already existing with option NX") {
28 | val key = "key100"
29 | client.set(key, "value100").futureValue should be (true)
30 |
31 | client
32 | .set(key, "value200", None, Some(NX))
33 | .futureValue match {
34 | case true => fail("an existing key with an value should not be set with NX option")
35 | case false => client.get(key).futureValue should equal (Some("value100"))
36 | }
37 |
38 | // convenient alternative
39 | client
40 | .set(key, "value200", NX)
41 | .futureValue match {
42 | case true => fail("an existing key with an value should not be set with NX option")
43 | case false => client.get(key).futureValue should equal (Some("value100"))
44 | }
45 | }
46 |
47 | it("should not set values to non-existing keys with option XX") {
48 | val key = "value200"
49 | client
50 | .set(key, "value200", None, Some(XX))
51 | .futureValue match {
52 | case true => fail("set on a non existing key with XX will fail")
53 | case false => client.get(key).futureValue should equal (None)
54 | }
55 |
56 | // convenient alternative
57 | client
58 | .set(key, "value200", XX)
59 | .futureValue match {
60 | case true => fail("set on a non existing key with XX will fail")
61 | case false => client.get(key).futureValue should equal (None)
62 | }
63 | }
64 |
65 | it("should not encode a byte array to a UTF-8 string") {
66 | val bytes = Array(0x85.toByte)
67 |
68 | client.set("bytes", bytes)
69 | client.get[String]("bytes").futureValue.get should not equal (bytes)
70 | client.get[Array[Byte]]("bytes").futureValue.get.toList should equal (bytes.toList)
71 | }
72 | }
73 |
74 | describe("get") {
75 | it("should get results for keys set earlier") {
76 | val numKeys = 3
77 | val (keys, values) = (1 to numKeys map { num => ("get" + num, "value" + num) }).unzip
78 | keys zip values foreach { case (key, value) => client.set(key, value) }
79 |
80 | val reads = keys map (client.get(_))
81 | val readResults = Future.sequence(reads).futureValue
82 |
83 | readResults zip values foreach { case (result, expectedValue) =>
84 | result should equal (Some(expectedValue))
85 | }
86 | readResults should equal (List(Some("value1"), Some("value2"), Some("value3")))
87 | }
88 |
89 | it("should give none for unknown keys") {
90 | client.get("get_unkown").futureValue should equal (None)
91 | }
92 | }
93 |
94 | describe("mset") {
95 | it("should set multiple keys") {
96 | val numKeys = 3
97 | val keyvals = (1 to numKeys map { num => ("get" + num, "value" + num) })
98 | client.mset(keyvals: _*).futureValue should be (true)
99 |
100 | val (keys, vals) = keyvals.unzip
101 | val reads = keys map (client.get(_))
102 | val readResults = Future.sequence(reads).futureValue
103 | readResults should equal (vals.map(Some.apply))
104 | }
105 |
106 | it("should support various value types in a single command") {
107 | import com.redis.serialization.DefaultFormats._
108 |
109 | client
110 | .mset(("int" -> 1), ("long" -> 2L), ("pi" -> 3.14), ("string" -> "string"))
111 | .futureValue should be (true)
112 |
113 | client
114 | .mget("int", "long", "pi", "string")
115 | .futureValue should equal (Map("int" -> "1", "long" -> "2", "pi" -> "3.14", "string" -> "string"))
116 | }
117 | }
118 |
119 | describe("mget") {
120 | it("should get results from existing keys") {
121 | val numKeys = 3
122 | val keyvals = (1 to numKeys map { num => ("get" + num, "value" + num) })
123 | client.mset(keyvals: _*).futureValue
124 |
125 | val (keys, _) = keyvals.unzip
126 | val readResult = client.mget("nonExistingKey", keys.take(2): _*).futureValue
127 | readResult should equal (keyvals.take(2).toMap)
128 | }
129 | }
130 |
131 | describe("bitop") {
132 | it("should perform bitwise operations") {
133 | val _ = Future.sequence(
134 | client.set("key1", "abc") ::
135 | client.set("key2", "def") ::
136 | Nil
137 | ).futureValue
138 |
139 | val res = (for {
140 | _ <- client.bitop("AND", "dest", "key1", "key2")
141 | r <- client.get("dest")
142 | } yield r).futureValue
143 |
144 | res should equal (Some("``b"))
145 | }
146 |
147 | // Refer to https://github.com/debasishg/scala-redis-nb/issues/27
148 | it("should handle irregular byte arrays") {
149 | val _ = Future.sequence(
150 | client.set("key", "z") ::
151 | Nil
152 | ).futureValue
153 |
154 | val res = (for {
155 | _ <- client.bitop("NOT", "dest", "key")
156 | r <- client.get[Array[Byte]]("dest")
157 | } yield r).futureValue
158 |
159 | res.get should equal (Array(0x85.toByte))
160 | }
161 | }
162 |
163 | describe("bitpos") {
164 | it("should return the position of the first bit set to 1") {
165 | val key = "bitpos1"
166 | val bits = Stringified(ByteString(0x00, 0xf0, 0x00).map(_.toByte))
167 | client.set(key, bits).futureValue should equal (true)
168 | val bit = true
169 | client.bitpos(key, bit).futureValue should equal (8)
170 | }
171 |
172 | it("should return the position of the first bit set to 0") {
173 | val key = "bitpos2"
174 | val bits = Stringified(ByteString(Array(0xff, 0xf0, 0x00).map(_.toByte)))
175 | client.set(key, bits).futureValue should equal (true)
176 | val bit = false
177 | client.bitpos(key, bit).futureValue should equal (12)
178 | }
179 |
180 | it("should return the position of hte first bit set to 1 after start position") {
181 | val key = "bitpos3"
182 | val bits = Stringified(ByteString(Array(0xff, 0xf0, 0xf0, 0xf0, 0x00).map(_.toByte)))
183 | client.set(key, bits).futureValue should equal (true)
184 | val bit = false
185 | client.bitpos(key, bit, 2).futureValue should equal (20)
186 | }
187 |
188 | it("should return the position of the first bit set to 1 between start and end") {
189 | val key = "bitpos4"
190 | val bits = Stringified(ByteString(Array(0xff, 0xf0, 0xf0, 0xf0, 0x00).map(_.toByte)))
191 | client.set(key, bits).futureValue should equal (true)
192 | val bit = false
193 | client.bitpos(key, bit, 2, 3).futureValue should equal (20)
194 | }
195 |
196 | it("should return -1 if 1 bit is specified and the string is composed of just zero bytes") {
197 | val key = "bitpos5"
198 | val bits = Stringified(ByteString(Array(0x00, 0x00, 0x00).map(_.toByte)))
199 | client.set(key, bits).futureValue should equal (true)
200 | val bit = true
201 | client.bitpos(key, bit).futureValue should equal (-1)
202 | }
203 |
204 | it("should return the first bit not part of the string on the right if 0 bit is specified and the string only contains bit set to 1") {
205 | val key = "bitpos6"
206 | val bits = Stringified(ByteString(Array(0xff, 0xff, 0xff).map(_.toByte)))
207 | client.set(key, bits).futureValue should equal (true)
208 | val bit = false
209 | client.bitpos(key, bit).futureValue should equal (24)
210 | }
211 |
212 | it("should return -1 if 0 bit is specified and the string contains and clear bit is not found in the specified range") {
213 | val key = "bitpos7"
214 | val bits = Stringified(ByteString(Array(0xff, 0xff, 0xff).map(_.toByte)))
215 | client.set(key, bits).futureValue should equal (true)
216 | val bit = false
217 | client.bitpos(key, bit, 0, 2).futureValue should equal (-1)
218 | }
219 |
220 | it("should fail if start is not specified but end is specified") {
221 | val key = "bitpos8"
222 | val bits = Stringified(ByteString(Array(0xff, 0xff, 0xff).map(_.toByte)))
223 | client.set(key, bits).futureValue should equal (true)
224 | val bit = false
225 | intercept[IllegalArgumentException] {
226 | client.bitpos(key, bit, None, Some(2)).futureValue
227 | }
228 | }
229 | }
230 |
231 | }
232 |
--------------------------------------------------------------------------------
/src/main/scala/com/redis/protocol/SortedSetCommands.scala:
--------------------------------------------------------------------------------
1 | package com.redis.protocol
2 |
3 | import com.redis.serialization._
4 |
5 |
6 | object SortedSetCommands {
7 | import DefaultWriters._
8 |
9 |
10 | final val `+Inf` = Double.PositiveInfinity
11 | final val `-Inf` = Double.NegativeInfinity
12 | final val `+LexInf` = "+"
13 | final val `-LexInf` = "-"
14 |
15 | case class ZAdd(key: String, scoreMembers: Seq[ScoredValue]) extends RedisCommand[Long]("ZADD") {
16 | require(scoreMembers.nonEmpty, "Score members should not be empty")
17 | def params = key +: scoreMembers.foldRight (ANil) { (x, acc) => x.score +: x.value +: acc }
18 | }
19 |
20 | object ZAdd {
21 | def apply(key: String, score: Double, member: Stringified): ZAdd =
22 | ZAdd(key, Seq(ScoredValue(score, member)))
23 |
24 | def apply(key: String, scoreMember: ScoredValue, scoreMembers: ScoredValue*): ZAdd =
25 | ZAdd(key, scoreMember +: scoreMembers)
26 | }
27 |
28 |
29 | case class ZRem(key: String, members: Seq[Stringified]) extends RedisCommand[Long]("ZREM") {
30 | require(members.nonEmpty, "Members should not be empty")
31 | def params = key +: members.toArgs
32 | }
33 |
34 | object ZRem {
35 | def apply(key: String, member: Stringified, members: Stringified*): ZRem = ZRem(key, member +: members)
36 | }
37 |
38 |
39 | case class ZIncrby(key: String, incr: Double, member: Stringified) extends RedisCommand[Option[Double]]("ZINCRBY") {
40 | def params = key +: incr +: member +: ANil
41 | }
42 |
43 | case class ZCard(key: String) extends RedisCommand[Long]("ZCARD") {
44 | def params = key +: ANil
45 | }
46 |
47 | case class ZScore(key: String, element: Stringified) extends RedisCommand[Option[Double]]("ZSCORE") {
48 | def params = key +: element +: ANil
49 | }
50 |
51 |
52 | case class ZRange[A](key: String, start: Int = 0, end: Int = -1)(implicit reader: Reader[A])
53 | extends RedisCommand[List[A]]("ZRANGE") {
54 | def params = key +: start +: end +: ANil
55 |
56 | def reverse = ZRevRange(key, start, end)(reader)
57 |
58 | def withScores = ZRangeWithScores[A](key, start, end)(reader)
59 | }
60 |
61 | case class ZRangeWithScores[A](key: String, start: Int = 0, end: Int = -1)(implicit reader: Reader[A])
62 | extends RedisCommand[List[(A, Double)]]("ZRANGE") {
63 | def params = key +: start +: end +: "WITHSCORES" +: ANil
64 |
65 | def reverse = ZRangeWithScores(key, start, end)(reader)
66 | }
67 |
68 |
69 | case class ZRevRange[A](key: String, start: Int = 0, end: Int = -1)(implicit reader: Reader[A])
70 | extends RedisCommand[List[A]]("ZREVRANGE") {
71 | def params = key +: start +: end +: ANil
72 |
73 | def withScores = ZRevRangeWithScores[A](key, start, end)(reader)
74 | }
75 |
76 | case class ZRevRangeWithScores[A: Reader](key: String, start: Int = 0, end: Int = -1)
77 | extends RedisCommand[List[(A, Double)]]("ZREVRANGE") {
78 | def params = key +: start +: end +: "WITHSCORES" +: ANil
79 | }
80 |
81 |
82 | case class ZRangeByScore[A](key: String,
83 | min: Double = `-Inf`, minInclusive: Boolean = true,
84 | max: Double = `+Inf`, maxInclusive: Boolean = true,
85 | limit: Option[(Int, Int)] = None)(implicit reader: Reader[A])
86 | extends RedisCommand[List[A]]("ZRANGEBYSCORE") {
87 |
88 | def params = key +: scoreParams(min, minInclusive, max, maxInclusive, limit, false)
89 |
90 | def reverse = ZRevRangeByScore(key, min, minInclusive, max, maxInclusive, limit)(reader)
91 |
92 | def withScores = ZRangeByScoreWithScores[A](key, min, minInclusive, max, maxInclusive, limit)(reader)
93 | }
94 |
95 | case class ZRangeByScoreWithScores[A](key: String,
96 | min: Double = `-Inf`, minInclusive: Boolean = true,
97 | max: Double = `+Inf`, maxInclusive: Boolean = true,
98 | limit: Option[(Int, Int)] = None)(implicit reader: Reader[A])
99 | extends RedisCommand[List[(A, Double)]]("ZRANGEBYSCORE") {
100 |
101 | def params = key +: scoreParams(min, minInclusive, max, maxInclusive, limit, true)
102 |
103 | def reverse = ZRevRangeByScoreWithScores(key, min, minInclusive, max, maxInclusive, limit)(reader)
104 | }
105 |
106 | case class ZRevRangeByScore[A](key: String,
107 | max: Double = `+Inf`, maxInclusive: Boolean = true,
108 | min: Double = `-Inf`, minInclusive: Boolean = true,
109 | limit: Option[(Int, Int)] = None)(implicit reader: Reader[A])
110 | extends RedisCommand[List[A]]("ZREVRANGEBYSCORE") {
111 |
112 | def params = key +: scoreParams(max, maxInclusive, min, minInclusive, limit, false)
113 |
114 | def withScores = ZRevRangeByScoreWithScores(key, min, minInclusive, max, maxInclusive, limit)(reader)
115 | }
116 |
117 | case class ZRevRangeByScoreWithScores[A: Reader](key: String,
118 | max: Double = `+Inf`, maxInclusive: Boolean = true,
119 | min: Double = `-Inf`, minInclusive: Boolean = true,
120 | limit: Option[(Int, Int)] = None)
121 | extends RedisCommand[List[(A, Double)]]("ZREVRANGEBYSCORE") {
122 |
123 | def params = key +: scoreParams(max, maxInclusive, min, minInclusive, limit, true)
124 | }
125 |
126 | private def scoreParams(from: Double, fromInclusive: Boolean, to: Double, toInclusive: Boolean,
127 | limit: Option[(Int, Int)], withScores: Boolean): Args = {
128 |
129 | formatDouble(from, fromInclusive) +: formatDouble(to, toInclusive) +: (
130 | (if (withScores) Seq("WITHSCORES") else Nil) ++:
131 | (limit match {
132 | case Some((from, to)) => "LIMIT" +: from +: to +: ANil
133 | case _ => ANil
134 | })
135 | )
136 | }
137 |
138 |
139 | case class ZRank(key: String, member: Stringified)
140 | extends RedisCommand[Option[Long]]("ZRANK")(PartialDeserializer.liftOptionPD[Long]) {
141 | def params = key +: member +: ANil
142 |
143 | def reverse = ZRevRank(key, member)
144 | }
145 |
146 | case class ZRevRank(key: String, member: Stringified)
147 | extends RedisCommand[Option[Long]]("ZREVRANK")(PartialDeserializer.liftOptionPD[Long]) {
148 | def params = key +: member +: ANil
149 | }
150 |
151 |
152 | case class ZRemRangeByRank(key: String, start: Int = 0, end: Int = -1) extends RedisCommand[Long]("ZREMRANGEBYRANK") {
153 | def params = key +: start +: end +: ANil
154 | }
155 |
156 | case class ZRemRangeByScore(key: String, start: Double = `-Inf`, end: Double = `+Inf`) extends RedisCommand[Long]("ZREMRANGEBYSCORE") {
157 | def params = key +: start +: end +: ANil
158 | }
159 |
160 |
161 | sealed trait Aggregate
162 | case object SUM extends Aggregate
163 | case object MIN extends Aggregate
164 | case object MAX extends Aggregate
165 |
166 | case class ZInterStore(dstKey: String, keys: Iterable[String],
167 | aggregate: Aggregate = SUM) extends RedisCommand[Long]("ZINTERSTORE") {
168 |
169 | def params =
170 | (Iterator(dstKey, keys.size.toString) ++ keys.iterator ++ Iterator("AGGREGATE", aggregate.toString)).toSeq.toArgs
171 | }
172 |
173 | case class ZUnionStore(dstKey: String, keys: Iterable[String],
174 | aggregate: Aggregate = SUM) extends RedisCommand[Long]("ZUNIONSTORE") {
175 |
176 | def params =
177 | (Iterator(dstKey, keys.size.toString) ++ keys.iterator ++ Iterator("AGGREGATE", aggregate.toString)).toSeq.toArgs
178 | }
179 |
180 | case class ZInterStoreWeighted(dstKey: String, kws: Iterable[Product2[String, Double]],
181 | aggregate: Aggregate = SUM) extends RedisCommand[Long]("ZINTERSTORE") {
182 |
183 | def params =
184 | (Iterator(dstKey, kws.size.toString) ++ kws.iterator.map(_._1) ++ Iterator.single("WEIGHTS") ++
185 | kws.iterator.map(_._2.toString) ++ Iterator("AGGREGATE", aggregate.toString)).toSeq.toArgs
186 | }
187 |
188 | case class ZUnionStoreWeighted(dstKey: String, kws: Iterable[Product2[String, Double]],
189 | aggregate: Aggregate = SUM) extends RedisCommand[Long]("ZUNIONSTORE") {
190 |
191 | def params =
192 | (Iterator(dstKey, kws.size.toString) ++ kws.iterator.map(_._1) ++ Iterator.single("WEIGHTS") ++
193 | kws.iterator.map(_._2.toString) ++ Iterator("AGGREGATE", aggregate.toString)).toSeq.toArgs
194 | }
195 |
196 | case class ZCount(key: String,
197 | min: Double = `-Inf`, minInclusive: Boolean = true,
198 | max: Double = `+Inf`, maxInclusive: Boolean = true) extends RedisCommand[Long]("ZCOUNT") {
199 |
200 | def params = key +: formatDouble(min, minInclusive) +: formatDouble(max, maxInclusive) +: ANil
201 | }
202 |
203 | case class ZLexCount(key: String,
204 | minKey: String = `-LexInf`, minInclusive: Boolean = true,
205 | maxKey: String = `+LexInf`, maxInclusive: Boolean = true) extends RedisCommand[Long]("ZLEXCOUNT") {
206 | def params = key +: formatLex(minKey, minInclusive) +: formatLex(maxKey, maxInclusive) +: ANil
207 | }
208 |
209 | case class ZRangeByLex[A](key: String,
210 | min: String, minInclusive: Boolean = true,
211 | max: String, maxInclusive: Boolean = true,
212 | limit: Option[(Int, Int)] = None)(implicit reader: Reader[A])
213 | extends RedisCommand[List[A]]("ZRANGEBYLEX") {
214 |
215 | def params = key +: lexParams(min, minInclusive, max, maxInclusive, limit)
216 | }
217 |
218 | case class ZRemRangeByLex[A](key: String,
219 | min: String, minInclusive: Boolean = true,
220 | max: String, maxInclusive: Boolean = true)
221 | extends RedisCommand[Long]("ZREMRANGEBYLEX") {
222 |
223 | def params = key +: lexParams(min, minInclusive, max, maxInclusive)
224 | }
225 |
226 | private def lexParams(min: String, minInclusive: Boolean, max: String, maxInclusive: Boolean,
227 | limit: Option[(Int, Int)] = None): Args = {
228 |
229 | formatLex(min, minInclusive) +: formatLex(max, maxInclusive) +:
230 | (
231 | limit match {
232 | case Some((from, to)) => "LIMIT" +: from +: to +: ANil
233 | case _ => ANil
234 | }
235 | )
236 | }
237 |
238 | private def formatLex(key: String, inclusive: Boolean) = Stringified(
239 | if (key == `+LexInf` || key == `-LexInf`) key
240 | else {
241 | if (inclusive) s"[$key" else s"($key"
242 | }
243 | )
244 |
245 | private def formatDouble(d: Double, inclusive: Boolean = true) = Stringified(
246 | (if (inclusive) ("") else ("(")) + {
247 | if (d.isInfinity) {
248 | if (d > 0.0) "+inf" else "-inf"
249 | } else {
250 | d.toString
251 | }
252 | }
253 | )
254 | }
255 |
--------------------------------------------------------------------------------
/src/test/scala/com/redis/api/SortedSetOperationsSpec.scala:
--------------------------------------------------------------------------------
1 | package com.redis.api
2 |
3 | import org.scalatest.junit.JUnitRunner
4 | import org.junit.runner.RunWith
5 |
6 | import com.redis.RedisSpecBase
7 |
8 |
9 | @RunWith(classOf[JUnitRunner])
10 | class SortedSetOperationsSpec extends RedisSpecBase {
11 |
12 | private def add() = {
13 | val add1 = client.zadd("hackers", 1965, "yukihiro matsumoto")
14 | val add2 = client.zadd("hackers", 1953, "richard stallman")
15 | val add3 = client.zadd("hackers", (1916, "claude shannon"), (1969, "linus torvalds"))
16 | val add4 = client.zadd("hackers", Seq((1940, "alan kay"), (1912, "alan turing")))
17 | add1.futureValue should equal (1)
18 | add2.futureValue should equal (1)
19 | add3.futureValue should equal (2)
20 | add4.futureValue should equal (2)
21 | }
22 |
23 | private def addLex() = {
24 | val add1 = client.zadd("myzset", 0, "a")
25 | val add2 = client.zadd("myzset", 0, "b")
26 | val add3 = client.zadd("myzset", 0, "c")
27 | val add4 = client.zadd("myzset", 0, "d")
28 | val add5 = client.zadd("myzset", 0, "e")
29 | val add6 = client.zadd("myzset", 0, "f")
30 | val add7 = client.zadd("myzset", 0, "g")
31 | add1.futureValue should equal (1)
32 | add2.futureValue should equal (1)
33 | add3.futureValue should equal (1)
34 | add4.futureValue should equal (1)
35 | add5.futureValue should equal (1)
36 | add6.futureValue should equal (1)
37 | add7.futureValue should equal (1)
38 | }
39 |
40 | describe("zadd") {
41 | it("should add based on proper sorted set semantics") {
42 | add
43 | client.zadd("hackers", 1912, "alan turing").futureValue should equal (0)
44 | client.zcard("hackers").futureValue should equal (6)
45 | }
46 | }
47 |
48 | describe("zrem") {
49 | it("should remove") {
50 | add
51 | client.zrem("hackers", "alan turing").futureValue should equal (1)
52 | client.zrem("hackers", "alan kay", "linus torvalds").futureValue should equal (2)
53 | client.zrem("hackers", "alan kay", "linus torvalds").futureValue should equal (0)
54 | }
55 | }
56 |
57 | describe("zrange") {
58 | it("should get the proper range") {
59 | add
60 | client.zrange("hackers").futureValue should have size (6)
61 | client.zrangeWithScores("hackers").futureValue should have size (6)
62 | }
63 | }
64 |
65 | describe("zrevrange") {
66 | it("should get the proper range") {
67 | client.zadd("myzset", 1, "one").futureValue
68 | client.zadd("myzset", 2, "two").futureValue
69 | client.zadd("myzset", 3, "three").futureValue
70 | client.zrevrange("myzset", 0, -1).futureValue should equal(List("three", "two", "one"))
71 | client.zrevrange("myzset", 2, 3).futureValue should equal(List("one"))
72 | client.zrevrange("myzset", -2, -1).futureValue should equal(List("two", "one"))
73 | }
74 | }
75 |
76 | describe("zrank") {
77 | it ("should give proper rank") {
78 | add
79 | client.zrank("hackers", "yukihiro matsumoto").futureValue should equal (Some(4))
80 | client.zrevrank("hackers", "yukihiro matsumoto").futureValue should equal (Some(1))
81 | client.zrank("hackers", "michael jackson").futureValue should equal (None)
82 | }
83 | }
84 |
85 | describe("zremrangebyrank") {
86 | it ("should remove based on rank range") {
87 | add
88 | client.zremrangebyrank("hackers", 0, 2).futureValue should equal (3)
89 | }
90 | }
91 |
92 | describe("zremrangebyscore") {
93 | it ("should remove based on score range") {
94 | add
95 | client.zremrangebyscore("hackers", 1912, 1940).futureValue should equal (3)
96 | client.zremrangebyscore("hackers", 0, 3).futureValue should equal (0)
97 | }
98 | }
99 |
100 | describe("zunion") {
101 | it ("should do a union") {
102 | client.zadd("hackers 1", 1965, "yukihiro matsumoto").futureValue should equal (1)
103 | client.zadd("hackers 1", 1953, "richard stallman").futureValue should equal (1)
104 | client.zadd("hackers 2", 1916, "claude shannon").futureValue should equal (1)
105 | client.zadd("hackers 2", 1969, "linus torvalds").futureValue should equal (1)
106 | client.zadd("hackers 3", 1940, "alan kay").futureValue should equal (1)
107 | client.zadd("hackers 4", 1912, "alan turing").futureValue should equal (1)
108 |
109 | // union with weight = 1
110 | client.zunionstore("hackers", List("hackers 1", "hackers 2", "hackers 3", "hackers 4")).futureValue should equal (6)
111 | client.zcard("hackers").futureValue should equal (6)
112 |
113 | client.zrangeWithScores("hackers").futureValue.map(_._2) should equal (List(1912, 1916, 1940, 1953, 1965, 1969))
114 |
115 | // union with modified weights
116 | client.zunionstoreweighted("hackers weighted", Map("hackers 1" -> 1.0, "hackers 2" -> 2.0, "hackers 3" -> 3.0, "hackers 4" -> 4.0)).futureValue should equal (6)
117 | client.zrangeWithScores("hackers weighted").futureValue.map(_._2.toInt) should equal (List(1953, 1965, 3832, 3938, 5820, 7648))
118 | }
119 | }
120 |
121 | describe("zinter") {
122 | it ("should do an intersection") {
123 | client.zadd("hackers", 1912, "alan turing").futureValue should equal (1)
124 | client.zadd("hackers", 1916, "claude shannon").futureValue should equal (1)
125 | client.zadd("hackers", 1927, "john mccarthy").futureValue should equal (1)
126 | client.zadd("hackers", 1940, "alan kay").futureValue should equal (1)
127 | client.zadd("hackers", 1953, "richard stallman").futureValue should equal (1)
128 | client.zadd("hackers", 1954, "larry wall").futureValue should equal (1)
129 | client.zadd("hackers", 1956, "guido van rossum").futureValue should equal (1)
130 | client.zadd("hackers", 1965, "paul graham").futureValue should equal (1)
131 | client.zadd("hackers", 1965, "yukihiro matsumoto").futureValue should equal (1)
132 | client.zadd("hackers", 1969, "linus torvalds").futureValue should equal (1)
133 |
134 | client.zadd("baby boomers", 1948, "phillip bobbit").futureValue should equal (1)
135 | client.zadd("baby boomers", 1953, "richard stallman").futureValue should equal (1)
136 | client.zadd("baby boomers", 1954, "cass sunstein").futureValue should equal (1)
137 | client.zadd("baby boomers", 1954, "larry wall").futureValue should equal (1)
138 | client.zadd("baby boomers", 1956, "guido van rossum").futureValue should equal (1)
139 | client.zadd("baby boomers", 1961, "lawrence lessig").futureValue should equal (1)
140 | client.zadd("baby boomers", 1965, "paul graham").futureValue should equal (1)
141 | client.zadd("baby boomers", 1965, "yukihiro matsumoto").futureValue should equal (1)
142 |
143 | // intersection with weight = 1
144 | client.zinterstore("baby boomer hackers", List("hackers", "baby boomers")).futureValue should equal (5)
145 | client.zcard("baby boomer hackers").futureValue should equal (5)
146 |
147 | client.zrange("baby boomer hackers").futureValue should equal (List("richard stallman", "larry wall", "guido van rossum", "paul graham", "yukihiro matsumoto"))
148 |
149 | // intersection with modified weights
150 | client.zinterstoreweighted("baby boomer hackers weighted", Map("hackers" -> 0.5, "baby boomers" -> 0.5)).futureValue should equal (5)
151 | client.zrangeWithScores("baby boomer hackers weighted").futureValue.map(_._2.toInt) should equal (List(1953, 1954, 1956, 1965, 1965))
152 | }
153 | }
154 |
155 | describe("zcount") {
156 | it ("should return the number of elements between min and max") {
157 | add
158 |
159 | client.zcount("hackers", 1912, true, 1920).futureValue should equal (2)
160 | }
161 | }
162 |
163 | describe("z(rev)rangeByScore") {
164 | it ("should return the elements between min and max") {
165 | add
166 |
167 | client
168 | .zrangeByScore("hackers", 1940, true, 1969, true, None)
169 | .futureValue should equal (
170 | List("alan kay", "richard stallman", "yukihiro matsumoto", "linus torvalds")
171 | )
172 |
173 | client
174 | .zrevrangeByScore("hackers", 1940, true, 1969, true, None)
175 | .futureValue should equal (
176 | List("linus torvalds", "yukihiro matsumoto", "richard stallman","alan kay")
177 | )
178 | }
179 |
180 | it("should return the elements between min and max and allow offset and limit") {
181 | add
182 |
183 | client
184 | .zrangeByScore("hackers", 1940, true, 1969, true, Some(0, 2))
185 | .futureValue should equal (List("alan kay", "richard stallman"))
186 |
187 | client
188 | .zrevrangeByScore("hackers", 1940, true, 1969, true, Some(0, 2))
189 | .futureValue should equal (List("linus torvalds", "yukihiro matsumoto"))
190 |
191 | client
192 | .zrangeByScore("hackers", 1940, true, 1969, true, Some(3, 1))
193 | .futureValue should equal (List("linus torvalds"))
194 |
195 | client
196 | .zrevrangeByScore("hackers", 1940, true, 1969, true, Some(3, 1))
197 | .futureValue should equal (List("alan kay"))
198 |
199 | client
200 | .zrangeByScore("hackers", 1940, false, 1969, true, Some(0, 2))
201 | .futureValue should equal (List("richard stallman", "yukihiro matsumoto"))
202 |
203 | client
204 | .zrevrangeByScore("hackers", 1940, true, 1969, false, Some(0, 2))
205 | .futureValue should equal (List("yukihiro matsumoto", "richard stallman"))
206 | }
207 | }
208 |
209 | describe("z(rev)rangeByScoreWithScore") {
210 | it ("should return the elements between min and max") {
211 | add
212 |
213 | client
214 | .zrangeByScoreWithScores("hackers", 1940, true, 1969, true, None)
215 | .futureValue should equal (List(
216 | ("alan kay", 1940.0), ("richard stallman", 1953.0),
217 | ("yukihiro matsumoto", 1965.0), ("linus torvalds", 1969.0)
218 | ))
219 |
220 | client
221 | .zrevrangeByScoreWithScores("hackers", 1940, true, 1969, true, None)
222 | .futureValue should equal (List(
223 | ("linus torvalds", 1969.0), ("yukihiro matsumoto", 1965.0),
224 | ("richard stallman", 1953.0),("alan kay", 1940.0)
225 | ))
226 |
227 | client
228 | .zrangeByScoreWithScores("hackers", 1940, true, 1969, true, Some(3, 1))
229 | .futureValue should equal (List(("linus torvalds", 1969.0)))
230 |
231 | client
232 | .zrevrangeByScoreWithScores("hackers", 1940, true, 1969, true, Some(3, 1))
233 | .futureValue should equal (List(("alan kay", 1940.0)))
234 | }
235 | }
236 |
237 | describe("zlexcount") {
238 | it ("should return the number of elements in lexicographic ordering between min and max") {
239 | addLex
240 | client.zlexcount("myzset", "a", true, "d", true).futureValue should equal(4)
241 | client.zlexcount("myzset", "a", true, "d", false).futureValue should equal(3)
242 | client.zlexcount("myzset", "a", false, "d", true).futureValue should equal(3)
243 | client.zlexcount("myzset", "a", false, "d", false).futureValue should equal(2)
244 | client.zlexcount("myzset", maxKey = "d", maxInclusive = false).futureValue should equal(3)
245 | client.zlexcount("myzset", "a", false).futureValue should equal(6)
246 | client.zlexcount("myzset").futureValue should equal(7)
247 | }
248 | }
249 |
250 | describe("zrangebylex") {
251 | it ("should return the elements in lexicographic ordering between min and max") {
252 | addLex
253 | client.zrangebylex("myzset", "a", true, "d", true).futureValue should equal(List("a", "b", "c", "d"))
254 | client.zrangebylex("myzset", "a", true, "d", false).futureValue should equal(List("a", "b", "c"))
255 | client.zrangebylex("myzset", "a", false, "d", true).futureValue should equal(List("b", "c", "d"))
256 | client.zrangebylex("myzset", "a", false, "d", false).futureValue should equal(List("b", "c"))
257 | client.zrangebylex("myzset", maxKey = "d", maxInclusive = false).futureValue should equal(List("a", "b", "c"))
258 | client.zrangebylex("myzset", "a", false).futureValue should equal(List("b", "c", "d", "e", "f", "g"))
259 | client.zrangebylex("myzset").futureValue should equal(List("a", "b", "c", "d", "e", "f", "g"))
260 | client.zrangebylex("myzset", "aaa", true, "g", false).futureValue should equal(List("b", "c", "d", "e", "f"))
261 | client.zrangebylex("myzset", "aaa", true, "g", false, Some((1, 2))).futureValue should equal(List("c", "d"))
262 | }
263 | }
264 |
265 | describe("zremrangebylex") {
266 | it ("should return the number of elements deleted in lexicographic ordering between min and max") {
267 | client.zadd("myzset", Seq((0, "aaaa"), (0, "b"), (0, "c"), (0, "d"), (0, "e"))).futureValue should equal(5)
268 | client.zadd("myzset", Seq((0, "foo"), (0, "zap"), (0, "zip"), (0, "ALPHA"), (0, "alpha"))).futureValue should equal(5)
269 | client.zremrangebylex("myzset", "alpha", true, "omega", true).futureValue should equal(6)
270 | }
271 | }
272 |
273 | }
274 |
--------------------------------------------------------------------------------