├── .gitignore ├── README.md ├── project ├── Build.scala ├── build.properties └── plugins.sbt └── src ├── benchmark └── scala │ └── net │ └── fyrie │ └── redis │ └── Main.scala ├── main └── scala │ └── net │ └── fyrie │ └── redis │ ├── ConnectionPool.scala │ ├── Queued.scala │ ├── RedisClient.scala │ ├── ResultFunctor.scala │ ├── actors │ └── Actors.scala │ ├── commands │ ├── Hashes.scala │ ├── Keys.scala │ ├── Lists.scala │ ├── PubSub.scala │ ├── Script.scala │ ├── Servers.scala │ ├── Sets.scala │ ├── SortedSets.scala │ └── Strings.scala │ ├── lua.scala │ ├── messages │ └── Messages.scala │ ├── package.scala │ ├── protocol │ ├── Constants.scala │ └── Iteratees.scala │ ├── pubsub │ └── PubSub.scala │ ├── serialization │ └── Serialization.scala │ └── types │ └── Types.scala └── test └── scala └── net └── fyrie └── redis ├── DBSpec.scala ├── HashSpec.scala ├── ListSpec.scala ├── PubSubSpec.scala ├── ScriptsSpec.scala ├── SetSpec.scala ├── SortedSetSpec.scala ├── StringSpec.scala └── TestServer.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .ensime 3 | lib_managed 4 | src_managed 5 | project/boot 6 | project/plugins/project 7 | project/plugins/src_managed 8 | target 9 | target/ 10 | target/**/* 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fyrie Redis Scala client 2 | 3 | ## Key features of the library 4 | 5 | - All commands are pipelined asynchronously. 6 | - User defined formatting of command parameters and parsing of replies. 7 | - Uses nonblocking IO and actors for increased throughput and responsiveness while minimizing the use of threads and sockets. 8 | 9 | ## Information about Redis 10 | 11 | Redis is an advanced key-value store. It is similar to memcached but the dataset is not volatile, and values can be strings, exactly like in memcached, but also lists, sets, and ordered sets. 12 | 13 | [code.google.com/p/redis](http://code.google.com/p/redis/) 14 | 15 | ### Key features of Redis 16 | 17 | - Fast in-memory store with asynchronous save to disk. 18 | - Key value get, set, delete, etc. 19 | - Atomic operations on sets and lists, union, intersection, trim, etc. 20 | 21 | ## Requirements 22 | 23 | Fyrie Redis version numbers match the version of Akka that it is compatible with. The recommended version is currently 1.2, which can be found on the ['akka-1.2' branch](https://github.com/derekjw/fyrie-redis/tree/akka-1.2). 24 | 25 | If building from source sbt 0.10 ([github.com/harrah/xsbt/wiki](https://github.com/harrah/xsbt/wiki)) is required. 26 | 27 | Releases and snapshots are located at [repo.fyrie.net](http://repo.fyrie.net), which can be used in sbt like this: 28 | 29 | resolvers += "fyrie snapshots" at "http://repo.fyrie.net/snapshots" 30 | 31 | libraryDependencies += "net.fyrie" %% "fyrie-redis" % "1.2-SNAPSHOT" 32 | 33 | ## All tests are functional tests and require a running instance of Redis 34 | 35 | ## Usage 36 | 37 | Start your redis instance (usually redis-server will do it). Tests default to port 6379, but can be changed by editing src/test/resources/akka.conf. Do not run tests on a production redis server as all data on the redis server will be deleted. 38 | 39 | $ cd fyrie-redis 40 | $ sbt 41 | > update 42 | > test (optional to run the tests) 43 | > console 44 | 45 | And you are ready to start issuing commands to the server(s) 46 | 47 | let's connect and get a key: 48 | 49 | scala> import net.fyrie.redis._ 50 | scala> val r = new RedisClient("localhost", 6379) 51 | scala> r.set("key", "some value") 52 | scala> r.sync.get("key").parse[String] 53 | 54 | There are 3 ways to run a command: 55 | 56 | - using 'async': result will be returned asynchronously within a Future ([akka.io/docs/akka/snapshot/scala/futures.html](http://akka.io/docs/akka/snapshot/scala/futures.html)). 57 | - using 'sync': will wait for the result. Only recommended when testing. 58 | - using 'quiet': No result will be returned. Command is sent asynchronously. Good for quickly making updates. 59 | 60 | The default is 'async' if none of the above are used. 61 | 62 | MultiExec commands can also be given: 63 | 64 | val result = r.multi { rq => 65 | for { 66 | _ <- rq.set("testkey1", "testvalue1") 67 | _ <- rq.set("testkey2", "testvalue2") 68 | x <- rq.mget(List("testkey1", "testkey2")).parse[String] 69 | } yield x 70 | } 71 | 72 | The above example will return a 'Future[List[String]]' 73 | 74 | Another MultiExec example: 75 | 76 | r.lpush("mylist", "value1") 77 | r.lpush("mylist", "value2") 78 | 79 | val result = r.multi { rq => 80 | for { 81 | _ <- rq.lpush("mylist", "value3") 82 | a <- rq.rpop("mylist").parse[String] 83 | b <- rq.rpop("mylist").parse[String] 84 | c <- rq.rpop("mylist").parse[String] 85 | } yield (a, b, c) 86 | } 87 | 88 | This example will return a '(Future[String], Future[String], Future[String])' 89 | 90 | ## Documentation 91 | 92 | The api is published at ([http://derekjw.github.com/fyrie-redis](http://derekjw.github.com/fyrie-redis)), but it's documentation is far from complete. 93 | 94 | Until proper documentation is completed, the tests can be used as examples: ([github.com/derekjw/fyrie-redis/tree/master/src/test/scala/net/fyrie/redis](https://github.com/derekjw/fyrie-redis/tree/master/src/test/scala/net/fyrie/redis)) 95 | 96 | ## TODO 97 | 98 | - Support clustering 99 | - Documentation 100 | 101 | ## Acknowledgements 102 | 103 | I would like to thank Alejandro Crosa and Debasish Ghosh for the work they have done creating [scala-redis](http://github.com/debasishg/scala-redis). I began this project as a fork of scala-redis, but after my third rewriting of it I decided I should split this off into a seperate project. Without the starting point that their work provided I never would have created this. 104 | 105 | ## License 106 | 107 | This software is licensed under the Apache 2 license, quoted below. 108 | 109 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 110 | use this file except in compliance with the License. You may obtain a copy of 111 | the License at 112 | 113 | http://www.apache.org/licenses/LICENSE-2.0 114 | 115 | Unless required by applicable law or agreed to in writing, software 116 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 117 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 118 | License for the specific language governing permissions and limitations under 119 | the License. 120 | 121 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | import com.typesafe.sbtscalariform.ScalariformPlugin 5 | import com.typesafe.sbtscalariform.ScalariformPlugin.ScalariformKeys 6 | 7 | object FyrieRedisBuild extends Build { 8 | lazy val core = (Project("fyrie-redis", file(".")) 9 | configs(Benchmark) 10 | settings(coreSettings: _*)) 11 | 12 | val coreSettings = Defaults.defaultSettings ++ inConfig(Benchmark)(Defaults.configSettings) ++ ScalariformPlugin.scalariformSettings ++ Seq( 13 | scalaVersion := "2.9.1", 14 | crossScalaVersions := Seq("2.9.0-1", "2.9.1", "2.10.0-M1"), 15 | name := "fyrie-redis", 16 | organization := "net.fyrie", 17 | version := "2.0-SNAPSHOT", 18 | resolvers ++= Seq("Sonatype OSS Repo" at "http://oss.sonatype.org/content/repositories/snapshots", 19 | "Akka Snapshot Repo" at "http://akka.io/snapshots"), 20 | libraryDependencies ++= Seq("com.typesafe.akka" % "akka-actor" % "2.0-20120124-000638", 21 | "com.typesafe.akka" % "akka-testkit" % "2.0-20120124-000638" % "test", 22 | "org.specs2" % "specs2_2.9.1" % "1.6.1", 23 | "org.specs2" % "specs2-scalaz-core_2.9.1" % "6.0.1" % "test"), 24 | //"com.google.code.caliper" % "caliper" % "1.0-SNAPSHOT" % "benchmark", 25 | //"com.google.code.gson" % "gson" % "1.7.1" % "benchmark"), 26 | parallelExecution := false, 27 | scalacOptions ++= Seq("-encoding", "UTF-8", "-deprecation", "-unchecked", "-optimize"), 28 | ScalariformKeys.preferences in Compile := formattingPreferences, 29 | ScalariformKeys.preferences in Test := formattingPreferences, 30 | runner in Benchmark in run <<= (thisProject, taskTemporaryDirectory, scalaInstance, baseDirectory, javaOptions, outputStrategy, javaHome, connectInput) map { 31 | (tp, tmp, si, base, options, strategy, javaHomeDir, connectIn) => 32 | new BenchmarkRunner(tp.id, ForkOptions(scalaJars = si.jars, javaHome = javaHomeDir, connectInput = connectIn, outputStrategy = strategy, 33 | runJVMOptions = options, workingDirectory = Some(base)) ) 34 | }, 35 | publishTo <<= (version) { version: String => 36 | val repo = (s: String) => 37 | Resolver.ssh(s, "repo.fyrie.net", "/home/repo/" + s + "/") as("derek", file("/home/derek/.ssh/id_rsa")) withPermissions("0644") 38 | Some(if (version.trim.endsWith("SNAPSHOT")) repo("snapshots") else repo("releases")) 39 | }) 40 | 41 | def formattingPreferences = { 42 | import scalariform.formatter.preferences._ 43 | FormattingPreferences() 44 | .setPreference(RewriteArrowSymbols, true) 45 | .setPreference(AlignParameters, true) 46 | .setPreference(AlignSingleLineCaseStatements, true) 47 | } 48 | 49 | lazy val Benchmark = config("benchmark") extend(Test) 50 | 51 | } 52 | 53 | class BenchmarkRunner(subproject: String, config: ForkScalaRun) extends sbt.ScalaRun { 54 | def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Option[String] = { 55 | log.info("Running " + subproject + " " + mainClass + " " + options.mkString(" ")) 56 | 57 | val javaOptions = classpathOption(classpath) ::: mainClass :: options.toList 58 | val strategy = config.outputStrategy getOrElse LoggedOutput(log) 59 | val process = Fork.java.fork(config.javaHome, 60 | /* Seq("-XX:+UseConcMarkSweepGC", "-XX:+TieredCompilation", "-XX:SurvivorRatio=1", "-Xmn1g", "-Xms2g", "-Xmx2g") ++ */ config.runJVMOptions ++ javaOptions, 61 | config.workingDirectory, 62 | Map.empty, 63 | config.connectInput, 64 | strategy) 65 | def cancel() = { 66 | log.warn("Run canceled.") 67 | process.destroy() 68 | 1 69 | } 70 | val exitCode = try process.exitValue() catch { case e: InterruptedException => cancel() } 71 | processExitCode(exitCode, "runner") 72 | } 73 | private def classpathOption(classpath: Seq[File]) = "-classpath" :: Path.makeString(classpath) :: Nil 74 | private def processExitCode(exitCode: Int, label: String) = { 75 | if(exitCode == 0) None 76 | else Some("Nonzero exit code returned from " + label + ": " + exitCode) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.11.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Typesafe Repo" at "http://repo.typesafe.com/typesafe/releases/" 2 | 3 | addSbtPlugin("com.typesafe.sbtscalariform" % "sbtscalariform" % "0.3.1") 4 | -------------------------------------------------------------------------------- /src/benchmark/scala/net/fyrie/redis/Main.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import akka.actor.ActorSystem 4 | import akka.dispatch.{ Future, Await } 5 | import akka.util.{ ByteString, Duration } 6 | 7 | import annotation.tailrec 8 | import com.google.caliper.{ 9 | Runner => CaliperRunner, 10 | Param, 11 | SimpleBenchmark 12 | } 13 | 14 | object Main extends App { 15 | //CaliperRunner.main(classOf[Benchmark], (args :+ "--vm" :+ "java -XX:+TieredCompilation"): _*) 16 | 17 | CaliperRunner.main(classOf[Benchmark], (args :+ "--vm" :+ "java -XX:+UseParallelOldGC -XX:+TieredCompilation -XX:SurvivorRatio=1 -Xmn1g -Xms2g -Xmx2g"): _*) 18 | 19 | //CaliperRunner.main(classOf[Benchmark], (args :+ "-Jgc=-XX:+UseParallelOldGC,-XX:+UseConcMarkSweepGC,-XX:+UseG1GC" :+ "--vm" :+ "java -XX:+TieredCompilation -XX:SurvivorRatio=1 -Xmn1g -Xms2g -Xmx2g"): _*) 20 | 21 | /* 22 | val bm = new Benchmark 23 | bm.setUp 24 | 25 | while(true) { 26 | val ops = 100000 27 | val start = System.nanoTime 28 | bm.timeScatterGather(ops) 29 | val end = System.nanoTime 30 | println("ops/sec: " + (ops.toDouble / ((end - start).toDouble / 1000000000.0)).toInt) 31 | System.gc 32 | } 33 | 34 | bm.system.shutdown 35 | */ 36 | } 37 | 38 | class Benchmark extends SimpleScalaBenchmark { 39 | 40 | // default key: 41 | 42 | // amount of unique keys to use, not implemented 43 | // val keyspace = 1 44 | 45 | val dataSize = 3 46 | 47 | val data = ByteString("x" * dataSize) 48 | 49 | implicit val system: ActorSystem = TestSystem.system 50 | 51 | var client: RedisClient = _ 52 | 53 | override def setUp() { 54 | client = RedisClient(config = RedisClientConfig(retryOnReconnect = false)) 55 | client.sync.flushdb 56 | } 57 | 58 | override def tearDown() { 59 | // client.sync.flushdb 60 | client.disconnect 61 | client = null 62 | } 63 | 64 | def timeSet(reps: Int) = { 65 | repeat(reps) { client set ("foo:rand:000000000000", data) } 66 | 67 | val result = client.sync get "foo:rand:000000000000" 68 | 69 | assert(result == Some(data)) 70 | 71 | result 72 | } 73 | 74 | def timeGet(reps: Int) = { 75 | client.quiet set ("foo:rand:000000000000", data) 76 | repeat(reps) { client get "foo:rand:000000000000" } 77 | 78 | val result = client.sync get "foo:rand:000000000000" 79 | 80 | assert(result == Some(data)) 81 | 82 | result 83 | } 84 | 85 | def timeIncrFast(reps: Int) = { 86 | repeat(reps) { client.quiet incr "counter:rand:000000000000" } 87 | 88 | val result = (client.sync getset ("counter:rand:000000000000", 0)).parse[Int] 89 | 90 | assert(result == Some(reps)) 91 | 92 | result 93 | } 94 | 95 | def timeIncr(reps: Int) = { 96 | repeat(reps) { client incr "counter:rand:000000000000" } 97 | 98 | val result = (client.sync getset ("counter:rand:000000000000", 0)).parse[Int] 99 | 100 | assert(result == Some(reps)) 101 | 102 | result 103 | } 104 | 105 | def timeScatterGather(reps: Int) = { 106 | val keys = (1 to 100).toList map ("list_" + _) 107 | 108 | val ops = reps / 200 // 2 ops per rep 109 | 110 | val scatter = { (key: String) ⇒ 111 | (1 to ops) foreach (i ⇒ client.quiet rpush (key, i)) 112 | client llen key map (x ⇒ assert(x == ops)) 113 | } 114 | 115 | val gather = { (key: String) ⇒ 116 | val sum = (Future(0) /: (1 to ops)) { (fr, _) ⇒ 117 | for { 118 | n ← client lpop key 119 | r ← fr 120 | } yield n.parse[Int].get + r 121 | } 122 | client llen key map (x ⇒ assert(x == 0)) flatMap (_ ⇒ sum) 123 | } 124 | 125 | val future = for { 126 | _ ← Future.traverse(keys)(scatter) 127 | n ← Future.traverse(keys)(gather) 128 | } yield n.sum 129 | 130 | val result = Await.result(future, Duration.Inf) 131 | 132 | assert(result == ((1 to ops).sum * 100)) 133 | 134 | } 135 | 136 | } 137 | 138 | trait SimpleScalaBenchmark extends SimpleBenchmark { 139 | 140 | def repeat[@specialized A](reps: Int)(f: => A): A = { 141 | def run(i: Int, result: A): A = { 142 | if (i == 0) result else run(i - 1, f) 143 | } 144 | run(reps - 1, f) 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/ConnectionPool.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie 2 | package redis 3 | 4 | import messages.{ RequestClient, ReleaseClient, Disconnect } 5 | import akka.actor._ 6 | import akka.dispatch.{ Promise, Future } 7 | import collection.immutable.Queue 8 | 9 | private[redis] class ConnectionPool(initialSize: Range, factory: (ActorRefFactory) ⇒ RedisClientPoolWorker) extends Actor { 10 | var ready: List[RedisClientPoolWorker] = Nil 11 | var active: Set[RedisClientPoolWorker] = Set.empty 12 | var limit: Range = initialSize 13 | var size: Int = 0 14 | var queue: Queue[Promise[RedisClientPoolWorker]] = Queue.empty 15 | 16 | def receive = { 17 | case RequestClient(promise) ⇒ 18 | if (active.size >= limit.max) 19 | queue = queue enqueue promise 20 | else { 21 | val client = ready match { 22 | case h :: t ⇒ 23 | ready = t 24 | h 25 | case _ ⇒ 26 | size += 1 27 | factory(context) 28 | } 29 | active += client 30 | promise complete Right(client) 31 | } 32 | case ReleaseClient(client) ⇒ 33 | if (active.size > limit.max) { 34 | killClient(client) 35 | } else if (queue.nonEmpty) { 36 | val (promise, rest) = queue.dequeue 37 | promise complete Right(client) 38 | queue = rest 39 | } else if ((size - active.size) == limit.min) { 40 | killClient(client) 41 | } else { 42 | ready ::= client 43 | active -= client 44 | } 45 | } 46 | 47 | def killClient(client: RedisClientPoolWorker) { 48 | client.disconnect 49 | active -= client 50 | size -= 1 51 | } 52 | 53 | override def postStop { 54 | queue foreach (_ complete Left(RedisConnectionException("Connection pool shutting down"))) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/Queued.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import types._ 4 | 5 | import akka.dispatch.{ Promise, Future } 6 | import akka.util.ByteString 7 | 8 | object Queued { 9 | private[redis] def apply[A](value: A, request: (ByteString, Promise[RedisType]), response: Promise[RedisType]): Queued[A] = 10 | new QueuedSingle(value, request, response) 11 | def apply[A](value: A): Queued[A] = new QueuedValue(value) 12 | 13 | private[redis] final class QueuedValue[+A](val value: A) extends Queued[A] { 14 | def requests = Vector.empty 15 | def responses = Vector.empty 16 | def flatMap[B](f: A ⇒ Queued[B]): Queued[B] = f(value) 17 | def map[B](f: A ⇒ B): Queued[B] = new QueuedValue(f(value)) 18 | } 19 | 20 | private[redis] final class QueuedSingle[+A](val value: A, val request: (ByteString, Promise[RedisType]), val response: Promise[RedisType]) extends Queued[A] { 21 | def requests = Vector(request) 22 | def responses = Vector(response) 23 | def flatMap[B](f: A ⇒ Queued[B]): Queued[B] = { 24 | val that = f(value) 25 | new QueuedList(that.value, request +: that.requests, response +: that.responses) 26 | } 27 | def map[B](f: A ⇒ B): Queued[B] = new QueuedSingle(f(value), request, response) 28 | } 29 | 30 | private[redis] final class QueuedList[+A](val value: A, val requests: Vector[(ByteString, Promise[RedisType])], val responses: Vector[Promise[RedisType]]) extends Queued[A] { 31 | def flatMap[B](f: A ⇒ Queued[B]): Queued[B] = { 32 | f(value) match { 33 | case that: QueuedList[_] ⇒ 34 | new QueuedList(that.value, requests ++ that.requests, responses ++ that.responses) 35 | case that: QueuedSingle[_] ⇒ 36 | new QueuedList(that.value, requests :+ that.request, responses :+ that.response) 37 | case that: QueuedValue[_] ⇒ 38 | new QueuedList(that.value, requests, responses) 39 | } 40 | } 41 | def map[B](f: A ⇒ B): Queued[B] = new QueuedList(f(value), requests, responses) 42 | } 43 | } 44 | 45 | sealed trait Queued[+A] { 46 | def value: A 47 | private[redis] def requests: Vector[(ByteString, Promise[RedisType])] 48 | private[redis] def responses: Vector[Promise[RedisType]] 49 | def flatMap[B](f: A ⇒ Queued[B]): Queued[B] 50 | def map[B](f: A ⇒ B): Queued[B] 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/RedisClient.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie 2 | package redis 3 | 4 | import actors._ 5 | import messages._ 6 | import protocol.Constants 7 | import Constants.EOL 8 | import types._ 9 | import serialization.{ Parse, Store } 10 | 11 | import akka.actor._ 12 | import akka.dispatch.{ Future, Promise, Await } 13 | import akka.util.{ ByteString, ByteStringBuilder, Timeout, Duration } 14 | 15 | case class RedisClientConfig(connections: Int = 1, 16 | autoReconnect: Boolean = true, 17 | retryOnReconnect: Boolean = true, 18 | poolSize: Range = 50 to 50) 19 | 20 | object RedisClient { 21 | def apply(host: String = "localhost", port: Int = 6379, config: RedisClientConfig = RedisClientConfig())(implicit system: ActorSystem) = 22 | new RedisClient(host, port, config) 23 | 24 | def subscriber(listener: ActorRef)(host: String = "localhost", port: Int = 6379, config: RedisClientConfig = RedisClientConfig())(implicit system: ActorSystem): ActorRef = 25 | system.actorOf(Props(new RedisSubscriberSession(listener)(host, port, config))) 26 | } 27 | 28 | final class RedisClient(val host: String, val port: Int, val config: RedisClientConfig)(implicit val system: ActorSystem) extends RedisClientAsync(config) { 29 | 30 | protected val actor = system.actorOf(Props(new RedisClientSession(host, port, config))) 31 | 32 | protected val pool: ActorRef = system.actorOf(Props(new ConnectionPool(config.poolSize, parent ⇒ 33 | new RedisClientPoolWorker(parent.actorOf(Props(new RedisClientSession(host, port, config))), config, pool)))) 34 | 35 | def disconnect = { 36 | system stop actor 37 | system stop pool 38 | } 39 | 40 | val sync: RedisClientSync = new RedisClientSync(config) { 41 | def system = RedisClient.this.system 42 | def actor = RedisClient.this.actor 43 | 44 | def async: RedisClientAsync = RedisClient.this 45 | def quiet: RedisClientQuiet = RedisClient.this.quiet 46 | } 47 | 48 | val quiet: RedisClientQuiet = new RedisClientQuiet(config) { 49 | def system = RedisClient.this.system 50 | def actor = RedisClient.this.actor 51 | 52 | def async: RedisClientAsync = RedisClient.this 53 | def sync: RedisClientSync = RedisClient.this.sync 54 | } 55 | 56 | def multi[T](block: (RedisClientMulti) ⇒ Queued[T]): T = { 57 | val rq = new RedisClientMulti(config) { 58 | def system = RedisClient.this.system 59 | def actor = RedisClient.this.actor 60 | } 61 | rq.exec(block(rq)) 62 | } 63 | 64 | def atomic[T](block: (RedisClientWatch) ⇒ Future[Queued[T]]): Future[T] = { 65 | val promise = Promise[RedisClientPoolWorker]() 66 | pool ! RequestClient(promise) 67 | promise flatMap (_.run(block)) 68 | } 69 | 70 | def subscriber(listener: ActorRef): ActorRef = 71 | system.actorOf(Props(new RedisSubscriberSession(listener)(host, port, config))) 72 | 73 | /** 74 | * Callback to be run when a request is sent to the server. The callback 75 | * must accept 2 Longs, the first is the id for the request (sequentially 76 | * incremented starting at 1, and is not shared between clients), and the 77 | * second is the time the request was made (in millis). 78 | */ 79 | def onRequest(callback: (Long, Long) ⇒ Unit) { 80 | actor ! RequestCallback(callback) 81 | } 82 | 83 | /** 84 | * Callback to be run when a result is received from the sender. The callback 85 | * must accept 2 Longs, the first is the id for the result (sequentially 86 | * incremented, and is not shared between clients), and the second is the time 87 | * the result was fully received (in millis). 88 | * 89 | * FIXME: The id should match with the request id, but if any requests were 90 | * lost due to connection failures they may lose sync. A more reliable method 91 | * will be implemented later. 92 | */ 93 | def onResult(callback: (Long, Long) ⇒ Unit) { 94 | actor ! ResultCallback(callback) 95 | } 96 | 97 | } 98 | 99 | sealed trait ConfigurableRedisClient { 100 | 101 | def sync: RedisClientSync 102 | def async: RedisClientAsync 103 | def quiet: RedisClientQuiet 104 | 105 | } 106 | 107 | sealed abstract class RedisClientAsync(config: RedisClientConfig) extends Commands[Future] with ConfigurableRedisClient { 108 | val async: RedisClientAsync = this 109 | 110 | final private[redis] def send(in: List[ByteString]): Future[Any] = { 111 | val promise = Promise[RedisType]() 112 | actor ! Request(PromiseRequestor(promise), format(in)) 113 | promise 114 | } 115 | } 116 | 117 | sealed abstract class RedisClientSync(config: RedisClientConfig) extends Commands[({ type λ[α] = α })#λ] with ConfigurableRedisClient { 118 | val sync: RedisClientSync = this 119 | 120 | final private[redis] def send(in: List[ByteString]): Any = { 121 | val promise = Promise[RedisType]() 122 | actor ! Request(PromiseRequestor(promise), format(in)) 123 | Await.result(promise, Duration.Inf) 124 | } 125 | } 126 | 127 | sealed abstract class RedisClientQuiet(config: RedisClientConfig) extends Commands[({ type λ[_] = Unit })#λ] with ConfigurableRedisClient { 128 | val quiet: RedisClientQuiet = this 129 | 130 | final private[redis] def send(in: List[ByteString]) = actor ! Request(NoRequestor, format(in)) 131 | } 132 | 133 | sealed abstract class RedisClientMulti(config: RedisClientConfig) extends Commands[({ type λ[α] = Queued[Future[α]] })#λ]()(ResultFunctor.multi) { 134 | 135 | implicit def system: ActorSystem 136 | 137 | final private[redis] def send(in: List[ByteString]): Queued[Future[Any]] = { 138 | val result = Promise[RedisType]() 139 | Queued(result, (format(in), Promise[RedisType]()), result) 140 | } 141 | 142 | final private[redis] def exec[T](q: Queued[T]): T = { 143 | val promise = Promise[RedisType]() 144 | promise foreach { 145 | case RedisMulti(m) ⇒ 146 | var responses = q.responses 147 | (q.responses.iterator zip (q.requests.iterator map (_._2.value))) foreach { 148 | case (resp, Some(Right(status))) ⇒ try { toStatus(status) } catch { case e ⇒ resp complete Left(e) } 149 | case (resp, _) ⇒ resp complete Left(RedisProtocolException("Unexpected response")) 150 | } 151 | for { 152 | list ← m 153 | rtype ← list 154 | } { 155 | while (responses.head.isCompleted) { responses = responses.tail } 156 | responses.head complete Right(rtype) 157 | responses = responses.tail 158 | } 159 | case RedisError(e) ⇒ 160 | val re = RedisErrorException(e) 161 | q.responses foreach (_.complete(Left(re))) 162 | case _ ⇒ 163 | val re = RedisProtocolException("Unexpected response") 164 | q.responses foreach (_.complete(Left(re))) 165 | } 166 | actor ! MultiRequest(PromiseRequestor(promise), format(List(Constants.MULTI)), q.requests, format(List(Constants.EXEC))) 167 | q.value 168 | } 169 | } 170 | 171 | sealed abstract class RedisClientWatch(config: RedisClientConfig) extends Commands[Future]()(ResultFunctor.async) { 172 | self: RedisClientPoolWorker ⇒ 173 | 174 | final private[redis] def send(in: List[ByteString]): Future[Any] = { 175 | val promise = Promise[RedisType]() 176 | actor ! Request(PromiseRequestor(promise), format(in)) 177 | promise 178 | } 179 | 180 | final private[redis] def run[T](block: (RedisClientWatch) ⇒ Future[Queued[T]]): Future[T] = { 181 | val promise = Promise[T]() 182 | exec(promise, block) 183 | promise onComplete { _ ⇒ this.release } 184 | } 185 | 186 | final private[redis] def exec[T](promise: Promise[T], block: (RedisClientWatch) ⇒ Future[Queued[T]]): Unit = { 187 | block(this) onFailure { case e ⇒ promise complete Left(e) } foreach { q ⇒ 188 | val multiPromise = Promise[RedisType]() 189 | multiPromise foreach { 190 | case RedisMulti(None) ⇒ 191 | exec(promise, block) // TODO: add timeout? 192 | case RedisMulti(m) ⇒ 193 | var responses = q.responses 194 | (q.responses.iterator zip (q.requests.iterator map (_._2.value))) foreach { 195 | case (resp, Some(Right(status))) ⇒ try { toStatus(status) } catch { case e ⇒ resp complete Left(e) } 196 | case (resp, _) ⇒ resp complete Left(RedisProtocolException("Unexpected response")) 197 | } 198 | for { 199 | list ← m 200 | rtype ← list 201 | } { 202 | while (responses.head.isCompleted) { responses = responses.tail } 203 | responses.head complete Right(rtype) 204 | responses = responses.tail 205 | } 206 | promise complete Right(q.value) 207 | case RedisError(e) ⇒ 208 | val re = RedisErrorException(e) 209 | q.responses foreach (_.complete(Left(re))) 210 | promise complete Left(re) 211 | case _ ⇒ 212 | val re = RedisProtocolException("Unexpected response") 213 | q.responses foreach (_.complete(Left(re))) 214 | promise complete Left(re) 215 | } 216 | actor ! MultiRequest(PromiseRequestor(multiPromise), format(List(Constants.MULTI)), q.requests, format(List(Constants.EXEC))) 217 | } 218 | } 219 | 220 | private val rq = new RedisClientMulti(config) { 221 | def system = RedisClientWatch.this.system 222 | def actor = RedisClientWatch.this.actor 223 | } 224 | 225 | def multi[T](block: (RedisClientMulti) ⇒ Queued[T]): Queued[T] = block(rq) 226 | 227 | def watch[A: serialization.Store](key: A): Future[Unit] = send(Constants.WATCH :: serialization.Store(key) :: Nil) 228 | 229 | } 230 | 231 | private[redis] final class RedisClientPoolWorker(protected val actor: ActorRef, config: RedisClientConfig, pool: ActorRef)(implicit val system: ActorSystem) extends RedisClientWatch(config) { 232 | def disconnect = actor ! Disconnect 233 | def release = pool ! ReleaseClient(this) 234 | } 235 | 236 | private[redis] final class RedisClientSub(protected val actor: ActorRef, config: RedisClientConfig, val system: ActorSystem) extends Commands[({ type X[_] = ByteString })#X] { 237 | import Constants._ 238 | 239 | final def send(in: List[ByteString]): ByteString = format(in) 240 | 241 | def subscribe[A: Store](channels: Iterable[A]): ByteString = send(SUBSCRIBE :: (channels.map(Store(_))(collection.breakOut): List[ByteString])) 242 | 243 | def unsubscribe[A: Store](channels: Iterable[A]): ByteString = send(UNSUBSCRIBE :: (channels.map(Store(_))(collection.breakOut): List[ByteString])) 244 | 245 | def psubscribe[A: Store](patterns: Iterable[A]): ByteString = send(PSUBSCRIBE :: (patterns.map(Store(_))(collection.breakOut): List[ByteString])) 246 | 247 | def punsubscribe[A: Store](patterns: Iterable[A]): ByteString = send(PUNSUBSCRIBE :: (patterns.map(Store(_))(collection.breakOut): List[ByteString])) 248 | 249 | } 250 | 251 | import commands._ 252 | private[redis] sealed abstract class Commands[Result[_]](implicit rf: ResultFunctor[Result]) extends Keys[Result] with Servers[Result] with Strings[Result] with Lists[Result] with Sets[Result] with SortedSets[Result] with Hashes[Result] with PubSub[Result] with Scripts[Result] { 253 | 254 | implicit def system: ActorSystem 255 | 256 | protected def actor: ActorRef 257 | 258 | private[redis] def send(in: List[ByteString]): Result[Any] 259 | 260 | final private[redis] def format(in: Seq[ByteString]): ByteString = { 261 | val builder = new ByteStringBuilder 262 | val count = in.length 263 | builder ++= ByteString("*" + count) 264 | builder ++= EOL 265 | in foreach { bytes ⇒ 266 | builder ++= ByteString("$" + bytes.length) 267 | builder ++= EOL 268 | builder ++= bytes 269 | builder ++= EOL 270 | } 271 | builder.result.compact 272 | } 273 | 274 | final private[redis] implicit def resultAsRedisType(raw: Result[Any]): Result[RedisType] = rf.fmap(raw)(toRedisType) 275 | final private[redis] implicit def resultAsMultiBulk(raw: Result[Any]): Result[Option[List[Option[ByteString]]]] = rf.fmap(raw)(toMultiBulk) 276 | final private[redis] implicit def resultAsMultiBulkList(raw: Result[Any]): Result[List[Option[ByteString]]] = rf.fmap(raw)(toMultiBulkList) 277 | final private[redis] implicit def resultAsMultiBulkFlat(raw: Result[Any]): Result[Option[List[ByteString]]] = rf.fmap(raw)(toMultiBulkFlat) 278 | final private[redis] implicit def resultAsMultiBulkFlatList(raw: Result[Any]): Result[List[ByteString]] = rf.fmap(raw)(toMultiBulkFlatList) 279 | final private[redis] implicit def resultAsMultiBulkSet(raw: Result[Any]): Result[Set[ByteString]] = rf.fmap(raw)(toMultiBulkSet) 280 | final private[redis] implicit def resultAsMultiBulkMap(raw: Result[Any]): Result[Map[ByteString, ByteString]] = rf.fmap(raw)(toMultiBulkMap) 281 | final private[redis] implicit def resultAsMultiBulkScored(raw: Result[Any]): Result[List[(ByteString, Double)]] = rf.fmap(raw)(toMultiBulkScored) 282 | final private[redis] implicit def resultAsMultiBulkSinglePair(raw: Result[Any]): Result[Option[(ByteString, ByteString)]] = rf.fmap(raw)(toMultiBulkSinglePair) 283 | final private[redis] implicit def resultAsMultiBulkSinglePairK[K: Parse](raw: Result[Any]): Result[Option[(K, ByteString)]] = rf.fmap(raw)(toMultiBulkSinglePair(_).map(kv ⇒ (Parse(kv._1), kv._2))) 284 | final private[redis] implicit def resultAsBulk(raw: Result[Any]): Result[Option[ByteString]] = rf.fmap(raw)(toBulk) 285 | final private[redis] implicit def resultAsDouble(raw: Result[Any]): Result[Double] = rf.fmap(raw)(toDouble) 286 | final private[redis] implicit def resultAsDoubleOption(raw: Result[Any]): Result[Option[Double]] = rf.fmap(raw)(toDoubleOption) 287 | final private[redis] implicit def resultAsLong(raw: Result[Any]): Result[Long] = rf.fmap(raw)(toLong) 288 | final private[redis] implicit def resultAsInt(raw: Result[Any]): Result[Int] = rf.fmap(raw)(toLong(_).toInt) 289 | final private[redis] implicit def resultAsIntOption(raw: Result[Any]): Result[Option[Int]] = rf.fmap(raw)(toIntOption) 290 | final private[redis] implicit def resultAsBool(raw: Result[Any]): Result[Boolean] = rf.fmap(raw)(toBool) 291 | final private[redis] implicit def resultAsStatus(raw: Result[Any]): Result[String] = rf.fmap(raw)(toStatus) 292 | final private[redis] implicit def resultAsOkStatus(raw: Result[Any]): Result[Unit] = rf.fmap(raw)(toOkStatus) 293 | 294 | final private[redis] val toRedisType: Any ⇒ RedisType = _ match { 295 | case r: RedisType ⇒ r 296 | case _ ⇒ throw RedisProtocolException("Unexpected response") 297 | } 298 | 299 | final private[redis] val toMultiBulk: Any ⇒ Option[List[Option[ByteString]]] = _ match { 300 | case RedisMulti(m) ⇒ m map (_ map toBulk) 301 | case RedisError(e) ⇒ throw RedisErrorException(e) 302 | case _ ⇒ throw RedisProtocolException("Unexpected response") 303 | } 304 | 305 | final private[redis] val toMultiBulkList: Any ⇒ List[Option[ByteString]] = _ match { 306 | case RedisMulti(Some(m)) ⇒ m map toBulk 307 | case RedisMulti(None) ⇒ Nil 308 | case RedisError(e) ⇒ throw RedisErrorException(e) 309 | case _ ⇒ throw RedisProtocolException("Unexpected response") 310 | } 311 | 312 | final private[redis] val toMultiBulkFlat: Any ⇒ Option[List[ByteString]] = _ match { 313 | case RedisMulti(m) ⇒ m map (_ flatMap (toBulk(_).toList)) 314 | case RedisError(e) ⇒ throw RedisErrorException(e) 315 | case _ ⇒ throw RedisProtocolException("Unexpected response") 316 | } 317 | 318 | final private[redis] val toMultiBulkFlatList: Any ⇒ List[ByteString] = _ match { 319 | case RedisMulti(Some(m)) ⇒ m flatMap (toBulk(_).toList) 320 | case RedisMulti(None) ⇒ Nil 321 | case RedisError(e) ⇒ throw RedisErrorException(e) 322 | case _ ⇒ throw RedisProtocolException("Unexpected response") 323 | } 324 | 325 | final private[redis] val toMultiBulkSet: Any ⇒ Set[ByteString] = _ match { 326 | case RedisMulti(Some(m)) ⇒ m.flatMap(toBulk(_).toList)(collection.breakOut) 327 | case RedisMulti(None) ⇒ Set.empty 328 | case RedisError(e) ⇒ throw RedisErrorException(e) 329 | case _ ⇒ throw RedisProtocolException("Unexpected response") 330 | } 331 | 332 | final private[redis] val toMultiBulkMap: Any ⇒ Map[ByteString, ByteString] = _ match { 333 | case RedisMulti(Some(m)) ⇒ m.grouped(2).collect { 334 | case List(RedisBulk(Some(a)), RedisBulk(Some(b))) ⇒ (a, b) 335 | } toMap 336 | case RedisMulti(None) ⇒ Map.empty 337 | case RedisError(e) ⇒ throw RedisErrorException(e) 338 | case _ ⇒ throw RedisProtocolException("Unexpected response") 339 | } 340 | 341 | final private[redis] val toMultiBulkScored: Any ⇒ List[(ByteString, Double)] = _ match { 342 | case RedisMulti(Some(m)) ⇒ m.grouped(2).collect { 343 | case List(RedisBulk(Some(a)), RedisBulk(Some(b))) ⇒ (a, Parse[Double](b)) 344 | } toList 345 | case RedisMulti(None) ⇒ Nil 346 | case RedisError(e) ⇒ throw RedisErrorException(e) 347 | case _ ⇒ throw RedisProtocolException("Unexpected response") 348 | } 349 | 350 | final private[redis] val toMultiBulkSinglePair: Any ⇒ Option[(ByteString, ByteString)] = _ match { 351 | case RedisMulti(Some(List(RedisBulk(Some(a)), RedisBulk(Some(b))))) ⇒ Some((a, b)) 352 | case RedisMulti(_) ⇒ None 353 | case RedisError(e) ⇒ throw RedisErrorException(e) 354 | case _ ⇒ throw RedisProtocolException("Unexpected response") 355 | } 356 | 357 | final private[redis] val toBulk: Any ⇒ Option[ByteString] = _ match { 358 | case RedisBulk(b) ⇒ b 359 | case RedisError(e) ⇒ throw RedisErrorException(e) 360 | case _ ⇒ throw RedisProtocolException("Unexpected response") 361 | } 362 | 363 | final private[redis] val toDouble: Any ⇒ Double = _ match { 364 | case RedisBulk(Some(b)) ⇒ Parse[Double](b) 365 | case RedisError(e) ⇒ throw RedisErrorException(e) 366 | case _ ⇒ throw RedisProtocolException("Unexpected response") 367 | } 368 | 369 | final private[redis] val toDoubleOption: Any ⇒ Option[Double] = _ match { 370 | case RedisBulk(b) ⇒ b map (Parse[Double](_)) 371 | case RedisError(e) ⇒ throw RedisErrorException(e) 372 | case _ ⇒ throw RedisProtocolException("Unexpected response") 373 | } 374 | 375 | final private[redis] val toLong: Any ⇒ Long = _ match { 376 | case RedisInteger(n) ⇒ n 377 | case RedisError(e) ⇒ throw RedisErrorException(e) 378 | case _ ⇒ throw RedisProtocolException("Unexpected response") 379 | } 380 | 381 | final private[redis] val toIntOption: Any ⇒ Option[Int] = _ match { 382 | case RedisInteger(n) ⇒ if (n >= 0) Some(n.toInt) else None 383 | case RedisBulk(None) ⇒ None 384 | case RedisError(e) ⇒ throw RedisErrorException(e) 385 | case _ ⇒ throw RedisProtocolException("Unexpected response") 386 | } 387 | 388 | final private[redis] val toBool: Any ⇒ Boolean = _ match { 389 | case RedisInteger(1) ⇒ true 390 | case RedisInteger(0) ⇒ false 391 | case RedisError(e) ⇒ throw RedisErrorException(e) 392 | case _ ⇒ throw RedisProtocolException("Unexpected response") 393 | } 394 | 395 | final private[redis] val toStatus: Any ⇒ String = _ match { 396 | case RedisString(s) ⇒ s 397 | case RedisBulk(Some(b)) ⇒ Parse[String](b) 398 | case RedisError(e) ⇒ throw RedisErrorException(e) 399 | case _ ⇒ throw RedisProtocolException("Unexpected response") 400 | } 401 | 402 | final private[redis] val toOkStatus: Any ⇒ Unit = _ match { 403 | case RedisString("OK") ⇒ Unit 404 | case RedisError(e) ⇒ throw RedisErrorException(e) 405 | case _ ⇒ throw RedisProtocolException("Unexpected response") 406 | } 407 | } 408 | 409 | case class RedisErrorException(message: String) extends RuntimeException(message) 410 | case class RedisProtocolException(message: String) extends RuntimeException(message) 411 | case class RedisConnectionException(message: String) extends RuntimeException(message) 412 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/ResultFunctor.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import akka.dispatch.Future 4 | import akka.util.ByteString 5 | 6 | private[redis] sealed abstract class ResultFunctor[R[_]] { 7 | def fmap[A, B](a: R[A])(f: A ⇒ B): R[B] 8 | } 9 | 10 | private[redis] object ResultFunctor { 11 | implicit val async: ResultFunctor[Future] = new ResultFunctor[Future] { 12 | def fmap[A, B](a: Future[A])(f: A ⇒ B): Future[B] = a map f 13 | } 14 | implicit val sync: ResultFunctor[({ type λ[α] = α })#λ] = new ResultFunctor[({ type λ[α] = α })#λ] { 15 | def fmap[A, B](a: A)(f: A ⇒ B): B = f(a) 16 | } 17 | implicit val quiet: ResultFunctor[({ type λ[_] = Unit })#λ] = new ResultFunctor[({ type λ[_] = Unit })#λ] { 18 | def fmap[A, B](a: Unit)(f: A ⇒ B): Unit = () 19 | } 20 | implicit val multi: ResultFunctor[({ type λ[α] = Queued[Future[α]] })#λ] = new ResultFunctor[({ type λ[α] = Queued[Future[α]] })#λ] { 21 | def fmap[A, B](a: Queued[Future[A]])(f: A ⇒ B): Queued[Future[B]] = a map (_ map f) 22 | } 23 | implicit val raw: ResultFunctor[({ type X[_] = ByteString })#X] = new ResultFunctor[({ type X[_] = ByteString })#X] { 24 | def fmap[A, B](a: ByteString)(f: A ⇒ B): ByteString = a 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/actors/Actors.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie 2 | package redis 3 | package actors 4 | 5 | import messages._ 6 | import types._ 7 | import pubsub._ 8 | import protocol._ 9 | 10 | import akka.actor._ 11 | import akka.util.ByteString 12 | import akka.util.duration._ 13 | import akka.event.Logging 14 | 15 | import java.util.concurrent.TimeUnit 16 | import java.net.ConnectException 17 | 18 | import scala.collection.mutable.Queue 19 | 20 | private[redis] final class RedisClientSession(host: String, port: Int, config: RedisClientConfig) extends Actor { 21 | import Constants._ 22 | import Iteratees._ 23 | 24 | val log = Logging(context.system, this) 25 | 26 | var sockets: Vector[IO.SocketHandle] = _ 27 | 28 | var nextSocket: Int = 0 29 | 30 | val state = IO.IterateeRef.Map.async[IO.Handle]()(context.dispatcher) 31 | 32 | var results = 0L 33 | var resultCallbacks = Seq.empty[(Long, Long) ⇒ Unit] 34 | var requestCallbacks = Seq.empty[(Long, Long) ⇒ Unit] 35 | 36 | val waiting = Queue.empty[RequestMessage] 37 | var requests = 0L 38 | 39 | override def preStart = { 40 | log info ("Connecting") 41 | sockets = Vector.fill(config.connections)(IOManager(context.system).connect(host, port)) 42 | } 43 | 44 | def receive = { 45 | 46 | case req @ Request(requestor, bytes) ⇒ 47 | val socket = sockets(nextSocket) 48 | nextSocket = (nextSocket + 1) % config.connections 49 | socket write bytes 50 | onRequest(req) 51 | for { 52 | _ ← state(socket) 53 | result ← readResult 54 | } yield { 55 | requestor respond result 56 | onResult() 57 | } 58 | 59 | case req @ MultiRequest(requestor, _, cmds, _) ⇒ 60 | val socket = sockets(nextSocket) 61 | nextSocket = (nextSocket + 1) % config.connections 62 | sendMulti(req, socket) 63 | onRequest(req) 64 | for { 65 | _ ← state(socket) 66 | _ ← readResult 67 | _ ← IO.fold((), cmds.map(_._2))((_, p) ⇒ readResult map (p success)) 68 | exec ← readResult 69 | } yield { 70 | requestor respond exec 71 | onResult() 72 | } 73 | 74 | case IO.Read(handle, bytes) ⇒ 75 | state(handle)(IO Chunk bytes) 76 | 77 | case RequestCallback(callback) ⇒ 78 | requestCallbacks +:= callback 79 | 80 | case ResultCallback(callback) ⇒ 81 | resultCallbacks +:= callback 82 | /* 83 | case IO.Closed(handle, Some(cause: ConnectException)) if socket == handle && config.autoReconnect ⇒ 84 | log info ("Connection refused, retrying in 1 second") 85 | context.system.scheduler.scheduleOnce(1 second, self, IO.Closed(handle, None)) 86 | 87 | case IO.Closed(handle, cause) if socket == handle && config.autoReconnect ⇒ 88 | log info ("Reconnecting" + (cause map (e ⇒ ", cause: " + e.toString) getOrElse "")) 89 | socket = IOManager(context.system).connect(host, port) 90 | if (config.retryOnReconnect) { 91 | waiting foreach { 92 | case req: Request ⇒ socket write req.bytes 93 | case req: MultiRequest ⇒ sendMulti(req) 94 | } 95 | if (waiting.nonEmpty) log info ("Retrying " + waiting.length + " commands") 96 | } */ 97 | 98 | case IO.Closed(handle, cause) if sockets contains handle ⇒ 99 | log info ("Connection closed" + (cause map (e ⇒ ", cause: " + e.toString) getOrElse "")) 100 | // FIXME: stop actors 101 | // sendToSupervisor(Disconnect) 102 | state(handle)(IO EOF cause) 103 | 104 | case Received ⇒ 105 | //waiting.dequeue 106 | 107 | } 108 | 109 | def onRequest(req: RequestMessage): Unit = { 110 | //if (config.retryOnReconnect) waiting enqueue req 111 | requests += 1L 112 | if (requestCallbacks.nonEmpty) { 113 | val atTime = System.currentTimeMillis 114 | requestCallbacks foreach (_(requests, atTime)) 115 | } 116 | } 117 | 118 | def sendMulti(req: MultiRequest, socket: IO.SocketHandle): Unit = { 119 | socket write req.multi 120 | req.cmds foreach { cmd ⇒ 121 | socket write cmd._1 122 | } 123 | socket write req.exec 124 | } 125 | 126 | def onResult() { 127 | //if (config.retryOnReconnect) self ! Received 128 | results += 1L 129 | if (resultCallbacks.nonEmpty) { 130 | val atTime = System.currentTimeMillis 131 | resultCallbacks foreach (_(results, atTime)) 132 | } 133 | } 134 | 135 | override def postStop() { 136 | log info ("Shutting down") 137 | sockets.foreach(_.close) 138 | // TODO: Complete all waiting requests with a RedisConnectionException 139 | } 140 | 141 | } 142 | 143 | private[redis] final class RedisSubscriberSession(listener: ActorRef)(host: String, port: Int, config: RedisClientConfig) extends Actor { 144 | import Iteratees._ 145 | 146 | val log = Logging(context.system, this) 147 | 148 | var socket: IO.SocketHandle = _ 149 | val state = IO.IterateeRef.async()(context.dispatcher) 150 | 151 | val client = new RedisClientSub(self, config, context.system) 152 | 153 | override def preStart = { 154 | log info ("Connecting") 155 | socket = IOManager(context.system).connect(host, port) 156 | state flatMap (_ ⇒ subscriber(listener)) 157 | } 158 | 159 | def receive = { 160 | 161 | case Subscribe(channels) ⇒ 162 | socket write (client.subscribe(channels)) 163 | 164 | case Unsubscribe(channels) ⇒ 165 | socket write (client.unsubscribe(channels)) 166 | 167 | case PSubscribe(patterns) ⇒ 168 | socket write (client.psubscribe(patterns)) 169 | 170 | case PUnsubscribe(patterns) ⇒ 171 | socket write (client.punsubscribe(patterns)) 172 | 173 | case IO.Read(handle, bytes) ⇒ 174 | state(IO Chunk bytes) 175 | 176 | case IO.Closed(handle, cause) if socket == handle ⇒ 177 | log info ("Connection closed" + (cause map (e ⇒ ", cause: " + e.toString) getOrElse "")) 178 | // FIXME: stop actors 179 | // sendToSupervisor(Disconnect) 180 | state(IO EOF cause) 181 | } 182 | 183 | override def postStop() { 184 | socket.close 185 | } 186 | 187 | def subscriber(listener: ActorRef): IO.Iteratee[Unit] = readResult flatMap { result ⇒ 188 | result match { 189 | case RedisMulti(Some(List(RedisBulk(Some(Constants.message)), RedisBulk(Some(channel)), RedisBulk(Some(message))))) ⇒ 190 | listener ! pubsub.Message(channel, message) 191 | case RedisMulti(Some(List(RedisBulk(Some(Constants.pmessage)), RedisBulk(Some(pattern)), RedisBulk(Some(channel)), RedisBulk(Some(message))))) ⇒ 192 | listener ! pubsub.PMessage(pattern, channel, message) 193 | case RedisMulti(Some(List(RedisBulk(Some(Constants.subscribe)), RedisBulk(Some(channel)), RedisInteger(count)))) ⇒ 194 | listener ! pubsub.Subscribed(channel, count) 195 | case RedisMulti(Some(List(RedisBulk(Some(Constants.unsubscribe)), RedisBulk(Some(channel)), RedisInteger(count)))) ⇒ 196 | listener ! pubsub.Unsubscribed(channel, count) 197 | case RedisMulti(Some(List(RedisBulk(Some(Constants.psubscribe)), RedisBulk(Some(pattern)), RedisInteger(count)))) ⇒ 198 | listener ! pubsub.PSubscribed(pattern, count) 199 | case RedisMulti(Some(List(RedisBulk(Some(Constants.punsubscribe)), RedisBulk(Some(pattern)), RedisInteger(count)))) ⇒ 200 | listener ! pubsub.PUnsubscribed(pattern, count) 201 | case other ⇒ 202 | throw RedisProtocolException("Unexpected response") 203 | } 204 | subscriber(listener) 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/Hashes.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait Hashes[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | def hdel[K: Store, F: Store](key: K, field: F): Result[Boolean] = 12 | send(HDEL :: Store(key) :: Store(field) :: Nil) 13 | 14 | def hexists[K: Store, F: Store](key: K, field: F): Result[Boolean] = 15 | send(HEXISTS :: Store(key) :: Store(field) :: Nil) 16 | 17 | def hget[K: Store, F: Store](key: K, field: F): Result[Option[ByteString]] = 18 | send(HGET :: Store(key) :: Store(field) :: Nil) 19 | 20 | def hgetall[K: Store](key: K): Result[Map[ByteString, ByteString]] = 21 | send(HGETALL :: Store(key) :: Nil) 22 | 23 | def hincrby[K: Store, F: Store](key: K, field: F, value: Long = 1): Result[Long] = 24 | send(HINCRBY :: Store(key) :: Store(field) :: Store(value) :: Nil) 25 | 26 | def hkeys[K: Store](key: K): Result[Set[ByteString]] = 27 | send(HKEYS :: Store(key) :: Nil) 28 | 29 | def hlen[K: Store](key: K): Result[Long] = 30 | send(HLEN :: Store(key) :: Nil) 31 | 32 | def hmget[K: Store, F: Store](key: K, fields: Seq[F]): Result[List[Option[ByteString]]] = 33 | send(HMGET :: Store(key) :: (fields.map(Store(_))(collection.breakOut): List[ByteString])) 34 | 35 | def hmset[K: Store, F: Store, V: Store](key: K, fvs: Iterable[Product2[F, V]]): Result[Unit] = 36 | send(HMSET :: Store(key) :: (fvs.flatMap(fv ⇒ Iterable(Store(fv._1), Store(fv._2)))(collection.breakOut): List[ByteString])) 37 | 38 | def hset[K: Store, F: Store, V: Store](key: K, field: F, value: V): Result[Boolean] = 39 | send(HSET :: Store(key) :: Store(field) :: Store(value) :: Nil) 40 | 41 | def hsetnx[K: Store, F: Store, V: Store](key: K, field: F, value: V): Result[Boolean] = 42 | send(HSETNX :: Store(key) :: Store(field) :: Store(value) :: Nil) 43 | 44 | def hvals[K: Store](key: K): Result[Set[ByteString]] = 45 | send(HVALS :: Store(key) :: Nil) 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/Keys.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait Keys[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | /** 12 | * Request keys matching `pattern`. 13 | * 14 | * Returns: `Set[ByteString]` 15 | * 16 | * @param pattern Use "*" as wildcard 17 | * 18 | * @see Redis Command Reference 19 | */ 20 | def keys[A: Store](pattern: A): Result[Set[ByteString]] = send(KEYS :: Store(pattern) :: Nil) 21 | def keys(): Result[Set[ByteString]] = send(KEYS :: ALLKEYS :: Nil) 22 | 23 | /** 24 | * Request a random key. 25 | * 26 | * Returns: `Option[ByteString]` 27 | * 28 | * @see Redis Command Reference 29 | */ 30 | def randomkey(): Result[Option[ByteString]] = send(List(RANDOMKEY)) 31 | 32 | /** 33 | * Rename a key. 34 | * 35 | * Returns: `Unit` 36 | * 37 | * @param oldkey The existing key to rename. 38 | * @param newkey The new key. 39 | * 40 | * @see Redis Command Reference 41 | */ 42 | def rename[A: Store, B: Store](oldkey: A, newkey: B): Result[Unit] = 43 | send(RENAME :: Store(oldkey) :: Store(newkey) :: Nil) 44 | 45 | /** 46 | * Rename a key if `newkey` does not already exist. Returns true if 47 | * successfully renamed. 48 | * 49 | * Returns: `Boolean` 50 | * 51 | * @param oldkey The existing key to rename. 52 | * @param newkey The new key. 53 | * 54 | * @see Redis Command Reference 55 | */ 56 | def renamenx[A: Store, B: Store](oldkey: A, newkey: B): Result[Boolean] = 57 | send(RENAMENX :: Store(oldkey) :: Store(newkey) :: Nil) 58 | 59 | /** 60 | * Request the number of keys in the current database. 61 | * 62 | * Returns: `Int` 63 | * 64 | * @see Redis Command Reference 65 | */ 66 | def dbsize(): Result[Int] = send(DBSIZE :: Nil) 67 | 68 | /** 69 | * Tests if `key` exists. 70 | * 71 | * Returns: `Boolean` 72 | * 73 | * @param key The key to test the existance of. 74 | * 75 | * @see Redis Command Reference 76 | */ 77 | def exists[A: Store](key: A): Result[Boolean] = send(EXISTS :: Store(key) :: Nil) 78 | 79 | /** 80 | * Delete each key in `keys`. Returns the actual number of keys deleted. 81 | * 82 | * Note: Be aware that this command takes an `Iterable[Any]`, so if a 83 | * single `String` is passed it will be converted into a `Seq[Char]` 84 | * which is probably not what you want. Instead, provide a `List[String]` 85 | * or similar collection. 86 | * 87 | * Returns: `Int` 88 | * 89 | * @param keys A collection of keys to delete. 90 | * 91 | * @see Redis Command Reference 92 | */ 93 | def del[A: Store](keys: Iterable[A]): Result[Int] = send(DEL :: (keys.map(Store(_))(collection.breakOut): List[ByteString])) 94 | def del[A: Store](key: A): Result[Int] = send(DEL :: Store(key) :: Nil) 95 | 96 | /** 97 | * Requests the type of the value stored at `key`. 98 | * 99 | * Returns: `String` 100 | * 101 | * @param key The key of the value to check. 102 | * 103 | * @see Redis Command Reference 104 | */ 105 | def typeof[A: Store](key: A): Result[String] = send(TYPE :: Store(key) :: Nil) 106 | 107 | /** 108 | * Set a timeout of the specified key. After the timeout the key will be 109 | * automatically deleted. Returns 'true' if the expire command is successful. 110 | * 111 | * Returns: `Boolean` 112 | * 113 | * @param key The key to expire. 114 | * @param seconds The timeout in seconds. 115 | * 116 | * @see Redis Command Reference 117 | */ 118 | def expire[A: Store](key: A, seconds: Long): Result[Boolean] = 119 | send(EXPIRE :: Store(key) :: Store(seconds) :: Nil) 120 | 121 | /** 122 | * Set a timeout of the specified key. After the timeout the key will be 123 | * automatically deleted. Returns 'true' if the expire command is successful. 124 | * 125 | * Returns: `Boolean` 126 | * 127 | * @param key The key to expire. 128 | * @param unixtime The timeout in the form of a UNIX timestamp. 129 | * 130 | * @see Redis Command Reference 131 | */ 132 | def expireat[A: Store](key: A, unixtime: Long): Result[Boolean] = 133 | send(EXPIREAT :: Store(key) :: Store(unixtime) :: Nil) 134 | 135 | /** 136 | * Select a DB with the supplied zero-based index. 137 | * 138 | * Returns: `Unit` 139 | * 140 | * @param index Zero-based index of database. 141 | * 142 | * @see Redis Command Reference 143 | */ 144 | def select(index: Int = 0): Result[Unit] = send(SELECT :: Store(index) :: Nil) 145 | 146 | /** 147 | * Delete all keys in the currently selected database. 148 | * 149 | * Returns: `Unit` 150 | * 151 | * @see Redis Command Reference 152 | */ 153 | def flushdb(): Result[Unit] = send(FLUSHDB :: Nil) 154 | 155 | /** 156 | * Delete all keys in all databases. 157 | * 158 | * Returns: `Unit` 159 | * 160 | * @see Redis Command Reference 161 | */ 162 | def flushall(): Result[Unit] = send(FLUSHALL :: Nil) 163 | 164 | /** 165 | * Move `key` from the currently selected database to the database at index `db`. 166 | * Returns `true` if successful. 167 | * 168 | * Returns: `Boolean` 169 | * 170 | * @param key The key to move. 171 | * @param db The zero-based index of the destination database. 172 | * 173 | * @see Redis Command Reference 174 | */ 175 | def move[A: Store](key: A, db: Int = 0): Result[Boolean] = send(MOVE :: Store(key) :: Store(db) :: Nil) 176 | 177 | /** 178 | * Asks the server to close the connection. 179 | * 180 | * Returns: `Unit` 181 | * 182 | * @see Redis Command Reference 183 | */ 184 | def quit(): Result[Unit] = send(QUIT :: Nil) 185 | 186 | /** 187 | * Supply a password if required to send commands. 188 | * 189 | * Returns: `Unit` 190 | * 191 | * @param secret Server authentication password. 192 | * 193 | * @see Redis Command Reference 194 | */ 195 | def auth[A: Store](secret: A): Result[Unit] = send(AUTH :: Store(secret) :: Nil) 196 | 197 | def ping(): Result[String] = send(PING :: Nil) 198 | 199 | def echo[A: Store](value: A): Result[Option[ByteString]] = send(ECHO :: Store(value) :: Nil) 200 | 201 | /** 202 | * Sorts the elements contained in a List, Set, or Sorted Set value at `key`. 203 | * 204 | * Returns: `List[Option[A]]` 205 | * 206 | * @param key The key of the List, Set, or Sorted Set to sort. 207 | * @param by Optional pattern used to generate the key names of the weights used for sorting. 208 | * Use "nosort" if no sorting is required, which can be useful if using 209 | * the `get` parameter to return other values. 210 | * @param limit Optional zero-based start-index and count of items to return. 211 | * @param get List of patterns used to generate key names of values to return. 212 | * Use "#" to include the elements of the sorted list as well. 213 | * @param order Optional `SortOrder` 214 | * @param alpha If `true`, sort lexicalgraphically. 215 | * 216 | * @see Redis Command Reference 217 | */ 218 | def sort[K: Store, B: Store, G: Store](key: K, by: Option[B] = Option.empty[ByteString], limit: RedisLimit = NoLimit, get: Seq[G] = List.empty[ByteString], order: Option[SortOrder] = None, alpha: Boolean = false): Result[List[Option[ByteString]]] = { 219 | var cmd: List[ByteString] = Nil 220 | if (alpha) cmd ::= ALPHA 221 | order foreach (o ⇒ cmd ::= Store(o)) 222 | get.reverse foreach (g ⇒ cmd = GET :: Store(g) :: cmd) 223 | limit match { 224 | case Limit(o, c) ⇒ cmd = LIMIT :: Store(o) :: Store(c) :: cmd 225 | case NoLimit ⇒ 226 | } 227 | by foreach (b ⇒ cmd = BY :: Store(b) :: cmd) 228 | send(SORT :: Store(key) :: cmd) 229 | } 230 | 231 | } 232 | 233 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/Lists.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait Lists[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | def blpop[K: Store: Parse](keys: Seq[K], timeout: Int = 0): Result[Option[(K, ByteString)]] = 12 | send(BLPOP :: (Store(timeout) :: (keys.map(Store(_))(collection.breakOut): List[ByteString])).reverse) 13 | 14 | def brpop[K: Store: Parse](keys: Seq[K], timeout: Int = 0): Result[Option[(K, ByteString)]] = 15 | send(BRPOP :: (Store(timeout) :: (keys.map(Store(_))(collection.breakOut): List[ByteString])).reverse) 16 | 17 | def brpoplpush[A: Store, B: Store](srcKey: A, dstKey: B, timeout: Int = 0): Result[Option[ByteString]] = 18 | send(BRPOPLPUSH :: Store(srcKey) :: Store(dstKey) :: Store(timeout) :: Nil) 19 | 20 | def lindex[K: Store](key: K, index: Int): Result[Option[ByteString]] = 21 | send(LINDEX :: Store(key) :: Store(index) :: Nil) 22 | 23 | def linsertbefore[K: Store, A: Store, B: Store](key: K, pivot: A, value: B): Result[Option[Int]] = 24 | send(LINSERT :: Store(key) :: BEFORE :: Store(pivot) :: Store(value) :: Nil) 25 | 26 | def linsertafter[K: Store, A: Store, B: Store](key: K, pivot: A, value: B): Result[Option[Int]] = 27 | send(LINSERT :: Store(key) :: AFTER :: Store(pivot) :: Store(value) :: Nil) 28 | 29 | def llen[K: Store](key: K): Result[Int] = 30 | send(LLEN :: Store(key) :: Nil) 31 | 32 | def lpop[K: Store](key: K): Result[Option[ByteString]] = 33 | send(LPOP :: Store(key) :: Nil) 34 | 35 | def lpush[K: Store, V: Store](key: K, value: V): Result[Int] = 36 | send(LPUSH :: Store(key) :: Store(value) :: Nil) 37 | 38 | def lpushx[K: Store, V: Store](key: K, value: V): Result[Int] = 39 | send(LPUSHX :: Store(key) :: Store(value) :: Nil) 40 | 41 | def lrange[K: Store](key: K, start: Int = 0, end: Int = -1): Result[Option[List[ByteString]]] = 42 | send(LRANGE :: Store(key) :: Store(start) :: Store(end) :: Nil) 43 | 44 | def lrem[K: Store, V: Store](key: K, value: V, count: Int = 0): Result[Int] = 45 | send(LREM :: Store(key) :: Store(count) :: Store(value) :: Nil) 46 | 47 | def lset[K: Store, V: Store](key: K, index: Int, value: V): Result[Unit] = 48 | send(LSET :: Store(key) :: Store(index) :: Store(value) :: Nil) 49 | 50 | def ltrim[K: Store](key: K, start: Int = 0, end: Int = -1): Result[Unit] = 51 | send(LTRIM :: Store(key) :: Store(start) :: Store(end) :: Nil) 52 | 53 | def rpop[K: Store](key: K): Result[Option[ByteString]] = 54 | send(RPOP :: Store(key) :: Nil) 55 | 56 | def rpoplpush[A: Store, B: Store](srcKey: A, dstKey: B): Result[Option[ByteString]] = 57 | send(RPOPLPUSH :: Store(srcKey) :: Store(dstKey) :: Nil) 58 | 59 | def rpush[K: Store, V: Store](key: K, value: V): Result[Int] = 60 | send(RPUSH :: Store(key) :: Store(value) :: Nil) 61 | 62 | def rpushx[K: Store, V: Store](key: K, value: V): Result[Int] = 63 | send(RPUSHX :: Store(key) :: Store(value) :: Nil) 64 | } 65 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/PubSub.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait PubSub[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | def publish[A: Store, B: Store](channel: A, message: B): Result[Int] = send(PUBLISH :: Store(channel) :: Store(message) :: Nil) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/Script.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait Scripts[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | def eval[S: Store, K: Store, A: Store](script: S, keys: Seq[K] = Seq.empty[Store.Dummy], args: Seq[A] = Seq.empty[Store.Dummy]): Result[types.RedisType] = 12 | send(EVAL :: Store(script) :: Store(keys.size) :: (keys.map(Store(_))(collection.breakOut): List[ByteString]) ::: (args.map(Store(_))(collection.breakOut): List[ByteString])) 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/Servers.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait Servers[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | def save(): Result[Unit] = send(SAVE :: Nil) 12 | 13 | def bgsave(): Result[Unit] = send(BGSAVE :: Nil) 14 | 15 | def lastsave(): Result[Int] = send(LASTSAVE :: Nil) 16 | 17 | //def shutdown(): Result[Nothing] 18 | 19 | def bgrewriteaof(): Result[Unit] = send(BGREWRITEAOF :: Nil) 20 | 21 | def info(): Result[String] = send(INFO :: Nil) 22 | 23 | //def monitor extends Command(OkStatus) 24 | /* 25 | def slaveof(hostPort: Option[(String, Int)])(implicit format: Format) extends Command(OkStatus) { 26 | override def args = hostPort map (x => Iterator(x._1, x._2)) getOrElse Iterator("NO ONE") 27 | } 28 | 29 | object config { 30 | 31 | def get(param: Any)(implicit format: Format, parse: Parse[A]) extends Command(MultiBulk[A]()(implicitly, parse.manifest)) { 32 | override def name = "CONFIG" 33 | override def args = Iterator("GET", param) 34 | } 35 | 36 | def set(param: Any, value: Any)(implicit format: Format) extends Command(OkStatus) { 37 | override def name = "CONFIG" 38 | override def args = Iterator("SET", param, value) 39 | } 40 | 41 | } 42 | */ 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/Sets.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait Sets[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | def sadd[K: Store, V: Store](key: K, value: V): Result[Boolean] = 12 | send(SADD :: Store(key) :: Store(value) :: Nil) 13 | 14 | def scard[K: Store](key: K): Result[Int] = 15 | send(SCARD :: Store(key) :: Nil) 16 | 17 | def sdiff[A: Store, B: Store](key: A, diffkeys: Iterable[B]): Result[Set[ByteString]] = 18 | send(SDIFF :: Store(key) :: (diffkeys.map(Store(_))(collection.breakOut): List[ByteString])) 19 | 20 | def sdiffstore[A: Store, B: Store, C: Store](dstkey: A, key: B, diffkeys: Iterable[C]): Result[Int] = 21 | send(SDIFFSTORE :: Store(dstkey) :: Store(key) :: (diffkeys.map(Store(_))(collection.breakOut): List[ByteString])) 22 | 23 | def sinter[K: Store](keys: Iterable[K]): Result[Set[ByteString]] = 24 | send(SINTER :: (keys.map(Store(_))(collection.breakOut): List[ByteString])) 25 | 26 | def sinterstore[A: Store, B: Store](dstkey: A, keys: Iterable[B]): Result[Int] = 27 | send(SINTERSTORE :: Store(dstkey) :: (keys.map(Store(_))(collection.breakOut): List[ByteString])) 28 | 29 | def sismember[K: Store, V: Store](key: K, value: V): Result[Boolean] = 30 | send(SISMEMBER :: Store(key) :: Store(value) :: Nil) 31 | 32 | def smembers[K: Store](key: K): Result[Set[ByteString]] = 33 | send(SMEMBERS :: Store(key) :: Nil) 34 | 35 | def smove[A: Store, B: Store, V: Store](srcKey: A, dstKey: B, value: V): Result[Boolean] = 36 | send(SMOVE :: Store(srcKey) :: Store(dstKey) :: Store(value) :: Nil) 37 | 38 | def spop[K: Store](key: K): Result[Option[ByteString]] = 39 | send(SPOP :: Store(key) :: Nil) 40 | 41 | def srandmember[K: Store](key: K): Result[Option[ByteString]] = 42 | send(SRANDMEMBER :: Store(key) :: Nil) 43 | 44 | def srem[K: Store, V: Store](key: K, value: V): Result[Boolean] = 45 | send(SREM :: Store(key) :: Store(value) :: Nil) 46 | 47 | def sunion[K: Store](keys: Iterable[K]): Result[Set[ByteString]] = 48 | send(SUNION :: (keys.map(Store(_))(collection.breakOut): List[ByteString])) 49 | 50 | def sunionstore[A: Store, B: Store](dstkey: A, keys: Iterable[B]): Result[Int] = 51 | send(SUNIONSTORE :: Store(dstkey) :: (keys.map(Store(_))(collection.breakOut): List[ByteString])) 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/SortedSets.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait SortedSets[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | def zadd[K: Store, M: Store](key: K, member: M, score: Double = 1.0): Result[Boolean] = 12 | send(ZADD :: Store(key) :: Store(score) :: Store(member) :: Nil) 13 | 14 | def zcard[K: Store](key: K): Result[Int] = 15 | send(ZCARD :: Store(key) :: Nil) 16 | 17 | def zcount[K: Store](key: K, min: RedisScore = RedisScore.min, max: RedisScore = RedisScore.max): Result[Int] = 18 | send(ZCOUNT :: Store(key) :: Store(min) :: Store(max) :: Nil) 19 | 20 | def zincrby[K: Store, M: Store](key: K, member: M, incr: Double = 1.0): Result[Double] = 21 | send(ZINCRBY :: Store(key) :: Store(incr) :: Store(member) :: Nil) 22 | 23 | def zinterstore[D: Store, K: Store](dstKey: D, keys: Iterable[K], aggregate: Aggregate = Aggregate.Sum): Result[Int] = { 24 | var cmd = AGGREGATE :: Store(aggregate) :: Nil 25 | var keyCount = 0 26 | keys foreach { k ⇒ 27 | cmd ::= Store(k) 28 | keyCount += 1 29 | } 30 | send(ZINTERSTORE :: Store(dstKey) :: Store(keyCount) :: cmd) 31 | } 32 | 33 | def zinterstoreWeighted[D: Store, K: Store](dstKey: D, kws: Iterable[Product2[K, Double]], aggregate: Aggregate = Aggregate.Sum): Result[Int] = { 34 | var cmd: List[ByteString] = Nil 35 | var keyCount = 0 36 | kws foreach { kw ⇒ 37 | cmd ::= Store(kw._1) 38 | keyCount += 1 39 | } 40 | cmd ::= WEIGHTS 41 | kws foreach { kw ⇒ cmd ::= Store(kw._2) } 42 | send(ZINTERSTORE :: Store(dstKey) :: Store(keyCount) :: (Store(aggregate) :: AGGREGATE :: cmd).reverse) 43 | } 44 | 45 | def zrange[K: Store](key: K, start: Int = 0, stop: Int = -1): Result[List[ByteString]] = 46 | send(ZRANGE :: Store(key) :: Store(start) :: Store(stop) :: Nil) 47 | 48 | def zrangeWithScores[K: Store](key: K, start: Int = 0, stop: Int = -1): Result[List[(ByteString, Double)]] = 49 | send(ZRANGE :: Store(key) :: Store(start) :: Store(stop) :: WITHSCORES :: Nil) 50 | 51 | def zrangebyscore[K: Store](key: K, min: RedisScore = RedisScore.min, max: RedisScore = RedisScore.max, limit: RedisLimit = NoLimit): Result[List[ByteString]] = 52 | send(ZRANGEBYSCORE :: Store(key) :: Store(min) :: Store(max) :: (limit match { 53 | case NoLimit ⇒ Nil 54 | case Limit(o, c) ⇒ LIMIT :: Store(o) :: Store(c) :: Nil 55 | })) 56 | 57 | def zrangebyscoreWithScores[K: Store](key: K, min: RedisScore = RedisScore.min, max: RedisScore = RedisScore.max, limit: RedisLimit = NoLimit): Result[List[(ByteString, Double)]] = 58 | send(ZRANGEBYSCORE :: Store(key) :: Store(min) :: Store(max) :: WITHSCORES :: (limit match { 59 | case NoLimit ⇒ Nil 60 | case Limit(o, c) ⇒ LIMIT :: Store(o) :: Store(c) :: Nil 61 | })) 62 | 63 | def zrank[K: Store, M: Store](key: K, member: M): Result[Option[Int]] = 64 | send(ZRANK :: Store(key) :: Store(member) :: Nil) 65 | 66 | def zrem[K: Store, M: Store](key: K, member: M): Result[Boolean] = 67 | send(ZREM :: Store(key) :: Store(member) :: Nil) 68 | 69 | def zremrangebyrank[K: Store](key: K, start: Int = 0, stop: Int = -1): Result[Int] = 70 | send(ZREMRANGEBYRANK :: Store(key) :: Store(start) :: Store(stop) :: Nil) 71 | 72 | def zremrangebyscore[K: Store](key: K, min: RedisScore = RedisScore.min, max: RedisScore = RedisScore.max): Result[Int] = 73 | send(ZREMRANGEBYSCORE :: Store(key) :: Store(min) :: Store(max) :: Nil) 74 | 75 | def zrevrange[K: Store](key: K, start: Int = 0, stop: Int = -1): Result[List[ByteString]] = 76 | send(ZREVRANGE :: Store(key) :: Store(start) :: Store(stop) :: Nil) 77 | 78 | def zrevrangeWithScores[K: Store](key: K, start: Int = 0, stop: Int = -1): Result[List[(ByteString, Double)]] = 79 | send(ZREVRANGE :: Store(key) :: Store(start) :: Store(stop) :: WITHSCORES :: Nil) 80 | 81 | def zrevrangebyscore[K: Store](key: K, min: RedisScore = RedisScore.min, max: RedisScore = RedisScore.max, limit: RedisLimit = NoLimit): Result[List[ByteString]] = 82 | send(ZREVRANGEBYSCORE :: Store(key) :: Store(min) :: Store(max) :: (limit match { 83 | case NoLimit ⇒ Nil 84 | case Limit(o, c) ⇒ LIMIT :: Store(o) :: Store(c) :: Nil 85 | })) 86 | 87 | def zrevrangebyscoreWithScores[K: Store](key: K, min: RedisScore = RedisScore.min, max: RedisScore = RedisScore.max, limit: RedisLimit = NoLimit): Result[List[(ByteString, Double)]] = 88 | send(ZREVRANGEBYSCORE :: Store(key) :: Store(min) :: Store(max) :: WITHSCORES :: (limit match { 89 | case NoLimit ⇒ Nil 90 | case Limit(o, c) ⇒ LIMIT :: Store(o) :: Store(c) :: Nil 91 | })) 92 | 93 | def zrevrank[K: Store, M: Store](key: K, member: M): Result[Option[Int]] = 94 | send(ZREVRANK :: Store(key) :: Store(member) :: Nil) 95 | 96 | def zscore[K: Store, M: Store](key: K, member: M): Result[Option[Double]] = 97 | send(ZSCORE :: Store(key) :: Store(member) :: Nil) 98 | 99 | def zunionstore[D: Store, K: Store](dstKey: D, keys: Iterable[K], aggregate: Aggregate = Aggregate.Sum): Result[Int] = { 100 | var cmd = AGGREGATE :: Store(aggregate) :: Nil 101 | var keyCount = 0 102 | keys foreach { k ⇒ 103 | cmd ::= Store(k) 104 | keyCount += 1 105 | } 106 | send(ZUNIONSTORE :: Store(dstKey) :: Store(keyCount) :: cmd) 107 | } 108 | 109 | def zunionstoreWeighted[D: Store, K: Store](dstKey: D, kws: Iterable[Product2[K, Double]], aggregate: Aggregate = Aggregate.Sum): Result[Int] = { 110 | var cmd: List[ByteString] = Nil 111 | var keyCount = 0 112 | kws foreach { kw ⇒ 113 | cmd ::= Store(kw._1) 114 | keyCount += 1 115 | } 116 | cmd ::= WEIGHTS 117 | kws foreach { kw ⇒ cmd ::= Store(kw._2) } 118 | send(ZUNIONSTORE :: Store(dstKey) :: Store(keyCount) :: (Store(aggregate) :: AGGREGATE :: cmd).reverse) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/commands/Strings.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package commands 3 | 4 | import serialization._ 5 | import akka.util.ByteString 6 | 7 | private[redis] trait Strings[Result[_]] { 8 | this: Commands[Result] ⇒ 9 | import protocol.Constants._ 10 | 11 | def append[K: Store, V: Store](key: K, value: V): Result[Int] = 12 | send(APPEND :: Store(key) :: Store(value) :: Nil) 13 | 14 | def decr[K: Store](key: K): Result[Long] = 15 | send(DECR :: Store(key) :: Nil) 16 | 17 | def decrby[K: Store](key: K, decrement: Long): Result[Long] = 18 | send(DECRBY :: Store(key) :: Store(decrement) :: Nil) 19 | 20 | def get[K: Store](key: K): Result[Option[ByteString]] = 21 | send(GET :: Store(key) :: Nil) 22 | 23 | def getbit[K: Store](key: K, offset: Int): Result[Int] = 24 | send(GETBIT :: Store(key) :: Store(offset) :: Nil) 25 | 26 | def getrange[K: Store](key: K, start: Int = 0, end: Int = -1): Result[Int] = 27 | send(GETRANGE :: Store(key) :: Store(start) :: Store(end) :: Nil) 28 | 29 | def getset[K: Store, V: Store](key: K, value: V): Result[Option[ByteString]] = 30 | send(GETSET :: Store(key) :: Store(value) :: Nil) 31 | 32 | def incr[K: Store](key: K): Result[Long] = 33 | send(INCR :: Store(key) :: Nil) 34 | 35 | def incrby[K: Store](key: K, increment: Long): Result[Long] = 36 | send(INCRBY :: Store(key) :: Store(increment) :: Nil) 37 | 38 | def mget[K: Store](keys: Seq[K]): Result[List[Option[ByteString]]] = 39 | send(MGET :: (keys.map(Store(_))(collection.breakOut): List[ByteString])) 40 | 41 | def mset[K: Store, V: Store](kvs: Iterable[Product2[K, V]]): Result[Unit] = 42 | send(MSET :: (kvs.flatMap(kv ⇒ Iterable(Store(kv._1), Store(kv._2)))(collection.breakOut): List[ByteString])) 43 | 44 | def msetnx[K: Store, V: Store](kvs: Iterable[Product2[K, V]]): Result[Boolean] = 45 | send(MSETNX :: (kvs.flatMap(kv ⇒ Iterable(Store(kv._1), Store(kv._2)))(collection.breakOut): List[ByteString])) 46 | 47 | def set[K: Store, V: Store](key: K, value: V): Result[Unit] = 48 | send(SET :: Store(key) :: Store(value) :: Nil) 49 | 50 | def setbit[K: Store](key: K, offset: Int, value: Int): Result[Int] = 51 | send(SETBIT :: Store(key) :: Store(offset) :: Store(value) :: Nil) 52 | 53 | def setex[K: Store, V: Store](key: K, seconds: Int, value: V): Result[Unit] = 54 | send(SETEX :: Store(key) :: Store(seconds) :: Store(value) :: Nil) 55 | 56 | def setnx[K: Store, V: Store](key: K, value: V): Result[Boolean] = 57 | send(SETNX :: Store(key) :: Store(value) :: Nil) 58 | 59 | def setrange[K: Store, V: Store](key: K, offset: Int, value: V): Result[Int] = 60 | send(SETRANGE :: Store(key) :: Store(offset) :: Store(value) :: Nil) 61 | 62 | def strlen[K: Store](key: K): Result[Int] = 63 | send(STRLEN :: Store(key) :: Nil) 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/lua.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie 2 | package redis 3 | package lua 4 | 5 | import akka.util.ByteString 6 | 7 | private[lua] object Literals { 8 | val ADD = ByteString(" + ") 9 | val ASSIGN = ByteString(" = ") 10 | val COMMA = ByteString(", ") 11 | val DIV = ByteString(" / ") 12 | val DO = ByteString("do ") 13 | val ELSE = ByteString("else ") 14 | val ELSEIF = ByteString("elseif ") 15 | val END = ByteString("end") 16 | val EQ = ByteString(" == ") 17 | val GT = ByteString(" > ") 18 | val GTEQ = ByteString(" >= ") 19 | val IF = ByteString("if ") 20 | val LT = ByteString(" < ") 21 | val LTEQ = ByteString(" <= ") 22 | val MULT = ByteString(" * ") 23 | val PARENL = ByteString("(") 24 | val PARENR = ByteString(")") 25 | val REPEAT = ByteString("repeat ") 26 | val RETURN = ByteString("return ") 27 | val SEMICOLON = ByteString("; ") 28 | val SUB = ByteString(" - ") 29 | val THEN = ByteString(" then ") 30 | val UNTIL = ByteString("until ") 31 | val WHILE = ByteString("while ") 32 | val CURLYL = ByteString("{") 33 | val CURLYR = ByteString("}") 34 | } 35 | 36 | import Literals._ 37 | 38 | sealed trait Chunk { 39 | def bytes: ByteString 40 | } 41 | sealed trait Block extends Chunk { 42 | def ::(stat: Stat) = StatList(stat, this) 43 | } 44 | 45 | case class StatList(stat: Stat, next: Block) extends Block { 46 | val bytes = stat.bytes ++ SEMICOLON ++ next.bytes 47 | } 48 | 49 | sealed trait Stat { 50 | def bytes: ByteString 51 | } 52 | //case class Call(fun: String) extends Stat 53 | case class Assign(name: Var, exp: Exp) extends Stat { 54 | val bytes = name.bytes ++ ASSIGN ++ exp.bytes 55 | } 56 | //case class Local(name: Var, exp: Exp) extends Stat 57 | case class Do(block: Block) extends Stat { 58 | val bytes = DO ++ block.bytes ++ END 59 | } 60 | case class While(test: Exp, block: Block) extends Stat { 61 | val bytes = WHILE ++ test.bytes ++ DO ++ block.bytes ++ END 62 | } 63 | case class Repeat(block: Block, test: Exp) extends Stat { 64 | val bytes = REPEAT ++ block.bytes ++ UNTIL ++ test.bytes 65 | } 66 | case class If(test: Exp) { 67 | def Then(block: Block) = IfThen(test, block, Vector.empty) 68 | } 69 | case class IfThen(test: Exp, then: Block, elseIf: Vector[ElseIfThen]) extends Stat { 70 | def ElseIf(test: Exp) = lua.ElseIf(test, this) 71 | def Else(block: Block) = IfThenElse(test, then, elseIf, block) 72 | val bytes = ((IF ++ test.bytes ++ THEN ++ then.bytes) /: elseIf)(_ ++ _.bytes) ++ END 73 | } 74 | case class IfThenElse(test: Exp, then: Block, elseIf: Vector[ElseIfThen], `else`: Block) extends Stat { 75 | val bytes = ((IF ++ test.bytes ++ THEN ++ then.bytes) /: elseIf)(_ ++ _.bytes) ++ ELSE ++ `else`.bytes ++ END 76 | } 77 | case class ElseIf(test: Exp, parent: IfThen) { 78 | def Then(block: Block) = parent.copy(elseIf = parent.elseIf :+ ElseIfThen(test, block)) 79 | } 80 | case class ElseIfThen(test: Exp, block: Block) { 81 | val bytes = ELSEIF ++ test.bytes ++ THEN ++ block.bytes 82 | } 83 | 84 | sealed trait LastStat extends Block 85 | case class Return(exps: Exp*) extends LastStat { 86 | val bytes = RETURN ++ (if (exps.nonEmpty) (exps.head.bytes /: exps.tail)(_ ++ COMMA ++ _.bytes) else ByteString.empty) 87 | } 88 | case object Break extends LastStat { 89 | def bytes = ByteString("break") 90 | } 91 | case object End extends LastStat { 92 | def bytes = ByteString.empty 93 | } 94 | 95 | object Exp { 96 | def apply(exp: Exp) = exp match { 97 | case _: BinOp ⇒ Par(exp) 98 | case _ ⇒ exp 99 | } 100 | implicit def boolToExp(b: Boolean): Exp = if (b) True else False 101 | implicit def intToExp(n: Int): Exp = Num(n) 102 | implicit def strToExp(s: String): Exp = Str(s) 103 | } 104 | 105 | sealed trait Exp { 106 | def bytes: ByteString 107 | def :+(that: Exp) = Add(this, Exp(that)) 108 | def :-(that: Exp) = Sub(this, Exp(that)) 109 | def :*(that: Exp) = Mult(this, Exp(that)) 110 | def :\(that: Exp) = Div(this, Exp(that)) 111 | def :<(that: Exp) = Lt(this, Exp(that)) 112 | def :<=(that: Exp) = LtEq(this, Exp(that)) 113 | def :>(that: Exp) = Lt(this, Exp(that)) 114 | def :>=(that: Exp) = LtEq(this, Exp(that)) 115 | def :==(that: Exp) = Eq(this, Exp(that)) 116 | } 117 | case class Par(exp: Exp) extends Exp { 118 | val bytes = PARENL ++ exp.bytes ++ PARENR 119 | } 120 | //case class Func 121 | case class Var(name: String) extends Exp { 122 | val bytes = ByteString(name) 123 | def :=(exp: Exp) = Assign(this, exp) 124 | } 125 | case object True extends Exp { 126 | val bytes = ByteString("true") 127 | } 128 | case object False extends Exp { 129 | val bytes = ByteString("false") 130 | } 131 | case object nil extends Exp { 132 | val bytes = ByteString("nil") 133 | } 134 | case class Num(value: Double) extends Exp { 135 | val bytes = ByteString(value.toString) 136 | } 137 | case class Str(value: String) extends Exp { 138 | val bytes = ByteString("\"" + value + "\"") 139 | } 140 | case class Table(fields: Field*) extends Exp { 141 | val bytes = CURLYL ++ (if (fields.nonEmpty) (fields.head.bytes /: fields.tail)(_ ++ COMMA ++ _.bytes) else ByteString.empty) ++ CURLYR 142 | } 143 | object Field { 144 | implicit def expToField(exp: Exp): Field = FieldV(exp) 145 | } 146 | sealed trait Field { 147 | def bytes: ByteString 148 | } 149 | case class FieldV(value: Exp) extends Field { 150 | val bytes = value.bytes 151 | } 152 | /* case class Fun extends Exp { 153 | def bytes = this.toString 154 | } 155 | */ 156 | 157 | sealed trait BinOp extends Exp { 158 | def a: Exp 159 | def b: Exp 160 | def bytes: ByteString 161 | } 162 | case class Add(a: Exp, b: Exp) extends BinOp { 163 | val bytes = a.bytes ++ ADD ++ b.bytes 164 | } 165 | case class Sub(a: Exp, b: Exp) extends BinOp { 166 | val bytes = a.bytes ++ SUB ++ b.bytes 167 | } 168 | case class Mult(a: Exp, b: Exp) extends BinOp { 169 | val bytes = a.bytes ++ MULT ++ b.bytes 170 | } 171 | case class Div(a: Exp, b: Exp) extends BinOp { 172 | val bytes = a.bytes ++ DIV ++ b.bytes 173 | } 174 | case class Lt(a: Exp, b: Exp) extends BinOp { 175 | val bytes = a.bytes ++ LT ++ b.bytes 176 | } 177 | case class LtEq(a: Exp, b: Exp) extends BinOp { 178 | val bytes = a.bytes ++ LTEQ ++ b.bytes 179 | } 180 | case class Gt(a: Exp, b: Exp) extends BinOp { 181 | val bytes = a.bytes ++ GT ++ b.bytes 182 | } 183 | case class GtEq(a: Exp, b: Exp) extends BinOp { 184 | val bytes = a.bytes ++ GTEQ ++ b.bytes 185 | } 186 | case class Eq(a: Exp, b: Exp) extends BinOp { 187 | val bytes = a.bytes ++ EQ ++ b.bytes 188 | } 189 | 190 | /* 191 | sealed trait UnOp extends Exp 192 | case class Minus(exp: Exp) extends UnOp 193 | case class Not(exp: Exp) extends UnOp*/ 194 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/messages/Messages.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie 2 | package redis 3 | package messages 4 | 5 | import akka.util.ByteString 6 | import akka.actor.{ IO, ActorRef } 7 | import akka.dispatch.Promise 8 | 9 | import types.RedisType 10 | import serialization.Store 11 | 12 | private[redis] sealed trait Requestor { 13 | def respond(value: RedisType): Unit 14 | def failure(err: Exception): Unit 15 | } 16 | private[redis] case object NoRequestor extends Requestor { 17 | def respond(value: RedisType) = () 18 | def failure(err: Exception) = () 19 | } 20 | private[redis] case class PromiseRequestor(promise: Promise[RedisType]) extends Requestor { 21 | def respond(value: RedisType) = promise success value 22 | def failure(err: Exception) = promise failure err 23 | } 24 | 25 | private[redis] sealed trait Message 26 | private[redis] sealed trait RequestMessage extends Message 27 | private[redis] case class Request(requestor: Requestor, bytes: ByteString) extends RequestMessage 28 | private[redis] case class MultiRequest(requestor: Requestor, multi: ByteString, cmds: Seq[(ByteString, Promise[RedisType])], exec: ByteString) extends RequestMessage 29 | private[redis] case object Disconnect extends Message 30 | private[redis] case class MultiRun(requestor: Requestor, promises: Seq[Promise[RedisType]]) extends Message 31 | private[redis] case class Socket(handle: IO.SocketHandle) extends Message 32 | private[redis] case object Received extends Message 33 | private[redis] case class Subscriber(listener: ActorRef) extends Message 34 | private[redis] case class RequestCallback(callback: (Long, Long) ⇒ Unit) extends Message 35 | private[redis] case class ResultCallback(callback: (Long, Long) ⇒ Unit) extends Message 36 | private[redis] case class ReleaseClient(client: RedisClientPoolWorker) extends Message 37 | private[redis] case class RequestClient(promise: Promise[RedisClientPoolWorker]) extends Message 38 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/package.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie 2 | 3 | import akka.util.{ ByteString, Duration } 4 | import akka.dispatch.{ Future, Promise } 5 | import redis.serialization.Parse 6 | 7 | package redis { 8 | 9 | object RedisScore { 10 | val default: RedisScore = InclusiveScore(1.0) 11 | val max: RedisScore = InclusiveScore(Double.PositiveInfinity) 12 | val min: RedisScore = InclusiveScore(Double.NegativeInfinity) 13 | } 14 | sealed trait RedisScore { 15 | def value: Double 16 | def inclusive: InclusiveScore 17 | def exclusive: ExclusiveScore 18 | } 19 | case class InclusiveScore(value: Double) extends RedisScore { 20 | def inclusive = this 21 | def exclusive = ExclusiveScore(value) 22 | } 23 | case class ExclusiveScore(value: Double) extends RedisScore { 24 | def inclusive = InclusiveScore(value) 25 | def exclusive = this 26 | } 27 | 28 | sealed trait RedisLimit 29 | case class Limit(offset: Int, count: Int) extends RedisLimit 30 | case object NoLimit extends RedisLimit 31 | 32 | sealed trait SortOrder 33 | object SortOrder { 34 | case object Asc extends SortOrder 35 | case object Desc extends SortOrder 36 | } 37 | 38 | sealed trait Aggregate 39 | object Aggregate { 40 | case object Sum extends Aggregate 41 | case object Min extends Aggregate 42 | case object Max extends Aggregate 43 | } 44 | 45 | private[redis] class BoxRedisFloat 46 | 47 | } 48 | 49 | package object redis { 50 | implicit def doubleToRedisScore(value: Double): RedisScore = InclusiveScore(value) 51 | 52 | implicit def parseBulk(result: Option[ByteString]) = new ParseBulk[({ type λ[α] = α })#λ](result) 53 | implicit def parseBulk(result: Future[Option[ByteString]]) = new ParseBulk[Future](result) 54 | implicit def parseBulk(result: Queued[Future[Option[ByteString]]]) = new ParseBulk[({ type λ[α] = Queued[Future[α]] })#λ](result) 55 | 56 | implicit def parseMultiBulk(result: Option[List[Option[ByteString]]]) = new ParseMultiBulk[({ type λ[α] = α })#λ](result) 57 | implicit def parseMultiBulk(result: Future[Option[List[Option[ByteString]]]]) = new ParseMultiBulk[Future](result) 58 | implicit def parseMultiBulk(result: Queued[Future[Option[List[Option[ByteString]]]]]) = new ParseMultiBulk[({ type λ[α] = Queued[Future[α]] })#λ](result) 59 | 60 | implicit def parseMultiBulkList(result: List[Option[ByteString]]) = new ParseMultiBulkList[({ type λ[α] = α })#λ](result) 61 | implicit def parseMultiBulkList(result: Future[List[Option[ByteString]]]) = new ParseMultiBulkList[Future](result) 62 | implicit def parseMultiBulkList(result: Queued[Future[List[Option[ByteString]]]]) = new ParseMultiBulkList[({ type λ[α] = Queued[Future[α]] })#λ](result) 63 | 64 | implicit def parseMultiBulkFlat(result: Option[List[ByteString]]) = new ParseMultiBulkFlat[({ type λ[α] = α })#λ](result) 65 | implicit def parseMultiBulkFlat(result: Future[Option[List[ByteString]]]) = new ParseMultiBulkFlat[Future](result) 66 | implicit def parseMultiBulkFlat(result: Queued[Future[Option[List[ByteString]]]]) = new ParseMultiBulkFlat[({ type λ[α] = Queued[Future[α]] })#λ](result) 67 | 68 | implicit def parseMultiBulkFlatList(result: List[ByteString]) = new ParseMultiBulkFlatList[({ type λ[α] = α })#λ](result) 69 | implicit def parseMultiBulkFlatList(result: Future[List[ByteString]]) = new ParseMultiBulkFlatList[Future](result) 70 | implicit def parseMultiBulkFlatList(result: Queued[Future[List[ByteString]]]) = new ParseMultiBulkFlatList[({ type λ[α] = Queued[Future[α]] })#λ](result) 71 | 72 | implicit def parseMultiBulkSet(result: Set[ByteString]) = new ParseMultiBulkSet[({ type λ[α] = α })#λ](result) 73 | implicit def parseMultiBulkSet(result: Future[Set[ByteString]]) = new ParseMultiBulkSet[Future](result) 74 | implicit def parseMultiBulkSet(result: Queued[Future[Set[ByteString]]]) = new ParseMultiBulkSet[({ type λ[α] = Queued[Future[α]] })#λ](result) 75 | 76 | implicit def parseMultiBulkMap(result: Map[ByteString, ByteString]) = new ParseMultiBulkMap[({ type λ[α] = α })#λ](result) 77 | implicit def parseMultiBulkMap(result: Future[Map[ByteString, ByteString]]) = new ParseMultiBulkMap[Future](result) 78 | implicit def parseMultiBulkMap(result: Queued[Future[Map[ByteString, ByteString]]]) = new ParseMultiBulkMap[({ type λ[α] = Queued[Future[α]] })#λ](result) 79 | 80 | implicit def parseMultiBulkScored(result: List[(ByteString, Double)]) = new ParseMultiBulkScored[({ type λ[α] = α })#λ](result) 81 | implicit def parseMultiBulkScored(result: Future[List[(ByteString, Double)]]) = new ParseMultiBulkScored[Future](result) 82 | implicit def parseMultiBulkScored(result: Queued[Future[List[(ByteString, Double)]]]) = new ParseMultiBulkScored[({ type λ[α] = Queued[Future[α]] })#λ](result) 83 | 84 | private[redis] class ParseBulk[Result[_]](value: Result[Option[ByteString]])(implicit f: ResultFunctor[Result]) { 85 | def parse[A: Parse]: Result[Option[A]] = f.fmap(value)(_.map(Parse(_))) 86 | } 87 | private[redis] class ParseMultiBulk[Result[_]](value: Result[Option[List[Option[ByteString]]]])(implicit f: ResultFunctor[Result]) { 88 | def parse[A: Parse]: Result[Option[List[Option[A]]]] = f.fmap(value)(_.map(_.map(_.map(Parse(_))))) 89 | } 90 | private[redis] class ParseMultiBulkList[Result[_]](value: Result[List[Option[ByteString]]])(implicit f: ResultFunctor[Result]) { 91 | def parse[A: Parse]: Result[List[Option[A]]] = f.fmap(value)(_.map(_.map(Parse(_)))) 92 | def parse[A: Parse, B: Parse] = f.fmap(value)(_.grouped(2).collect { 93 | case List(a, b) ⇒ (a map (Parse[A](_)), b map (Parse[B](_))) 94 | } toList) 95 | def parse[A: Parse, B: Parse, C: Parse] = f.fmap(value)(_.grouped(3).collect { 96 | case List(a, b, c) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_))) 97 | } toList) 98 | def parse[A: Parse, B: Parse, C: Parse, D: Parse] = f.fmap(value)(_.grouped(4).collect { 99 | case List(a, b, c, d) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_))) 100 | } toList) 101 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse] = f.fmap(value)(_.grouped(5).collect { 102 | case List(a, b, c, d, e) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_))) 103 | } toList) 104 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse] = f.fmap(value)(_.grouped(6).collect { 105 | case List(a, b, c, d, e, f) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_))) 106 | } toList) 107 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse] = f.fmap(value)(_.grouped(7).collect { 108 | case List(a, b, c, d, e, f, g) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_))) 109 | } toList) 110 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse] = f.fmap(value)(_.grouped(8).collect { 111 | case List(a, b, c, d, e, f, g, h) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_))) 112 | } toList) 113 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse] = f.fmap(value)(_.grouped(9).collect { 114 | case List(a, b, c, d, e, f, g, h, i) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_))) 115 | } toList) 116 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse] = f.fmap(value)(_.grouped(10).collect { 117 | case List(a, b, c, d, e, f, g, h, i, j) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_))) 118 | } toList) 119 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse] = f.fmap(value)(_.grouped(11).collect { 120 | case List(a, b, c, d, e, f, g, h, i, j, k) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_))) 121 | } toList) 122 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse] = f.fmap(value)(_.grouped(12).collect { 123 | case List(a, b, c, d, e, f, g, h, i, j, k, l) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_))) 124 | } toList) 125 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse] = f.fmap(value)(_.grouped(13).collect { 126 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_))) 127 | } toList) 128 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse] = f.fmap(value)(_.grouped(14).collect { 129 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_))) 130 | } toList) 131 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse, O: Parse] = f.fmap(value)(_.grouped(15).collect { 132 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_)), o map (Parse[O](_))) 133 | } toList) 134 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse, O: Parse, P: Parse] = f.fmap(value)(_.grouped(16).collect { 135 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_)), o map (Parse[O](_)), p map (Parse[P](_))) 136 | } toList) 137 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse, O: Parse, P: Parse, Q: Parse] = f.fmap(value)(_.grouped(17).collect { 138 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_)), o map (Parse[O](_)), p map (Parse[P](_)), q map (Parse[Q](_))) 139 | } toList) 140 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse, O: Parse, P: Parse, Q: Parse, R: Parse] = f.fmap(value)(_.grouped(18).collect { 141 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_)), o map (Parse[O](_)), p map (Parse[P](_)), q map (Parse[Q](_)), r map (Parse[R](_))) 142 | } toList) 143 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse, O: Parse, P: Parse, Q: Parse, R: Parse, S: Parse] = f.fmap(value)(_.grouped(19).collect { 144 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_)), o map (Parse[O](_)), p map (Parse[P](_)), q map (Parse[Q](_)), r map (Parse[R](_)), s map (Parse[S](_))) 145 | } toList) 146 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse, O: Parse, P: Parse, Q: Parse, R: Parse, S: Parse, T: Parse] = f.fmap(value)(_.grouped(20).collect { 147 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_)), o map (Parse[O](_)), p map (Parse[P](_)), q map (Parse[Q](_)), r map (Parse[R](_)), s map (Parse[S](_)), t map (Parse[T](_))) 148 | } toList) 149 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse, O: Parse, P: Parse, Q: Parse, R: Parse, S: Parse, T: Parse, U: Parse] = f.fmap(value)(_.grouped(21).collect { 150 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_)), o map (Parse[O](_)), p map (Parse[P](_)), q map (Parse[Q](_)), r map (Parse[R](_)), s map (Parse[S](_)), t map (Parse[T](_)), u map (Parse[U](_))) 151 | } toList) 152 | def parse[A: Parse, B: Parse, C: Parse, D: Parse, E: Parse, F: Parse, G: Parse, H: Parse, I: Parse, J: Parse, K: Parse, L: Parse, M: Parse, N: Parse, O: Parse, P: Parse, Q: Parse, R: Parse, S: Parse, T: Parse, U: Parse, V: Parse] = f.fmap(value)(_.grouped(22).collect { 153 | case List(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) ⇒ (a map (Parse[A](_)), b map (Parse[B](_)), c map (Parse[C](_)), d map (Parse[D](_)), e map (Parse[E](_)), f map (Parse[F](_)), g map (Parse[G](_)), h map (Parse[H](_)), i map (Parse[I](_)), j map (Parse[J](_)), k map (Parse[K](_)), l map (Parse[L](_)), m map (Parse[M](_)), n map (Parse[N](_)), o map (Parse[O](_)), p map (Parse[P](_)), q map (Parse[Q](_)), r map (Parse[R](_)), s map (Parse[S](_)), t map (Parse[T](_)), u map (Parse[U](_)), v map (Parse[V](_))) 154 | } toList) 155 | } 156 | private[redis] class ParseMultiBulkFlat[Result[_]](value: Result[Option[List[ByteString]]])(implicit f: ResultFunctor[Result]) { 157 | def parse[A: Parse]: Result[Option[List[A]]] = f.fmap(value)(_.map(_.map(Parse(_)))) 158 | } 159 | private[redis] class ParseMultiBulkFlatList[Result[_]](value: Result[List[ByteString]])(implicit f: ResultFunctor[Result]) { 160 | def parse[A: Parse]: Result[List[A]] = f.fmap(value)(_.map(Parse(_))) 161 | } 162 | private[redis] class ParseMultiBulkSet[Result[_]](value: Result[Set[ByteString]])(implicit f: ResultFunctor[Result]) { 163 | def parse[A: Parse]: Result[Set[A]] = f.fmap(value)(_.map(Parse(_))) 164 | } 165 | private[redis] class ParseMultiBulkMap[Result[_]](value: Result[Map[ByteString, ByteString]])(implicit f: ResultFunctor[Result]) { 166 | def parse[K: Parse, V: Parse]: Result[Map[K, V]] = f.fmap(value)(_.map(kv ⇒ (Parse[K](kv._1), Parse[V](kv._2)))) 167 | } 168 | private[redis] class ParseMultiBulkScored[Result[_]](value: Result[List[(ByteString, Double)]])(implicit f: ResultFunctor[Result]) { 169 | def parse[A: Parse]: Result[List[(A, Double)]] = f.fmap(value)(_.map(kv ⇒ (Parse(kv._1), kv._2))) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/protocol/Constants.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package protocol 3 | 4 | import akka.util.ByteString 5 | 6 | private[redis] object Constants { 7 | final val AFTER = ByteString("AFTER") 8 | final val AGGREGATE = ByteString("AGGREGATE") 9 | final val ALLKEYS = ByteString("*") 10 | final val ALPHA = ByteString("ALPHA") 11 | final val APPEND = ByteString("APPEND") 12 | final val ASC = ByteString("ASC") 13 | final val AUTH = ByteString("AUTH") 14 | final val BEFORE = ByteString("BEFORE") 15 | final val BGREWRITEAOF = ByteString("BGREWRITEAOF") 16 | final val BGSAVE = ByteString("BGSAVE") 17 | final val BLPOP = ByteString("BLPOP") 18 | final val BRPOP = ByteString("BRPOP") 19 | final val BRPOPLPUSH = ByteString("BRPOPLPUSH") 20 | final val BY = ByteString("BY") 21 | final val CONFIG = ByteString("CONFIG") 22 | final val DBSIZE = ByteString("DBSIZE") 23 | final val DEBUG = ByteString("DEBUG") 24 | final val DECR = ByteString("DECR") 25 | final val DECRBY = ByteString("DECRBY") 26 | final val DEL = ByteString("DEL") 27 | final val DESC = ByteString("DESC") 28 | final val DISCARD = ByteString("DISCARD") 29 | final val ECHO = ByteString("ECHO") 30 | final val EOL = ByteString("\r\n") 31 | final val EVAL = ByteString("EVAL") 32 | final val EXEC = ByteString("EXEC") 33 | final val EXISTS = ByteString("EXISTS") 34 | final val EXPIRE = ByteString("EXPIRE") 35 | final val EXPIREAT = ByteString("EXPIREAT") 36 | final val FLUSHDB = ByteString("FLUSHDB") 37 | final val FLUSHALL = ByteString("FLUSHALL") 38 | final val GET = ByteString("GET") 39 | final val GETBIT = ByteString("GETBIT") 40 | final val GETRANGE = ByteString("GETRANGE") 41 | final val GETSET = ByteString("GETSET") 42 | final val HDEL = ByteString("HDEL") 43 | final val HEXISTS = ByteString("HEXISTS") 44 | final val HGET = ByteString("HGET") 45 | final val HGETALL = ByteString("HGETALL") 46 | final val HINCRBY = ByteString("HINCRBY") 47 | final val HKEYS = ByteString("HKEYS") 48 | final val HLEN = ByteString("HLEN") 49 | final val HMGET = ByteString("HMGET") 50 | final val HMSET = ByteString("HMSET") 51 | final val HSET = ByteString("HSET") 52 | final val HSETNX = ByteString("HSETNX") 53 | final val HVALS = ByteString("HVALS") 54 | final val INCR = ByteString("INCR") 55 | final val INCRBY = ByteString("INCRBY") 56 | final val INFO = ByteString("INFO") 57 | final val INFPOS = ByteString("+inf") 58 | final val INFNEG = ByteString("-inf") 59 | final val KEYS = ByteString("KEYS") 60 | final val LASTSAVE = ByteString("LASTSAVE") 61 | final val LIMIT = ByteString("LIMIT") 62 | final val LINDEX = ByteString("LINDEX") 63 | final val LINSERT = ByteString("LINSERT") 64 | final val LLEN = ByteString("LLEN") 65 | final val LPOP = ByteString("LPOP") 66 | final val LPUSH = ByteString("LPUSH") 67 | final val LPUSHX = ByteString("LPUSHX") 68 | final val LRANGE = ByteString("LRANGE") 69 | final val LREM = ByteString("LREM") 70 | final val LSET = ByteString("LSET") 71 | final val LTRIM = ByteString("LTRIM") 72 | final val MAX = ByteString("MAX") 73 | final val message = ByteString("message") 74 | final val MGET = ByteString("MGET") 75 | final val MIN = ByteString("MIN") 76 | final val MONITOR = ByteString("MONITOR") 77 | final val MOVE = ByteString("MOVE") 78 | final val MSET = ByteString("MSET") 79 | final val MSETNX = ByteString("MSETNX") 80 | final val MULTI = ByteString("MULTI") 81 | final val OBJECT = ByteString("OBJECT") 82 | final val PERSIST = ByteString("PERSIST") 83 | final val PING = ByteString("PING") 84 | final val pmessage = ByteString("pmessage") 85 | final val PSUBSCRIBE = ByteString("PSUBSCRIBE") 86 | final val psubscribe = ByteString("psubscribe") 87 | final val PUBLISH = ByteString("PUBLISH") 88 | final val PUNSUBSCRIBE = ByteString("PUNSUBSCRIBE") 89 | final val punsubscribe = ByteString("punsubscribe") 90 | final val QUIT = ByteString("QUIT") 91 | final val RANDOMKEY = ByteString("RANDOMKEY") 92 | final val RENAME = ByteString("RENAME") 93 | final val RENAMENX = ByteString("RENAMENX") 94 | final val RESETSTAT = ByteString("RESETSTAT") 95 | final val RPOP = ByteString("RPOP") 96 | final val RPOPLPUSH = ByteString("RPOPLPUSH") 97 | final val RPUSH = ByteString("RPUSH") 98 | final val RPUSHX = ByteString("RPUSHX") 99 | final val SADD = ByteString("SADD") 100 | final val SAVE = ByteString("SAVE") 101 | final val SCARD = ByteString("SCARD") 102 | final val SDIFF = ByteString("SDIFF") 103 | final val SDIFFSTORE = ByteString("SDIFFSTORE") 104 | final val SEGFAULT = ByteString("SEGFAULT") 105 | final val SELECT = ByteString("SELECT") 106 | final val SET = ByteString("SET") 107 | final val SETBIT = ByteString("SETBIT") 108 | final val SETEX = ByteString("SETEX") 109 | final val SETNX = ByteString("SETNX") 110 | final val SETRANGE = ByteString("SETRANGE") 111 | final val SHUTDOWN = ByteString("SHUTDOWN") 112 | final val SINTER = ByteString("SINTER") 113 | final val SINTERSTORE = ByteString("SINTERSTORE") 114 | final val SISMEMBER = ByteString("SISMEMBER") 115 | final val SLAVEOF = ByteString("SLAVEOF") 116 | final val SMEMBERS = ByteString("SMEMBERS") 117 | final val SMOVE = ByteString("SMOVE") 118 | final val SORT = ByteString("SORT") 119 | final val SPOP = ByteString("SPOP") 120 | final val SRANDMEMBER = ByteString("SRANDMEMBER") 121 | final val SREM = ByteString("SREM") 122 | final val STRLEN = ByteString("STRLEN") 123 | final val SUBSCRIBE = ByteString("SUBSCRIBE") 124 | final val subscribe = ByteString("subscribe") 125 | final val SUM = ByteString("SUM") 126 | final val SUNION = ByteString("SUNION") 127 | final val SUNIONSTORE = ByteString("SUNIONSTORE") 128 | final val SYNC = ByteString("SYNC") 129 | final val TTL = ByteString("TTL") 130 | final val TYPE = ByteString("TYPE") 131 | final val UNSUBSCRIBE = ByteString("UNSUBSCRIBE") 132 | final val unsubscribe = ByteString("unsubscribe") 133 | final val UNWATCH = ByteString("UNWATCH") 134 | final val WATCH = ByteString("WATCH") 135 | final val WEIGHTS = ByteString("WEIGHTS") 136 | final val WITHSCORES = ByteString("WITHSCORES") 137 | final val ZADD = ByteString("ZADD") 138 | final val ZCARD = ByteString("ZCARD") 139 | final val ZCOUNT = ByteString("ZCOUNT") 140 | final val ZINCRBY = ByteString("ZINCRBY") 141 | final val ZINTERSTORE = ByteString("ZINTERSTORE") 142 | final val ZRANGE = ByteString("ZRANGE") 143 | final val ZRANGEBYSCORE = ByteString("ZRANGEBYSCORE") 144 | final val ZRANK = ByteString("ZRANK") 145 | final val ZREM = ByteString("ZREM") 146 | final val ZREMRANGEBYRANK = ByteString("ZREMRANGEBYRANK") 147 | final val ZREMRANGEBYSCORE = ByteString("ZREMRANGEBYSCORE") 148 | final val ZREVRANGE = ByteString("ZREVRANGE") 149 | final val ZREVRANGEBYSCORE = ByteString("ZREVRANGEBYSCORE") 150 | final val ZREVRANK = ByteString("ZREVRANK") 151 | final val ZSCORE = ByteString("ZSCORE") 152 | final val ZUNIONSTORE = ByteString("ZUNIONSTORE") 153 | } 154 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/protocol/Iteratees.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package protocol 3 | 4 | import types._ 5 | import akka.util.ByteString 6 | import akka.actor.IO 7 | 8 | private[redis] object Iteratees { 9 | import Constants.EOL 10 | 11 | final val readUntilEOL = IO takeUntil EOL 12 | 13 | final val readType = (_: ByteString).head match { 14 | case 43 /* '+' */ ⇒ readString 15 | case 45 /* '-' */ ⇒ readError 16 | case 58 /* ':' */ ⇒ readInteger 17 | case 36 /* '$' */ ⇒ readBulk 18 | case 42 /* '*' */ ⇒ readMulti 19 | case x ⇒ IO throwErr RedisProtocolException("Invalid result type: " + x) 20 | } 21 | 22 | final val readResult: IO.Iteratee[RedisType] = IO take 1 flatMap readType 23 | 24 | final val bytesToString = (bytes: ByteString) ⇒ RedisString(bytes.utf8String) 25 | final val readString: IO.Iteratee[RedisString] = readUntilEOL map bytesToString 26 | 27 | final val bytesToError = (bytes: ByteString) ⇒ RedisError(bytes.utf8String) 28 | final val readError: IO.Iteratee[RedisError] = readUntilEOL map bytesToError 29 | 30 | final val bytesToInteger = (bytes: ByteString) ⇒ RedisInteger(bytes.utf8String.toLong) 31 | final val readInteger: IO.Iteratee[RedisInteger] = readUntilEOL map bytesToInteger 32 | 33 | final val notFoundBulk = IO Done RedisBulk.notfound 34 | final val emptyBulk = IO Done RedisBulk.empty 35 | final val bytesToBulk = (_: ByteString).utf8String.toInt match { 36 | case -1 ⇒ notFoundBulk 37 | case 0 ⇒ emptyBulk 38 | case n ⇒ for (bytes ← IO take n; _ ← readUntilEOL) yield RedisBulk(Some(bytes)) 39 | } 40 | final val readBulk: IO.Iteratee[RedisBulk] = readUntilEOL flatMap bytesToBulk 41 | 42 | final val notFoundMulti = IO Done RedisMulti.notfound 43 | final val emptyMulti = IO Done RedisMulti.empty 44 | final val bytesToMulti = (bytes: ByteString) ⇒ bytes.utf8String.toInt match { 45 | case -1 ⇒ notFoundMulti 46 | case 0 ⇒ emptyMulti 47 | case n ⇒ IO.takeList(n)(readResult) map (x ⇒ RedisMulti(Some(x))) 48 | } 49 | final val readMulti: IO.Iteratee[RedisMulti] = readUntilEOL flatMap bytesToMulti 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/pubsub/PubSub.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package pubsub 3 | 4 | import akka.util.ByteString 5 | import serialization.Store 6 | 7 | sealed trait PubSubMessage 8 | case class Subscribed(channel: ByteString, count: Long) extends PubSubMessage 9 | case class Unsubscribed(channel: ByteString, count: Long) extends PubSubMessage 10 | case class Message(channel: ByteString, content: ByteString) extends PubSubMessage 11 | case class PSubscribed(pattern: ByteString, count: Long) extends PubSubMessage 12 | case class PUnsubscribed(pattern: ByteString, count: Long) extends PubSubMessage 13 | case class PMessage(pattern: ByteString, channel: ByteString, content: ByteString) extends PubSubMessage 14 | 15 | object Subscribe { 16 | def apply[A: Store](channel: A): Subscribe = new Subscribe(List(Store(channel))) 17 | def apply[A: Store](channels: Iterable[A]): Subscribe = new Subscribe(channels.map(Store(_))(collection.breakOut)) 18 | } 19 | case class Subscribe(channels: List[ByteString]) extends PubSubMessage 20 | 21 | object Unsubscribe { 22 | def apply[A: Store](channel: A): Unsubscribe = new Unsubscribe(List(Store(channel))) 23 | def apply[A: Store](channels: Iterable[A]): Unsubscribe = new Unsubscribe(channels.map(Store(_))(collection.breakOut)) 24 | } 25 | case class Unsubscribe(channels: List[ByteString]) extends PubSubMessage 26 | 27 | object PSubscribe { 28 | def apply[A: Store](pattern: A): PSubscribe = new PSubscribe(List(Store(pattern))) 29 | def apply[A: Store](patterns: Iterable[A]): PSubscribe = new PSubscribe(patterns.map(Store(_))(collection.breakOut)) 30 | } 31 | case class PSubscribe(patterns: List[ByteString]) extends PubSubMessage 32 | 33 | object PUnsubscribe { 34 | def apply[A: Store](pattern: A): PUnsubscribe = new PUnsubscribe(List(Store(pattern))) 35 | def apply[A: Store](patterns: Iterable[A]): PUnsubscribe = new PUnsubscribe(patterns.map(Store(_))(collection.breakOut)) 36 | } 37 | case class PUnsubscribe(patterns: List[ByteString]) extends PubSubMessage 38 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/serialization/Serialization.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | package serialization 3 | 4 | import protocol.Constants 5 | 6 | import akka.util.ByteString 7 | 8 | import java.util.Date 9 | 10 | trait Store[-A] { 11 | def apply(value: A): ByteString 12 | } 13 | 14 | object Store { 15 | sealed trait Dummy 16 | 17 | def apply[A](value: A)(implicit store: Store[A]): ByteString = store(value) 18 | 19 | def asString[A]: Store[A] = new Store[A] { def apply(value: A) = ByteString(value.toString) } 20 | 21 | implicit val storeByteString = new Store[ByteString] { def apply(value: ByteString) = value } 22 | implicit val storeByteArray = new Store[Array[Byte]] { def apply(value: Array[Byte]) = ByteString(value) } 23 | implicit val storeString = new Store[String] { def apply(value: String) = ByteString(value) } 24 | implicit val storeInt = Store.asString[Int] 25 | implicit val storeLong = Store.asString[Long] 26 | implicit val storeFloat = Store.asString[Float] 27 | implicit val storeDouble = Store.asString[Double] 28 | implicit val storeDummy = new Store[Dummy] { def apply(value: Dummy) = sys.error("Don't store the Dummy!") } 29 | implicit val storeLua = new Store[lua.Chunk] { def apply(value: lua.Chunk) = value.bytes } 30 | implicit val storeDate = new Store[Date] { def apply(value: Date) = storeLong(value.getTime) } 31 | implicit val storeScore = new Store[RedisScore] { 32 | def apply(score: RedisScore) = score match { 33 | case _ if score.value == Double.PositiveInfinity ⇒ Constants.INFPOS 34 | case _ if score.value == Double.NegativeInfinity ⇒ Constants.INFNEG 35 | case InclusiveScore(d) ⇒ ByteString(d.toString) 36 | case ExclusiveScore(d) ⇒ ByteString("(" + d.toString) 37 | } 38 | } 39 | implicit val storeAggregate = new Store[Aggregate] { 40 | def apply(value: Aggregate) = value match { 41 | case Aggregate.Sum ⇒ Constants.SUM 42 | case Aggregate.Min ⇒ Constants.MIN 43 | case Aggregate.Max ⇒ Constants.MAX 44 | } 45 | } 46 | implicit val storeSortOrder = new Store[SortOrder] { 47 | def apply(value: SortOrder) = value match { 48 | case SortOrder.Asc ⇒ Constants.ASC 49 | case SortOrder.Desc ⇒ Constants.DESC 50 | } 51 | } 52 | } 53 | 54 | trait Parse[+A] { 55 | def apply(bytes: ByteString): A 56 | } 57 | 58 | object Parse { 59 | def apply[A](bytes: ByteString)(implicit parse: Parse[A]): A = parse(bytes) 60 | 61 | implicit val parseByteString = new Parse[ByteString] { def apply(bytes: ByteString) = bytes.compact } 62 | implicit val parseByteArray = new Parse[Array[Byte]] { def apply(bytes: ByteString) = bytes.toArray } 63 | implicit val parseString = new Parse[String] { def apply(bytes: ByteString) = bytes.utf8String } 64 | implicit val parseInt = new Parse[Int] { def apply(bytes: ByteString) = bytes.utf8String.toInt } 65 | implicit val parseLong = new Parse[Long] { def apply(bytes: ByteString) = bytes.utf8String.toLong } 66 | implicit val parseFloat = new Parse[Float] { def apply(bytes: ByteString) = bytes.utf8String.toFloat } 67 | implicit val parseDouble = new Parse[Double] { def apply(bytes: ByteString) = bytes.utf8String.toDouble } 68 | implicit val parseDate = new Parse[Date] { def apply(bytes: ByteString) = new Date(parseLong(bytes)) } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/scala/net/fyrie/redis/types/Types.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis.types 2 | 3 | import akka.util.ByteString 4 | 5 | sealed trait RedisType 6 | 7 | case class RedisString(value: String) extends RedisType 8 | 9 | case class RedisInteger(value: Long) extends RedisType 10 | 11 | object RedisBulk { 12 | val notfound = new RedisBulk(None) 13 | val empty = new RedisBulk(Some(ByteString.empty)) 14 | } 15 | 16 | case class RedisBulk(value: Option[ByteString]) extends RedisType 17 | 18 | object RedisMulti { 19 | val notfound = new RedisMulti(None) 20 | val empty = new RedisMulti(Some(Nil)) 21 | } 22 | 23 | case class RedisMulti(value: Option[List[RedisType]]) extends RedisType 24 | 25 | case class RedisError(value: String) extends RedisType 26 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/DBSpec.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | 5 | import akka.dispatch.Future 6 | import akka.testkit.{ filterEvents, EventFilter } 7 | 8 | class KeysSpec extends mutable.Specification with TestClient { 9 | 10 | "keys" >> { 11 | "should fetch keys" ! client { r ⇒ 12 | r.set("anshin-1", "debasish") 13 | r.set("anshin-2", "maulindu") 14 | r.sync.keys("anshin*").size must_== 2 15 | } 16 | 17 | "should fetch keys with spaces" ! client { r ⇒ 18 | r.set("anshin 1", "debasish") 19 | r.set("anshin 2", "maulindu") 20 | r.sync.keys("anshin*").size must_== 2 21 | } 22 | } 23 | 24 | "randomkey" >> { 25 | "should give" ! client { r ⇒ 26 | r.set("anshin-1", "debasish") 27 | r.set("anshin-2", "maulindu") 28 | r.sync.randomkey().parse[String].get must startWith("anshin") 29 | } 30 | } 31 | 32 | "rename" >> { 33 | "should give" ! client { r ⇒ 34 | r.set("anshin-1", "debasish") 35 | r.set("anshin-2", "maulindu") 36 | r.rename("anshin-2", "anshin-2-new") 37 | r.sync.rename("anshin-2", "anshin-2-new") must throwA[RedisErrorException]("ERR no such key") 38 | } 39 | } 40 | 41 | "renamenx" >> { 42 | "should give" ! client { r ⇒ 43 | r.set("anshin-1", "debasish") 44 | r.set("anshin-2", "maulindu") 45 | r.sync.renamenx("anshin-2", "anshin-2-new") must_== true 46 | r.sync.renamenx("anshin-1", "anshin-2-new") must_== false 47 | } 48 | } 49 | 50 | "dbsize" >> { 51 | "should give" ! client { r ⇒ 52 | r.set("anshin-1", "debasish") 53 | r.set("anshin-2", "maulindu") 54 | r.sync.dbsize() must_== 2 55 | } 56 | } 57 | 58 | "exists" >> { 59 | "should give" ! client { r ⇒ 60 | r.set("anshin-1", "debasish") 61 | r.set("anshin-2", "maulindu") 62 | r.sync.exists("anshin-2") must_== true 63 | r.sync.exists("anshin-1") must_== true 64 | r.sync.exists("anshin-3") must_== false 65 | } 66 | } 67 | 68 | "del" >> { 69 | "should give" ! client { r ⇒ 70 | r.set("anshin-1", "debasish") 71 | r.set("anshin-2", "maulindu") 72 | r.sync.del(Set("anshin-2", "anshin-1")) must_== 2 73 | r.sync.del(Set("anshin-2", "anshin-1")) must_== 0 74 | } 75 | } 76 | 77 | "type" >> { 78 | "should give" ! client { r ⇒ 79 | r.set("anshin-1", "debasish") 80 | r.set("anshin-2", "maulindu") 81 | r.sync.typeof("anshin-2") must_== "string" 82 | } 83 | } 84 | 85 | "expire" >> { 86 | "should give" ! client { r ⇒ 87 | r.set("anshin-1", "debasish") 88 | r.set("anshin-2", "maulindu") 89 | r.sync.expire("anshin-2", 1000) must_== true 90 | r.sync.expire("anshin-3", 1000) must_== false 91 | } 92 | } 93 | 94 | /* "quit" >> { 95 | "should reconnect" ! client { r ⇒ 96 | r.quit 97 | r.set("key1", "value1") 98 | r.quit 99 | r.sync.get("key1").parse[String] must_== Some("value1") 100 | } 101 | /* ignore("should reconnect with many commands") { // ignore until fixed 102 | (1 to 1000) foreach (_ ⇒ r.incr("incKey")) 103 | r.quit 104 | val result1 = r.get("incKey").parse[Int] 105 | (1 to 1000) foreach (_ ⇒ r.incr("incKey")) 106 | val result2 = r.get("incKey").parse[Int] 107 | r.quit 108 | (1 to 1000) foreach (_ ⇒ r.incr("incKey")) 109 | val result3 = r.get("incKey").parse[Int] 110 | result1.get must_==(Some(1000)) 111 | result2.get must_==(Some(2000)) 112 | result3.get must_==(Some(3000)) 113 | }*/ 114 | } */ 115 | 116 | "Multi exec commands" >> { 117 | "should work with single commands" ! client { r ⇒ 118 | r.multi { rq ⇒ 119 | rq.set("testkey1", "testvalue1") 120 | } map (_ === ()) 121 | } 122 | "should work with several commands" ! client { r ⇒ 123 | r.multi { rq ⇒ 124 | for { 125 | _ ← rq.set("testkey1", "testvalue1") 126 | _ ← rq.set("testkey2", "testvalue2") 127 | x ← rq.mget(List("testkey1", "testkey2")).parse[String] 128 | } yield x 129 | } map (_ === List(Some("testvalue1"), Some("testvalue2"))) 130 | } 131 | "should work with a list of commands" ! client { r ⇒ 132 | val values = List.range(1, 100) 133 | Future sequence { 134 | r.multi { rq ⇒ 135 | val setq = (Queued[Any](()) /: values)((q, v) ⇒ q flatMap (_ ⇒ rq.set(v, v * 2))) 136 | (setq.map(_ ⇒ List[Future[Option[Int]]]()) /: values)((q, v) ⇒ q flatMap (l ⇒ rq.get(v).parse[Int].map(_ :: l))).map(_.reverse) 137 | } 138 | } map (_.flatten === values.map(2*)) 139 | } 140 | "should throw an error" ! client { r ⇒ 141 | filterEvents(EventFilter[RedisErrorException]("ERR Operation against a key holding the wrong kind of value")) { 142 | val result = r.multi { rq ⇒ 143 | for { 144 | _ ← rq.set("a", "abc") 145 | x ← rq.lpop("a").parse[String] 146 | y ← rq.get("a").parse[String] 147 | } yield (x, y) 148 | } 149 | for { 150 | a ← result._1 recover { case _: RedisErrorException ⇒ success } 151 | b ← result._2 152 | } yield { 153 | b === Some("abc") 154 | } 155 | } 156 | } 157 | "should handle invalid requests" ! client { r ⇒ 158 | filterEvents(EventFilter[RedisErrorException]("ERR Operation against a key holding the wrong kind of value")) { 159 | val result = r.multi { rq ⇒ 160 | for { 161 | _ ← rq.set("testkey1", "testvalue1") 162 | _ ← rq.set("testkey2", "testvalue2") 163 | x ← rq.mget(List[String]()).parse[String] 164 | y ← rq.mget(List("testkey1", "testkey2")).parse[String] 165 | } yield (x, y) 166 | } 167 | for { 168 | a ← result._1 recover { case _: RedisErrorException ⇒ success } 169 | b ← result._2 170 | } yield { 171 | b === List(Some("testvalue1"), Some("testvalue2")) 172 | } 173 | } 174 | } 175 | } 176 | 177 | "watch" >> { 178 | "should fail without watch" ! client { r ⇒ 179 | r.set("key", 0) flatMap { _ ⇒ 180 | val clients = List.fill(10)(RedisClient()) 181 | val futures = for (client ← clients; _ ← 1 to 10) yield client.get("key").parse[Int] flatMap { n ⇒ client.set("key", n.get + 1) } 182 | Future sequence futures flatMap { _ ⇒ 183 | clients foreach (_.disconnect) 184 | r.get("key").parse[Int] map (_ must_!= Some(100)) 185 | } 186 | } 187 | } 188 | "should succeed with watch" ! client { r ⇒ 189 | r.sync.set("key", 0) 190 | val futures = for (_ ← 1 to 100) yield { 191 | r atomic { rw ⇒ 192 | rw watch "key" 193 | for { 194 | Some(n) ← rw.get("key").parse[Int] 195 | } yield rw multi (_.set("key", n + 1)) 196 | } 197 | } 198 | Future sequence futures flatMap { _ ⇒ 199 | r.get("key").parse[Int] map (_ === Some(100)) 200 | } 201 | } 202 | "should handle complex request" ! client { r ⇒ 203 | r.sync.rpush("mykey1", 5) 204 | r.set("mykey2", "hello") 205 | r.hset("mykey3", "hello", 7) 206 | val result = r atomic { rw ⇒ 207 | for { 208 | _ ← rw.watch("mykey1") 209 | _ ← rw.watch("mykey2") 210 | _ ← rw.watch("mykey3") 211 | Some(a) ← rw.lindex("mykey1", 0).parse[Int] 212 | Some(b) ← rw.get("mykey2").parse[String] 213 | Some(c) ← rw.hget("mykey3", b).parse[Int] 214 | } yield rw.multi { rq ⇒ 215 | for { 216 | _ ← rq.rpush("mykey1", a + 1) 217 | _ ← rq.hset("mykey3", b, c + 1) 218 | } yield (a, b, c) 219 | } 220 | } 221 | for { 222 | a ← result 223 | b ← r.lrange("mykey1").parse[Int] 224 | c ← r.hget("mykey3", "hello").parse[Int] 225 | } yield { 226 | a === (5, "hello", 7) 227 | b === Some(List(5, 6)) 228 | c === Some(8) 229 | } 230 | } 231 | } 232 | 233 | "sort" >> { 234 | "should do a simple sort" ! client { r ⇒ 235 | List(6, 3, 5, 47, 1, 1, 4, 9) foreach (r.lpush("sortlist", _)) 236 | r.sync.sort("sortlist").parse[Int].flatten must_== List(1, 1, 3, 4, 5, 6, 9, 47) 237 | } 238 | "should do a lexical sort" ! client { r ⇒ 239 | List("lorem", "ipsum", "dolor", "sit", "amet") foreach (r.lpush("sortlist", _)) 240 | List(3, 7) foreach (r.lpush("sortlist", _)) 241 | r.sync.sort("sortlist", alpha = true).parse[String].flatten must_== List("3", "7", "amet", "dolor", "ipsum", "lorem", "sit") 242 | } 243 | "should return an empty list if key not found" ! client { r ⇒ 244 | r.sync.sort("sortnotfound") must beEmpty 245 | } 246 | "should return multiple items" ! client { r ⇒ 247 | val list = List(("item1", "data1", 1, 4), 248 | ("item2", "data2", 2, 8), 249 | ("item3", "data3", 3, 1), 250 | ("item4", "data4", 4, 6), 251 | ("item5", "data5", 5, 3)) 252 | for ((key, data, num, rank) ← list) { 253 | r.quiet.sadd("items", key) 254 | r.quiet.set("data::" + key, data) 255 | r.quiet.set("num::" + key, num) 256 | r.quiet.set("rank::" + key, rank) 257 | } 258 | r.quiet.del(List("num::item1")) 259 | r.sync.sort("items", 260 | get = Seq("#", "data::*", "num::*"), 261 | by = Some("rank::*"), 262 | limit = Limit(1, 3)).parse[String] must_== List(Some("item5"), Some("data5"), Some("5"), 263 | Some("item1"), Some("data1"), None, 264 | Some("item4"), Some("data4"), Some("4")) 265 | r.sync.sort("items", 266 | get = Seq("#", "data::*", "num::*"), 267 | by = Some("rank::*"), 268 | limit = Limit(1, 3)).parse[String, String, Int] must_== (List((Some("item5"), Some("data5"), Some(5)), 269 | (Some("item1"), Some("data1"), None), 270 | (Some("item4"), Some("data4"), Some(4)))) 271 | } 272 | } 273 | 274 | } 275 | 276 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/HashSpec.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | 5 | class HashSpec extends mutable.Specification with TestClient { 6 | 7 | "hset" >> { 8 | "should set and get fields" ! client { r ⇒ 9 | r.quiet.hset("hash1", "field1", "val") 10 | r.sync.hget("hash1", "field1").parse[String] must_== Some("val") 11 | } 12 | 13 | "should set and get maps" ! client { r ⇒ 14 | r.quiet.hmset("hash2", Map("field1" -> "val1", "field2" -> "val2")) 15 | r.sync.hmget("hash2", Seq("field1")).parse[String] must_== List(Some("val1")) 16 | r.sync.hmget("hash2", Seq("field1", "field2")).parse[String] must_== List(Some("val1"), Some("val2")) 17 | r.sync.hmget("hash2", Seq("field1", "field2", "field3")).parse[String] must_== List(Some("val1"), Some("val2"), None) 18 | } 19 | 20 | "should increment map values" ! client { r ⇒ 21 | r.sync.hincrby("hash3", "field1", 1) must_== 1 22 | r.sync.hget("hash3", "field1").parse[String] must_== Some("1") 23 | } 24 | 25 | "should check existence" ! client { r ⇒ 26 | r.quiet.hset("hash4", "field1", "val") 27 | r.sync.hexists("hash4", "field1") must beTrue 28 | r.sync.hexists("hash4", "field2") must beFalse 29 | } 30 | 31 | "should delete fields" ! client { r ⇒ 32 | r.quiet.hset("hash5", "field1", "val") 33 | r.sync.hexists("hash5", "field1") must beTrue 34 | r.quiet.hdel("hash5", "field1") 35 | r.sync.hexists("hash5", "field1") must beFalse 36 | } 37 | 38 | "should return the length of the fields" ! client { r ⇒ 39 | r.quiet.hmset("hash6", Map("field1" -> "val1", "field2" -> "val2")) 40 | r.sync.hlen("hash6") must_== 2 41 | } 42 | 43 | "should return the aggregates" ! client { r ⇒ 44 | r.quiet.hmset("hash7", Map("field1" -> "val1", "field2" -> "val2")) 45 | r.sync.hkeys("hash7").parse[String] must_== Set("field1", "field2") 46 | r.sync.hvals("hash7").parse[String] must_== Set("val1", "val2") 47 | r.sync.hgetall("hash7").parse[String, String] must_== Map("field1" -> "val1", "field2" -> "val2") 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/ListSpec.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | 5 | class ListSpec extends mutable.Specification with TestClient { 6 | 7 | "lpush" >> { 8 | "should add to the head of the list" ! client { r ⇒ 9 | r.sync.lpush("list-1", "foo") === (1) 10 | r.sync.lpush("list-1", "bar") === (2) 11 | } 12 | "should throw if the key has a non-list value" ! client { r ⇒ 13 | r.set("anshin-1", "debasish") 14 | r.sync.lpush("anshin-1", "bar") must throwA[RedisErrorException]("ERR Operation against a key holding the wrong kind of value") 15 | } 16 | } 17 | 18 | "rpush" >> { 19 | "should add to the head of the list" ! client { r ⇒ 20 | r.sync.rpush("list-1", "foo") === (1) 21 | r.sync.rpush("list-1", "bar") === (2) 22 | } 23 | "should throw if the key has a non-list value" ! client { r ⇒ 24 | r.set("anshin-1", "debasish") 25 | r.sync.rpush("anshin-1", "bar") must throwA[RedisErrorException]("ERR Operation against a key holding the wrong kind of value") 26 | } 27 | } 28 | 29 | "llen" >> { 30 | "should return the length of the list" ! client { r ⇒ 31 | r.sync.lpush("list-1", "foo") === (1) 32 | r.sync.lpush("list-1", "bar") === (2) 33 | r.sync.llen("list-1") === (2) 34 | } 35 | "should return 0 for a non-existent key" ! client { r ⇒ 36 | r.sync.llen("list-2") === (0) 37 | } 38 | "should throw for a non-list key" ! client { r ⇒ 39 | r.set("anshin-1", "debasish") 40 | r.sync.llen("anshin-1") must throwA[RedisErrorException]("ERR Operation against a key holding the wrong kind of value") 41 | } 42 | } 43 | 44 | "lrange" >> { 45 | "should return the range" ! client { r ⇒ 46 | r.sync.lpush("list-1", "6") === (1) 47 | r.sync.lpush("list-1", "5") === (2) 48 | r.sync.lpush("list-1", "4") === (3) 49 | r.sync.lpush("list-1", "3") === (4) 50 | r.sync.lpush("list-1", "2") === (5) 51 | r.sync.lpush("list-1", "1") === (6) 52 | r.sync.llen("list-1") === (6) 53 | r.sync.lrange("list-1", 0, 4).parse[String] === (Some(List("1", "2", "3", "4", "5"))) 54 | } 55 | "should return empty list if start > end" ! client { r ⇒ 56 | r.sync.lpush("list-1", "3") === (1) 57 | r.sync.lpush("list-1", "2") === (2) 58 | r.sync.lpush("list-1", "1") === (3) 59 | r.sync.lrange("list-1", 2, 0).parse[String] === (Some(Nil)) 60 | } 61 | "should treat as end of list if end is over the actual end of list" ! client { r ⇒ 62 | r.sync.lpush("list-1", "3") === (1) 63 | r.sync.lpush("list-1", "2") === (2) 64 | r.sync.lpush("list-1", "1") === (3) 65 | r.sync.lrange("list-1", 0, 7).parse[String] === (Some(List("1", "2", "3"))) 66 | } 67 | } 68 | 69 | "ltrim" >> { 70 | "should trim to the input size" ! client { r ⇒ 71 | r.sync.lpush("list-1", "6") === (1) 72 | r.sync.lpush("list-1", "5") === (2) 73 | r.sync.lpush("list-1", "4") === (3) 74 | r.sync.lpush("list-1", "3") === (4) 75 | r.sync.lpush("list-1", "2") === (5) 76 | r.sync.lpush("list-1", "1") === (6) 77 | r.ltrim("list-1", 0, 3) 78 | r.sync.llen("list-1") === (4) 79 | } 80 | "should should return empty list for start > end" ! client { r ⇒ 81 | r.sync.lpush("list-1", "6") === (1) 82 | r.sync.lpush("list-1", "5") === (2) 83 | r.sync.lpush("list-1", "4") === (3) 84 | r.ltrim("list-1", 6, 3) 85 | r.sync.llen("list-1") === (0) 86 | } 87 | "should treat as end of list if end is over the actual end of list" ! client { r ⇒ 88 | r.sync.lpush("list-1", "6") === (1) 89 | r.sync.lpush("list-1", "5") === (2) 90 | r.sync.lpush("list-1", "4") === (3) 91 | r.ltrim("list-1", 0, 12) 92 | r.sync.llen("list-1") === (3) 93 | } 94 | } 95 | 96 | "lindex" >> { 97 | "should return the value at index" ! client { r ⇒ 98 | r.sync.lpush("list-1", "6") === (1) 99 | r.sync.lpush("list-1", "5") === (2) 100 | r.sync.lpush("list-1", "4") === (3) 101 | r.sync.lpush("list-1", "3") === (4) 102 | r.sync.lpush("list-1", "2") === (5) 103 | r.sync.lpush("list-1", "1") === (6) 104 | r.sync.lindex("list-1", 2).parse[String] === (Some("3")) 105 | r.sync.lindex("list-1", 3).parse[String] === (Some("4")) 106 | r.sync.lindex("list-1", -1).parse[String] === (Some("6")) 107 | } 108 | "should return None if the key does not point to a list" ! client { r ⇒ 109 | r.set("anshin-1", "debasish") 110 | r.sync.lindex("list-1", 0).parse[String] === (None) 111 | } 112 | "should return empty string for an index out of range" ! client { r ⇒ 113 | r.sync.lpush("list-1", "6") === (1) 114 | r.sync.lpush("list-1", "5") === (2) 115 | r.sync.lpush("list-1", "4") === (3) 116 | r.sync.lindex("list-1", 8).parse[String] === (None) // the protocol says it will return empty string 117 | } 118 | } 119 | 120 | "lset" >> { 121 | "should set value for key at index" ! client { r ⇒ 122 | r.sync.lpush("list-1", "6") === (1) 123 | r.sync.lpush("list-1", "5") === (2) 124 | r.sync.lpush("list-1", "4") === (3) 125 | r.sync.lpush("list-1", "3") === (4) 126 | r.sync.lpush("list-1", "2") === (5) 127 | r.sync.lpush("list-1", "1") === (6) 128 | r.lset("list-1", 2, "30") 129 | r.sync.lindex("list-1", 2).parse[String] === (Some("30")) 130 | } 131 | "should generate error for out of range index" ! client { r ⇒ 132 | r.sync.lpush("list-1", "6") === (1) 133 | r.sync.lpush("list-1", "5") === (2) 134 | r.sync.lpush("list-1", "4") === (3) 135 | r.sync.lset("list-1", 12, "30") must throwA[RedisErrorException]("ERR index out of range") 136 | } 137 | } 138 | 139 | "lrem" >> { 140 | "should remove count elements matching value from beginning" ! client { r ⇒ 141 | r.sync.lpush("list-1", "6") === (1) 142 | r.sync.lpush("list-1", "hello") === (2) 143 | r.sync.lpush("list-1", "4") === (3) 144 | r.sync.lpush("list-1", "hello") === (4) 145 | r.sync.lpush("list-1", "hello") === (5) 146 | r.sync.lpush("list-1", "hello") === (6) 147 | r.sync.lrem("list-1", "hello", 2) === (2) 148 | r.sync.llen("list-1") === (4) 149 | } 150 | "should remove all elements matching value from beginning" ! client { r ⇒ 151 | r.sync.lpush("list-1", "6") === (1) 152 | r.sync.lpush("list-1", "hello") === (2) 153 | r.sync.lpush("list-1", "4") === (3) 154 | r.sync.lpush("list-1", "hello") === (4) 155 | r.sync.lpush("list-1", "hello") === (5) 156 | r.sync.lpush("list-1", "hello") === (6) 157 | r.sync.lrem("list-1", "hello") === (4) 158 | r.sync.llen("list-1") === (2) 159 | } 160 | "should remove count elements matching value from end" ! client { r ⇒ 161 | r.sync.lpush("list-1", "6") === (1) 162 | r.sync.lpush("list-1", "hello") === (2) 163 | r.sync.lpush("list-1", "4") === (3) 164 | r.sync.lpush("list-1", "hello") === (4) 165 | r.sync.lpush("list-1", "hello") === (5) 166 | r.sync.lpush("list-1", "hello") === (6) 167 | r.sync.lrem("list-1", "hello", -2) === (2) 168 | r.sync.llen("list-1") === (4) 169 | r.sync.lindex("list-1", -2).parse[String] === (Some("4")) 170 | } 171 | } 172 | 173 | "lpop" >> { 174 | "should pop the first one from head" ! client { r ⇒ 175 | r.sync.lpush("list-1", "6") === (1) 176 | r.sync.lpush("list-1", "5") === (2) 177 | r.sync.lpush("list-1", "4") === (3) 178 | r.sync.lpush("list-1", "3") === (4) 179 | r.sync.lpush("list-1", "2") === (5) 180 | r.sync.lpush("list-1", "1") === (6) 181 | r.sync.lpop("list-1").parse[String] === (Some("1")) 182 | r.sync.lpop("list-1").parse[String] === (Some("2")) 183 | r.sync.lpop("list-1").parse[String] === (Some("3")) 184 | r.sync.llen("list-1") === (3) 185 | } 186 | "should give nil for non-existent key" ! client { r ⇒ 187 | r.sync.lpush("list-1", "6") === (1) 188 | r.sync.lpush("list-1", "5") === (2) 189 | r.sync.lpop("list-2").parse[String] === (None) 190 | r.sync.llen("list-1") === (2) 191 | } 192 | } 193 | 194 | "rpop" >> { 195 | "should pop the first one from tail" ! client { r ⇒ 196 | r.sync.lpush("list-1", "6") === (1) 197 | r.sync.lpush("list-1", "5") === (2) 198 | r.sync.lpush("list-1", "4") === (3) 199 | r.sync.lpush("list-1", "3") === (4) 200 | r.sync.lpush("list-1", "2") === (5) 201 | r.sync.lpush("list-1", "1") === (6) 202 | r.sync.rpop("list-1").parse[String] === (Some("6")) 203 | r.sync.rpop("list-1").parse[String] === (Some("5")) 204 | r.sync.rpop("list-1").parse[String] === (Some("4")) 205 | r.sync.llen("list-1") === (3) 206 | } 207 | "should give nil for non-existent key" ! client { r ⇒ 208 | r.sync.lpush("list-1", "6") === (1) 209 | r.sync.lpush("list-1", "5") === (2) 210 | r.sync.rpop("list-2").parse[String] === (None) 211 | r.sync.llen("list-1") === (2) 212 | } 213 | } 214 | 215 | "rpoplpush" >> { 216 | "should do" ! client { r ⇒ 217 | r.sync.rpush("list-1", "a") === (1) 218 | r.sync.rpush("list-1", "b") === (2) 219 | r.sync.rpush("list-1", "c") === (3) 220 | r.sync.rpush("list-2", "foo") === (1) 221 | r.sync.rpush("list-2", "bar") === (2) 222 | r.sync.rpoplpush("list-1", "list-2").parse[String] === (Some("c")) 223 | r.sync.lindex("list-2", 0).parse[String] === (Some("c")) 224 | r.sync.llen("list-1") === (2) 225 | r.sync.llen("list-2") === (3) 226 | } 227 | 228 | "should rotate the list when src and dest are the same" ! client { r ⇒ 229 | r.sync.rpush("list-1", "a") === (1) 230 | r.sync.rpush("list-1", "b") === (2) 231 | r.sync.rpush("list-1", "c") === (3) 232 | r.sync.rpoplpush("list-1", "list-1").parse[String] === (Some("c")) 233 | r.sync.lindex("list-1", 0).parse[String] === (Some("c")) 234 | r.sync.lindex("list-1", 2).parse[String] === (Some("b")) 235 | r.sync.llen("list-1") === (3) 236 | } 237 | 238 | "should give None for non-existent key" ! client { r ⇒ 239 | r.sync.rpoplpush("list-1", "list-2").parse[String] === (None) 240 | r.sync.rpush("list-1", "a") === (1) 241 | r.sync.rpush("list-1", "b") === (2) 242 | r.sync.rpoplpush("list-1", "list-2").parse[String] === (Some("b")) 243 | } 244 | } 245 | 246 | "lpush with newlines in strings" >> { 247 | "should add to the head of the list" ! client { r ⇒ 248 | r.sync.lpush("list-1", "foo\nbar\nbaz") === (1) 249 | r.sync.lpush("list-1", "bar\nfoo\nbaz") === (2) 250 | r.sync.lpop("list-1").parse[String] === (Some("bar\nfoo\nbaz")) 251 | r.sync.lpop("list-1").parse[String] === (Some("foo\nbar\nbaz")) 252 | } 253 | } 254 | 255 | } 256 | 257 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/PubSubSpec.scala: -------------------------------------------------------------------------------- 1 | /* package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | import execute._ 5 | import specification._ 6 | 7 | import akka.dispatch.{ Future, PromiseStream } 8 | import akka.testkit.{ filterEvents, EventFilter } 9 | import akka.actor.{ Actor, ActorRef } 10 | import pubsub._ 11 | import serialization.Parse 12 | 13 | class PubSubSpec extends Specification with PubSubHelper { 14 | 15 | def is = args(sequential = true) ^ 16 | p ^ 17 | "A PubSub Subscriber" ^ 18 | "subscribing to channels and a pattern" ! { 19 | pubsub.subscribe("Test Channel", "Test Channel 2") and pubsub.psubscribe("Test*") 20 | } ^ 21 | "should be notified of subscription" ! { 22 | pubsub.get("[Test Channel] Subscribed", "[Test Channel 2] Subscribed", "[Test*] Subscribed") 23 | } ^ 24 | "and when messages are published" ! { 25 | pubsub.publish("Test Channel", "Hello!") and pubsub.publish("Test Channel 3", "World!") 26 | } ^ 27 | "they should be received by the subscriber." ! { 28 | pubsub.get("[Test Channel] Message: Hello!", "[Test*][Test Channel] Message: Hello!", "[Test*][Test Channel 3] Message: World!") 29 | } ^ 30 | pubsub.stop ^ 31 | end 32 | 33 | } 34 | 35 | trait PubSubHelper { self: Specification ⇒ 36 | val pubsub = new { 37 | implicit val system = TestSystem.system 38 | val redis = RedisClient() 39 | val ps = PromiseStream[String]() 40 | val sub = system.actorOf(new TestSubscriber(redis, ps)) 41 | 42 | def get(msgs: String*) = ((success: Result) /: msgs)((r, m) ⇒ r and (ps.dequeue.get must_== m)) 43 | 44 | def subscribe(channels: String*) = { 45 | sub ! Subscribe(channels) 46 | success 47 | } 48 | 49 | def psubscribe(patterns: String*) = { 50 | sub ! PSubscribe(patterns) 51 | success 52 | } 53 | 54 | def publish(channel: String, msg: String) = { 55 | redis.publish(channel, msg) 56 | success 57 | } 58 | 59 | def stop = Step { 60 | sub.stop 61 | redis.disconnect 62 | } 63 | 64 | } 65 | } 66 | 67 | class TestSubscriber(redisClient: RedisClient, ps: PromiseStream[String]) extends Actor { 68 | var client: ActorRef = _ 69 | override def preStart { 70 | client = redisClient.subscriber(self) 71 | } 72 | 73 | def receive = { 74 | case msg: Subscribe ⇒ client ! msg 75 | case msg: Unsubscribe ⇒ client ! msg 76 | case msg: PSubscribe ⇒ client ! msg 77 | case msg: PUnsubscribe ⇒ client ! msg 78 | case Subscribed(channel, count) ⇒ ps enqueue ("[" + Parse[String](channel) + "] Subscribed") 79 | case Unsubscribed(channel, count) ⇒ ps enqueue ("[" + Parse[String](channel) + "] Unsubscribed") 80 | case Message(channel, bytes) ⇒ ps enqueue ("[" + Parse[String](channel) + "] Message: " + Parse[String](bytes)) 81 | case PSubscribed(pattern, count) ⇒ ps enqueue ("[" + Parse[String](pattern) + "] Subscribed") 82 | case PUnsubscribed(pattern, count) ⇒ ps enqueue ("[" + Parse[String](pattern) + "] Unsubscribed") 83 | case PMessage(pattern, channel, bytes) ⇒ ps enqueue ("[" + Parse[String](pattern) + "][" + Parse[String](channel) + "] Message: " + Parse[String](bytes)) 84 | } 85 | } 86 | 87 | */ 88 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/ScriptsSpec.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | 5 | import net.fyrie.redis.types._ 6 | import net.fyrie.redis.serialization._ 7 | 8 | class ScriptsSpec extends mutable.Specification with UnstableClient { 9 | 10 | "eval" >> { 11 | "should eval simple script returning a number" ! client { r ⇒ 12 | r.sync.eval("return ARGV[1] + ARGV[2]", args = List(4, 9)) === RedisInteger(13) 13 | } 14 | } 15 | 16 | "lua dsl" >> { 17 | "should produce valid lua" ! client { r ⇒ 18 | import lua._ 19 | 20 | val v1 = Var("v1") 21 | val v2 = Var("v2") 22 | val v3 = Var("v3") 23 | 24 | val script = { 25 | (v1 := 34) :: 26 | Do { 27 | (v2 := true) :: 28 | End 29 | } :: 30 | (v3 := "hello") :: 31 | Return(Table(v1, v2, v3)) 32 | } 33 | 34 | script.bytes.utf8String === "v1 = 34.0; do v2 = true; end; v3 = \"hello\"; return {v1, v2, v3}" 35 | r.sync.eval(script) === RedisMulti(Some(List(RedisInteger(34), RedisInteger(1), RedisBulk(Some(Store("hello")))))) 36 | } 37 | "should provide If/Then/Else statement" ! client { r ⇒ 38 | import lua._ 39 | 40 | def script(x: Exp) = { 41 | val v = Var("result") 42 | 43 | { 44 | If(x :== "a") Then { 45 | (v := 1) :: End 46 | } ElseIf (x :== "b") Then { 47 | (v := 6) :: End 48 | } ElseIf (x :== "c") Then { 49 | (v := 3) :: End 50 | } ElseIf (x :== "d") Then { 51 | (v := 8) :: End 52 | } Else { 53 | (v := "I don't know!") :: End 54 | } 55 | } :: Return(v) 56 | } 57 | 58 | r.sync.eval(script("a")) === RedisInteger(1) 59 | r.sync.eval(script("b")) === RedisInteger(6) 60 | r.sync.eval(script("c")) === RedisInteger(3) 61 | r.sync.eval(script("d")) === RedisInteger(8) 62 | r.sync.eval(script("e")) === RedisBulk(Some(Store("I don't know!"))) 63 | } 64 | "should produce valid arithmetic expressions" ! client { r ⇒ 65 | import lua._ 66 | 67 | val n = Var("n") 68 | 69 | (n :+ 3 :* n :- 5).bytes.utf8String === "n + 3.0 * n - 5.0" 70 | (Exp(n) :+ 3 :* n :- 5).bytes.utf8String === "n + 3.0 * n - 5.0" 71 | (n :+ 3 :* (n :- 5)).bytes.utf8String === "n + 3.0 * (n - 5.0)" 72 | (Exp(n :+ 3) :* (n :- 5)).bytes.utf8String === "(n + 3.0) * (n - 5.0)" 73 | } 74 | } 75 | 76 | } 77 | 78 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/SetSpec.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | 5 | class SetSpec extends mutable.Specification with TestClient { 6 | 7 | "sadd" >> { 8 | "should add a non-existent value to the set" ! client { r ⇒ 9 | r.sync.sadd("set-1", "foo") === true 10 | r.sync.sadd("set-1", "bar") === true 11 | } 12 | "should not add an existing value to the set" ! client { r ⇒ 13 | r.sync.sadd("set-1", "foo") === true 14 | r.sync.sadd("set-1", "foo") === false 15 | } 16 | "should fail if the key points to a non-set" ! client { r ⇒ 17 | r.sync.lpush("list-1", "foo") === (1) 18 | r.sync.sadd("list-1", "foo") must throwA[RedisErrorException]("ERR Operation against a key holding the wrong kind of value") 19 | } 20 | } 21 | 22 | "srem" >> { 23 | "should remove a value from the set" ! client { r ⇒ 24 | r.sync.sadd("set-1", "foo") === true 25 | r.sync.sadd("set-1", "bar") === true 26 | r.sync.srem("set-1", "bar") === true 27 | r.sync.srem("set-1", "foo") === true 28 | } 29 | "should not do anything if the value does not exist" ! client { r ⇒ 30 | r.sync.sadd("set-1", "foo") === true 31 | r.sync.srem("set-1", "bar") === false 32 | } 33 | "should fail if the key points to a non-set" ! client { r ⇒ 34 | r.sync.lpush("list-1", "foo") === 1 35 | r.sync.srem("list-1", "foo") must throwA[RedisErrorException]("ERR Operation against a key holding the wrong kind of value") 36 | } 37 | } 38 | 39 | "spop" >> { 40 | "should pop a random element" ! client { r ⇒ 41 | r.sync.sadd("set-1", "foo") === true 42 | r.sync.sadd("set-1", "bar") === true 43 | r.sync.sadd("set-1", "baz") === true 44 | r.sync.spop("set-1").parse[String] must beOneOf(Some("foo"), Some("bar"), Some("baz")) 45 | } 46 | "should return nil if the key does not exist" ! client { r ⇒ 47 | r.sync.spop("set-1") === (None) 48 | } 49 | } 50 | 51 | "smove" >> { 52 | "should move from one set to another" ! client { r ⇒ 53 | r.sync.sadd("set-1", "foo") === true 54 | r.sync.sadd("set-1", "bar") === true 55 | r.sync.sadd("set-1", "baz") === true 56 | 57 | r.sync.sadd("set-2", "1") === true 58 | r.sync.sadd("set-2", "2") === true 59 | 60 | r.sync.smove("set-1", "set-2", "baz") === true 61 | r.sync.sadd("set-2", "baz") === false 62 | r.sync.sadd("set-1", "baz") === true 63 | } 64 | "should return 0 if the element does not exist in source set" ! client { r ⇒ 65 | r.sync.sadd("set-1", "foo") === true 66 | r.sync.sadd("set-1", "bar") === true 67 | r.sync.sadd("set-1", "baz") === true 68 | r.sync.smove("set-1", "set-2", "bat") === false 69 | r.sync.smove("set-3", "set-2", "bat") === false 70 | } 71 | "should give error if the source or destination key is not a set" ! client { r ⇒ 72 | r.sync.lpush("list-1", "foo") === (1) 73 | r.sync.lpush("list-1", "bar") === (2) 74 | r.sync.lpush("list-1", "baz") === (3) 75 | r.sync.sadd("set-1", "foo") === true 76 | r.sync.smove("list-1", "set-1", "bat") must throwA[RedisErrorException]("ERR Operation against a key holding the wrong kind of value") 77 | } 78 | } 79 | 80 | "scard" >> { 81 | "should return cardinality" ! client { r ⇒ 82 | r.sync.sadd("set-1", "foo") === true 83 | r.sync.sadd("set-1", "bar") === true 84 | r.sync.sadd("set-1", "baz") === true 85 | r.sync.scard("set-1") === (3) 86 | } 87 | "should return 0 if key does not exist" ! client { r ⇒ 88 | r.sync.scard("set-1") === (0) 89 | } 90 | } 91 | 92 | "sismember" >> { 93 | "should return true for membership" ! client { r ⇒ 94 | r.sync.sadd("set-1", "foo") === true 95 | r.sync.sadd("set-1", "bar") === true 96 | r.sync.sadd("set-1", "baz") === true 97 | r.sync.sismember("set-1", "foo") === true 98 | } 99 | "should return false for no membership" ! client { r ⇒ 100 | r.sync.sadd("set-1", "foo") === true 101 | r.sync.sadd("set-1", "bar") === true 102 | r.sync.sadd("set-1", "baz") === true 103 | r.sync.sismember("set-1", "fo") === false 104 | } 105 | "should return false if key does not exist" ! client { r ⇒ 106 | r.sync.sismember("set-1", "fo") === false 107 | } 108 | } 109 | 110 | "sinter" >> { 111 | "should return intersection" ! client { r ⇒ 112 | r.sync.sadd("set-1", "foo") === true 113 | r.sync.sadd("set-1", "bar") === true 114 | r.sync.sadd("set-1", "baz") === true 115 | 116 | r.sync.sadd("set-2", "foo") === true 117 | r.sync.sadd("set-2", "bat") === true 118 | r.sync.sadd("set-2", "baz") === true 119 | 120 | r.sync.sadd("set-3", "for") === true 121 | r.sync.sadd("set-3", "bat") === true 122 | r.sync.sadd("set-3", "bay") === true 123 | 124 | r.sync.sinter(Set("set-1", "set-2")).parse[String] === (Set("foo", "baz")) 125 | r.sync.sinter(Set("set-1", "set-3")).parse[String] === (Set.empty) 126 | } 127 | "should return empty set for non-existing key" ! client { r ⇒ 128 | r.sync.sadd("set-1", "foo") === true 129 | r.sync.sadd("set-1", "bar") === true 130 | r.sync.sadd("set-1", "baz") === true 131 | r.sync.sinter(Set("set-1", "set-4")).parse[String] === (Set.empty) 132 | } 133 | } 134 | 135 | "sinterstore" >> { 136 | "should store intersection" ! client { r ⇒ 137 | r.sync.sadd("set-1", "foo") === true 138 | r.sync.sadd("set-1", "bar") === true 139 | r.sync.sadd("set-1", "baz") === true 140 | 141 | r.sync.sadd("set-2", "foo") === true 142 | r.sync.sadd("set-2", "bat") === true 143 | r.sync.sadd("set-2", "baz") === true 144 | 145 | r.sync.sadd("set-3", "for") === true 146 | r.sync.sadd("set-3", "bat") === true 147 | r.sync.sadd("set-3", "bay") === true 148 | 149 | r.sync.sinterstore("set-r", Set("set-1", "set-2")) === (2) 150 | r.sync.scard("set-r") === (2) 151 | r.sync.sinterstore("set-s", Set("set-1", "set-3")) === (0) 152 | r.sync.scard("set-s") === (0) 153 | } 154 | "should return empty set for non-existing key" ! client { r ⇒ 155 | r.sync.sadd("set-1", "foo") === true 156 | r.sync.sadd("set-1", "bar") === true 157 | r.sync.sadd("set-1", "baz") === true 158 | r.sync.sinterstore("set-r", Seq("set-1", "set-4")) === (0) 159 | r.sync.scard("set-r") === (0) 160 | } 161 | } 162 | 163 | "sunion" >> { 164 | "should return union" ! client { r ⇒ 165 | r.sync.sadd("set-1", "foo") === true 166 | r.sync.sadd("set-1", "bar") === true 167 | r.sync.sadd("set-1", "baz") === true 168 | 169 | r.sync.sadd("set-2", "foo") === true 170 | r.sync.sadd("set-2", "bat") === true 171 | r.sync.sadd("set-2", "baz") === true 172 | 173 | r.sync.sadd("set-3", "for") === true 174 | r.sync.sadd("set-3", "bat") === true 175 | r.sync.sadd("set-3", "bay") === true 176 | 177 | r.sync.sunion(Set("set-1", "set-2")).parse[String] === (Set("foo", "bar", "baz", "bat")) 178 | r.sync.sunion(Set("set-1", "set-3")).parse[String] === (Set("foo", "bar", "baz", "for", "bat", "bay")) 179 | } 180 | "should return empty set for non-existing key" ! client { r ⇒ 181 | r.sync.sadd("set-1", "foo") === true 182 | r.sync.sadd("set-1", "bar") === true 183 | r.sync.sadd("set-1", "baz") === true 184 | r.sync.sunion(Seq("set-1", "set-2")).parse[String] === (Set("foo", "bar", "baz")) 185 | } 186 | } 187 | 188 | "sunionstore" >> { 189 | "should store union" ! client { r ⇒ 190 | r.sync.sadd("set-1", "foo") === true 191 | r.sync.sadd("set-1", "bar") === true 192 | r.sync.sadd("set-1", "baz") === true 193 | 194 | r.sync.sadd("set-2", "foo") === true 195 | r.sync.sadd("set-2", "bat") === true 196 | r.sync.sadd("set-2", "baz") === true 197 | 198 | r.sync.sadd("set-3", "for") === true 199 | r.sync.sadd("set-3", "bat") === true 200 | r.sync.sadd("set-3", "bay") === true 201 | 202 | r.sync.sunionstore("set-r", Set("set-1", "set-2")) === (4) 203 | r.sync.scard("set-r") === (4) 204 | r.sync.sunionstore("set-s", Set("set-1", "set-3")) === (6) 205 | r.sync.scard("set-s") === (6) 206 | } 207 | "should treat non-existing keys as empty sets" ! client { r ⇒ 208 | r.sync.sadd("set-1", "foo") === true 209 | r.sync.sadd("set-1", "bar") === true 210 | r.sync.sadd("set-1", "baz") === true 211 | r.sync.sunionstore("set-r", Set("set-1", "set-4")) === (3) 212 | r.sync.scard("set-r") === (3) 213 | } 214 | } 215 | 216 | "sdiff" >> { 217 | "should return diff" ! client { r ⇒ 218 | r.sync.sadd("set-1", "foo") === true 219 | r.sync.sadd("set-1", "bar") === true 220 | r.sync.sadd("set-1", "baz") === true 221 | 222 | r.sync.sadd("set-2", "foo") === true 223 | r.sync.sadd("set-2", "bat") === true 224 | r.sync.sadd("set-2", "baz") === true 225 | 226 | r.sync.sadd("set-3", "for") === true 227 | r.sync.sadd("set-3", "bat") === true 228 | r.sync.sadd("set-3", "bay") === true 229 | 230 | r.sync.sdiff("set-1", Set("set-2", "set-3")).parse[String] === (Set("bar")) 231 | } 232 | "should treat non-existing keys as empty sets" ! client { r ⇒ 233 | r.sync.sadd("set-1", "foo") === true 234 | r.sync.sadd("set-1", "bar") === true 235 | r.sync.sadd("set-1", "baz") === true 236 | r.sync.sdiff("set-1", Set("set-2")).parse[String] === (Set("foo", "bar", "baz")) 237 | } 238 | } 239 | 240 | "smembers" >> { 241 | "should return members of a set" ! client { r ⇒ 242 | r.sync.sadd("set-1", "foo") === true 243 | r.sync.sadd("set-1", "bar") === true 244 | r.sync.sadd("set-1", "baz") === true 245 | r.sync.smembers("set-1").parse[String] === (Set("foo", "bar", "baz")) 246 | } 247 | "should return None for an empty set" ! client { r ⇒ 248 | r.sync.smembers("set-1").parse[String] === (Set.empty) 249 | } 250 | } 251 | 252 | "srandmember" >> { 253 | "should return a random member" ! client { r ⇒ 254 | r.sync.sadd("set-1", "foo") === true 255 | r.sync.sadd("set-1", "bar") === true 256 | r.sync.sadd("set-1", "baz") === true 257 | r.sync.srandmember("set-1").parse[String] must beOneOf(Some("foo"), Some("bar"), Some("baz")) 258 | } 259 | "should return None for a non-existing key" ! client { r ⇒ 260 | r.sync.srandmember("set-1").parse[String] === (None) 261 | } 262 | } 263 | } 264 | 265 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/SortedSetSpec.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | 5 | class SortedSetSpec extends mutable.Specification with TestClient { 6 | 7 | private def add(r: RedisClient) = { 8 | r.sync.zadd("hackers", "yukihiro matsumoto", 1965) === (true) 9 | r.sync.zadd("hackers", "richard stallman", 1953) === (true) 10 | r.sync.zadd("hackers", "claude shannon", 1916) === (true) 11 | r.sync.zadd("hackers", "linus torvalds", 1969) === (true) 12 | r.sync.zadd("hackers", "alan kay", 1940) === (true) 13 | r.sync.zadd("hackers", "alan turing", 1912) === (true) 14 | } 15 | 16 | "zadd" >> { 17 | "should add based on proper sorted set semantics" ! client { r ⇒ 18 | add(r) 19 | r.sync.zadd("hackers", "alan turing", 1912) === (false) 20 | r.sync.zcard("hackers") === (6) 21 | } 22 | } 23 | 24 | "zrange" >> { 25 | "should get the proper range" ! client { r ⇒ 26 | add(r) 27 | r.sync.zrange("hackers", 0, -1) must have size (6) 28 | r.sync.zrangeWithScores("hackers", 0, -1) must have size (6) 29 | } 30 | } 31 | 32 | "zrank" >> { 33 | "should give proper rank" ! client { r ⇒ 34 | add(r) 35 | r.sync.zrank("hackers", "yukihiro matsumoto") === (Some(4)) 36 | r.sync.zrevrank("hackers", "yukihiro matsumoto") === (Some(1)) 37 | } 38 | } 39 | 40 | "zremrangebyrank" >> { 41 | "should remove based on rank range" ! client { r ⇒ 42 | add(r) 43 | r.sync.zremrangebyrank("hackers", 0, 2) === (3) 44 | } 45 | } 46 | 47 | "zremrangebyscore" >> { 48 | "should remove based on score range" ! client { r ⇒ 49 | add(r) 50 | r.sync.zremrangebyscore("hackers", 1912, 1940) === (3) 51 | r.sync.zremrangebyscore("hackers", 0, 3) === (0) 52 | } 53 | } 54 | 55 | "zunion" >> { 56 | "should do a union" ! client { r ⇒ 57 | r.sync.zadd("hackers 1", "yukihiro matsumoto", 1965) === (true) 58 | r.sync.zadd("hackers 1", "richard stallman", 1953) === (true) 59 | r.sync.zadd("hackers 2", "claude shannon", 1916) === (true) 60 | r.sync.zadd("hackers 2", "linus torvalds", 1969) === (true) 61 | r.sync.zadd("hackers 3", "alan kay", 1940) === (true) 62 | r.sync.zadd("hackers 4", "alan turing", 1912) === (true) 63 | 64 | // union with weight = 1 65 | r.sync.zunionstore("hackers", Set("hackers 1", "hackers 2", "hackers 3", "hackers 4")) === (6) 66 | r.sync.zcard("hackers") === (6) 67 | 68 | r.sync.zrangeWithScores("hackers").parse[String] === (List(("alan turing", 1912), ("claude shannon", 1916), ("alan kay", 1940), ("richard stallman", 1953), ("yukihiro matsumoto", 1965), ("linus torvalds", 1969))) 69 | 70 | // union with modified weights 71 | r.sync.zunionstoreWeighted("hackers weighted", Map(("hackers 1", 1), ("hackers 2", 2), ("hackers 3", 3), ("hackers 4", 4))) === (6) 72 | r.sync.zrangeWithScores("hackers weighted").parse[String] map (_._2) must_== (List(1953, 1965, 3832, 3938, 5820, 7648)) 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/StringSpec.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | 5 | class StringSpec extends mutable.Specification with TestClient { 6 | 7 | "set" >> { 8 | "should set key/value pairs" ! client { r ⇒ 9 | for { 10 | a ← r.set("anshin-1", "debasish") 11 | b ← r.set("anshin-2", "maulindu") 12 | } yield { 13 | a === () 14 | b === () 15 | } 16 | } 17 | } 18 | 19 | "get" >> { 20 | "should retrieve key/value pairs for existing keys" ! client { r ⇒ 21 | r.quiet.set("anshin-1", "debasish") 22 | r.get("anshin-1").parse[String] map (_ === Some("debasish")) 23 | } 24 | "should fail for non-existent keys" ! client { r ⇒ 25 | r.get("anshin-2").parse[String] map (_ === None) 26 | } 27 | } 28 | 29 | "getset" >> { 30 | "should set new values and return old values" ! client { r ⇒ 31 | for { 32 | _ ← r.set("anshin-1", "debasish") 33 | a ← r.get("anshin-1").parse[String] 34 | b ← r.getset("anshin-1", "maulindu").parse[String] 35 | c ← r.get("anshin-1").parse[String] 36 | } yield { 37 | a === Some("debasish") 38 | b === Some("debasish") 39 | c === Some("maulindu") 40 | } 41 | } 42 | } 43 | 44 | "setnx" >> { 45 | "should set only if the key does not exist" ! client { r ⇒ 46 | r.set("anshin-1", "debasish") 47 | r.sync.setnx("anshin-1", "maulindu") === (false) 48 | r.sync.setnx("anshin-2", "maulindu") === (true) 49 | } 50 | } 51 | 52 | "incr" >> { 53 | "should increment by 1 for a key that contains a number" ! client { r ⇒ 54 | r.set("anshin-1", "10") 55 | r.sync.incr("anshin-1") === (11) 56 | } 57 | "should reset to 0 and then increment by 1 for a key that contains a diff type" ! client { r ⇒ 58 | r.set("anshin-2", "debasish") 59 | r.sync.incr("anshin-2") must throwA[RedisErrorException].like { 60 | case e ⇒ e.getMessage must startWith("ERR value is not an integer") 61 | } 62 | } 63 | "should increment by 5 for a key that contains a number" ! client { r ⇒ 64 | r.set("anshin-3", "10") 65 | r.sync.incrby("anshin-3", 5) === (15) 66 | } 67 | "should reset to 0 and then increment by 5 for a key that contains a diff type" ! client { r ⇒ 68 | r.set("anshin-4", "debasish") 69 | r.sync.incrby("anshin-4", 5) must throwA[RedisErrorException].like { 70 | case e ⇒ e.getMessage must startWith("ERR value is not an integer") 71 | } 72 | } 73 | } 74 | 75 | "decr" >> { 76 | "should decrement by 1 for a key that contains a number" ! client { r ⇒ 77 | r.set("anshin-1", "10") 78 | r.sync.decr("anshin-1") === (9) 79 | } 80 | "should reset to 0 and then decrement by 1 for a key that contains a diff type" ! client { r ⇒ 81 | r.set("anshin-2", "debasish") 82 | r.sync.decr("anshin-2") must throwA[RedisErrorException].like { 83 | case e ⇒ e.getMessage must startWith("ERR value is not an integer") 84 | } 85 | } 86 | "should decrement by 5 for a key that contains a number" ! client { r ⇒ 87 | r.set("anshin-3", "10") 88 | r.sync.decrby("anshin-3", 5) === (5) 89 | } 90 | "should reset to 0 and then decrement by 5 for a key that contains a diff type" ! client { r ⇒ 91 | r.set("anshin-4", "debasish") 92 | r.sync.decrby("anshin-4", 5) must throwA[RedisErrorException].like { 93 | case e ⇒ e.getMessage must startWith("ERR value is not an integer") 94 | } 95 | } 96 | } 97 | 98 | "mget" >> { 99 | "should get values for existing keys" ! client { r ⇒ 100 | r.set("anshin-1", "debasish") 101 | r.set("anshin-2", "maulindu") 102 | r.set("anshin-3", "nilanjan") 103 | r.sync.mget(Seq("anshin-1", "anshin-2", "anshin-3")).parse[String] === (List(Some("debasish"), Some("maulindu"), Some("nilanjan"))) 104 | } 105 | "should give None for non-existing keys" ! client { r ⇒ 106 | r.set("anshin-1", "debasish") 107 | r.set("anshin-2", "maulindu") 108 | r.sync.mget(Seq("anshin-1", "anshin-2", "anshin-4")).parse[String] === (List(Some("debasish"), Some("maulindu"), None)) 109 | } 110 | } 111 | 112 | "mset" >> { 113 | "should set all keys irrespective of whether they exist" ! client { r ⇒ 114 | r.sync.mset(Map( 115 | ("anshin-1", "debasish"), 116 | ("anshin-2", "maulindu"), 117 | ("anshin-3", "nilanjan"))) === () 118 | } 119 | 120 | "should set all keys only if none of them exist" ! client { r ⇒ 121 | r.sync.msetnx(Map( 122 | ("anshin-4", "debasish"), 123 | ("anshin-5", "maulindu"), 124 | ("anshin-6", "nilanjan"))) === (true) 125 | r.sync.msetnx(Map( 126 | ("anshin-7", "debasish"), 127 | ("anshin-8", "maulindu"), 128 | ("anshin-6", "nilanjan"))) === (false) 129 | r.sync.msetnx(Map( 130 | ("anshin-4", "debasish"), 131 | ("anshin-5", "maulindu"), 132 | ("anshin-6", "nilanjan"))) === (false) 133 | } 134 | } 135 | 136 | "get with spaces in keys" >> { 137 | "should retrieve key/value pairs for existing keys" ! client { r ⇒ 138 | r.set("anshin software", "debasish ghosh") 139 | r.sync.get("anshin software").parse[String] === (Some("debasish ghosh")) 140 | 141 | r.set("test key with spaces", "I am a value with spaces") 142 | r.sync.get("test key with spaces").parse[String] === (Some("I am a value with spaces")) 143 | } 144 | } 145 | 146 | "set with newline values" >> { 147 | "should set key/value pairs" ! client { r ⇒ 148 | r.sync.set("anshin-x", "debasish\nghosh\nfather") === () 149 | r.sync.set("anshin-y", "maulindu\nchatterjee") === () 150 | } 151 | } 152 | 153 | "get with newline values" >> { 154 | "should retrieve key/value pairs for existing keys" ! client { r ⇒ 155 | r.set("anshin-x", "debasish\nghosh\nfather") 156 | r.sync.get("anshin-x").parse[String] === (Some("debasish\nghosh\nfather")) 157 | } 158 | } 159 | } 160 | 161 | -------------------------------------------------------------------------------- /src/test/scala/net/fyrie/redis/TestServer.scala: -------------------------------------------------------------------------------- 1 | package net.fyrie.redis 2 | 3 | import org.specs2._ 4 | import org.specs2.specification._ 5 | import org.specs2.execute._ 6 | import akka.actor.ActorSystem 7 | import akka.dispatch.{ Future, Await } 8 | import akka.util.Duration 9 | 10 | object TestSystem { 11 | val config = com.typesafe.config.ConfigFactory.parseString(""" 12 | akka { 13 | event-handlers = ["akka.testkit.TestEventListener"] 14 | loglevel = "WARNING" 15 | actor { 16 | default-dispatcher { 17 | core-pool-size-min = 4 18 | core-pool-size-factor = 2.0 19 | throughput = 10 20 | } 21 | } 22 | } 23 | """) 24 | 25 | val system = ActorSystem("TestSystem", config) 26 | } 27 | 28 | trait TestClient { self: mutable.Specification ⇒ 29 | 30 | implicit def futureResult[T](future: Future[T])(implicit toResult: T ⇒ Result): Result = 31 | Await.result(future map toResult, akka.util.Timeout(1000).duration) //Duration.Inf) 32 | 33 | implicit val system = TestSystem.system 34 | 35 | implicit val arguments = args(sequential = true) 36 | 37 | val config = RedisClientConfig(connections = 1) 38 | 39 | def client = new AroundOutside[RedisClient] { 40 | 41 | val r = RedisClient(config = config) 42 | r.sync.flushall 43 | 44 | def around[T <% Result](t: ⇒ T) = { 45 | val result = t 46 | r.sync.flushall 47 | r.disconnect 48 | result 49 | } 50 | 51 | def outside: RedisClient = r 52 | 53 | } 54 | 55 | } 56 | 57 | trait UnstableClient { self: mutable.Specification ⇒ 58 | 59 | implicit val system = TestSystem.system 60 | 61 | implicit val arguments = args(sequential = true) 62 | 63 | def client = new AroundOutside[RedisClient] { 64 | 65 | val r = RedisClient() 66 | r.sync.flushall 67 | 68 | def around[T <% Result](t: ⇒ T) = { 69 | val result: Result = r.sync.info.lines.find(_ startsWith "redis_version").map(_.split(Array(':', '.')).tail.map(_.toInt).toList) match { 70 | case Some(a :: b :: _) if (a > 2) || ((a == 2) && (b > 4)) ⇒ t 71 | case _ ⇒ skipped 72 | } 73 | r.sync.flushall 74 | r.disconnect 75 | result 76 | } 77 | 78 | def outside: RedisClient = r 79 | 80 | } 81 | 82 | } 83 | --------------------------------------------------------------------------------