From 5929b6cefc775e1449275c0b442486e34a1cfd40 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Fri, 14 Jan 2022 15:53:16 -0800 Subject: [PATCH 1/3] Switch to fully binary command support --- .../rediculous/RedisCommands.scala | 1 + .../rediculous/RedisConnection.scala | 14 ++++----- .../chrisdavenport/rediculous/RedisCtx.scala | 29 ++++++++++++++++--- .../rediculous/RedisPipeline.scala | 9 +++--- .../rediculous/RedisPubSub.scala | 3 +- .../rediculous/RedisTransaction.scala | 18 +++++++----- .../io/chrisdavenport/rediculous/Resp.scala | 6 ++-- .../chrisdavenport/rediculous/RespRaw.scala | 13 +++++---- .../rediculous/cluster/CRC16.scala | 9 ++++++ .../rediculous/cluster/ClusterCommands.scala | 1 + .../rediculous/cluster/HashSlot.scala | 15 ++++++---- .../rediculous/cluster/HashSlotSpec.scala | 13 +++++---- 12 files changed, 87 insertions(+), 44 deletions(-) diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala index dda56cd..216ffcb 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala @@ -6,6 +6,7 @@ import cats.data.{NonEmptyList => NEL} import RedisProtocol._ import _root_.io.chrisdavenport.rediculous.implicits._ import scala.collection.immutable.Nil +import RedisCtx.syntax.all._ object RedisCommands { diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala index dbb7832..a793ea9 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala @@ -31,7 +31,7 @@ object RedisConnection{ private[rediculous] case class DirectConnection[F[_]](socket: Socket[F]) extends RedisConnection[F] - private[rediculous] case class Cluster[F[_]](queue: Queue[F, Chunk[(Either[Throwable, Resp] => F[Unit], Option[String], Option[(Host, Port)], Int, Resp)]], slots: F[ClusterSlots], usePool: (Host, Port) => Resource[F, Managed[F, Socket[F]]]) extends RedisConnection[F] + private[rediculous] case class Cluster[F[_]](queue: Queue[F, Chunk[(Either[Throwable, Resp] => F[Unit], Option[ByteVector], Option[(Host, Port)], Int, Resp)]], slots: F[ClusterSlots], usePool: (Host, Port) => Resource[F, Managed[F, Socket[F]]]) extends RedisConnection[F] // Guarantees With Socket That Each Call Receives a Response // Chunk must be non-empty but to do so incurs a penalty @@ -47,8 +47,8 @@ object RedisConnection{ } def runRequestInternal[F[_]: Concurrent](connection: RedisConnection[F])( - inputs: Chunk[NonEmptyList[String]], - key: Option[String] + inputs: Chunk[NonEmptyList[ByteVector]], + key: Option[ByteVector] ): F[Chunk[Resp]] = { val chunk = Chunk.seq(inputs.toList.map(Resp.renderRequest)) def withSocket(socket: Socket[F]): F[Chunk[Resp]] = explicitPipelineRequest[F](socket, chunk) @@ -83,10 +83,10 @@ object RedisConnection{ chunk.head.liftTo[F](RedisError.Generic("Rediculous: Impossible Return List was Empty but we guarantee output matches input")) // Can Be used to implement any low level protocols. - def runRequest[F[_]: Concurrent, A: RedisResult](connection: RedisConnection[F])(input: NonEmptyList[String], key: Option[String]): F[Either[Resp, A]] = + def runRequest[F[_]: Concurrent, A: RedisResult](connection: RedisConnection[F])(input: NonEmptyList[ByteVector], key: Option[ByteVector]): F[Either[Resp, A]] = runRequestInternal(connection)(Chunk.singleton(input), key).flatMap(head[F]).map(resp => RedisResult[A].decode(resp)) - def runRequestTotal[F[_]: Concurrent, A: RedisResult](input: NonEmptyList[String], key: Option[String]): Redis[F, A] = Redis(Kleisli{(connection: RedisConnection[F]) => + def runRequestTotal[F[_]: Concurrent, A: RedisResult](input: NonEmptyList[ByteVector], key: Option[ByteVector]): Redis[F, A] = Redis(Kleisli{(connection: RedisConnection[F]) => runRequest(connection)(input, key).flatMap{ case Right(a) => a.pure[F] case Left(e@Resp.Error(_)) => ApplicativeError[F, Throwable].raiseError[A](e) @@ -409,7 +409,7 @@ object RedisConnection{ .flatMap(s => Clock[F].realTime.map(_.toMillis).flatMap(now => refTopology.set((s,now)))) } ) - queue <- Resource.eval(Queue.bounded[F, Chunk[(Either[Throwable,Resp] => F[Unit], Option[String], Option[(Host, Port)], Int, Resp)]](maxQueued)) + queue <- Resource.eval(Queue.bounded[F, Chunk[(Either[Throwable,Resp] => F[Unit], Option[ByteVector], Option[(Host, Port)], Int, Resp)]](maxQueued)) cluster = Cluster(queue, refTopology.get.map(_._1), {case(host, port) => keypool.take((host, port)).map(_.map(_._1))}) _ <- Stream.fromQueueUnterminatedChunk(queue, chunkSizeLimit).chunks.map{chunk => @@ -450,7 +450,7 @@ object RedisConnection{ serverRedirect match { case s@Some(_) => // This is a Special One Off, Requires a Redirect // Deferred[F, Either[Throwable, Resp]].flatMap{d => // No One Cares About this Callback - val asking = ({(_: Either[Throwable, Resp]) => Applicative[F].unit}, key, s, 6, Resp.renderRequest(NonEmptyList.of("ASKING"))) // Never Repeat Asking + val asking = ({(_: Either[Throwable, Resp]) => Applicative[F].unit}, key, s, 6, Resp.renderRequest(NonEmptyList.of(ByteVector.encodeAscii("ASKING").fold(throw _, identity(_))))) // Never Repeat Asking val repeat = (toSet, key, s, retries + 1, initialCommand) val chunk = Chunk(asking, repeat) cluster.queue.tryOffer(chunk) // Offer To Have it reprocessed. diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisCtx.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisCtx.scala index 6e9f7f0..4c57fd5 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisCtx.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisCtx.scala @@ -3,6 +3,7 @@ package io.chrisdavenport.rediculous import cats.data.NonEmptyList import cats.effect.Concurrent import scala.annotation.implicitNotFound +import scodec.bits.ByteVector /** * RedisCtx is the Context in Which RedisOperations operate. @@ -14,18 +15,38 @@ If you are leveraging a custom context not provided by rediculous, please consult your library documentation. """) trait RedisCtx[F[_]]{ - def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): F[A] - def unkeyed[A: RedisResult](command: NonEmptyList[String]): F[A] + def keyedBV[A: RedisResult](key: ByteVector, command: NonEmptyList[ByteVector]): F[A] + def unkeyedBV[A: RedisResult](command: NonEmptyList[ByteVector]): F[A] } object RedisCtx { def apply[F[_]](implicit ev: RedisCtx[F]): ev.type = ev + object syntax { + object all extends StringSyntax + + trait StringSyntax { + implicit class RedisContext[F[_]](private val ctx: RedisCtx[F]){ + private def encodeUnsafe(s: String): ByteVector = ByteVector.encodeUtf8(s).fold(throw _, identity(_)) + // UTF8 String + def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): F[A] = { + val k = encodeUnsafe(key) + val c = command.map(encodeUnsafe) + ctx.keyedBV(k, c) + } + def unkeyed[A: RedisResult](command: NonEmptyList[String]): F[A] = { + val c = command.map(encodeUnsafe(_)) + ctx.unkeyedBV(c) + } + } + } + } + implicit def redis[F[_]: Concurrent]: RedisCtx[({ type M[A] = Redis[F, A] })#M] = new RedisCtx[({ type M[A] = Redis[F, A] })#M]{ - def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): Redis[F,A] = + def keyedBV[A: RedisResult](key: ByteVector, command: NonEmptyList[ByteVector]): Redis[F,A] = RedisConnection.runRequestTotal(command, Some(key)) - def unkeyed[A: RedisResult](command: NonEmptyList[String]): Redis[F, A] = + def unkeyedBV[A: RedisResult](command: NonEmptyList[ByteVector]): Redis[F, A] = RedisConnection.runRequestTotal(command, None) } } \ No newline at end of file diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisPipeline.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisPipeline.scala index 21608a7..3a7bce8 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisPipeline.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisPipeline.scala @@ -4,6 +4,7 @@ import cats._ import cats.implicits._ import cats.data._ import cats.effect._ +import scodec.bits.ByteVector /** * For When you don't trust automatic pipelining. @@ -21,15 +22,15 @@ final case class RedisPipeline[A](value: RedisTransaction.RedisTxState[RedisTran object RedisPipeline { implicit val ctx: RedisCtx[RedisPipeline] = new RedisCtx[RedisPipeline]{ - def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): RedisPipeline[A] = + def keyedBV[A: RedisResult](key: ByteVector, command: NonEmptyList[ByteVector]): RedisPipeline[A] = RedisPipeline(RedisTransaction.RedisTxState{for { - s1 <- State.get[(Int, List[NonEmptyList[String]], Option[String])] + s1 <- State.get[(Int, List[NonEmptyList[ByteVector]], Option[ByteVector])] (i, base, value) = s1 _ <- State.set((i + 1, command :: base, value.orElse(Some(key)))) } yield RedisTransaction.Queued(l => RedisResult[A].decode(l(i)))}) - def unkeyed[A: RedisResult](command: NonEmptyList[String]): RedisPipeline[A] = RedisPipeline(RedisTransaction.RedisTxState{for { - out <- State.get[(Int, List[NonEmptyList[String]], Option[String])] + def unkeyedBV[A: RedisResult](command: NonEmptyList[ByteVector]): RedisPipeline[A] = RedisPipeline(RedisTransaction.RedisTxState{for { + out <- State.get[(Int, List[NonEmptyList[ByteVector]], Option[ByteVector])] (i, base, value) = out _ <- State.set((i + 1, command :: base, value)) } yield RedisTransaction.Queued(l => RedisResult[A].decode(l(i)))}) diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisPubSub.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisPubSub.scala index bc140d1..ae9eb2f 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisPubSub.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisPubSub.scala @@ -12,6 +12,7 @@ import _root_.io.chrisdavenport.rediculous.implicits._ import org.typelevel.keypool.Reusable import scodec.bits._ import java.nio.charset.StandardCharsets +import RedisCtx.syntax.all._ /** * A RedisPubSub Represent an connection or group of connections @@ -170,7 +171,7 @@ object RedisPubSub { def nonMessages(cb: RedisPubSub.PubSubReply => F[Unit]): F[Unit] = onNonMessage.set(cb) private def encodeResp(nel: NonEmptyList[String]): F[Chunk[Byte]] = { - val resp = Resp.renderRequest(nel) + val resp = Resp.renderRequest(nel.map(ByteVector.encodeUtf8(_).fold(throw _, identity(_)))) Resp.CodecUtils.codec.encode(resp) .toEither .leftMap(err => new RuntimeException(s"Encoding Error - $err")) diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisTransaction.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisTransaction.scala index 9d6c299..a96c3d5 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisTransaction.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisTransaction.scala @@ -6,6 +6,8 @@ import cats.data._ import cats.effect._ import fs2.Chunk import RedisProtocol._ +import RedisCtx.syntax.all._ +import scodec.bits.ByteVector /** @@ -43,15 +45,15 @@ final case class RedisTransaction[A](value: RedisTransaction.RedisTxState[RedisT object RedisTransaction { implicit val ctx: RedisCtx[RedisTransaction] = new RedisCtx[RedisTransaction]{ - def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): RedisTransaction[A] = + def keyedBV[A: RedisResult](key: ByteVector, command: NonEmptyList[ByteVector]): RedisTransaction[A] = RedisTransaction(RedisTxState{for { - out <- State.get[(Int, List[NonEmptyList[String]], Option[String])] + out <- State.get[(Int, List[NonEmptyList[ByteVector]], Option[ByteVector])] (i, base, value) = out _ <- State.set((i + 1, command :: base, value.orElse(Some(key)))) } yield Queued(l => RedisResult[A].decode(l(i)))}) - def unkeyed[A: RedisResult](command: NonEmptyList[String]): RedisTransaction[A] = RedisTransaction(RedisTxState{for { - out <- State.get[(Int, List[NonEmptyList[String]], Option[String])] + def unkeyedBV[A: RedisResult](command: NonEmptyList[ByteVector]): RedisTransaction[A] = RedisTransaction(RedisTxState{for { + out <- State.get[(Int, List[NonEmptyList[ByteVector]], Option[ByteVector])] (i, base, value) = out _ <- State.set((i + 1, command :: base, value)) } yield Queued(l => RedisResult[A].decode(l(i)))}) @@ -79,11 +81,11 @@ object RedisTransaction { final case class Error(value: String) extends TxResult[Nothing] } - final case class RedisTxState[A](value: State[(Int, List[NonEmptyList[String]], Option[String]), A]) + final case class RedisTxState[A](value: State[(Int, List[NonEmptyList[ByteVector]], Option[ByteVector]), A]) object RedisTxState { implicit val m: Monad[RedisTxState] = new StackSafeMonad[RedisTxState]{ - def pure[A](a: A): RedisTxState[A] = RedisTxState(Monad[({ type F[A] = State[(Int, List[NonEmptyList[String]], Option[String]), A]})#F].pure(a)) + def pure[A](a: A): RedisTxState[A] = RedisTxState(Monad[({ type F[A] = State[(Int, List[NonEmptyList[ByteVector]], Option[ByteVector]), A]})#F].pure(a)) def flatMap[A, B](fa: RedisTxState[A])(f: A => RedisTxState[B]): RedisTxState[B] = RedisTxState( fa.value.flatMap(f.andThen(_.value)) ) @@ -123,9 +125,9 @@ object RedisTransaction { val ((_, commandsR, key), Queued(f)) = tx.value.value.run((0, List.empty, None)).value val commands = commandsR.reverse val all = NonEmptyList( - NonEmptyList.of("MULTI"), + NonEmptyList.of(ByteVector.encodeAscii("MULTI").fold(throw _, identity(_))), commands ++ - List(NonEmptyList.of("EXEC")) + List(NonEmptyList.of(ByteVector.encodeAscii("EXEC").fold(throw _, identity(_)))) ) RedisConnection.runRequestInternal(c)(Chunk.seq(all.toList), key) .flatMap{_.last match { diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/Resp.scala b/core/src/main/scala/io/chrisdavenport/rediculous/Resp.scala index 8216e09..dbbc30b 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/Resp.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/Resp.scala @@ -41,14 +41,14 @@ object Resp { // First Byte is * case class Array(a: Option[List[Resp]]) extends Resp - def renderRequest(nel: NonEmptyList[String]): Resp = { + def renderRequest(nel: NonEmptyList[ByteVector]): Resp = { Resp.Array(Some( nel.toList.map(renderArg) )) } - def renderArg(arg: String): Resp = { - Resp.BulkString(Some(ByteVector.encodeString(arg)(StandardCharsets.UTF_8).fold(throw _ , identity))) + def renderArg(arg: ByteVector): Resp = { + Resp.BulkString(Some(arg)) } def toStringProtocol(resp: Resp)(implicit C: Charset = StandardCharsets.UTF_8) = { diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RespRaw.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RespRaw.scala index a898b5c..915b3a8 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RespRaw.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RespRaw.scala @@ -3,6 +3,7 @@ package io.chrisdavenport.rediculous import fs2.Chunk import cats.data.NonEmptyList import cats.Applicative +import scodec.bits.ByteVector object RespRaw { @@ -10,14 +11,14 @@ object RespRaw { object Commands { - final case class SingleCommand[A](key: Option[String], command: NonEmptyList[String]) extends Commands[A] + final case class SingleCommand[A](key: Option[ByteVector], command: NonEmptyList[ByteVector]) extends Commands[A] final case class CompositeCommands[A](commands: Chunk[SingleCommand[_]]) extends Commands[A] implicit val rawRespCommandsCtx: RedisCtx[Commands] = new RedisCtx[Commands] { - def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): Commands[A] = + def keyedBV[A: RedisResult](key: ByteVector, command: NonEmptyList[ByteVector]): Commands[A] = SingleCommand(Some(key), command) - def unkeyed[A: RedisResult](command: NonEmptyList[String]): Commands[A] = + def unkeyedBV[A: RedisResult](command: NonEmptyList[ByteVector]): Commands[A] = SingleCommand(None, command) } def combine[C](c1: Commands[_], c2: Commands[_]): Commands[C] = (c1, c2) match { @@ -36,7 +37,7 @@ object RespRaw { } - final case class RawPipeline[A](key: Option[String], commands: Chunk[NonEmptyList[String]]){ + final case class RawPipeline[A](key: Option[ByteVector], commands: Chunk[NonEmptyList[ByteVector]]){ final def pipeline[F[_]](c: RedisConnection[F])(implicit F: cats.effect.Concurrent[F]): F[Chunk[Resp]] = RedisConnection.runRequestInternal(c)(commands, key) @@ -46,10 +47,10 @@ object RespRaw { implicit val ctx: RedisCtx[RawPipeline] = { new RedisCtx[RawPipeline] { - def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): RawPipeline[A] = + def keyedBV[A: RedisResult](key: ByteVector, command: NonEmptyList[ByteVector]): RawPipeline[A] = RawPipeline(Some(key), Chunk.singleton(command)) - def unkeyed[A: RedisResult](command: NonEmptyList[String]): RawPipeline[A] = + def unkeyedBV[A: RedisResult](command: NonEmptyList[ByteVector]): RawPipeline[A] = RawPipeline(None, Chunk.singleton(command)) } } diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/cluster/CRC16.scala b/core/src/main/scala/io/chrisdavenport/rediculous/cluster/CRC16.scala index a971770..31d7a38 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/cluster/CRC16.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/cluster/CRC16.scala @@ -2,6 +2,7 @@ package io.chrisdavenport.rediculous.cluster import java.nio.charset.Charset import java.nio.charset.StandardCharsets +import scodec.bits.ByteVector /** * XMODEM CRC 16 CRC16 - 16-bit Cyclic Redundancy Check (CRC16) @@ -26,6 +27,14 @@ object CRC16 { crc & 0xFFFF } + def bytevector(bv: ByteVector): Int = { + var crc: Int = 0 + bv.foreach{ b => + crc = (crc << 8) ^ table(((crc >>> 8) ^ (b & 0xff)) & 0xff) + } + crc & 0xFFFF + } + private[CRC16] lazy val table : Array[Int] = Array( 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/cluster/ClusterCommands.scala b/core/src/main/scala/io/chrisdavenport/rediculous/cluster/ClusterCommands.scala index fb9abdb..6140681 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/cluster/ClusterCommands.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/cluster/ClusterCommands.scala @@ -6,6 +6,7 @@ import cats.data.NonEmptyList import cats.effect._ import com.comcast.ip4s._ import scodec.bits.ByteVector +import RedisCtx.syntax.all._ object ClusterCommands { diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/cluster/HashSlot.scala b/core/src/main/scala/io/chrisdavenport/rediculous/cluster/HashSlot.scala index 86c176a..8a85da5 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/cluster/HashSlot.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/cluster/HashSlot.scala @@ -2,6 +2,7 @@ package io.chrisdavenport.rediculous.cluster import java.nio.charset.Charset import java.nio.charset.StandardCharsets +import scodec.bits.ByteVector /** * HashSlots are values 0-16384, They are the result of parsing keys, and then @@ -22,16 +23,18 @@ import java.nio.charset.StandardCharsets */ object HashSlot { - def find(key: String)(implicit C: Charset = StandardCharsets.UTF_8): Int = { + def find(key: ByteVector)(implicit C: Charset = StandardCharsets.UTF_8): Int = { val toHash = hashKey(key) - CRC16.string(toHash) % 16384 + CRC16.bytevector(toHash) % 16384 } - def hashKey(key: String): String = { - val s = key.indexOf('{') + + + def hashKey(key: ByteVector): ByteVector = { + val s = key.indexOfSlice(ByteVector('{')) if (s >= 0) { - val e = key.indexOf('}') - if (e >= 0 && e != s + 1) key.substring(s + 1, e) + val e = key.indexOfSlice(ByteVector('}')) + if (e >= 0 && e != s + 1) key.slice(s + 1, e) else key } else key } diff --git a/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala b/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala index 1030d95..29b87b9 100644 --- a/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala +++ b/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala @@ -1,27 +1,30 @@ package io.chrisdavenport.rediculous.cluster import cats.syntax.all._ +import scodec.bits.ByteVector class HashSlotSpec extends munit.FunSuite { + + def toBV(bv: String): ByteVector = ByteVector.encodeUtf8(bv).fold(throw _, identity(_)) test("HashSlot.hashKey Find the right key section for a keyslot"){ val input = "{user.name}.foo" - assert(HashSlot.hashKey(input) === "user.name") + assert(HashSlot.hashKey(toBV(input)) === toBV("user.name")) } test("HashSlot.hashKey Find the right key in middle of key") { val input = "bar{foo}baz" - assert(HashSlot.hashKey(input) === "foo") + assert(HashSlot.hashKey(toBV(input)) === toBV("foo")) } test("HashSlot.hashKey Find the right key at end of key"){ val input = "barbaz{foo}" - assert(HashSlot.hashKey(input) === "foo") + assert(HashSlot.hashKey(toBV(input)) === toBV("foo")) } test("HashSlot.hashKey output original key if braces are directly next to each other"){ val input = "{}.bar" - assert(HashSlot.hashKey(input) === input) + assert(HashSlot.hashKey(toBV(input)) === toBV(input)) } test("HashSlot.hashKey output the full value if no keyslot present") { val input = "bazbarfoo" - assert(HashSlot.hashKey(input) === input) + assert(HashSlot.hashKey(toBV(input)) === toBV(input)) } /** From c0c8a6c5918c168dd799246aa0ea62c8db1be5b4 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Fri, 14 Jan 2022 16:16:34 -0800 Subject: [PATCH 2/3] Show Binary Command Utilization --- .../chrisdavenport/rediculous/RedisCommands.scala | 15 +++++++++++++++ .../chrisdavenport/rediculous/RedisResult.scala | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala index 216ffcb..bc7e207 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisCommands.scala @@ -7,6 +7,7 @@ import RedisProtocol._ import _root_.io.chrisdavenport.rediculous.implicits._ import scala.collection.immutable.Nil import RedisCtx.syntax.all._ +import scodec.bits.ByteVector object RedisCommands { @@ -142,6 +143,16 @@ object RedisCommands { RedisCtx[F].keyed(key, NEL("SET", key.encode :: value.encode :: ex ::: px ::: condition ::: keepTTL)) } + + + def setBV[F[_]: RedisCtx](key: ByteVector, value: ByteVector, setOpts: SetOpts = SetOpts.default): F[Option[Status]] = { + val ex = setOpts.setSeconds.toList.flatMap(l => List("EX", l.encode)).map(toBV) + val px = setOpts.setMilliseconds.toList.flatMap(l => List("PX", l.encode)).map(toBV) + val condition = setOpts.setCondition.toList.map(_.encode).map(toBV) + val keepTTL = Alternative[List].guard(setOpts.keepTTL).as("KEEPTTL").map(toBV) + RedisCtx[F].keyedBV(key, NEL(toBV("SET"), key :: value :: ex ::: px ::: condition ::: keepTTL)) + } + final case class ZAddOpts( condition: Option[Condition], change: Boolean, @@ -551,6 +562,9 @@ object RedisCommands { def get[F[_]: RedisCtx](key: String): F[Option[String]] = RedisCtx[F].keyed(key, NEL.of("GET", key.encode)) + def getBV[F[_]: RedisCtx](key: ByteVector): F[Option[ByteVector]] = + RedisCtx[F].keyedBV(key, NEL.of(toBV("GET"), key)) + def getrange[F[_]: RedisCtx](key: String, start: Long, end: Long): F[String] = RedisCtx[F].keyed(key, NEL.of("GETRANGE", key.encode, start.encode, end.encode)) @@ -739,4 +753,5 @@ object RedisCommands { def publish[F[_]: RedisCtx](channel: String, message: String): F[Int] = RedisCtx[F].unkeyed[Int](cats.data.NonEmptyList.of("PUBLISH", channel, message)) + private def toBV(s: String): ByteVector = ByteVector.encodeUtf8(s).fold(throw _, identity(_)) } diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala index 994a303..61cdd8a 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala @@ -2,6 +2,7 @@ package io.chrisdavenport.rediculous import cats._ import cats.implicits._ +import scodec.bits.ByteVector trait RedisResult[+A]{ def decode(resp: Resp): Either[Resp, A] @@ -27,6 +28,13 @@ object RedisResult extends RedisResultLowPriority{ } } + implicit val bytevector: RedisResult[ByteVector] = new RedisResult[ByteVector] { + def decode(resp: Resp): Either[Resp,ByteVector] = resp match { + case Resp.BulkString(Some(value)) => value.asRight + case otherwise => otherwise.asLeft + } + } + implicit def option[A: RedisResult]: RedisResult[Option[A]] = new RedisResult[Option[A]] { def decode(resp: Resp): Either[Resp,Option[A]] = resp match { case Resp.BulkString(None) => None.asRight From 0d4512b1796b7aa8caf55ed35617a1291131239d Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Thu, 10 Mar 2022 08:43:50 -0800 Subject: [PATCH 3/3] Fix Style --- .../rediculous/RedisConnection.scala | 2 +- .../rediculous/cluster/HashSlotSpec.scala | 41 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala index a793ea9..42ff640 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala @@ -66,7 +66,7 @@ object RedisConnection{ val x: F[Chunk[Either[Throwable, Resp]]] = c.traverse{ case (d, _) => d.get } val y: F[Chunk[Resp]] = x.flatMap(_.sequence.liftTo[F]) y - } + } } case Cluster(queue, _, _) => chunk.traverse(resp => Deferred[F, Either[Throwable, Resp]].map(d => (d, ({(e: Either[Throwable, Resp]) => d.complete(e).void}, key, None, 0, resp)))).flatMap{ c => queue.offer(c.map(_._2)) >> { diff --git a/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala b/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala index 29b87b9..7807773 100644 --- a/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala +++ b/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala @@ -6,26 +6,27 @@ import scodec.bits.ByteVector class HashSlotSpec extends munit.FunSuite { def toBV(bv: String): ByteVector = ByteVector.encodeUtf8(bv).fold(throw _, identity(_)) - test("HashSlot.hashKey Find the right key section for a keyslot"){ - val input = "{user.name}.foo" - assert(HashSlot.hashKey(toBV(input)) === toBV("user.name")) - } - test("HashSlot.hashKey Find the right key in middle of key") { - val input = "bar{foo}baz" - assert(HashSlot.hashKey(toBV(input)) === toBV("foo")) - } - test("HashSlot.hashKey Find the right key at end of key"){ - val input = "barbaz{foo}" - assert(HashSlot.hashKey(toBV(input)) === toBV("foo")) - } - test("HashSlot.hashKey output original key if braces are directly next to each other"){ - val input = "{}.bar" - assert(HashSlot.hashKey(toBV(input)) === toBV(input)) - } - test("HashSlot.hashKey output the full value if no keyslot present") { - val input = "bazbarfoo" - assert(HashSlot.hashKey(toBV(input)) === toBV(input)) - } + + test("HashSlot.hashKey Find the right key section for a keyslot"){ + val input = "{user.name}.foo" + assert(HashSlot.hashKey(toBV(input)) === toBV("user.name")) + } + test("HashSlot.hashKey Find the right key in middle of key") { + val input = "bar{foo}baz" + assert(HashSlot.hashKey(toBV(input)) === toBV("foo")) + } + test("HashSlot.hashKey Find the right key at end of key"){ + val input = "barbaz{foo}" + assert(HashSlot.hashKey(toBV(input)) === toBV("foo")) + } + test("HashSlot.hashKey output original key if braces are directly next to each other"){ + val input = "{}.bar" + assert(HashSlot.hashKey(toBV(input)) === toBV(input)) + } + test("HashSlot.hashKey output the full value if no keyslot present") { + val input = "bazbarfoo" + assert(HashSlot.hashKey(toBV(input)) === toBV(input)) + } /** import cats.implicits._