-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for Redis 5 #228
base: master
Are you sure you want to change the base?
Changes from all commits
840c3ac
f326e52
fc80454
ebd9a83
0b3a479
b9fa5d3
cc2ce81
b722f03
05a9bbc
39c012f
3a9cc3d
7715428
94669c2
1ff1a1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,5 +15,5 @@ Session.vim | |
.DS_Store | ||
*.rdb | ||
*.aof | ||
redis-3.2.0/ | ||
redis-3.2.0.tar.gz | ||
redis-4.0.14/ | ||
redis-4.0.14.tar.gz |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
#!/usr/bin/env bash | ||
set -ex | ||
wget http://download.redis.io/releases/redis-3.2.0.tar.gz | ||
tar -xzvf redis-3.2.0.tar.gz | ||
cd redis-3.2.0 && make | ||
wget http://download.redis.io/releases/redis-5.0.5.tar.gz | ||
tar -xzvf redis-5.0.5.tar.gz | ||
cd redis-5.0.5 && make |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package redis.api.bsortedsets | ||
|
||
import scala.concurrent.duration.{Duration, FiniteDuration} | ||
import redis._ | ||
import akka.util.ByteString | ||
import redis.protocol.MultiBulk | ||
|
||
case class Bzpopmax[KK: ByteStringSerializer, R: ByteStringDeserializer](keys: Seq[KK], timeout: FiniteDuration = Duration.Zero) | ||
extends Bzpopx[KK, R]("BZPOPMAX") | ||
|
||
case class Bzpopmin[KK: ByteStringSerializer, R: ByteStringDeserializer](keys: Seq[KK], timeout: FiniteDuration = Duration.Zero) | ||
extends Bzpopx[KK, R]("BZPOPMIN") | ||
|
||
private[redis] abstract class Bzpopx[KK, R](command: String)(implicit redisKeys: ByteStringSerializer[KK], deserializerR: ByteStringDeserializer[R]) | ||
extends RedisCommandMultiBulk[Option[(String, R, Double)]] { | ||
val isMasterOnly = true | ||
val keys: Seq[KK] | ||
val timeout: FiniteDuration | ||
|
||
val encodedRequest: ByteString = encode(command, keys.map(redisKeys.serialize) ++ Seq(ByteString(timeout.toSeconds.toString))) | ||
|
||
def decodeReply(mb: MultiBulk) = MultiBulkConverter.toOptionStringByteStringDouble(mb) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package redis.api.streams | ||
|
||
import akka.util.ByteString | ||
import redis._ | ||
import redis.api.{RequestStreamId, StreamEntry, StreamId, TrimStrategy} | ||
import redis.protocol.{Bulk, MultiBulk, RedisReply} | ||
|
||
case class Xadd[K, F, V](key: K, id: RequestStreamId, fields: Seq[(F, V)], trimStrategy: Option[TrimStrategy])(implicit serializerK: ByteStringSerializer[K], serializerF: ByteStringSerializer[F], serializerV: ByteStringSerializer[V]) | ||
extends SimpleClusterKey[K] with RedisCommandBulk[StreamId] { | ||
val isMasterOnly = true | ||
|
||
val encodedRequest: ByteString = { | ||
val builder = Seq.newBuilder[ByteString] | ||
|
||
builder += keyAsString | ||
|
||
if (trimStrategy.isDefined) { | ||
builder ++= trimStrategy.get.toByteString | ||
} | ||
|
||
builder += id.serialize | ||
|
||
fields.foreach { f => | ||
builder += serializerF.serialize(f._1) | ||
builder += serializerV.serialize(f._2) | ||
} | ||
|
||
encode("XADD", builder.result()) | ||
} | ||
|
||
def decodeReply(bulk: Bulk): StreamId = bulk.response.map(StreamId.deserialize).get | ||
} | ||
|
||
case class Xdel[K](key: K, ids: Seq[StreamId])(implicit serializerK: ByteStringSerializer[K]) | ||
extends SimpleClusterKey[K] with RedisCommandIntegerLong { | ||
val isMasterOnly = true | ||
val encodedRequest: ByteString = encode("XDEL", Seq(keyAsString) ++ ids.map(_.serialize)) | ||
} | ||
|
||
case class Xlen[K](key: K)(implicit redisKey: ByteStringSerializer[K]) | ||
extends SimpleClusterKey[K] with RedisCommandIntegerLong { | ||
val isMasterOnly = false | ||
val encodedRequest: ByteString = encode("XLEN", Seq(keyAsString)) | ||
} | ||
|
||
case class Xrange[K, F, V](key: K, start: RequestStreamId, end: RequestStreamId, count: Option[Long] = None)(implicit serializerK: ByteStringSerializer[K], deserializerF: ByteStringDeserializer[F], deserializerV: ByteStringDeserializer[V]) | ||
extends SimpleClusterKey[K] with RedisCommandMultiBulk[Seq[StreamEntry[F, V]]] { | ||
val isMasterOnly = false | ||
val encodedRequest: ByteString = encode("XRANGE", Xrange.buildArgs(key, start, end, count)) | ||
def decodeReply(reply: MultiBulk): Seq[StreamEntry[F, V]] = StreamEntryDecoder.toSeqEntry(reply) | ||
} | ||
|
||
private [redis] object Xrange { | ||
def buildArgs[K](key: K, id1: RequestStreamId, id2: RequestStreamId, count: Option[Long])(implicit serializerK: ByteStringSerializer[K]): Seq[ByteString] = { | ||
val builder = Seq.newBuilder[ByteString] | ||
builder += serializerK.serialize(key) | ||
builder += id1.serialize | ||
builder += id2.serialize | ||
if (count.isDefined) { | ||
builder += ByteString("COUNT") | ||
builder += ByteString(count.get.toString) | ||
} | ||
builder.result() | ||
} | ||
} | ||
|
||
case class Xrevrange[K, F, V](key: K, end: RequestStreamId, start: RequestStreamId, count: Option[Long] = None)(implicit serializerK: ByteStringSerializer[K], deserializerF: ByteStringDeserializer[F], deserializerV: ByteStringDeserializer[V]) | ||
extends SimpleClusterKey[K] with RedisCommandMultiBulk[Seq[StreamEntry[F, V]]] { | ||
val isMasterOnly = false | ||
val encodedRequest: ByteString = encode("XREVRANGE", Xrange.buildArgs(key, end, start, count)) | ||
def decodeReply(reply: MultiBulk): Seq[StreamEntry[F, V]] = StreamEntryDecoder.toSeqEntry(reply) | ||
} | ||
|
||
case class Xtrim[K](key: K, trimStrategy: TrimStrategy)(implicit serializerK: ByteStringSerializer[K]) | ||
extends SimpleClusterKey[K] with RedisCommandIntegerLong { | ||
val isMasterOnly = true | ||
val encodedRequest: ByteString = encode("XTRIM", Seq(keyAsString) ++ trimStrategy.toByteString) | ||
} | ||
|
||
case class Xread[K, F, V](streams: Seq[(K, RequestStreamId)], count: Option[Long])(implicit serializerK: ByteStringSerializer[K], deserializerK: ByteStringDeserializer[K], deserializerF: ByteStringDeserializer[F], deserializerV: ByteStringDeserializer[V]) | ||
extends RedisCommandMultiBulk[Option[Seq[(K, Seq[StreamEntry[F, V]])]]] { | ||
val keys = streams.map(_._1) | ||
val isMasterOnly = false | ||
val encodedRequest: ByteString = { | ||
val builder = Seq.newBuilder[ByteString] | ||
|
||
if (count.isDefined) { | ||
builder += ByteString("COUNT") | ||
builder += ByteString(count.get.toString) | ||
} | ||
|
||
builder += ByteString("STREAMS") | ||
builder ++= streams.map(p => serializerK.serialize(p._1)) | ||
builder ++= streams.map(p => p._2.serialize) | ||
|
||
encode("XREAD", builder.result()) | ||
} | ||
|
||
def decodeReply(r: MultiBulk): Option[Seq[(K, Seq[StreamEntry[F, V]])]] = | ||
StreamEntryDecoder.toOptionSeqStringSeqEntry(r)(deserializerK, deserializerF, deserializerV) | ||
} | ||
|
||
private [redis] object StreamEntryDecoder { | ||
def toEntry[F, V](mb: MultiBulk)(implicit deserializerF: ByteStringDeserializer[F], deserializerV: ByteStringDeserializer[V]): StreamEntry[F, V] = { | ||
val r = mb.responses.get | ||
val id = StreamId.deserialize(r(0).toByteString) | ||
val fields = r(1).asInstanceOf[MultiBulk].responses.get.map(_.toByteString).grouped(2).map(p => (deserializerF.deserialize(p(0)), deserializerV.deserialize(p(1)))).toSeq | ||
StreamEntry(id, fields) | ||
} | ||
|
||
def toSeqEntry[F, V](mb: MultiBulk)(implicit deserializerF: ByteStringDeserializer[F], deserializerV: ByteStringDeserializer[V]): Seq[StreamEntry[F, V]] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. relatively similar to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While the method signature is similar, the underlying reply from Redis is significantly different. The Stream commands use nested arrays in their replies to a much greater extent than commands for the other data structures. For example, [
[ ID1, [ FIELD1, VALUE1, FIELD2, VALUE2, ... ] ],
[ ID2, [ FIELD1, VALUE1, FIELD2, VALUE2, ... ] ],
...
] where The Because the replies are structured in such specific ways for Stream comamnds, I opted to put the decoding logic in a separate object (StreamEntryDecoder) rather than adding it to MultiBulkConverter. I opted to use unsafe operations (casting to MultiBulk, accessing elements by index) for two reasons. First, I think it's best for decoding to break immediately and loudly if we get a reply that doesn't match our understanding/implementation of the Redis spec. Second, I don't see good fallback options, aside from silently dropping parts of the response entirely. Throwing an exception seems like the best response. It seems like there is precedent here in the use of head/tail in How would you feel about introducing a new exception specifically for decode errors? This feels cleaner than emitting low-level exceptions like |
||
mb.responses.map(r => r.map(_.asInstanceOf[MultiBulk]).map(toEntry[F,V])).getOrElse(Seq()) | ||
} | ||
|
||
def toOptionSeqStringSeqEntry[K, F, V](mb: MultiBulk)(implicit deserializerK: ByteStringDeserializer[K], deserializerF: ByteStringDeserializer[F], deserializerV: ByteStringDeserializer[V]): Option[Seq[(K, Seq[StreamEntry[F, V]])]] = | ||
mb.responses.map { r => | ||
r.map(_.asInstanceOf[MultiBulk]).map(toStringSeqEntry[K,F,V]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could squeeze some perf by doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. I'll include this in my next pull request. |
||
} | ||
|
||
def toStringSeqEntry[K, F, V](mb: MultiBulk)(implicit deserializerK: ByteStringDeserializer[K], deserializerF: ByteStringDeserializer[F], deserializerV: ByteStringDeserializer[V]): (K, Seq[StreamEntry[F, V]]) = { | ||
val r = mb.responses.get | ||
val key = deserializerK.deserialize(r(0).toByteString) | ||
val entries = toSeqEntry[F,V](r(1).asInstanceOf[MultiBulk]) | ||
(key, entries) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are using "unsafe" method here and there (
.get
, r(1) (seq.apply(index))I don't know how safe it is to do it here.
Maybe you could try to compare with what we did in this file
rediscala/src/main/scala/redis/Converter.scala
Line 13 in 55573df
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please refer to the comment above for the reasoning behind usage of "unsafe" methods.