├── .gitignore ├── README.md ├── buildfile └── src ├── main └── scala │ └── com │ └── fotolog │ └── redis │ ├── RedisClient.scala │ ├── RedisCluster.scala │ └── RedisIO.scala └── test └── scala └── com └── fotolog └── redis └── RedisClientTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | reports 2 | target 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [Redis](http://code.google.com/p/redis/) scala client library 2 | 3 | This is VERY old and unmaintained. Plenty of good redis libs out there, just google. 4 | -------------------------------------------------------------------------------- /buildfile: -------------------------------------------------------------------------------- 1 | Buildr.settings.build['scala.version'] = "2.8.0" 2 | 3 | require 'buildr/scala' 4 | 5 | 6 | VERSION_NUMBER = "1.0.0" 7 | GROUP = "redis-client-scala-netty" 8 | COPYRIGHT = "Fotolog, Inc" 9 | 10 | repositories.remote << 'http://scala-tools.org/repo-releases/' 11 | repositories.remote << "http://www.ibiblio.org/maven2/" 12 | repositories.remote << 'http://repository.jboss.org/maven2/' 13 | 14 | desc "The Redis-client-scala-netty project" 15 | define "redis-client-scala-netty" do 16 | 17 | project.version = VERSION_NUMBER 18 | project.group = GROUP 19 | manifest["Fotolog"] = COPYRIGHT 20 | 21 | compile.with 'org.jboss.netty:netty:jar:3.1.5.GA' 22 | compile.with 'apache-log4j:log4j:jar:1.2.15' 23 | compile.using :other=>'-g:line', :optimise=>'true' 24 | 25 | test.using :junit 26 | 27 | package :jar 28 | end 29 | -------------------------------------------------------------------------------- /src/main/scala/com/fotolog/redis/RedisClient.scala: -------------------------------------------------------------------------------- 1 | package com.fotolog.redis 2 | 3 | import java.util.concurrent.Future 4 | import java.util.concurrent.TimeUnit 5 | 6 | sealed abstract class KeyType 7 | object KeyType { 8 | def apply(s: String): KeyType = { 9 | s match { 10 | case "none" => None 11 | case "string" => String 12 | case "list" => List 13 | case "set" => Set 14 | case "zset" => Zset 15 | case "hash" => Hash 16 | } 17 | } 18 | 19 | case object None extends KeyType // key does not exist 20 | case object String extends KeyType // binary String value, any seq of bytes 21 | case object List extends KeyType // contains a List value 22 | case object Set extends KeyType // contains a Set value 23 | case object Zset extends KeyType // contains a Sorted Set value 24 | case object Hash extends KeyType // contains a Hash value 25 | } 26 | 27 | trait StringConversions { 28 | import RedisClientTypes._ 29 | implicit def convertStringToByteArray(s: String): BinVal = s.getBytes 30 | implicit def convertByteArrayToString(b: BinVal): String = new String(b) 31 | } 32 | trait FixedIntConversions { 33 | import RedisClientTypes._ 34 | 35 | implicit def convertIntToByteArray(i: Int): Array[Byte] = { 36 | Array[Byte]( 37 | ((i )&0xFF).asInstanceOf[Byte], 38 | ((i>> 8)&0xFF).asInstanceOf[Byte], 39 | ((i>> 16)&0xFF).asInstanceOf[Byte], 40 | ((i>> 24)&0xFF).asInstanceOf[Byte] 41 | ) 42 | } 43 | implicit def convertByteArrayToInt(b: Array[Byte]): Int = { 44 | (b(0).asInstanceOf[Int] & 0xFF) | 45 | (b(1).asInstanceOf[Int] & 0xFF) << 8 | 46 | (b(2).asInstanceOf[Int] & 0xFF) << 16 | 47 | (b(3).asInstanceOf[Int] & 0xFF) << 24 48 | } 49 | 50 | implicit def convertLongToByteArray(i: Long): Array[Byte] = { 51 | Array[Byte]( 52 | ((i )&0xFF).asInstanceOf[Byte], 53 | ((i>> 8)&0xFF).asInstanceOf[Byte], 54 | ((i>> 16)&0xFF).asInstanceOf[Byte], 55 | ((i>> 24)&0xFF).asInstanceOf[Byte], 56 | ((i>> 32)&0xFF).asInstanceOf[Byte], 57 | ((i>> 40)&0xFF).asInstanceOf[Byte], 58 | ((i>> 48)&0xFF).asInstanceOf[Byte], 59 | ((i>> 56)&0xFF).asInstanceOf[Byte] 60 | ) 61 | } 62 | implicit def convertByteArrayToLong(b: Array[Byte]): Long = { 63 | (b(0).asInstanceOf[Long] & 0xFF) | 64 | (b(1).asInstanceOf[Long] & 0xFF) << 8 | 65 | (b(2).asInstanceOf[Long] & 0xFF) << 16 | 66 | (b(3).asInstanceOf[Long] & 0xFF) << 24 | 67 | (b(4).asInstanceOf[Long] & 0xFF) << 32 | 68 | (b(5).asInstanceOf[Long] & 0xFF) << 40 | 69 | (b(6).asInstanceOf[Long] & 0xFF) << 48 | 70 | (b(7).asInstanceOf[Long] & 0xFF) << 56 71 | } 72 | } 73 | 74 | class Conversions 75 | object Conversions extends Conversions with StringConversions with FixedIntConversions 76 | 77 | 78 | object RedisClient { 79 | import RedisClientTypes._ 80 | import scala.collection.{ Set => SCSet } 81 | 82 | def apply(): RedisClient = new RedisClient(new RedisConnection) 83 | def apply(host: String, port: Int) = new RedisClient(new RedisConnection(host, port)) 84 | 85 | private[redis] val integerResultAsBoolean: PartialFunction[Result, Boolean] = { 86 | case IntegerResult(1) => true 87 | case IntegerResult(0) => false 88 | } 89 | private[redis] val okResultAsBoolean: PartialFunction[Result, Boolean] = { 90 | case SingleLineResult("OK") => true 91 | // for cases where any other val should produce an error 92 | } 93 | private[redis] val integerResultAsInt: PartialFunction[Result, Int] = { 94 | case IntegerResult(x) => x 95 | } 96 | 97 | private[redis] def bulkDataResultToOpt[T](convert: (BinVal)=>T): PartialFunction[Result, Option[T]] = { 98 | case BulkDataResult(data) => data match { 99 | case None => None 100 | case Some(data) => Some(convert(data)) 101 | } 102 | } 103 | 104 | private[redis] def multiBulkDataResultToFilteredSeq[T](convert: (BinVal)=>T): PartialFunction[Result, Seq[T]] = { 105 | case MultiBulkDataResult(results) => results.filter{ r => r match { 106 | case BulkDataResult(Some(_)) => true 107 | case BulkDataResult(None) => false 108 | }}.map{ r => convert(r.data.get)} 109 | } 110 | 111 | private[redis] def multiBulkDataResultToSet[T](convert: (BinVal)=>T): PartialFunction[Result, SCSet[T]] = { 112 | case MultiBulkDataResult(results) => SCSet.empty[T] ++ results.filter{ r => r match { 113 | case BulkDataResult(Some(_)) => true 114 | case BulkDataResult(None) => false 115 | }}.map{ r => convert(r.data.get)} 116 | } 117 | 118 | private[redis] def multiBulkDataResultToMap[T](keys: Seq[String], convert: (BinVal)=>T): PartialFunction[Result, Map[String,T]] = { 119 | case BulkDataResult(data) => data match { 120 | case None => Map() 121 | case Some(data) => Map(keys.head -> convert(data)) 122 | } 123 | case MultiBulkDataResult(results) => { 124 | Map.empty[String, T] ++ 125 | keys.zip(results).filter{ kv => kv match { 126 | case (k, BulkDataResult(Some(_))) => true 127 | case (k, BulkDataResult(None)) => false 128 | }}.map{ kv => kv._1 -> convert(kv._2.data.get)} 129 | } 130 | } 131 | } 132 | 133 | class RedisClient(val r: RedisConnection) { 134 | import RedisClientTypes._ 135 | import RedisClient.integerResultAsBoolean 136 | import RedisClient.integerResultAsInt 137 | import RedisClient.okResultAsBoolean 138 | import RedisClient.bulkDataResultToOpt 139 | import RedisClient.multiBulkDataResultToFilteredSeq 140 | import RedisClient.multiBulkDataResultToSet 141 | import RedisClient.multiBulkDataResultToMap 142 | import Conversions._ 143 | import scala.collection.{ Set => SCSet } 144 | 145 | def isConnected(): Boolean = r.isOpen 146 | def shutdown() { r.shutdown } 147 | 148 | def flushall = r(FlushAll()).get 149 | 150 | def ping(): Boolean = ClientFuture(r(Ping())){ 151 | case SingleLineResult("PONG") => true 152 | }.get 153 | 154 | def info(): Map[String,String] = ClientFuture(r(Info())){ 155 | case BulkDataResult(Some(data)) => { 156 | val info = convertByteArrayToString(data) 157 | Map.empty[String,String] ++ info.split("\r\n").map{_.split(":")}.map{x=>x(0)->x(1)} 158 | } 159 | }.get 160 | 161 | def keytypeAsync(key: String): Future[KeyType] = ClientFuture(r(Type(key))){ 162 | case SingleLineResult(s) => KeyType(s) 163 | } 164 | def keytype(key: String): KeyType = keytypeAsync(key).get 165 | 166 | def existsAsync(key: String): Future[Boolean] = ClientFuture(r(Exists(key)))(integerResultAsBoolean) 167 | def exists(key: String): Boolean = existsAsync(key).get 168 | def ? (key: String): Boolean = exists(key) 169 | 170 | def setAsync[T](key: String, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Set(key -> convert(value))))(okResultAsBoolean) 171 | def set[T](key: String, value: T)(implicit convert: (T)=>BinVal): Boolean = setAsync(key, value)(convert).get 172 | def setAsync[T](kvs: (String,T)*)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Set(kvs.map{kv => kv._1 -> convert(kv._2)} : _*)))(okResultAsBoolean) 173 | def set[T](kvs: (String,T)*)(implicit convert: (T)=>BinVal): Boolean = setAsync(kvs: _*)(convert).get 174 | def setAsync[T](key: String, expiration: Int, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(SetEx(key, expiration, convert(value))))(okResultAsBoolean) 175 | def set[T](key: String, expiration: Int, value: T)(implicit convert: (T)=>BinVal): Boolean = setAsync(key, expiration, value)(convert).get 176 | def +[T](key: String, value: T)(implicit convert: (T)=>BinVal): Boolean = set(key, value)(convert) 177 | def +[T](kvs: (String,T)*)(implicit convert: (T)=>BinVal): Boolean = set(kvs: _*)(convert) 178 | def ++[T](kvs: TraversableOnce[(String,T)])(implicit convert: (T)=>BinVal): Boolean = set(kvs.toSeq: _*)(convert) 179 | 180 | def delAsync(key: String): Future[Boolean] = ClientFuture(r(Del(key)))(integerResultAsBoolean) 181 | def del(key: String): Boolean = delAsync(key).get 182 | def -(key: String): Boolean = del(key) 183 | 184 | def getAsync[T](key: String)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(Get(key)))(bulkDataResultToOpt(convert)) 185 | def getAsync[T](keys: String*)(implicit convert: (BinVal)=>T): Future[Seq[Option[T]]] = ClientFuture(r(Get(keys: _*))) { 186 | case BulkDataResult(data) => data match { 187 | case None => Seq(None) 188 | case Some(data) => Seq(Some(convert(data))) 189 | } 190 | case MultiBulkDataResult(results) => results.map { _.data match { 191 | case None => None 192 | case Some(bytes) => Some(convert(bytes)) 193 | } 194 | } 195 | } 196 | def get[T](key: String)(implicit convert: (BinVal)=>T): Option[T] = getAsync(key)(convert).get 197 | def get[T](keys: String*)(implicit convert: (BinVal)=>T): Seq[Option[T]] = getAsync(keys: _*)(convert).get 198 | def apply[T](key: String)(implicit convert: (BinVal)=>T): Option[T] = get(key)(convert) 199 | 200 | def mgetAsync[T](keys: String*)(implicit convert: (BinVal)=>T): Future[Map[String,T]] = ClientFuture(r(Get(keys: _*)))(multiBulkDataResultToMap(keys,convert)) 201 | def mget[T](keys: String*)(implicit convert: (BinVal)=>T): Map[String,T] = mgetAsync(keys: _*)(convert).get 202 | def apply[T](keys: String*)(implicit convert: (BinVal)=>T): Map[String,T] = mget(keys: _*)(convert) 203 | def apply[T](keys: TraversableOnce[String])(implicit convert: (BinVal)=>T): Map[String,T] = mget(keys.toSeq: _*)(convert) 204 | 205 | def setnxAsync[T](key: String, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(SetNx(key -> convert(value))))(integerResultAsBoolean) 206 | def setnxAsync[T](kvs: (String,T)*)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(SetNx(kvs.map{kv => kv._1 -> convert(kv._2)} : _*)))(integerResultAsBoolean) 207 | def setnx[T](key: String, value: T)(implicit convert: (T)=>BinVal): Boolean = setnxAsync(key, value)(convert).get 208 | def setnx[T](kvs: (String,T)*)(implicit convert: (T)=>BinVal): Boolean = setnxAsync(kvs: _*)(convert).get 209 | def +? [T](key: String, value: T)(implicit convert: (T)=>BinVal): Boolean = setnx(key, value)(convert) 210 | def +? [T](kvs: (String,T)*)(implicit convert: (T)=>BinVal): Boolean = setnx(kvs: _*)(convert) 211 | 212 | def getsetAsync[T](key: String, value: T)(implicit convertTo: (T)=>BinVal, convertFrom: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(GetSet(key -> convertTo(value))))(bulkDataResultToOpt(convertFrom)) 213 | def getset[T](key: String, value: T)(implicit convertTo: (T)=>BinVal, convertFrom: (BinVal)=>T): Option[T] = getsetAsync(key, value)(convertTo, convertFrom).get 214 | 215 | def incrAsync[T](key: String, delta: Int = 1): Future[Int] = ClientFuture(r(Incr(key, delta)))(integerResultAsInt) 216 | def incr[T](key: String, delta: Int = 1): Int = incrAsync(key, delta).get 217 | 218 | def decrAsync[T](key: String, delta: Int = 1): Future[Int] = ClientFuture(r(Decr(key, delta)))(integerResultAsInt) 219 | def decr[T](key: String, delta: Int = 1): Int = decrAsync(key, delta).get 220 | 221 | def appendAsync[T](key: String, value: T)(implicit convert: (T)=>BinVal): Future[Int] = ClientFuture(r(Append(key -> convert(value))))(integerResultAsInt) 222 | def append[T](key: String, value: T)(implicit convert: (T)=>BinVal): Int = appendAsync(key, value)(convert).get 223 | 224 | def substrAsync[T](key: String, startOffset: Int, endOffset: Int)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(Substr(key, startOffset, endOffset)))(bulkDataResultToOpt(convert)) 225 | def substr[T](key: String, startOffset: Int, endOffset: Int)(implicit convert: (BinVal)=>T): Option[T] = substrAsync(key, startOffset, endOffset)(convert).get 226 | 227 | def expireAsync(key: String, seconds: Int): Future[Boolean] = ClientFuture(r(Expire(key, seconds)))(integerResultAsBoolean) 228 | def expire(key: String, seconds: Int): Boolean = expireAsync(key, seconds).get 229 | 230 | def persistAsync(key: String): Future[Boolean] = ClientFuture(r(Persist(key)))(integerResultAsBoolean) 231 | def persist(key: String): Boolean = persistAsync(key).get 232 | 233 | def rpushAsync[T](key: String, value: T)(implicit convert: (T)=>BinVal): Future[Int] = ClientFuture(r(Rpush(key, convert(value))))(integerResultAsInt) 234 | def rpush[T](key: String, value: T)(implicit convert: (T)=>BinVal): Int = rpushAsync(key, value).get 235 | 236 | def lpushAsync[T](key: String, value: T)(implicit convert: (T)=>BinVal): Future[Int] = ClientFuture(r(Lpush(key, convert(value))))(integerResultAsInt) 237 | def lpush[T](key: String, value: T)(implicit convert: (T)=>BinVal): Int = lpushAsync(key, value).get 238 | 239 | def llenAsync[T](key: String): Future[Int] = ClientFuture(r(Llen(key)))(integerResultAsInt) 240 | def llen[T](key: String): Int = llenAsync(key).get 241 | 242 | def lrangeAsync[T](key: String, start: Int, end: Int)(implicit convert: (BinVal)=>T): Future[Seq[T]] = ClientFuture(r(Lrange(key, start, end)))(multiBulkDataResultToFilteredSeq(convert)) 243 | def lrange[T](key: String, start: Int, end: Int)(implicit convert: (BinVal)=>T): Seq[T] = lrangeAsync(key, start, end)(convert).get 244 | 245 | def ltrimAsync(key: String, start: Int, end: Int): Future[Boolean] = ClientFuture(r(Ltrim(key, start, end)))(okResultAsBoolean) 246 | def ltrim(key: String, start: Int, end: Int): Boolean = ltrimAsync(key, start, end).get 247 | 248 | def lindexAsync[T](key: String, idx: Int)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(Lindex(key, idx)))(bulkDataResultToOpt(convert)) 249 | def lindex[T](key: String, idx: Int)(implicit convert: (BinVal)=>T): Option[T] = lindexAsync(key, idx)(convert).get 250 | 251 | def lsetAsync[T](key: String, idx: Int, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Lset(key, idx, convert(value))))(okResultAsBoolean) 252 | def lset[T](key: String, idx: Int, value: T)(implicit convert: (T)=>BinVal): Boolean = lsetAsync(key, idx, value)(convert).get 253 | 254 | def lremAsync[T](key: String, count: Int, value: T)(implicit convert: (T)=>BinVal): Future[Int] = ClientFuture(r(Lrem(key, count, convert(value))))(integerResultAsInt) 255 | def lrem[T](key: String, count: Int, value: T)(implicit convert: (T)=>BinVal): Int = lremAsync(key, count, value)(convert).get 256 | 257 | def lpopAsync[T](key: String)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(Lpop(key)))(bulkDataResultToOpt(convert)) 258 | def lpop[T](key: String)(implicit convert: (BinVal)=>T): Option[T] = lpopAsync(key)(convert).get 259 | 260 | def rpopAsync[T](key: String)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(Rpop(key)))(bulkDataResultToOpt(convert)) 261 | def rpop[T](key: String)(implicit convert: (BinVal)=>T): Option[T] = rpopAsync(key)(convert).get 262 | 263 | def rpoplpushAsync[T](srcKey: String, destKey: String)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(RpopLpush(srcKey, destKey)))(bulkDataResultToOpt(convert)) 264 | def rpoplpush[T](srcKey: String, destKey: String)(implicit convert: (BinVal)=>T): Option[T] = rpoplpushAsync(srcKey, destKey)(convert).get 265 | 266 | def hsetAsync[T](key: String, field: String, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Hset(key, field, convert(value))))(integerResultAsBoolean) 267 | def hset[T](key: String, field: String, value: T)(implicit convert: (T)=>BinVal): Boolean = hsetAsync(key, field, value)(convert).get 268 | 269 | def hgetAsync[T](key: String, field: String)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(Hget(key, field)))(bulkDataResultToOpt(convert)) 270 | def hget[T](key: String, field: String)(implicit convert: (BinVal)=>T): Option[T] = hgetAsync(key, field)(convert).get 271 | 272 | def hmgetAsync[T](key: String, fields: String*)(implicit convert: (BinVal)=>T): Future[Map[String,T]] = ClientFuture(r(Hmget(key, fields: _*)))(multiBulkDataResultToMap(fields, convert)) 273 | def hmget[T](key: String, fields: String*)(implicit convert: (BinVal)=>T): Map[String,T] = hmgetAsync(key, fields: _*)(convert).get 274 | 275 | def hmsetAsync[T](key: String, kvs: (String,T)*)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Hmset(key, kvs.map{kv => kv._1 -> convert(kv._2)} : _*)))(okResultAsBoolean) 276 | def hmset[T](key: String, kvs: (String,T)*)(implicit convert: (T)=>BinVal): Boolean = hmsetAsync(key, kvs: _*)(convert).get 277 | 278 | def hincrAsync(key: String, field: String, delta: Int = 1): Future[Int] = ClientFuture(r(Hincrby(key, field, delta)))(integerResultAsInt) 279 | def hincr(key: String, field: String, delta: Int = 1): Int = hincrAsync(key, field, delta).get 280 | 281 | def hexistsAsync(key: String, field: String): Future[Boolean] = ClientFuture(r(Hexists(key, field)))(integerResultAsBoolean) 282 | def hexists(key: String, field: String): Boolean = hexistsAsync(key, field).get 283 | 284 | def hdelAsync(key: String, field: String): Future[Boolean] = ClientFuture(r(Hdel(key, field)))(integerResultAsBoolean) 285 | def hdel(key: String, field: String): Boolean = hdelAsync(key, field).get 286 | 287 | def hlenAsync(key: String): Future[Int] = ClientFuture(r(Hlen(key)))(integerResultAsInt) 288 | def hlen(key: String): Int = hlenAsync(key).get 289 | 290 | def hkeysAsync(key: String): Future[Seq[String]] = ClientFuture(r(Hkeys(key)))(multiBulkDataResultToFilteredSeq(convertByteArrayToString)) 291 | def hkeys(key: String): Seq[String] = hkeysAsync(key).get 292 | 293 | def hvalsAsync[T](key: String)(implicit convert: (BinVal)=>T): Future[Seq[T]] = ClientFuture(r(Hvals(key)))(multiBulkDataResultToFilteredSeq(convert)) 294 | def hvals[T](key: String)(implicit convert: (BinVal)=>T): Seq[T] = hvalsAsync(key)(convert).get 295 | 296 | def hgetallAsync[T](key: String)(implicit convert: (BinVal)=>T): Future[Map[String,T]] = ClientFuture(r(Hgetall(key))) { 297 | case MultiBulkDataResult(List()) => Map.empty[String,T] 298 | case MultiBulkDataResult(results) => Map.empty[String, T] ++ { 299 | var take = false 300 | results.zip(results.tail).filter{ (_) => take = !take; take }.filter{ kv => kv match { 301 | case (k, BulkDataResult(Some(_))) => true 302 | case (k, BulkDataResult(None)) => false 303 | }}.map{ kv => convertByteArrayToString(kv._1.data.get) -> convert(kv._2.data.get)} 304 | } 305 | } 306 | def hgetall[T](key: String)(implicit convert: (BinVal)=>T): Map[String,T] = hgetallAsync(key)(convert).get 307 | 308 | def saddAsync[T](key: String, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Sadd(key->convert(value))))(integerResultAsBoolean) 309 | def sadd[T](key: String, value: T)(implicit convert: (T)=>BinVal): Boolean = saddAsync(key,value)(convert).get 310 | 311 | def sremAsync[T](key: String, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Srem(key->convert(value))))(integerResultAsBoolean) 312 | def srem[T](key: String, value: T)(implicit convert: (T)=>BinVal): Boolean = sremAsync(key,value)(convert).get 313 | 314 | def spopAsync[T](key: String)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(Spop(key)))(bulkDataResultToOpt(convert)) 315 | def spop[T](key: String)(implicit convert: (BinVal)=>T): Option[T] = spopAsync(key)(convert).get 316 | 317 | def smoveAsync[T](srcKey: String, destKey: String, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Smove(srcKey, destKey, convert(value))))(integerResultAsBoolean) 318 | def smove[T](srcKey: String, destKey: String, value: T)(implicit convert: (T)=>BinVal): Boolean = smoveAsync(srcKey, destKey, value)(convert).get 319 | 320 | def scardAsync(key: String): Future[Int] = ClientFuture(r(Scard(key)))(integerResultAsInt) 321 | def scard(key: String): Int = scardAsync(key).get 322 | 323 | def sismemberAsync[T](key: String, value: T)(implicit convert: (T)=>BinVal): Future[Boolean] = ClientFuture(r(Sismember(key->convert(value))))(integerResultAsBoolean) 324 | def sismember[T](key: String, value: T)(implicit convert: (T)=>BinVal): Boolean = sismemberAsync(key, value)(convert).get 325 | 326 | def sinterAsync[T](keys: String*)(implicit convert: (BinVal)=>T): Future[SCSet[T]] = ClientFuture(r(Sinter(keys: _*)))(multiBulkDataResultToSet(convert)) 327 | def sinter[T](keys: String*)(implicit convert: (BinVal)=>T): SCSet[T] = sinterAsync(keys: _*)(convert).get 328 | 329 | def sinterstoreAsync[T](destKey: String, keys: String*): Future[Int] = ClientFuture(r(Sinterstore(destKey, keys: _*)))(integerResultAsInt) 330 | def sinterstore[T](destKey: String, keys: String*): Int = sinterstoreAsync(destKey, keys: _*).get 331 | 332 | def sunionAsync[T](keys: String*)(implicit convert: (BinVal)=>T): Future[SCSet[T]] = ClientFuture(r(Sunion(keys: _*)))(multiBulkDataResultToSet(convert)) 333 | def sunion[T](keys: String*)(implicit convert: (BinVal)=>T): SCSet[T] = sunionAsync(keys: _*)(convert).get 334 | 335 | def sunionstoreAsync[T](destKey: String, keys: String*): Future[Int] = ClientFuture(r(Sunionstore(destKey, keys: _*)))(integerResultAsInt) 336 | def sunionstore[T](destKey: String, keys: String*): Int = sunionstoreAsync(destKey, keys: _*).get 337 | 338 | def sdiffAsync[T](keys: String*)(implicit convert: (BinVal)=>T): Future[SCSet[T]] = ClientFuture(r(Sdiff(keys: _*)))(multiBulkDataResultToSet(convert)) 339 | def sdiff[T](keys: String*)(implicit convert: (BinVal)=>T): SCSet[T] = sdiffAsync(keys: _*)(convert).get 340 | 341 | def sdiffstoreAsync[T](destKey: String, keys: String*): Future[Int] = ClientFuture(r(Sdiffstore(destKey, keys: _*)))(integerResultAsInt) 342 | def sdiffstore[T](destKey: String, keys: String*): Int = sdiffstoreAsync(destKey, keys: _*).get 343 | 344 | def smembersAsync[T](key: String)(implicit convert: (BinVal)=>T): Future[SCSet[T]] = ClientFuture(r(Smembers(key)))(multiBulkDataResultToSet(convert)) 345 | def smembers[T](key: String)(implicit convert: (BinVal)=>T): SCSet[T] = smembersAsync(key)(convert).get 346 | 347 | def srandmemberAsync[T](key: String)(implicit convert: (BinVal)=>T): Future[Option[T]] = ClientFuture(r(Srandmember(key)))(bulkDataResultToOpt(convert)) 348 | def srandmember[T](key: String)(implicit convert: (BinVal)=>T): Option[T] = srandmemberAsync(key)(convert).get 349 | } 350 | 351 | object ClientFuture { 352 | def apply[T](redisResult: ResultFuture)(resultConverter: PartialFunction[Result, T]) = new ClientFuture(redisResult, resultConverter) 353 | } 354 | 355 | class ClientFuture[T](redisResult: ResultFuture, resultConverter: PartialFunction[Result, T]) extends Future[T] { 356 | override def get(): T = get(10, TimeUnit.SECONDS) 357 | override def isCancelled(): Boolean = redisResult.isCancelled 358 | override def cancel(p: Boolean): Boolean = redisResult.cancel(p) 359 | override def isDone(): Boolean = redisResult.isDone 360 | override def get(t: Long, unit: TimeUnit): T = { 361 | val rr = redisResult.get(t, unit) 362 | rr match { 363 | case ErrorResult(err) => throw new RedisClientException(err) 364 | case x => resultConverter(x) 365 | } 366 | } 367 | } 368 | 369 | class RedisClientException(msg: String) extends RuntimeException(msg) 370 | -------------------------------------------------------------------------------- /src/main/scala/com/fotolog/redis/RedisCluster.scala: -------------------------------------------------------------------------------- 1 | package com.fotolog.redis 2 | 3 | import java.util.concurrent.atomic.AtomicReference 4 | 5 | case class RedisHost(host: String, port: Int) 6 | 7 | class RedisCluster[Shard](hash: (Shard)=>Int, hosts: RedisHost*) { 8 | val h2cRef = new AtomicReference[Map[RedisHost,RedisClient]](Map.empty) 9 | 10 | def apply(s: Shard): RedisClient = { 11 | val h = hosts(hash(s).abs % hosts.length) 12 | h2cRef.get.get(h) match { 13 | case Some(c) => if (c.isConnected) c else newClient(h) 14 | case None => newClient(h) 15 | } 16 | } 17 | 18 | private[RedisCluster] def newClient(h: RedisHost): RedisClient = h.synchronized { 19 | var h2c = h2cRef.get 20 | def newClientThreadSafe(): RedisClient = { 21 | val c = RedisClient(h.host, h.port) 22 | while(!h2cRef.compareAndSet(h2c, h2c + (h->c))) h2c = h2cRef.get 23 | c 24 | } 25 | h2c.get(h) match { 26 | case None => newClientThreadSafe() 27 | case Some(c) => if (c.isConnected){ c } else { c.shutdown; newClientThreadSafe() } 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/scala/com/fotolog/redis/RedisIO.scala: -------------------------------------------------------------------------------- 1 | package com.fotolog.redis 2 | 3 | 4 | import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory 5 | import org.jboss.netty.channel._ 6 | import org.jboss.netty.buffer._ 7 | import org.jboss.netty.bootstrap.ClientBootstrap 8 | import org.jboss.netty.handler.codec.frame.FrameDecoder 9 | import java.net.InetSocketAddress 10 | import java.util.concurrent._ 11 | import org.apache.log4j.Logger 12 | 13 | object RedisClientTypes { 14 | type BinVal = Array[Byte] 15 | type KV = Tuple2[String, BinVal] 16 | } 17 | import RedisClientTypes._ 18 | 19 | // tested with 2.1.3 redis, ubuntu 10.04 ships with 1.2.0 where some newer commands won't work 20 | sealed abstract class Cmd 21 | case class Exists(key: String) extends Cmd 22 | case class Type(key: String) extends Cmd 23 | case class Del(key: String*) extends Cmd 24 | case class Get(key: String*) extends Cmd 25 | case class Set(kvs: KV*) extends Cmd 26 | case class SetNx(kvs: KV*) extends Cmd 27 | case class GetSet(kv: KV) extends Cmd 28 | case class SetEx(key: String, expTime: Int, value: BinVal) extends Cmd 29 | case class Incr(key: String, delta: Int = 1) extends Cmd 30 | case class Decr(key: String, delta: Int = 1) extends Cmd 31 | case class Append(kv: KV) extends Cmd 32 | case class Substr(key: String, startOffset: Int, endOffset: Int) extends Cmd 33 | case class Expire(key: String, seconds: Int) extends Cmd 34 | case class Persist(key: String) extends Cmd 35 | // lists 36 | case class Rpush(kv: KV) extends Cmd 37 | case class Lpush(kv: KV) extends Cmd 38 | case class Llen(key: String) extends Cmd 39 | case class Lrange(key: String, start: Int, end: Int) extends Cmd 40 | case class Ltrim(key: String, start: Int, end: Int) extends Cmd 41 | case class Lindex(key: String, idx: Int) extends Cmd 42 | case class Lset(key: String, idx: Int, value: BinVal) extends Cmd 43 | case class Lrem(key: String, count: Int, value: BinVal) extends Cmd 44 | case class Lpop(key: String) extends Cmd 45 | case class Rpop(key: String) extends Cmd 46 | case class RpopLpush(srcKey: String, destKey: String) extends Cmd 47 | // hashes 48 | case class Hset(key: String, field: String, value: BinVal) extends Cmd 49 | case class Hget(key: String, field: String) extends Cmd 50 | case class Hmget(key: String, fields: String*) extends Cmd 51 | case class Hmset(key:String, kvs: KV*) extends Cmd 52 | case class Hincrby(key: String, field: String, delta: Int) extends Cmd 53 | case class Hexists(key: String, field: String) extends Cmd 54 | case class Hdel(key: String, field: String) extends Cmd 55 | case class Hlen(key: String) extends Cmd 56 | case class Hkeys(key: String) extends Cmd 57 | case class Hvals(key: String) extends Cmd 58 | case class Hgetall(key: String) extends Cmd 59 | // sets 60 | case class Sadd(kv: KV) extends Cmd 61 | case class Srem(kv: KV) extends Cmd 62 | case class Spop(key: String) extends Cmd 63 | case class Smove(srcKey: String, destKey: String, value: BinVal) extends Cmd 64 | case class Scard(key: String) extends Cmd 65 | case class Sismember(kv: KV) extends Cmd 66 | case class Sinter(keys: String*) extends Cmd 67 | case class Sinterstore(destKey: String, keys: String*) extends Cmd 68 | case class Sunion(keys: String*) extends Cmd 69 | case class Sunionstore(destKey: String, keys: String*) extends Cmd 70 | case class Sdiff(keys: String*) extends Cmd 71 | case class Sdiffstore(destKey: String, keys: String*) extends Cmd 72 | case class Smembers(key: String) extends Cmd 73 | case class Srandmember(key: String) extends Cmd 74 | // 75 | case class Ping() extends Cmd 76 | case class Info() extends Cmd 77 | case class FlushAll() extends Cmd 78 | 79 | 80 | sealed abstract class Result 81 | case class ErrorResult(err: String) extends Result 82 | case class SingleLineResult(msg: String) extends Result 83 | case class IntegerResult(n: Int) extends Result 84 | case class BulkDataResult(data: Option[BinVal]) extends Result { 85 | override def toString(): String = { 86 | "BulkDataResult(%s)".format({ data match { case Some(barr) => new String(barr); case None => "" } }) 87 | } 88 | } 89 | case class MultiBulkDataResult(results: Seq[BulkDataResult]) extends Result 90 | 91 | 92 | class ResultFuture(val cmd: Cmd) extends Future[Result] { 93 | private [redis] val latch = new CountDownLatch(1) 94 | private [redis] var result: Result = null 95 | 96 | override def get(): Result = get(10, TimeUnit.SECONDS) 97 | override def isCancelled(): Boolean = false 98 | override def cancel(p: Boolean): Boolean = false 99 | override def isDone(): Boolean = latch.getCount == 0 100 | 101 | override def get(t: Long, unit: TimeUnit): Result = { 102 | if (latch.await(t, unit)) result 103 | else throw new TimeoutException 104 | } 105 | } 106 | 107 | 108 | object RedisConnection { 109 | private[redis] type OpQueue = ArrayBlockingQueue[ResultFuture] 110 | 111 | private[redis] val log = Logger.getLogger(getClass) 112 | private[redis] val executor = Executors.newCachedThreadPool() 113 | private[redis] val channelFactory = new NioClientSocketChannelFactory(executor, executor) 114 | private[redis] val commandEncoder = new RedisCommandEncoder() // stateless 115 | private[redis] val cmdQueue = new ArrayBlockingQueue[Pair[RedisConnection, ResultFuture]](2048) 116 | 117 | scala.actors.Actor.actor { while(true) { 118 | val (conn, f) = cmdQueue.take() 119 | try { 120 | if (conn.isOpen) { 121 | conn.enqueue(f) 122 | } else { 123 | log.error("Skipping cmd queued up into a closed channel (%s)".format(f.cmd)) 124 | f.result = new ErrorResult("Channel closed") 125 | f.latch.countDown 126 | } 127 | } catch { 128 | case e: Exception => { 129 | RedisConnection.log.error(e.getMessage, e) 130 | conn.shutdown 131 | } 132 | } 133 | }} 134 | } 135 | 136 | class RedisConnection(val host: String = "localhost", val port: Int = 6379) 137 | { 138 | import RedisConnection._ 139 | 140 | private[RedisConnection] var isRunning = true 141 | private[RedisConnection] val clientBootstrap = new ClientBootstrap(channelFactory) 142 | private[RedisConnection] val opQueue = new OpQueue(128) 143 | 144 | clientBootstrap.setPipelineFactory(new ChannelPipelineFactory() { 145 | override def getPipeline(): ChannelPipeline = { 146 | val p = Channels.pipeline 147 | p.addLast("response_decoder", new RedisResponseDecoder()) 148 | p.addLast("response_accumulator", new RedisResponseAccumulator(opQueue)) 149 | p.addLast("command_encoder", commandEncoder) 150 | p 151 | } 152 | }) 153 | 154 | clientBootstrap.setOption("tcpNoDelay", true); 155 | clientBootstrap.setOption("keepAlive", true); 156 | clientBootstrap.setOption("connectTimeoutMillis", 1000) 157 | 158 | private[RedisConnection] val channel = { 159 | val future = clientBootstrap.connect(new InetSocketAddress(host, port)); 160 | future.await(1, TimeUnit.MINUTES) 161 | if (!future.isSuccess()) { 162 | throw future.getCause 163 | } else { 164 | future.getChannel() 165 | } 166 | } 167 | 168 | log.info("Connecting to %s:%s".format(host,port)) 169 | forceChannelOpen() 170 | 171 | 172 | def apply(cmd: Cmd): ResultFuture = { 173 | val f = new ResultFuture(cmd) 174 | cmdQueue.offer((this -> f), 10, TimeUnit.SECONDS) 175 | f 176 | } 177 | 178 | def enqueue(f: ResultFuture) { 179 | opQueue.offer(f, 10, TimeUnit.SECONDS) 180 | channel.write(f).addListener(ChannelFutureListener.CLOSE_ON_FAILURE) 181 | } 182 | 183 | def isOpen(): Boolean = isRunning && channel.isOpen 184 | 185 | def shutdown(): Unit = try { 186 | isRunning = false 187 | channel.close().await(1, TimeUnit.MINUTES) 188 | } catch { 189 | case e: Exception => log.error(e.getMessage, e) 190 | } 191 | 192 | 193 | private def forceChannelOpen() { 194 | val f = new ResultFuture(Ping()) 195 | enqueue(f) 196 | f.get 197 | } 198 | } 199 | 200 | 201 | private[redis] object ResponseType { 202 | def apply(b: Byte): ResponseType = { 203 | b match { 204 | case Error.b => Error 205 | case SingleLine.b => SingleLine 206 | case BulkData.b => BulkData 207 | case MultiBulkData.b => MultiBulkData 208 | case Integer.b => Integer 209 | } 210 | } 211 | } 212 | private[redis] sealed abstract class ResponseType(val b: Byte) 213 | private[redis] case object Error extends ResponseType('-') 214 | private[redis] case object SingleLine extends ResponseType('+') 215 | private[redis] case object BulkData extends ResponseType('$') 216 | private[redis] case object MultiBulkData extends ResponseType('*') 217 | private[redis] case object Integer extends ResponseType(':') 218 | 219 | private[redis] case object Unknown extends ResponseType('?') 220 | private[redis] case class BinaryData(len: Int) extends ResponseType('B') 221 | private[redis] case object NullData extends ResponseType('N') 222 | 223 | 224 | private[redis] object RedisCommandEncoder { 225 | val SPACE = " ".getBytes 226 | val EOL = "\r\n".getBytes 227 | 228 | val DEL = "DEL".getBytes 229 | val GET = "GET".getBytes 230 | val MGET = "MGET".getBytes 231 | val SET = "SET".getBytes 232 | val MSET = "MSET".getBytes 233 | val GETSET = "GETSET".getBytes 234 | val SETNX = "SETNX".getBytes 235 | val MSETNX = "MSETNX".getBytes 236 | val SETEX = "SETEX".getBytes 237 | val INCR = "INCR".getBytes 238 | val INCRBY = "INCRBY".getBytes 239 | val DECR = "DECR".getBytes 240 | val DECRBY = "DECRBY".getBytes 241 | val APPEND = "APPEND".getBytes 242 | val SUBSTR = "SUBSTR".getBytes 243 | val EXPIRE = "EXPIRE".getBytes 244 | val PERSIST = "PERSIST".getBytes 245 | val RPUSH = "RPUSH".getBytes 246 | val LPUSH = "LPUSH".getBytes 247 | val LLEN = "LLEN".getBytes 248 | val LRANGE = "LRANGE".getBytes 249 | val LTRIM = "LTRIM".getBytes 250 | val LINDEX = "LINDEX".getBytes 251 | val LSET = "LSET".getBytes 252 | val LREM = "LREM".getBytes 253 | val LPOP = "LPOP".getBytes 254 | val RPOP = "RPOP".getBytes 255 | val BLPOP = "BLPOP".getBytes 256 | val BRPOP = "BRPOP".getBytes 257 | val RPOPLPUSH = "RPOPLPUSH".getBytes 258 | val HSET = "HSET".getBytes 259 | val HGET = "HGET".getBytes 260 | val HMGET = "HMGET".getBytes 261 | val HMSET = "HMSET".getBytes 262 | val HINCRBY = "HINCRBY".getBytes 263 | val HEXISTS = "HEXISTS".getBytes 264 | val HDEL = "HDEL".getBytes 265 | val HLEN = "HLEN".getBytes 266 | val HKEYS = "HKEYS".getBytes 267 | val HVALS = "HVALS".getBytes 268 | val HGETALL = "HGETALL".getBytes 269 | val SADD = "SADD".getBytes 270 | val SREM = "SREM".getBytes 271 | val SPOP = "SPOP".getBytes 272 | val SMOVE = "SMOVE".getBytes 273 | val SCARD = "SCARD".getBytes 274 | val SISMEMBER = "SISMEMBER".getBytes 275 | val SINTER = "SINTER".getBytes 276 | val SINTERSTORE = "SINTERSTORE".getBytes 277 | val SUNION = "SUNION".getBytes 278 | val SUNIONSTORE = "SUNIONSTORE".getBytes 279 | val SDIFF = "SDIFF".getBytes 280 | val SDIFFSTORE = "SDIFFSTORE".getBytes 281 | val SMEMBERS = "SMEMBERS".getBytes 282 | val SRANDMEMBER = "SRANDMEMBER".getBytes 283 | val SORT = "SORT".getBytes 284 | val PING = "PING".getBytes 285 | val EXISTS = "EXISTS".getBytes 286 | val TYPE = "TYPE".getBytes 287 | val INFO = "INFO".getBytes 288 | val FLUSHALL = "FLUSHALL".getBytes 289 | } 290 | 291 | @ChannelPipelineCoverage("all") 292 | private[redis] class RedisCommandEncoder() extends org.jboss.netty.handler.codec.oneone.OneToOneEncoder { 293 | import org.jboss.netty.buffer.ChannelBuffers._ 294 | import RedisCommandEncoder._ 295 | 296 | override def encode(ctx: ChannelHandlerContext, channel: Channel, msg: AnyRef): AnyRef = { 297 | //println("encode[%s]: %h -> %s".format(Thread.currentThread.getName, this, msg)) 298 | val opFuture = msg.asInstanceOf[ResultFuture] 299 | toChannelBuffer(opFuture.cmd) 300 | } 301 | 302 | private def toChannelBuffer(cmd: Cmd): ChannelBuffer = cmd match { 303 | case Del(key) => copiedBuffer(DEL, SPACE, key.getBytes, EOL) 304 | case Del(keys @ _*) => multiKeyCmd(DEL, keys) 305 | case Get(key) => copiedBuffer(GET, SPACE, key.getBytes, EOL) 306 | case Get(keys @ _*) => multiKeyCmd(MGET, keys) 307 | case Set((key, value)) => binaryCmd(SET, key.getBytes, value) 308 | case setMulti: Set => binarySetCmd(MSET, setMulti.kvs: _*) 309 | case GetSet((key, value)) => binaryCmd(GETSET, key.getBytes, value) 310 | case SetNx((key, value)) => binaryCmd(SETNX, key.getBytes, value) 311 | case setNxMulti: SetNx => binarySetCmd(MSETNX, setNxMulti.kvs: _*) 312 | case SetEx(key, expTime, value) => binaryCmd(SETEX, key.getBytes, expTime.toString.getBytes, value) 313 | case Incr(key, 1) => copiedBuffer(INCR, SPACE, key.getBytes, EOL) 314 | case Incr(key, delta) => copiedBuffer(INCRBY, SPACE, key.getBytes, SPACE, delta.toString.getBytes, EOL) 315 | case Decr(key, 1) => copiedBuffer(DECR, SPACE, key.getBytes, EOL) 316 | case Decr(key, delta) => copiedBuffer(DECRBY, SPACE, key.getBytes, SPACE, delta.toString.getBytes, EOL) 317 | case Append((key, value)) => binaryCmd(APPEND, key.getBytes, value) 318 | case Substr(key, startOffset, endOffset) => copiedBuffer(SUBSTR, SPACE, key.getBytes, SPACE, startOffset.toString.getBytes, SPACE, endOffset.toString.getBytes, EOL) 319 | case Persist(key) => copiedBuffer(PERSIST, SPACE, key.getBytes, EOL) 320 | case Expire(key, seconds) => copiedBuffer(EXPIRE, SPACE, key.getBytes, SPACE, seconds.toString.getBytes, EOL) 321 | case Rpush((key, value)) => binaryCmd(RPUSH, key.getBytes, value) 322 | case Lpush((key, value)) => binaryCmd(LPUSH, key.getBytes, value) 323 | case Llen(key) => copiedBuffer(LLEN, SPACE, key.getBytes, EOL) 324 | case Lrange(key, start, end) => copiedBuffer(LRANGE, SPACE, key.getBytes, SPACE, start.toString.getBytes, SPACE, end.toString.getBytes, EOL) 325 | case Ltrim(key, start, end) => copiedBuffer(LTRIM, SPACE, key.getBytes, SPACE, start.toString.getBytes, SPACE, end.toString.getBytes, EOL) 326 | case Lindex(key, idx) => copiedBuffer(LINDEX, SPACE, key.getBytes, SPACE, idx.toString.getBytes, EOL) 327 | case Lset(key, idx, value) => binaryCmd(LSET, key.getBytes, idx.toString.getBytes, value) 328 | case Lrem(key, count, value) => binaryCmd(LREM, key.getBytes, count.toString.getBytes, value) 329 | case Lpop(key) => copiedBuffer(LPOP, SPACE, key.getBytes, EOL) 330 | case Rpop(key) => copiedBuffer(RPOP, SPACE, key.getBytes, EOL) 331 | case RpopLpush(srcKey, destKey) => copiedBuffer(RPOPLPUSH, SPACE, srcKey.getBytes, SPACE, destKey.getBytes, EOL) 332 | case Hset(key, field, value) => binaryCmd(HSET, key.getBytes, field.getBytes, value) 333 | case Hget(key, field) => binaryCmd(HGET, key.getBytes, field.getBytes) 334 | case Hmget(key, fields @ _*) => binaryCmd(HMGET :: key.getBytes :: fields.toList.map{_.getBytes}: _*) 335 | case hmSet: Hmset => binaryHmSetCmd(hmSet) 336 | case Hincrby(key, field, delta) => binaryCmd(HINCRBY, key.getBytes, field.getBytes, delta.toString.getBytes) 337 | case Hexists(key, field) => binaryCmd(HEXISTS, key.getBytes, field.getBytes) 338 | case Hdel(key, field) => binaryCmd(HDEL, key.getBytes, field.getBytes) 339 | case Hlen(key) => binaryCmd(HLEN, key.getBytes) 340 | case Hkeys(key) => binaryCmd(HKEYS, key.getBytes) 341 | case Hvals(key) => binaryCmd(HVALS, key.getBytes) 342 | case Hgetall(key) => binaryCmd(HGETALL, key.getBytes) 343 | case Sadd((key, value)) => binaryCmd(SADD, key.getBytes, value) 344 | case Srem((key, value)) => binaryCmd(SREM, key.getBytes, value) 345 | case Spop(key) => copiedBuffer(SPOP, SPACE, key.getBytes, EOL) 346 | case Smove(srcKey, destKey, value) => binaryCmd(SMOVE, srcKey.getBytes, destKey.getBytes, value) 347 | case Scard(key) => copiedBuffer(SCARD, SPACE, key.getBytes, EOL) 348 | case Sismember((key, value)) => binaryCmd(SISMEMBER, key.getBytes, value) 349 | case Sinter(keys @ _*) => binaryCmd(SINTER :: keys.toList.map{_.getBytes}: _*) 350 | case Sinterstore(destKey, keys @ _*) => binaryCmd(SINTERSTORE :: destKey.getBytes :: keys.toList.map{_.getBytes}: _*) 351 | case Sunion(keys @ _*) => binaryCmd(SUNION:: keys.toList.map{_.getBytes}: _*) 352 | case Sunionstore(destKey, keys @ _*) => binaryCmd(SUNIONSTORE:: destKey.getBytes :: keys.toList.map{_.getBytes}: _*) 353 | case Sdiff(keys @ _*) => binaryCmd(SDIFF :: keys.toList.map{_.getBytes}: _*) 354 | case Sdiffstore(destKey, keys @ _*) => binaryCmd(SDIFFSTORE :: destKey.getBytes :: keys.toList.map{_.getBytes}: _*) 355 | case Smembers(key) => copiedBuffer(SMEMBERS, SPACE, key.getBytes, EOL) 356 | case Srandmember(key) => copiedBuffer(SRANDMEMBER, SPACE, key.getBytes, EOL) 357 | case Ping() => copiedBuffer(PING, EOL) 358 | case Exists(key) => copiedBuffer(EXISTS, SPACE, key.getBytes, EOL) 359 | case Type(key) => copiedBuffer(TYPE, SPACE, key.getBytes, EOL) 360 | case Info() => copiedBuffer(INFO, EOL) 361 | case FlushAll() => copiedBuffer(FLUSHALL, EOL) 362 | } 363 | 364 | private def multiKeyCmd(cmd: BinVal, keys: Seq[String]): ChannelBuffer = { 365 | val params = new Array[BinVal](2*keys.length +2) 366 | params(0) = cmd 367 | var i=1 368 | for(k <- keys) { 369 | params(i) = SPACE ; i = i+1 370 | params(i) = k.getBytes ; i = i+1 371 | } 372 | params(params.length-1) = EOL 373 | copiedBuffer(params: _*) 374 | } 375 | 376 | private def binaryHmSetCmd(hmSet: Hmset): ChannelBuffer = { 377 | binaryCmd(HMSET :: hmSet.key.getBytes :: hmSet.kvs.toList.map{kv => List(kv._1.getBytes, kv._2)}.flatten: _*) 378 | } 379 | 380 | private def binarySetCmd(cmd: BinVal, kvs: KV*): ChannelBuffer = { 381 | binaryCmd(cmd :: kvs.toList.map{kv => List(kv._1.getBytes, kv._2)}.flatten: _*) 382 | } 383 | 384 | private def binaryCmd(cmdParts: BinVal*): ChannelBuffer = { 385 | val params = new Array[BinVal](3*cmdParts.length +1) 386 | params(0) = "*%d\r\n".format(cmdParts.length).getBytes // num binary chunks 387 | var i=1 388 | for(p <- cmdParts) { 389 | params(i) = "$%d\r\n".format(p.length).getBytes // len of the chunk 390 | i = i+1 391 | params(i) = p 392 | i = i+1 393 | params(i) = EOL 394 | i = i+1 395 | } 396 | copiedBuffer(params: _*) 397 | } 398 | } 399 | 400 | 401 | private[redis] abstract trait ChannelExceptionHandler { 402 | def handleException(ctx: ChannelHandlerContext, e: ExceptionEvent) { 403 | RedisConnection.log.error(e.getCause.getMessage, e.getCause) 404 | e.getChannel().close() // don't allow any more ops on this channel, pipeline is busted 405 | } 406 | } 407 | 408 | 409 | private[redis] object RedisResponseDecoder { 410 | val EOL_FINDER: ChannelBufferIndexFinder = new ChannelBufferIndexFinder() { 411 | override def find(buf: ChannelBuffer, pos: Int): Boolean = { 412 | buf.getByte(pos) == '\r' && (pos < buf.writerIndex-1) && buf.getByte(pos+1) == '\n' 413 | } 414 | } 415 | 416 | val ASCII = "US-ASCII" 417 | } 418 | 419 | @ChannelPipelineCoverage("one") 420 | private[redis] class RedisResponseDecoder extends FrameDecoder with ChannelExceptionHandler { 421 | import RedisResponseDecoder._ 422 | 423 | var responseType: ResponseType = Unknown 424 | 425 | override def decode(ctx: ChannelHandlerContext, ch: Channel, buf: ChannelBuffer): AnyRef = { 426 | //println("decode[%s]: %h -> %s".format(Thread.currentThread.getName, this, responseType)); 427 | responseType match { 428 | case Unknown => if (buf.readable) { 429 | responseType = ResponseType(buf.readByte) 430 | decode(ctx, ch, buf) 431 | } else { 432 | null // need more data 433 | } 434 | case BulkData => readAsciiLine(buf) match { 435 | case None => null // need more data 436 | case Some(line) => line.toInt match { 437 | case -1 => { 438 | responseType = Unknown 439 | NullData 440 | } 441 | case n => { 442 | responseType = BinaryData(n) 443 | decode(ctx, ch, buf) 444 | } 445 | } 446 | } 447 | case BinaryData(len) => { 448 | if (buf.readableBytes >= (len+2)) { // +2 for eol 449 | responseType = Unknown 450 | val data = buf.readSlice(len) 451 | buf.skipBytes(2) // eol is there too 452 | data 453 | } else { 454 | null // need more data 455 | } 456 | } 457 | case x => readAsciiLine(buf) match { 458 | case None => null // need more data 459 | case Some(line) => { 460 | responseType = Unknown 461 | (x, line) 462 | } 463 | } 464 | }} 465 | 466 | private def readAsciiLine(buf: ChannelBuffer): Option[String] = if (buf.readable) { 467 | buf.indexOf(buf.readerIndex, buf.writerIndex, EOL_FINDER) match { 468 | case -1 => None 469 | case n => try { 470 | val line = buf.toString(buf.readerIndex, (n-buf.readerIndex), ASCII) 471 | buf.skipBytes(line.length + 2) 472 | Some(line) 473 | } catch { 474 | case x: Exception => println("[%s] -> [%s]".format(buf.toString(buf.readerIndex, (buf.writerIndex-buf.readerIndex), ASCII), buf)) ; throw x; 475 | } 476 | } 477 | } else { 478 | None 479 | } 480 | 481 | override def exceptionCaught(ctx: ChannelHandlerContext, e: ExceptionEvent) { 482 | handleException(ctx: ChannelHandlerContext, e: ExceptionEvent) 483 | } 484 | } 485 | 486 | 487 | 488 | @ChannelPipelineCoverage("one") 489 | private[redis] class RedisResponseAccumulator(opQueue: RedisConnection.OpQueue) extends SimpleChannelHandler with ChannelExceptionHandler { 490 | import scala.collection.mutable.ArrayBuffer 491 | 492 | val bulkDataBuffer = ArrayBuffer[BulkDataResult]() 493 | var numDataChunks = 0 494 | 495 | override def messageReceived(ctx: ChannelHandlerContext, e: MessageEvent) { 496 | //println("accum[%s]: %h -> %s".format(Thread.currentThread.getName, this, e.getMessage)) 497 | 498 | {e.getMessage match { 499 | case (resType:ResponseType, line:String) => { 500 | clear(); 501 | resType match { 502 | case Error => Some(ErrorResult(line)) 503 | case SingleLine => Some(SingleLineResult(line)) 504 | case Integer => Some(IntegerResult(line.toInt)) 505 | case MultiBulkData => line.toInt match { 506 | case x if x <= 0 => Some(MultiBulkDataResult(Seq())) 507 | case n => numDataChunks = line.toInt ; None // ask for bulk data chunks 508 | } 509 | case _ => throw new Exception("Unexpected %s -> %s".format(resType, line)) 510 | } 511 | } 512 | case data: ChannelBuffer => handleDataChunk(Some(data)) 513 | case NullData => handleDataChunk(None) 514 | case _ => throw new Exception("Unexpected %s".format(e.getMessage)) 515 | }} match { 516 | case Some(res) => { 517 | val responseFuture = opQueue.poll(60, TimeUnit.SECONDS) 518 | responseFuture.result = res 519 | responseFuture.latch.countDown 520 | } 521 | case None => // wait for more data 522 | } 523 | } 524 | 525 | override def exceptionCaught(ctx: ChannelHandlerContext, e: ExceptionEvent) { 526 | handleException(ctx: ChannelHandlerContext, e: ExceptionEvent) 527 | } 528 | 529 | private def handleDataChunk(bulkData: Option[ChannelBuffer]): Option[Result] = { 530 | val chunk = bulkData match { 531 | case None => BulkDataResult(None) 532 | case Some(buf) => { 533 | val bytes = new BinVal(buf.readableBytes()) 534 | buf.readBytes(bytes) 535 | BulkDataResult(Some(bytes)) 536 | } 537 | } 538 | 539 | numDataChunks match { 540 | case 0 => Some(chunk) 541 | case 1 => { 542 | bulkDataBuffer += chunk 543 | val allChunks = new Array[BulkDataResult](bulkDataBuffer.length) 544 | bulkDataBuffer.copyToArray(allChunks) 545 | clear() 546 | Some(MultiBulkDataResult(allChunks)) 547 | } 548 | case _ => { 549 | bulkDataBuffer += chunk 550 | numDataChunks = numDataChunks - 1 551 | None 552 | } 553 | } 554 | } 555 | 556 | private def clear() { 557 | numDataChunks = 0 558 | bulkDataBuffer.clear 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /src/test/scala/com/fotolog/redis/RedisClientTest.scala: -------------------------------------------------------------------------------- 1 | package com.fotolog.redis 2 | 3 | import junit.framework.TestCase 4 | 5 | import org.junit._ 6 | import Assert._ 7 | import Conversions._ 8 | 9 | class RedisClientTest extends TestCase { 10 | val c = RedisClient("localhost", 6380) // non-default port, just in case as this wipes out all data 11 | 12 | override def setUp() = super.setUp; c.flushall 13 | override def tearDown() = c.flushall 14 | 15 | def testPingGetSetExistsType() { 16 | assertTrue(c.ping) 17 | assertFalse(c.exists("foo")) 18 | assertTrue(c.set("foo", "bar")) 19 | assertTrue(c.exists("foo")) 20 | assertEquals("bar", c[String]("foo").get) 21 | assertEquals(KeyType.String, c.keytype("foo")) 22 | assertTrue(c.del("foo")) 23 | assertFalse(c.exists("foo")) 24 | assertFalse(c.del("foo")) 25 | } 26 | 27 | def testMgetMset() { 28 | assertTrue(c.set("foo" -> "foo1", "bar" -> "bar1", "baz" -> "baz1")) 29 | assertEquals(Seq(Some("foo1"), None, Some("bar1"), Some("baz1")), c.get[String]("foo", "blah", "bar", "baz")) 30 | assertEquals(Map("foo" -> "foo1", "bar" -> "bar1", "baz" -> "baz1"), c.mget[String]("foo", "blah", "baz", "bar")) 31 | assertTrue(c.set("foobar" -> "foo2")) 32 | assertEquals(Some("foo2"), c.get[String]("foobar")) 33 | } 34 | 35 | def testSetNxEx() { 36 | assertTrue(c.setnx("blah", "blah")) 37 | assertFalse(c.setnx("blah", "boo")) 38 | assertTrue(c.setnx("foobar" -> "foo2")) 39 | assertTrue(c.setnx("foo" -> "foo1", "bar" -> "bar1", "baz" -> "baz1")) 40 | assertFalse(c.setnx("xxx" -> "yyy", "bar" -> "blah")) 41 | } 42 | 43 | def testGetSet() { 44 | assertEquals(None, c.getset("foo", "bar")) 45 | assertEquals(Some("bar"), c.getset[String]("foo", "baz")) 46 | assertEquals(Some("baz"), c.getset[String]("foo", "blah")) 47 | } 48 | 49 | def testSetEx() { 50 | assertTrue(c.set("foo", 123, "bar")) 51 | assertEquals(Some("bar"), c.get[String]("foo")) 52 | } 53 | 54 | def testIncrDecr() { 55 | assertEquals(1, c.incr("foo")) 56 | assertEquals(3, c.incr("foo", 2)) 57 | assertEquals(2, c.decr("foo")) 58 | assertEquals(-1, c.decr("foo", 3)) 59 | } 60 | 61 | def testAppend() { 62 | assertTrue(c.set("foo", "bar")) 63 | assertEquals(6, c.append("foo", "baz")) 64 | assertEquals(Some("barbaz"), c.get[String]("foo")) 65 | } 66 | 67 | def testSubstr() { 68 | assertEquals(None, c.substr("foo", 0, 1)) 69 | assertTrue(c.set("foo", "bar")) 70 | assertEquals(Some("ba"), c.substr[String]("foo", 0, 1)) 71 | } 72 | 73 | def testExpirePersist() { 74 | assertTrue(c.set("foo", "bar")) 75 | assertTrue(c.expire("foo", 100)) 76 | //assertTrue(c.persist("foo")) // depends on the version of redis 77 | } 78 | 79 | def testLists() { 80 | assertEquals(Seq(), c.lrange("foo", 0, 100)) 81 | assertEquals(1, c.rpush("foo", "ccc")) 82 | assertEquals(2, c.lpush("foo", "bbb")) 83 | assertEquals(3, c.rpush("foo", "ddd")) 84 | assertEquals(4, c.lpush("foo", "aaa")) 85 | assertEquals(4, c.llen("foo")) 86 | assertEquals(Seq("aaa", "bbb", "ccc", "ddd"), c.lrange[String]("foo", 0, 100)) 87 | assertEquals(Seq("bbb", "ccc"), c.lrange[String]("foo", 1, 2)) 88 | assertTrue(c.ltrim("foo", 0, 2)) 89 | assertEquals(Seq("aaa", "bbb", "ccc"), c.lrange[String]("foo", 0, 100)) 90 | assertEquals(Some("bbb"), c.lindex[String]("foo", 1)) 91 | assertEquals(None, c.lindex[String]("foo", 100)) 92 | assertTrue(c.lset("foo", 1, "BBB")) 93 | assertEquals(Seq("aaa", "BBB", "ccc"), c.lrange[String]("foo", 0, 100)) 94 | assertEquals(1, c.lrem[String]("foo", 10, "BBB")) 95 | assertEquals(Seq("aaa", "ccc"), c.lrange[String]("foo", 0, 100)) 96 | assertEquals(Some("ccc"), c.rpop[String]("foo")) 97 | assertEquals(Seq("aaa"), c.lrange[String]("foo", 0, 100)) 98 | assertEquals(2, c.rpush("foo", "bbb")) 99 | assertEquals(Some("aaa"), c.lpop[String]("foo")) 100 | assertEquals(Some("bbb"), c.lpop[String]("foo")) 101 | assertEquals(None, c.lpop[String]("foo")) 102 | assertEquals(None, c.rpop[String]("foo")) 103 | 104 | assertEquals(1, c.rpush("foo", "aaa")) 105 | assertEquals(Some("aaa"), c.rpoplpush[String]("foo", "foo1")) 106 | assertEquals(Seq("aaa"), c.lrange[String]("foo1", 0, 100)) 107 | } 108 | 109 | def testHashes() { 110 | assertEquals(Map(), c.hgetall[String]("hk")) 111 | assertTrue(c.hset("hk", "foo", "bar")) 112 | assertEquals(Some("bar"), c.hget[String]("hk", "foo")) 113 | 114 | assertTrue(c.hmset("hk", "foo" -> "bar", "bar" -> "baz", "baz" -> "blah")) 115 | assertEquals(Map("foo" -> "bar", "bar" -> "baz", "baz" -> "blah"), c.hmget[String]("hk", "foo", "bar", "baz")) 116 | 117 | assertEquals(3, c.hlen("hk")) 118 | assertEquals(Seq("foo", "bar", "baz"), c.hkeys("hk")) 119 | assertEquals(Seq("bar", "baz", "blah"), c.hvals[String]("hk")) 120 | assertEquals(Map("foo" -> "bar", "bar" -> "baz", "baz" -> "blah"), c.hgetall[String]("hk")) 121 | 122 | assertEquals(1, c.hincr("hk", "counter")) 123 | assertEquals(3, c.hincr("hk", "counter", 2)) 124 | 125 | assertTrue(c.hexists("hk", "foo")) 126 | assertTrue(c.hdel("hk", "foo")) 127 | assertFalse(c.hexists("hk", "foo")) 128 | } 129 | 130 | def testSets() { 131 | import scala.collection.{ Set => SCSet } 132 | assertTrue(c.sadd("set1", "foo")) 133 | assertFalse(c.sadd("set1", "foo")) 134 | assertTrue(c.srem("set1", "foo")) 135 | assertFalse(c.srem("set1", "foo")) 136 | 137 | assertTrue(c.sadd("set1", "foo")) 138 | assertEquals(Option("foo"), c.spop[String]("set1")) 139 | assertFalse(c.srem("set1", "foo")) 140 | 141 | 142 | assertTrue(c.sadd("set1", "foo")) 143 | assertTrue(c.smove("set1", "set2", "foo")) 144 | assertEquals(None, c.spop[String]("set1")) 145 | assertEquals(Option("foo"), c.spop[String]("set2")) 146 | 147 | assertEquals(0, c.scard("set1")) 148 | assertTrue(c.sadd("set1", "foo")) 149 | assertEquals(1, c.scard("set1")) 150 | assertTrue(c.sadd("set1", "bar")) 151 | assertEquals(2, c.scard("set1")) 152 | 153 | assertTrue(c.sismember("set1", "foo")) 154 | assertTrue(c.sismember("set1", "bar")) 155 | assertFalse(c.sismember("set1", "boo")) 156 | 157 | assertTrue(c.sadd("set1", "baz")) 158 | assertTrue(c.sadd("set2", "foo")) 159 | assertTrue(c.sadd("set2", "bar")) 160 | assertTrue(c.sadd("set2", "blah")) 161 | 162 | assertEquals(SCSet("foo", "bar", "baz"), c.smembers[String]("set1")) 163 | assertEquals(SCSet("foo", "bar", "blah"), c.smembers[String]("set2")) 164 | 165 | assertEquals(SCSet("foo", "bar"), c.sinter[String]("set1", "set2")) 166 | assertEquals(SCSet("foo", "bar", "baz", "blah"), c.sunion[String]("set1", "set2")) 167 | assertEquals(SCSet("baz"), c.sdiff[String]("set1", "set2")) 168 | assertEquals(SCSet("blah"), c.sdiff[String]("set2", "set1")) 169 | 170 | 171 | assertEquals(2, c.sinterstore("setX", "set1", "set2")) 172 | assertEquals(SCSet("foo", "bar"), c.smembers[String]("setX")) 173 | 174 | assertEquals(4, c.sunionstore("setX", "set1", "set2")) 175 | assertEquals(SCSet("foo", "bar", "baz", "blah"), c.smembers[String]("setX")) 176 | 177 | assertEquals(1, c.sdiffstore("setX", "set1", "set2")) 178 | assertEquals(SCSet("baz"), c.smembers[String]("setX")) 179 | 180 | assertTrue(SCSet("foo", "bar", "baz").contains(c.srandmember[String]("set1").get)) 181 | } 182 | 183 | 184 | def testIntConversions(){ 185 | testIntVals.foreach{ i=> 186 | assertTrue(c.set("foo", i)) 187 | assertEquals(Some(i), c.get[Int]("foo")) 188 | } 189 | testLongVals.foreach{ i=> 190 | assertTrue(c.set("foo", i)) 191 | assertEquals(Some(i), c.get[Long]("foo")) 192 | } 193 | } 194 | 195 | private def testIntVals(): List[Int] = 0 :: {for(i<-0 to 30) yield List(1<