io.chrisdavenport.rediculous.RedisTransaction.scala Maven / Gradle / Ivy
The newest version!
package io.chrisdavenport.rediculous
import cats._
import cats.implicits._
import cats.data._
import cats.effect._
import fs2.Chunk
import RedisProtocol._
import RedisCtx.syntax.all._
import scodec.bits.ByteVector
/**
* Transactions Operate via typeclasses. RedisCtx allows us to abstract our operations into
* different types depending on the behavior we want. In the case of transactions that is
* [[RedisTransaction]]. These can be composed together via its Applicative
* instance to form a transaction consisting of multiple commands, then transacted via
* either multiExec or transact on the class.
*
* In Cluster Mode the first key operation defines the node the entire Transaction will
* be sent to. Transactions are required to only operate on operations containing
* keys in the same keyslot, and users are required to hold this imperative or else
* redis will reject the transaction.
*
* @example
* {{{
* import io.chrisdavenport.rediculous._
* import cats.effect.Concurrent
* val tx = (
* RedisCommands.ping[RedisTransaction],
* RedisCommands.del[RedisTransaction](List("foo")),
* RedisCommands.get[RedisTransaction]("foo"),
* RedisCommands.set[RedisTransaction]("foo", "value"),
* RedisCommands.get[RedisTransaction]("foo")
* ).tupled
*
* def operation[F[_]: Concurrent] = tx.transact[F]
* }}}
**/
final case class RedisTransaction[A](value: RedisTransaction.RedisTxState[RedisTransaction.Queued[A]]){
def transact[F[_]: Concurrent]: Redis[F, RedisTransaction.TxResult[A]] =
RedisTransaction.multiExec[F](this)
}
object RedisTransaction {
implicit val ctx: RedisCtx[RedisTransaction] = new RedisCtx[RedisTransaction]{
def keyedBV[A: RedisResult](key: ByteVector, 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.orElse(Some(key))))
} yield Queued(l => RedisResult[A].decode(l(i)))})
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)))})
}
implicit val applicative: Applicative[RedisTransaction] = new Applicative[RedisTransaction]{
def pure[A](a: A) = RedisTransaction(Monad[RedisTxState].pure(Monad[Queued].pure(a)))
override def ap[A, B](ff: RedisTransaction[A => B])(fa: RedisTransaction[A]): RedisTransaction[B] =
RedisTransaction(RedisTxState(
Nested(ff.value.value).ap(Nested(fa.value.value)).value
))
}
/**
* A TxResult Represent the state of a RedisTransaction when run.
* Success means it completed succesfully, Aborted means we received
* a Nil Arrary from Redis which represent that at least one key being watched
* has been modified. An error occurs depending on the succesful execution of
* the function built in Queued.
*/
sealed trait TxResult[+A]
object TxResult {
final case class Success[A](value: A) extends TxResult[A]
case object Aborted extends TxResult[Nothing]
final case class Error(value: String) extends TxResult[Nothing]
}
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[State[(Int, List[NonEmptyList[ByteVector]], Option[ByteVector]), *]].pure(a))
def flatMap[A, B](fa: RedisTxState[A])(f: A => RedisTxState[B]): RedisTxState[B] = RedisTxState(
fa.value.flatMap(f.andThen(_.value))
)
}
}
final case class Queued[A](f: Chunk[Resp] => Either[Resp, A])
object Queued {
implicit val m: Monad[Queued] = new StackSafeMonad[Queued]{
def pure[A](a: A) = Queued{_ => Either.right(a)}
def flatMap[A, B](fa: Queued[A])(f: A => Queued[B]): Queued[B] = {
Queued{l =>
for {
a <- fa.f(l)
b <- f(a).f(l)
} yield b
}
}
}
}
// ----------
// Operations
// ----------
def watch[F[_]: Concurrent](keys: List[String]): Redis[F, Status] =
RedisCtx[Redis[F, *]].unkeyed(NonEmptyList("WATCH", keys))
def unwatch[F[_]: Concurrent]: Redis[F, Status] =
RedisCtx[Redis[F, *]].unkeyed(NonEmptyList.of("UNWATCH"))
def multiExec[F[_]] = new MultiExecPartiallyApplied[F]
class MultiExecPartiallyApplied[F[_]]{
def apply[A](tx: RedisTransaction[A])(implicit F: Concurrent[F]): Redis[F, TxResult[A]] = {
Redis(Kleisli{(c: RedisConnection[F]) =>
val ((_, commandsR, key), Queued(f)) = tx.value.value.run((0, List.empty, None)).value
val commands = commandsR.reverse
val all = NonEmptyList(
NonEmptyList.of(ByteVector.encodeAscii("MULTI").fold(throw _, identity(_))),
commands ++
List(NonEmptyList.of(ByteVector.encodeAscii("EXEC").fold(throw _, identity(_))))
)
RedisConnection.runRequestInternal(c)(Chunk.seq(all.toList), key)
.flatMap{_.last match {
case Some(Resp.Array(Some(a))) => f(Chunk.seq(a)).fold[TxResult[A]](e => TxResult.Error(e.toString), TxResult.Success(_)).pure[F]
case Some(Resp.Array(None)) => (TxResult.Aborted: TxResult[A]).pure[F]
case other => ApplicativeError[F, Throwable].raiseError(RedisError.Generic(s"EXEC returned $other"))
}}
})
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy