├── 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 | --------------------------------------------------------------------------------