diff --git a/README.md b/README.md index 30fb2c8..5e12097 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ Backend | Client | Aerospike | [aerospike-client-java](https://github.com/aerospike/aerospike-client-java) Redis | [jedis](https://github.com/redis/jedis)
[lettuce](https://github.com/lettuce-io/lettuce-core)
[redisson](https://github.com/redisson/redisson) +**RateLimiter** + Class | Effect | ------------ | ------------- `TryRateLimiter` | `scala.util.Try` @@ -57,6 +59,30 @@ Class | Effect | `RedissonZioRateLimiter` | `zio.Task` `RedissonZioAsyncRateLimiter` | `zio.Task` +**ConcurrentRateLimiter** + +Class | Effect | +------------ | ------------- +`TryConcurrentRateLimiter` | `scala.util.Try` +`EitherConcurrentRateLimiter` | `Either` +`JedisSyncConcurrentRateLimiter` | None (`Identity`) +`JedisCatsConcurrentRateLimiter` | `F[_]: cats.effect.Sync: cats.effect.ContextShift` +`JedisZioConcurrentRateLimiter` | `zio.Task` +`LettuceSyncConcurrentRateLimiter` | None (`Identity`) +`LettuceAsyncConcurrentRateLimiter` | `scala.concurrent.Future` +`LettuceCatsConcurrentRateLimiter` | `F[_]: cats.effect.Sync: cats.effect.ContextShift` +`LettuceCatsAsyncConcurrentRateLimiter` | `F[_]: cats.effect.Concurrent` +`LettuceMonixAsyncConcurrentRateLimiter` | `monix.eval.Task` +`LettuceZioConcurrentRateLimiter` | `zio.Task` +`LettuceZioAsyncConcurrentRateLimiter` | `zio.Task` +`RedissonSyncConcurrentRateLimiter` | None (`Identity`) +`RedissonAsyncConcurrentRateLimiter` | `scala.concurrent.Future` +`RedissonCatsConcurrentRateLimiter` | `F[_]: cats.effect.Sync: cats.effect.ContextShift` +`RedissonCatsAsyncConcurrentRateLimiter` | `F[_]: cats.effect.Concurrent` +`RedissonMonixAsyncConcurrentRateLimiter` | `monix.eval.Task` +`RedissonZioConcurrentRateLimiter` | `zio.Task` +`RedissonZioAsyncConcurrentRateLimiter` | `zio.Task` + ## Usage ```scala import genkai._ diff --git a/modules/aerospike/src/main/scala/genkai/aerospike/AerospikeRateLimiter.scala b/modules/aerospike/src/main/scala/genkai/aerospike/AerospikeRateLimiter.scala index c1a8c90..04661a1 100644 --- a/modules/aerospike/src/main/scala/genkai/aerospike/AerospikeRateLimiter.scala +++ b/modules/aerospike/src/main/scala/genkai/aerospike/AerospikeRateLimiter.scala @@ -57,5 +57,5 @@ abstract class AerospikeRateLimiter[F[_]]( override def close(): F[Unit] = monad.whenA(closeClient)(monad.eval(client.close())) - override protected def monadError: MonadError[F] = monad + override def monadError: MonadError[F] = monad } diff --git a/modules/aerospike/src/test/scala/genkai/aerospike/AerospikeSpecForAll.scala b/modules/aerospike/src/test/scala/genkai/aerospike/AerospikeSpecForAll.scala index c485e08..b91210d 100644 --- a/modules/aerospike/src/test/scala/genkai/aerospike/AerospikeSpecForAll.scala +++ b/modules/aerospike/src/test/scala/genkai/aerospike/AerospikeSpecForAll.scala @@ -2,9 +2,9 @@ package genkai.aerospike import com.aerospike.client.AerospikeClient import com.dimafeng.testcontainers.scalatest.TestContainerForAll -import genkai.BaseSpec +import genkai.RateLimiterBaseSpec -trait AerospikeSpecForAll[F[_]] extends BaseSpec[F] with TestContainerForAll { +trait AerospikeSpecForAll[F[_]] extends RateLimiterBaseSpec[F] with TestContainerForAll { override val containerDef: AerospikeContainer.Def = AerospikeContainer.Def() var aerospikeClient: AerospikeClient = _ diff --git a/modules/core/src/main/scala/genkai/ConcurrentRateLimiter.scala b/modules/core/src/main/scala/genkai/ConcurrentRateLimiter.scala new file mode 100644 index 0000000..16e7564 --- /dev/null +++ b/modules/core/src/main/scala/genkai/ConcurrentRateLimiter.scala @@ -0,0 +1,40 @@ +package genkai + +import java.time.Instant + +/** + * @tparam F - effect type + */ +trait ConcurrentRateLimiter[F[_]] extends MonadErrorAware[F] { + + final def use[A: Key, B](key: A)(f: => F[B]): F[Either[ConcurrentLimitExhausted[A], B]] = + use(key, Instant.now())(f) + + private[genkai] def use[A: Key, B](key: A, instant: Instant)( + f: => F[B] + ): F[Either[ConcurrentLimitExhausted[A], B]] + + def reset[A: Key](key: A): F[Unit] + + final def acquire[A: Key](key: A): F[Boolean] = + acquire(key, Instant.now()) + + private[genkai] def acquire[A: Key]( + key: A, + instant: Instant + ): F[Boolean] + + final def release[A: Key](key: A): F[Boolean] = + release(key, Instant.now()) + + private[genkai] def release[A: Key]( + key: A, + instant: Instant + ): F[Boolean] + + final def permissions[A: Key](key: A): F[Long] = permissions(key, Instant.now()) + + private[genkai] def permissions[A: Key](key: A, instant: Instant): F[Long] + + def close(): F[Unit] +} diff --git a/modules/core/src/main/scala/genkai/ConcurrentRateLimiterError.scala b/modules/core/src/main/scala/genkai/ConcurrentRateLimiterError.scala new file mode 100644 index 0000000..0bb6d0d --- /dev/null +++ b/modules/core/src/main/scala/genkai/ConcurrentRateLimiterError.scala @@ -0,0 +1,10 @@ +package genkai + +sealed abstract class ConcurrentRateLimiterError(msg: String, cause: Throwable) + extends RuntimeException(msg, cause) + +final case class ConcurrentLimitExhausted[A: Key](key: A) + extends ConcurrentRateLimiterError(s"No available slots for key: ${Key[A].convert(key)}", null) + +final case class ConcurrentRateLimiterClientError(cause: Throwable) + extends ConcurrentRateLimiterError(cause.getLocalizedMessage, cause) diff --git a/modules/core/src/main/scala/genkai/ConcurrentStrategy.scala b/modules/core/src/main/scala/genkai/ConcurrentStrategy.scala new file mode 100644 index 0000000..4fc88d9 --- /dev/null +++ b/modules/core/src/main/scala/genkai/ConcurrentStrategy.scala @@ -0,0 +1,14 @@ +package genkai + +import scala.concurrent.duration.Duration + +sealed trait ConcurrentStrategy + +object ConcurrentStrategy { + + /** + * @param slots - available slots for concurrent requests + * @param ttl - default ttl for automatic slot acquisition cleanup if manual cleanup did not succeed + */ + final case class Default(slots: Long, ttl: Duration) extends ConcurrentStrategy +} diff --git a/modules/core/src/main/scala/genkai/EitherConcurrentRateLimiter.scala b/modules/core/src/main/scala/genkai/EitherConcurrentRateLimiter.scala new file mode 100644 index 0000000..862975c --- /dev/null +++ b/modules/core/src/main/scala/genkai/EitherConcurrentRateLimiter.scala @@ -0,0 +1,48 @@ +package genkai + +import java.time.Instant +import genkai.monad.{EitherMonadError, MonadError} + +class EitherConcurrentRateLimiter(concurrentRateLimiter: ConcurrentRateLimiter[Identity]) + extends ConcurrentRateLimiter[Either[Throwable, *]] { + type ResultRight[A, B] = Either[ConcurrentLimitExhausted[A], B] + type Result[A, B] = Either[Throwable, ResultRight[A, B]] + + override private[genkai] def use[A: Key, B](key: A, instant: Instant)( + f: => Either[Throwable, B] + ): Result[A, B] = + monadError.eval(concurrentRateLimiter.use(key, instant)(f)).flatMap { + case Left(value) => + Right[Throwable, ResultRight[A, B]](Left[ConcurrentLimitExhausted[A], B](value)) + case Right(value) => + value match { + case Left(value) => Left(value) + case Right(value) => Right(Right(value)) + } + } + + override def reset[A: Key](key: A): Either[Throwable, Unit] = + monadError.eval(concurrentRateLimiter.reset(key)) + + override private[genkai] def acquire[A: Key]( + key: A, + instant: Instant + ): Either[Throwable, Boolean] = + monadError.eval(concurrentRateLimiter.acquire(key, instant)) + + override private[genkai] def release[A: Key]( + key: A, + instant: Instant + ): Either[Throwable, Boolean] = + monadError.eval(concurrentRateLimiter.release(key, instant)) + + override private[genkai] def permissions[A: Key]( + key: A, + instant: Instant + ): Either[Throwable, Long] = + monadError.eval(concurrentRateLimiter.permissions(key, instant)) + + override def close(): Either[Throwable, Unit] = monadError.eval(concurrentRateLimiter.close()) + + override def monadError: MonadError[Either[Throwable, *]] = EitherMonadError +} diff --git a/modules/core/src/main/scala/genkai/EitherRateLimiter.scala b/modules/core/src/main/scala/genkai/EitherRateLimiter.scala index 52c69d1..2329c41 100644 --- a/modules/core/src/main/scala/genkai/EitherRateLimiter.scala +++ b/modules/core/src/main/scala/genkai/EitherRateLimiter.scala @@ -23,5 +23,5 @@ final class EitherRateLimiter(rateLimiter: RateLimiter[Identity]) override def close(): Either[Throwable, Unit] = monadError.eval(rateLimiter.close()) - override protected def monadError: MonadError[Either[Throwable, *]] = EitherMonadError + override def monadError: MonadError[Either[Throwable, *]] = EitherMonadError } diff --git a/modules/core/src/main/scala/genkai/Logging.scala b/modules/core/src/main/scala/genkai/Logging.scala index 02a7ca7..9c74df3 100644 --- a/modules/core/src/main/scala/genkai/Logging.scala +++ b/modules/core/src/main/scala/genkai/Logging.scala @@ -2,7 +2,7 @@ package genkai import org.slf4j.{Logger, LoggerFactory} -trait Logging[F[_]] { self: RateLimiter[F] => +trait Logging[F[_]] { self: MonadErrorAware[F] => protected val logger: Logger = LoggerFactory.getLogger(self.getClass) def trace(msg: String): F[Unit] = diff --git a/modules/core/src/main/scala/genkai/MonadErrorAware.scala b/modules/core/src/main/scala/genkai/MonadErrorAware.scala new file mode 100644 index 0000000..9c69707 --- /dev/null +++ b/modules/core/src/main/scala/genkai/MonadErrorAware.scala @@ -0,0 +1,7 @@ +package genkai + +import genkai.monad.MonadError + +trait MonadErrorAware[F[_]] { + def monadError: MonadError[F] +} diff --git a/modules/core/src/main/scala/genkai/RateLimiter.scala b/modules/core/src/main/scala/genkai/RateLimiter.scala index ec057a0..adc735c 100644 --- a/modules/core/src/main/scala/genkai/RateLimiter.scala +++ b/modules/core/src/main/scala/genkai/RateLimiter.scala @@ -2,12 +2,10 @@ package genkai import java.time.Instant -import genkai.monad.MonadError - /** * @tparam F - effect type */ -trait RateLimiter[F[_]] { +trait RateLimiter[F[_]] extends MonadErrorAware[F] { /** * @param key - ~ object id @@ -76,6 +74,4 @@ trait RateLimiter[F[_]] { * @return - unit if successfully closed or error wrapped in effect */ def close(): F[Unit] - - protected def monadError: MonadError[F] } diff --git a/modules/core/src/main/scala/genkai/RateLimiterError.scala b/modules/core/src/main/scala/genkai/RateLimiterError.scala index d5f9fed..9812b69 100644 --- a/modules/core/src/main/scala/genkai/RateLimiterError.scala +++ b/modules/core/src/main/scala/genkai/RateLimiterError.scala @@ -3,5 +3,5 @@ package genkai sealed abstract class RateLimiterError(msg: String, cause: Throwable) extends RuntimeException(msg, cause) -final case class ClientError(cause: Throwable) +final case class RateLimiterClientError(cause: Throwable) extends RateLimiterError(cause.getLocalizedMessage, cause) diff --git a/modules/core/src/main/scala/genkai/TryConcurrentRateLimiter.scala b/modules/core/src/main/scala/genkai/TryConcurrentRateLimiter.scala new file mode 100644 index 0000000..468c5e4 --- /dev/null +++ b/modules/core/src/main/scala/genkai/TryConcurrentRateLimiter.scala @@ -0,0 +1,43 @@ +package genkai + +import java.time.Instant + +import genkai.monad.{MonadError, TryMonadError} + +import scala.util.{Failure, Success, Try} + +final class TryConcurrentRateLimiter(concurrentRateLimiter: ConcurrentRateLimiter[Identity]) + extends ConcurrentRateLimiter[Try] { + override private[genkai] def use[A: Key, B](key: A, instant: Instant)( + f: => Try[B] + ): Try[Either[ConcurrentLimitExhausted[A], B]] = + monadError.eval(concurrentRateLimiter.use(key, instant)(f)).flatMap { + case Left(value) => Success(Left(value)) + case Right(value) => + value match { + case Failure(exception) => Failure(exception) + case Success(value) => Success(Right(value)) + } + } + + override private[genkai] def acquire[A: Key]( + key: A, + instant: Instant + ): Try[Boolean] = + monadError.eval(concurrentRateLimiter.acquire(key, instant)) + + override def reset[A: Key](key: A): Try[Unit] = monadError.eval(concurrentRateLimiter.reset(key)) + + override private[genkai] def release[A: Key]( + key: A, + instant: Instant + ): Try[Boolean] = + monadError.eval(concurrentRateLimiter.release(key, instant)) + + override private[genkai] def permissions[A: Key](key: A, instant: Instant): Try[Long] = + monadError.eval(concurrentRateLimiter.permissions(key, instant)) + + override def close(): Try[Unit] = monadError.eval(concurrentRateLimiter.close()) + + override def monadError: MonadError[Try] = TryMonadError +} diff --git a/modules/core/src/main/scala/genkai/TryRateLimiter.scala b/modules/core/src/main/scala/genkai/TryRateLimiter.scala index 003e177..47975b0 100644 --- a/modules/core/src/main/scala/genkai/TryRateLimiter.scala +++ b/modules/core/src/main/scala/genkai/TryRateLimiter.scala @@ -19,5 +19,5 @@ final class TryRateLimiter( override def close(): Try[Unit] = monadError.eval(rateLimiter.close()) - override protected def monadError: MonadError[Try] = TryMonadError + override def monadError: MonadError[Try] = TryMonadError } diff --git a/modules/core/src/main/scala/genkai/monad/EitherMonadError.scala b/modules/core/src/main/scala/genkai/monad/EitherMonadError.scala index da23468..59ed201 100644 --- a/modules/core/src/main/scala/genkai/monad/EitherMonadError.scala +++ b/modules/core/src/main/scala/genkai/monad/EitherMonadError.scala @@ -49,7 +49,7 @@ object EitherMonadError extends MonadError[Either[Throwable, *]] { case _ => fa } - override def ifA[A]( + override def ifM[A]( fcond: Either[Throwable, Boolean] )(ifTrue: => Either[Throwable, A], ifFalse: => Either[Throwable, A]): Either[Throwable, A] = fcond.flatMap { flag => @@ -66,7 +66,7 @@ object EitherMonadError extends MonadError[Either[Throwable, *]] { override def eval[A](f: => A): Either[Throwable, A] = Try(f).toEither override def guarantee[A]( - f: Either[Throwable, A] + f: => Either[Throwable, A] )(g: => Either[Throwable, Unit]): Either[Throwable, A] = { def tryE = Try(g) match { case Failure(exception) => Left(exception) diff --git a/modules/core/src/main/scala/genkai/monad/FutureMonadAsyncError.scala b/modules/core/src/main/scala/genkai/monad/FutureMonadAsyncError.scala index 4bb49c7..b06d421 100644 --- a/modules/core/src/main/scala/genkai/monad/FutureMonadAsyncError.scala +++ b/modules/core/src/main/scala/genkai/monad/FutureMonadAsyncError.scala @@ -34,7 +34,7 @@ class FutureMonadAsyncError(implicit ec: ExecutionContext) extends MonadAsyncErr ): Future[A] = fa.recoverWith(pf) - override def ifA[A]( + override def ifM[A]( fcond: Future[Boolean] )(ifTrue: => Future[A], ifFalse: => Future[A]): Future[A] = fcond.flatMap { flag => @@ -73,7 +73,7 @@ class FutureMonadAsyncError(implicit ec: ExecutionContext) extends MonadAsyncErr p.future } - override def guarantee[A](f: Future[A])(g: => Future[Unit]): Future[A] = { + override def guarantee[A](f: => Future[A])(g: => Future[Unit]): Future[A] = { val p = Promise[A]() def tryF = Try(g) match { diff --git a/modules/core/src/main/scala/genkai/monad/IdMonadError.scala b/modules/core/src/main/scala/genkai/monad/IdMonadError.scala index c282b45..fc599f4 100644 --- a/modules/core/src/main/scala/genkai/monad/IdMonadError.scala +++ b/modules/core/src/main/scala/genkai/monad/IdMonadError.scala @@ -29,7 +29,7 @@ object IdMonadError extends MonadError[Identity] { pf: PartialFunction[Throwable, Identity[A]] ): Identity[A] = fa - override def ifA[A]( + override def ifM[A]( fcond: Identity[Boolean] )(ifTrue: => Identity[A], ifFalse: => Identity[A]): Identity[A] = if (fcond) ifTrue @@ -43,7 +43,7 @@ object IdMonadError extends MonadError[Identity] { override def eval[A](f: => A): Identity[A] = f - override def guarantee[A](f: Identity[A])(g: => Identity[Unit]): Identity[A] = + override def guarantee[A](f: => Identity[A])(g: => Identity[Unit]): Identity[A] = try f finally g } diff --git a/modules/core/src/main/scala/genkai/monad/MonadError.scala b/modules/core/src/main/scala/genkai/monad/MonadError.scala index b1ed5bb..b008649 100644 --- a/modules/core/src/main/scala/genkai/monad/MonadError.scala +++ b/modules/core/src/main/scala/genkai/monad/MonadError.scala @@ -23,7 +23,7 @@ trait MonadError[F[_]] { def handleErrorWith[A](fa: F[A])(pf: PartialFunction[Throwable, F[A]]): F[A] - def ifA[A](fcond: F[Boolean])(ifTrue: => F[A], ifFalse: => F[A]): F[A] + def ifM[A](fcond: F[Boolean])(ifTrue: => F[A], ifFalse: => F[A]): F[A] def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] @@ -35,5 +35,5 @@ trait MonadError[F[_]] { def flatten[A](fa: F[F[A]]): F[A] = flatMap(fa)(v => identity(v)) - def guarantee[A](f: F[A])(g: => F[Unit]): F[A] + def guarantee[A](f: => F[A])(g: => F[Unit]): F[A] } diff --git a/modules/core/src/main/scala/genkai/monad/TryMonadError.scala b/modules/core/src/main/scala/genkai/monad/TryMonadError.scala index 3127944..b4635fa 100644 --- a/modules/core/src/main/scala/genkai/monad/TryMonadError.scala +++ b/modules/core/src/main/scala/genkai/monad/TryMonadError.scala @@ -35,7 +35,7 @@ object TryMonadError extends MonadError[Try] { case _ => fa } - override def ifA[A](fcond: Try[Boolean])(ifTrue: => Try[A], ifFalse: => Try[A]): Try[A] = + override def ifM[A](fcond: Try[Boolean])(ifTrue: => Try[A], ifFalse: => Try[A]): Try[A] = fcond.flatMap { flag => if (flag) ifTrue else ifFalse @@ -49,7 +49,7 @@ object TryMonadError extends MonadError[Try] { override def eval[A](f: => A): Try[A] = Try(f) - override def guarantee[A](f: Try[A])(g: => Try[Unit]): Try[A] = + override def guarantee[A](f: => Try[A])(g: => Try[Unit]): Try[A] = f match { case Failure(exception) => suspend(g).flatMap(_ => Failure(exception)) case Success(value) => suspend(g).map(_ => value) diff --git a/modules/core/src/test/scala/genkai/ConcurrentRateLimiterBaseSpec.scala b/modules/core/src/test/scala/genkai/ConcurrentRateLimiterBaseSpec.scala new file mode 100644 index 0000000..2dd8f18 --- /dev/null +++ b/modules/core/src/test/scala/genkai/ConcurrentRateLimiterBaseSpec.scala @@ -0,0 +1,160 @@ +package genkai + +import java.time.Instant +import java.time.temporal.ChronoUnit + +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, EitherValues} +import org.scalatest.funsuite.AsyncFunSuite +import org.scalatest.matchers.should.Matchers + +import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.duration._ + +trait ConcurrentRateLimiterBaseSpec[F[_]] + extends AsyncFunSuite + with Matchers + with EitherValues + with BeforeAndAfterAll + with BeforeAndAfterEach { + implicit val ec: ExecutionContext = ExecutionContext.global + + def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[F] + + def toFuture[A](v: F[A]): Future[A] + + test("acquire and automatically release slot") { + val limiter = concurrentRateLimiter(ConcurrentStrategy.Default(10, 5.minutes)) + val instant = Instant.now() + + for { + result <- toFuture( + limiter.use("key", instant)( + limiter.monadError.pure(true) + ) + ) + permissions <- toFuture(limiter.permissions("key", instant)) + } yield { + result.value shouldBe true + permissions shouldBe 10L + } + } + + test("return ConcurrentLimitExhausted when there is no more available slots") { + val limiter = concurrentRateLimiter(ConcurrentStrategy.Default(1, 5.minutes)) + val instant = Instant.now() + + for { + a1 <- toFuture(limiter.acquire("key", instant)) + result <- toFuture( + limiter.use("key", instant)( + limiter.monadError.pure(true) + ) + ) + permissions <- toFuture(limiter.permissions("key", instant)) + } yield { + a1 shouldBe true + result.left.value shouldBe ConcurrentLimitExhausted("key") + permissions shouldBe 0L + } + } + + test("release without acquiring") { + val limiter = concurrentRateLimiter(ConcurrentStrategy.Default(10, 5.minutes)) + val instant = Instant.now() + + for { + p1 <- toFuture(limiter.permissions("key", instant)) + r1 <- toFuture(limiter.release("key", instant)) + p2 <- toFuture(limiter.permissions("key", instant)) + } yield { + p1 shouldBe 10L + r1 shouldBe false + p2 shouldBe 10L + } + } + + test("manual acquire and manual release slot") { + val limiter = concurrentRateLimiter(ConcurrentStrategy.Default(10, 5.minutes)) + val instant = Instant.now() + + for { + a1 <- toFuture(limiter.acquire("key", instant)) + p1 <- toFuture(limiter.permissions("key", instant)) + r1 <- toFuture(limiter.release("key", instant)) + p2 <- toFuture(limiter.permissions("key", instant)) + } yield { + a1 shouldBe true + p1 shouldBe 9L + r1 shouldBe true + p2 shouldBe 10L + } + } + + test("return false if there is no available slot") { + val limiter = concurrentRateLimiter(ConcurrentStrategy.Default(1, 5.minutes)) + val instant = Instant.now() + + for { + a1 <- toFuture(limiter.acquire("key", instant)) + a2 <- toFuture(limiter.acquire("key", instant)) + } yield { + a1 shouldBe true + a2 shouldBe false + } + } + + test("automatically release expired slots when acquire a new one") { + val limiter = concurrentRateLimiter(ConcurrentStrategy.Default(1, 1.minutes)) + val instant = Instant.now() + + for { + a1 <- toFuture(limiter.acquire("key", instant)) + p1 <- toFuture(limiter.permissions("key", instant)) + a2 <- toFuture(limiter.acquire("key", instant.plus(2, ChronoUnit.MINUTES))) + } yield { + a1 shouldBe true + p1 shouldBe 0L + a2 shouldBe true + } + } + + test("automatically release expired slots when release") { + val limiter = concurrentRateLimiter(ConcurrentStrategy.Default(3, 5.minutes)) + val instant = Instant.now() + + for { + a1 <- toFuture(limiter.acquire("key", instant)) + a2 <- toFuture(limiter.acquire("key", instant.plus(3, ChronoUnit.MINUTES))) + a3 <- toFuture(limiter.acquire("key", instant.plus(4, ChronoUnit.MINUTES))) + p1 <- toFuture(limiter.permissions("key", instant.plus(3, ChronoUnit.MINUTES))) + r1 <- toFuture(limiter.release("key", instant.plus(10, ChronoUnit.MINUTES))) + p2 <- toFuture(limiter.permissions("key", instant.plus(10, ChronoUnit.MINUTES))) + } yield { + a1 shouldBe true + a2 shouldBe true + a3 shouldBe true + p1 shouldBe 0L + r1 shouldBe false // all slots were automatically released + p2 shouldBe 3L + } + } + + test("automatically release expired slots when get available permissions") { + val limiter = concurrentRateLimiter(ConcurrentStrategy.Default(3, 5.minutes)) + val instant = Instant.now() + + for { + a1 <- toFuture(limiter.acquire("key", instant)) + a2 <- toFuture(limiter.acquire("key", instant.plus(3, ChronoUnit.MINUTES))) + a3 <- toFuture(limiter.acquire("key", instant.plus(4, ChronoUnit.MINUTES))) + p1 <- toFuture(limiter.permissions("key", instant.plus(3, ChronoUnit.MINUTES))) + p2 <- toFuture(limiter.permissions("key", instant.plus(10, ChronoUnit.MINUTES))) + } yield { + a1 shouldBe true + a2 shouldBe true + a3 shouldBe true + p1 shouldBe 0L + p2 shouldBe 3L + } + } +} diff --git a/modules/core/src/test/scala/genkai/BaseSpec.scala b/modules/core/src/test/scala/genkai/RateLimiterBaseSpec.scala similarity index 99% rename from modules/core/src/test/scala/genkai/BaseSpec.scala rename to modules/core/src/test/scala/genkai/RateLimiterBaseSpec.scala index fa50ef3..0bdd311 100644 --- a/modules/core/src/test/scala/genkai/BaseSpec.scala +++ b/modules/core/src/test/scala/genkai/RateLimiterBaseSpec.scala @@ -10,7 +10,7 @@ import org.scalatest.matchers.should.Matchers import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -trait BaseSpec[F[_]] +trait RateLimiterBaseSpec[F[_]] extends AsyncFunSuite with Matchers with BeforeAndAfterAll diff --git a/modules/effects/cats/src/main/scala/genkai/effect/cats/CatsMonadAsyncError.scala b/modules/effects/cats/src/main/scala/genkai/effect/cats/CatsMonadAsyncError.scala index 748d7ae..c887e85 100644 --- a/modules/effects/cats/src/main/scala/genkai/effect/cats/CatsMonadAsyncError.scala +++ b/modules/effects/cats/src/main/scala/genkai/effect/cats/CatsMonadAsyncError.scala @@ -34,8 +34,8 @@ final class CatsMonadAsyncError[F[_]](implicit F: Concurrent[F]) extends MonadAs override def handleErrorWith[A](fa: F[A])(pf: PartialFunction[Throwable, F[A]]): F[A] = F.handleErrorWith(fa)(pf) - override def ifA[A](fcond: F[Boolean])(ifTrue: => F[A], ifFalse: => F[A]): F[A] = - F.ifA(fcond)(ifTrue, ifFalse) + override def ifM[A](fcond: F[Boolean])(ifTrue: => F[A], ifFalse: => F[A]): F[A] = + F.ifM(fcond)(ifTrue, ifFalse) override def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] = F.whenA(cond)(f) @@ -50,5 +50,5 @@ final class CatsMonadAsyncError[F[_]](implicit F: Concurrent[F]) extends MonadAs override def flatten[A](fa: F[F[A]]): F[A] = F.flatten(fa) - override def guarantee[A](f: F[A])(g: => F[Unit]): F[A] = F.guarantee(f)(g) + override def guarantee[A](f: => F[A])(g: => F[Unit]): F[A] = F.guarantee(f)(g) } diff --git a/modules/effects/cats/src/main/scala/genkai/effect/cats/CatsMonadError.scala b/modules/effects/cats/src/main/scala/genkai/effect/cats/CatsMonadError.scala index a3d3245..933f949 100644 --- a/modules/effects/cats/src/main/scala/genkai/effect/cats/CatsMonadError.scala +++ b/modules/effects/cats/src/main/scala/genkai/effect/cats/CatsMonadError.scala @@ -29,8 +29,8 @@ final class CatsMonadError[F[_]: ContextShift](blocker: Blocker)(implicit F: Syn override def handleErrorWith[A](fa: F[A])(pf: PartialFunction[Throwable, F[A]]): F[A] = F.handleErrorWith(fa)(pf) - override def ifA[A](fcond: F[Boolean])(ifTrue: => F[A], ifFalse: => F[A]): F[A] = - F.ifA(fcond)(ifTrue, ifFalse) + override def ifM[A](fcond: F[Boolean])(ifTrue: => F[A], ifFalse: => F[A]): F[A] = + F.ifM(fcond)(ifTrue, ifFalse) override def whenA[A](cond: Boolean)(f: => F[A]): F[Unit] = F.whenA(cond)(f) @@ -45,5 +45,5 @@ final class CatsMonadError[F[_]: ContextShift](blocker: Blocker)(implicit F: Syn override def flatten[A](fa: F[F[A]]): F[A] = F.flatten(fa) - override def guarantee[A](f: F[A])(g: => F[Unit]): F[A] = F.guarantee(f)(g) + override def guarantee[A](f: => F[A])(g: => F[Unit]): F[A] = F.guarantee(f)(g) } diff --git a/modules/effects/monix/src/main/scala/genkai/effect/monix/MonixMonadAsyncError.scala b/modules/effects/monix/src/main/scala/genkai/effect/monix/MonixMonadAsyncError.scala index 15d5ee6..6bfb0e9 100644 --- a/modules/effects/monix/src/main/scala/genkai/effect/monix/MonixMonadAsyncError.scala +++ b/modules/effects/monix/src/main/scala/genkai/effect/monix/MonixMonadAsyncError.scala @@ -48,7 +48,7 @@ final class MonixMonadAsyncError extends MonadAsyncError[Task] { override def handleErrorWith[A](fa: Task[A])(pf: PartialFunction[Throwable, Task[A]]): Task[A] = fa.onErrorRecoverWith(pf) - override def ifA[A](fcond: Task[Boolean])(ifTrue: => Task[A], ifFalse: => Task[A]): Task[A] = + override def ifM[A](fcond: Task[Boolean])(ifTrue: => Task[A], ifFalse: => Task[A]): Task[A] = fcond.flatMap { flag => if (flag) ifTrue else ifFalse @@ -60,7 +60,7 @@ final class MonixMonadAsyncError extends MonadAsyncError[Task] { override def eval[A](f: => A): Task[A] = Task.eval(f) - override def guarantee[A](f: Task[A])(g: => Task[Unit]): Task[A] = f.guarantee(g) + override def guarantee[A](f: => Task[A])(g: => Task[Unit]): Task[A] = f.guarantee(g) override def unit: Task[Unit] = Task.unit diff --git a/modules/effects/zio/src/main/scala/genkai/effect/zio/ZioMonadAsyncError.scala b/modules/effects/zio/src/main/scala/genkai/effect/zio/ZioMonadAsyncError.scala index cff34b5..8e7a129 100644 --- a/modules/effects/zio/src/main/scala/genkai/effect/zio/ZioMonadAsyncError.scala +++ b/modules/effects/zio/src/main/scala/genkai/effect/zio/ZioMonadAsyncError.scala @@ -50,7 +50,7 @@ final class ZioMonadAsyncError extends MonadAsyncError[Task] { override def handleErrorWith[A](fa: Task[A])(pf: PartialFunction[Throwable, Task[A]]): Task[A] = fa.catchSome(pf) - override def ifA[A](fcond: Task[Boolean])(ifTrue: => Task[A], ifFalse: => Task[A]): Task[A] = + override def ifM[A](fcond: Task[Boolean])(ifTrue: => Task[A], ifFalse: => Task[A]): Task[A] = Task.ifM(fcond)(ifTrue, ifFalse) override def whenA[A](cond: Boolean)(f: => Task[A]): Task[Unit] = @@ -66,6 +66,6 @@ final class ZioMonadAsyncError extends MonadAsyncError[Task] { override def flatten[A](fa: Task[Task[A]]): Task[A] = Task.flatten(fa) - override def guarantee[A](f: Task[A])(g: => Task[Unit]): Task[A] = + override def guarantee[A](f: => Task[A])(g: => Task[Unit]): Task[A] = f.ensuring(g.ignore) } diff --git a/modules/effects/zio/src/main/scala/genkai/effect/zio/ZioMonadError.scala b/modules/effects/zio/src/main/scala/genkai/effect/zio/ZioMonadError.scala index 10439e1..56aa819 100644 --- a/modules/effects/zio/src/main/scala/genkai/effect/zio/ZioMonadError.scala +++ b/modules/effects/zio/src/main/scala/genkai/effect/zio/ZioMonadError.scala @@ -33,7 +33,7 @@ final class ZioMonadError(blocking: Blocking.Service) extends MonadError[Task] { override def handleErrorWith[A](fa: Task[A])(pf: PartialFunction[Throwable, Task[A]]): Task[A] = fa.catchSome(pf) - override def ifA[A](fcond: Task[Boolean])(ifTrue: => Task[A], ifFalse: => Task[A]): Task[A] = + override def ifM[A](fcond: Task[Boolean])(ifTrue: => Task[A], ifFalse: => Task[A]): Task[A] = Task.ifM(fcond)(ifTrue, ifFalse) override def whenA[A](cond: Boolean)(f: => Task[A]): Task[Unit] = @@ -50,6 +50,6 @@ final class ZioMonadError(blocking: Blocking.Service) extends MonadError[Task] { override def flatten[A](fa: Task[Task[A]]): Task[A] = Task.flatten(fa) - override def guarantee[A](f: Task[A])(g: => Task[Unit]): Task[A] = + override def guarantee[A](f: => Task[A])(g: => Task[Unit]): Task[A] = f.ensuring(g.ignore) } diff --git a/modules/redis/common/src/main/scala/genkai/redis/LuaScript.scala b/modules/redis/common/src/main/scala/genkai/redis/LuaScript.scala index a1a5bae..81a9b0a 100644 --- a/modules/redis/common/src/main/scala/genkai/redis/LuaScript.scala +++ b/modules/redis/common/src/main/scala/genkai/redis/LuaScript.scala @@ -10,7 +10,7 @@ Scripts will be loaded only once per RateLimiter instance and then executed as a object LuaScript { /** - * args: key, current_timestamp, cost, maxTokens, refillAmount, refillTime + * args: key, current_timestamp (epoch seconds), cost, maxTokens, refillAmount, refillTime (seconds) * key format: token_bucket: * hash structure: f1: value, f2: lastRefillTime * @return - 1 if token acquired, 0 - otherwise @@ -48,7 +48,7 @@ object LuaScript { |""".stripMargin /** - * args: key, current_timestamp, maxTokens, refillAmount, refillTime + * args: key, current_timestamp (epoch seconds), maxTokens, refillAmount, refillTime (seconds) * key format: token_bucket: * hash structure: f1: value, f2: lastRefillTime * @return - unused tokens @@ -80,7 +80,7 @@ object LuaScript { |""".stripMargin /** - * args: key, windowStartTs, cost, maxTokens, ttl (windowSize) + * args: key, windowStartTs (epoch seconds), cost, maxTokens, ttl (windowSize, seconds) * key format: fixed_window:: where is truncated to the beginning of the window * @return - 1 if token acquired, 0 - otherwise */ @@ -122,7 +122,7 @@ object LuaScript { |""".stripMargin /** - * args: key, windowStartTs, maxTokens, ttl + * args: key, windowStartTs (epoch seconds), maxTokens, ttl (seconds) * key format: fixed_window:: where is truncated to the beginning of the window * @return - permissions */ @@ -156,7 +156,7 @@ object LuaScript { |""".stripMargin /** - * input: key, instant, cost, maxTokens, windowSize, precision, ttl + * input: key, instant (epoch seconds), cost, maxTokens, windowSize (seconds), precision, ttl (seconds) * key format: sliding_window: * @return - 1 if token acquired, 0 - otherwise */ @@ -219,7 +219,7 @@ object LuaScript { |""".stripMargin /** - * input: key, instant, maxTokens, windowSize, precision + * input: key, instant (epoch seconds), maxTokens, windowSize (seconds), precision * key format: sliding_window: * @return - permissions */ @@ -260,6 +260,72 @@ object LuaScript { | end |end | - |return math.max(0, maxTokens - current); + |return math.max(0, maxTokens - current) + |""".stripMargin + + /** + * input: key, instant (epoch millis), maxSlots, ttl (millis) + * key format: concurrent_limiter: + * @return - 0 if no slot was acquired, 1 otherwise. + */ + val concurrentRateLimiterAcquire: String = + """ + |local instant = tonumber(ARGV[1]) + |local maxSlots = tonumber(ARGV[2]) + |local ttl = tonumber(ARGV[3]) + | + |local expiredSlots = instant - ttl + |-- remove expired records (-inf, timestamp) + |redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', expiredSlots) + | + |local current = redis.call('ZCARD', KEYS[1]) + | + |if current + 1 <= maxSlots then + | redis.call('ZADD', KEYS[1], instant, instant) + | return 1 + |else + | return 0 + |end + |""".stripMargin + + /** + * input: key, instant (epoch millis), ttl (millis) + * key format: concurrent_limiter: + * @return - 0 if no slot was released, 1 otherwise. + */ + val concurrentRateLimiterRelease: String = + """ + |local instant = tonumber(ARGV[1]) + |local ttl = tonumber(ARGV[2]) + | + |local expiredSlots = instant - ttl + |-- remove expired records (-inf, timestamp) + |redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', expiredSlots) + |local removed = redis.call('ZPOPMIN', KEYS[1]) + |local removed = removed and #removed or 0 + | + |if removed > 0 then + | return 1 + |else + | return 0 + |end + |""".stripMargin + + /** + * input: key, instant (epoch millis), maxSlots, ttl (millis) + * key format: concurrent_limiter: + * @return - permissions + */ + val concurrentRateLimiterPermissions: String = + """ + |local instant = tonumber(ARGV[1]) + |local maxSlots = tonumber(ARGV[2]) + |local ttl = tonumber(ARGV[3]) + | + |local expiredSlots = instant - ttl + |-- remove expired records (-inf, timestamp) + |redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', expiredSlots) + | + |return math.max(0, maxSlots - redis.call('ZCARD', KEYS[1])) |""".stripMargin } diff --git a/modules/redis/common/src/main/scala/genkai/redis/RedisConcurrentStrategy.scala b/modules/redis/common/src/main/scala/genkai/redis/RedisConcurrentStrategy.scala new file mode 100644 index 0000000..9850d21 --- /dev/null +++ b/modules/redis/common/src/main/scala/genkai/redis/RedisConcurrentStrategy.scala @@ -0,0 +1,116 @@ +package genkai.redis + +import java.time.Instant + +import genkai.{ConcurrentStrategy, Key} + +sealed trait RedisConcurrentStrategy { + def underlying: ConcurrentStrategy + + /** + * Lua script which will be loaded once per ConcurrentRateLimiter. + * Used for acquiring slots. + * For more details see [[genkai.redis.LuaScript]] + */ + def acquireLuaScript: String + + /** + * Lua script which will be loaded once per ConcurrentRateLimiter. + * Used for releasing slots. + * For more details see [[genkai.redis.LuaScript]] + */ + def releaseLuaScript: String + + /** + * Lua script which will be loaded once per ConcurrentRateLimiter. + * Used for getting unused permissions. + * For more details see [[genkai.redis.LuaScript]] + */ + def permissionsLuaScript: String + + /** + * @param value - key + * @param instant - request time + * @tparam A - key type with implicit [[genkai.Key]] type class instance + * @return - list of script keys + */ + def keys[A: Key](value: A, instant: Instant): List[String] + + /** + * @param instant - request time + * @return - list of script args + */ + def permissionsArgs(instant: Instant): List[String] + + /** + * @param instant - request time + * @return + */ + def acquireArgs(instant: Instant): List[String] + + /** + * @param instant - request time + * @return + */ + def releaseArgs(instant: Instant): List[String] + + /** + * @param value - returned value after [[genkai.ConcurrentRateLimiter.acquire()]] + * @return - true if token was acquired otherwise false + */ + def isAllowed(value: Long): Boolean + + /** + * @param value - returned value after [[genkai.ConcurrentRateLimiter.release()]] + * @return - true if token was acquired otherwise false + */ + def isReleased(value: Long): Boolean + + /** + * @param value - returned value after [[genkai.ConcurrentRateLimiter.permissions()]] + * @return - unused permissions + */ + def toPermissions(value: Long): Long +} + +object RedisConcurrentStrategy { + def apply(underlying: ConcurrentStrategy): RedisConcurrentStrategy = underlying match { + case s: ConcurrentStrategy.Default => RedisDefault(s) + } + + final case class RedisDefault(underlying: ConcurrentStrategy.Default) + extends RedisConcurrentStrategy { + private val argsPart = List( + underlying.slots.toString, + underlying.ttl.toMillis.toString + ) + + private val releaseArgsPart = List( + underlying.ttl.toMillis.toString + ) + + override def acquireLuaScript: String = LuaScript.concurrentRateLimiterAcquire + + override def releaseLuaScript: String = LuaScript.concurrentRateLimiterRelease + + override def permissionsLuaScript: String = LuaScript.concurrentRateLimiterPermissions + + override def keys[A: Key](value: A, instant: Instant): List[String] = + List(Key[A].convert(value)) + + override def permissionsArgs(instant: Instant): List[String] = + instant.toEpochMilli.toString :: argsPart + + override def acquireArgs(instant: Instant): List[String] = + instant.toEpochMilli.toString :: argsPart + + override def releaseArgs(instant: Instant): List[String] = + instant.toEpochMilli.toString :: releaseArgsPart + + override def isAllowed(value: Long): Boolean = value == 1L + + override def isReleased(value: Long): Boolean = value == 1L + + override def toPermissions(value: Long): Long = value + } +} diff --git a/modules/redis/common/src/test/scala/genkai/redis/RedisConcurrentRateLimiterSpecForAll.scala b/modules/redis/common/src/test/scala/genkai/redis/RedisConcurrentRateLimiterSpecForAll.scala new file mode 100644 index 0000000..19214cf --- /dev/null +++ b/modules/redis/common/src/test/scala/genkai/redis/RedisConcurrentRateLimiterSpecForAll.scala @@ -0,0 +1,10 @@ +package genkai.redis + +import com.dimafeng.testcontainers.scalatest.TestContainerForAll +import genkai.ConcurrentRateLimiterBaseSpec + +trait RedisConcurrentRateLimiterSpecForAll[F[_]] + extends ConcurrentRateLimiterBaseSpec[F] + with TestContainerForAll { + override val containerDef: RedisContainer.Def = RedisContainer.Def() +} diff --git a/modules/redis/common/src/test/scala/genkai/redis/RedisSpecForAll.scala b/modules/redis/common/src/test/scala/genkai/redis/RedisRateLimiterSpecForAll.scala similarity index 55% rename from modules/redis/common/src/test/scala/genkai/redis/RedisSpecForAll.scala rename to modules/redis/common/src/test/scala/genkai/redis/RedisRateLimiterSpecForAll.scala index 8ffae4d..82dbc9b 100644 --- a/modules/redis/common/src/test/scala/genkai/redis/RedisSpecForAll.scala +++ b/modules/redis/common/src/test/scala/genkai/redis/RedisRateLimiterSpecForAll.scala @@ -1,8 +1,8 @@ package genkai.redis import com.dimafeng.testcontainers.scalatest.TestContainerForAll -import genkai.BaseSpec +import genkai.RateLimiterBaseSpec -trait RedisSpecForAll[F[_]] extends BaseSpec[F] with TestContainerForAll { +trait RedisRateLimiterSpecForAll[F[_]] extends RateLimiterBaseSpec[F] with TestContainerForAll { override val containerDef: RedisContainer.Def = RedisContainer.Def() } diff --git a/modules/redis/jedis/cats/src/main/scala/genkai/redis/jedis/cats/JedisCatsConcurrentRateLimiter.scala b/modules/redis/jedis/cats/src/main/scala/genkai/redis/jedis/cats/JedisCatsConcurrentRateLimiter.scala new file mode 100644 index 0000000..c4d9e93 --- /dev/null +++ b/modules/redis/jedis/cats/src/main/scala/genkai/redis/jedis/cats/JedisCatsConcurrentRateLimiter.scala @@ -0,0 +1,100 @@ +package genkai.redis.jedis.cats + +import cats.effect.{Blocker, ContextShift, Resource, Sync} +import genkai.ConcurrentStrategy +import genkai.monad.syntax._ +import genkai.effect.cats.CatsMonadError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.jedis.JedisConcurrentRateLimiter +import redis.clients.jedis.JedisPool + +class JedisCatsConcurrentRateLimiter[F[_]: Sync: ContextShift] private ( + pool: JedisPool, + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String, + monad: CatsMonadError[F] +) extends JedisConcurrentRateLimiter[F]( + pool, + monad, + strategy, + closeClient, + acquireSha, + releaseSha, + permissionsSha + ) {} + +object JedisCatsConcurrentRateLimiter { + def useClient[F[_]: Sync: ContextShift]( + pool: JedisPool, + strategy: ConcurrentStrategy, + blocker: Blocker + ): F[JedisCatsConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadError[F] = new CatsMonadError[F](blocker) + + val redisStrategy = RedisConcurrentStrategy(strategy) + + monad + .eval(pool.getResource) + .flatMap { client => + monad.guarantee { + monad.eval { + ( + client.scriptLoad(redisStrategy.acquireLuaScript), + client.scriptLoad(redisStrategy.releaseLuaScript), + client.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + }(monad.eval(client.close())) + } + .map { case (acquireSha, releaseSha, permissionsSha) => + new JedisCatsConcurrentRateLimiter( + pool = pool, + strategy = redisStrategy, + closeClient = false, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha, + monad = monad + ) + } + } + + def resource[F[_]: Sync: ContextShift]( + host: String, + port: Int, + strategy: ConcurrentStrategy, + blocker: Blocker + ): Resource[F, JedisCatsConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadError[F] = new CatsMonadError[F](blocker) + + val redisStrategy = RedisConcurrentStrategy(strategy) + + Resource.make { + for { + pool <- monad.eval(new JedisPool(host, port)) + sha <- monad.eval(pool.getResource).flatMap { client => + monad.guarantee { + monad.eval { + ( + client.scriptLoad(redisStrategy.acquireLuaScript), + client.scriptLoad(redisStrategy.releaseLuaScript), + client.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + }(monad.eval(client.close())) + } + } yield new JedisCatsConcurrentRateLimiter( + pool = pool, + strategy = redisStrategy, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad = monad + ) + }(_.close()) + } +} diff --git a/modules/redis/jedis/cats/src/test/scala/genkai/redis/jedis/cats/JedisCatsConcurrentRateLimiterSpec.scala b/modules/redis/jedis/cats/src/test/scala/genkai/redis/jedis/cats/JedisCatsConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..72e480a --- /dev/null +++ b/modules/redis/jedis/cats/src/test/scala/genkai/redis/jedis/cats/JedisCatsConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.jedis.cats + +import cats.effect.IO +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.cats.CatsBaseSpec +import genkai.redis.jedis.JedisConcurrentRateLimiterSpec + +import scala.concurrent.Future + +class JedisCatsConcurrentRateLimiterSpec + extends JedisConcurrentRateLimiterSpec[IO] + with CatsBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[IO] = + JedisCatsConcurrentRateLimiter.useClient[IO](jedisPool, strategy, blocker).unsafeRunSync() + + override def toFuture[A](v: IO[A]): Future[A] = v.unsafeToFuture() +} diff --git a/modules/redis/jedis/cats/src/test/scala/genkai/redis/jedis/cats/JedisCatsRateLimiterSpec.scala b/modules/redis/jedis/cats/src/test/scala/genkai/redis/jedis/cats/JedisCatsRateLimiterSpec.scala index 8753aa9..7fa450a 100644 --- a/modules/redis/jedis/cats/src/test/scala/genkai/redis/jedis/cats/JedisCatsRateLimiterSpec.scala +++ b/modules/redis/jedis/cats/src/test/scala/genkai/redis/jedis/cats/JedisCatsRateLimiterSpec.scala @@ -3,11 +3,11 @@ package genkai.redis.jedis.cats import cats.effect.IO import genkai.effect.cats.CatsBaseSpec import genkai.{RateLimiter, Strategy} -import genkai.redis.jedis.JedisSpec +import genkai.redis.jedis.JedisRateLimiterSpec import scala.concurrent.Future -class JedisCatsRateLimiterSpec extends JedisSpec[IO] with CatsBaseSpec { +class JedisCatsRateLimiterSpec extends JedisRateLimiterSpec[IO] with CatsBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[IO] = JedisCatsRateLimiter.useClient[IO](jedisPool, strategy, blocker).unsafeRunSync() diff --git a/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisConcurrentRateLimiter.scala b/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisConcurrentRateLimiter.scala new file mode 100644 index 0000000..13fb9f1 --- /dev/null +++ b/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisConcurrentRateLimiter.scala @@ -0,0 +1,79 @@ +package genkai.redis.jedis + +import java.time.Instant + +import redis.clients.jedis.{Jedis, JedisPool} +import genkai.monad.syntax._ +import genkai.monad.MonadError +import genkai.redis.RedisConcurrentStrategy +import genkai.{ConcurrentLimitExhausted, ConcurrentRateLimiter, Key, Logging} + +abstract class JedisConcurrentRateLimiter[F[_]]( + pool: JedisPool, + implicit val monad: MonadError[F], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends ConcurrentRateLimiter[F] + with Logging[F] { + + override private[genkai] def use[A: Key, B](key: A, instant: Instant)( + f: => F[B] + ): F[Either[ConcurrentLimitExhausted[A], B]] = + monad.ifM(acquire(key, instant))( + ifTrue = monad.guarantee(f)(release(key, instant).void).map(r => Right(r)), + ifFalse = monad.pure(Left(ConcurrentLimitExhausted(key))) + ) + + override def reset[A: Key](key: A): F[Unit] = { + val now = Instant.now() + val keyStr = strategy.keys(key, now) + useClient(client => + debug(s"Reset limits for: $keyStr").flatMap(_ => monad.eval(client.unlink(keyStr: _*))) + ) + } + + override private[genkai] def acquire[A: Key](key: A, instant: Instant): F[Boolean] = useClient { + client => + val keys = strategy.keys(key, instant) + val args = keys ::: strategy.acquireArgs(instant) + + for { + _ <- debug(s"Acquire request: $args") + tokens <- monad.eval(client.evalsha(acquireSha, keys.size, args: _*)) + } yield strategy.isAllowed(tokens.toString.toLong) + } + + override private[genkai] def release[A: Key](key: A, instant: Instant): F[Boolean] = useClient { + client => + val keys = strategy.keys(key, instant) + val args = keys ::: strategy.releaseArgs(instant) + + for { + _ <- debug(s"Release request: $args") + tokens <- monad.eval(client.evalsha(releaseSha, keys.size, args: _*)) + } yield strategy.isReleased(tokens.toString.toLong) + } + + override private[genkai] def permissions[A: Key](key: A, instant: Instant): F[Long] = useClient { + client => + val keys = strategy.keys(key, instant) + val args = keys ::: strategy.permissionsArgs(instant) + + for { + _ <- debug(s"Permissions request: $args") + tokens <- monad.eval(client.evalsha(permissionsSha, keys.size, args: _*)) + } yield strategy.toPermissions(tokens.toString.toLong) + } + + override def close(): F[Unit] = monad.whenA(closeClient)(monad.eval(pool.close())) + + override def monadError: MonadError[F] = monad + + private def useClient[A](fa: Jedis => F[A]): F[A] = + monad.eval(pool.getResource).flatMap { client => + monad.guarantee(fa(client))(monad.eval(client.close())) + } +} diff --git a/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisRateLimiter.scala b/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisRateLimiter.scala index 5236f02..8498475 100644 --- a/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisRateLimiter.scala +++ b/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisRateLimiter.scala @@ -50,7 +50,7 @@ abstract class JedisRateLimiter[F[_]]( override def close(): F[Unit] = monad.whenA(closeClient)(monad.eval(pool.close())) - override protected def monadError: MonadError[F] = monad + override def monadError: MonadError[F] = monad private def useClient[A](fa: Jedis => F[A]): F[A] = monad.eval(pool.getResource).flatMap { client => diff --git a/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisSyncConcurrentRateLimiter.scala b/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisSyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..232d9f8 --- /dev/null +++ b/modules/redis/jedis/src/main/scala/genkai/redis/jedis/JedisSyncConcurrentRateLimiter.scala @@ -0,0 +1,80 @@ +package genkai.redis.jedis + +import redis.clients.jedis.JedisPool +import genkai.monad.syntax._ +import genkai.monad.IdMonadError +import genkai.redis.RedisConcurrentStrategy +import genkai.{ConcurrentStrategy, Identity} + +class JedisSyncConcurrentRateLimiter private ( + pool: JedisPool, + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends JedisConcurrentRateLimiter[Identity]( + pool, + IdMonadError, + strategy, + closeClient, + acquireSha, + releaseSha, + permissionsSha + ) + +object JedisSyncConcurrentRateLimiter { + def apply( + pool: JedisPool, + strategy: ConcurrentStrategy + ): JedisSyncConcurrentRateLimiter = { + implicit val monad = IdMonadError + val redisStrategy = RedisConcurrentStrategy(strategy) + + val (acquireSha, releaseSha, permissionsSha) = monad.eval(pool.getResource).flatMap { client => + monad.guarantee { + ( + client.scriptLoad(redisStrategy.acquireLuaScript), + client.scriptLoad(redisStrategy.releaseLuaScript), + client.scriptLoad(redisStrategy.permissionsLuaScript) + ) + }(monad.eval(client.close())) + } + new JedisSyncConcurrentRateLimiter( + pool = pool, + strategy = redisStrategy, + closeClient = false, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + } + + def apply( + host: String, + port: Int, + strategy: ConcurrentStrategy + ): JedisSyncConcurrentRateLimiter = { + implicit val monad = IdMonadError + val redisStrategy = RedisConcurrentStrategy(strategy) + val pool = new JedisPool(host, port) + + val (acquireSha, releaseSha, permissionsSha) = monad.eval(pool.getResource).flatMap { client => + monad.guarantee { + ( + client.scriptLoad(redisStrategy.acquireLuaScript), + client.scriptLoad(redisStrategy.releaseLuaScript), + client.scriptLoad(redisStrategy.permissionsLuaScript) + ) + }(monad.eval(client.close())) + } + new JedisSyncConcurrentRateLimiter( + pool = pool, + strategy = redisStrategy, + closeClient = true, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + } +} diff --git a/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisConcurrentRateLimiterSpec.scala b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..ecc68c4 --- /dev/null +++ b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisConcurrentRateLimiterSpec.scala @@ -0,0 +1,29 @@ +package genkai.redis.jedis + +import genkai.redis.{RedisConcurrentRateLimiterSpecForAll, RedisContainer} +import org.apache.commons.pool2.impl.GenericObjectPoolConfig +import redis.clients.jedis.{Jedis, JedisPool} + +trait JedisConcurrentRateLimiterSpec[F[_]] extends RedisConcurrentRateLimiterSpecForAll[F] { + var jedisPool: JedisPool = _ + + override def afterContainersStart(redis: RedisContainer): Unit = { + val poolConfig = new GenericObjectPoolConfig[Jedis]() + poolConfig.setMinIdle(1) + poolConfig.setMaxIdle(2) + poolConfig.setMaxTotal(2) + jedisPool = new JedisPool(poolConfig, redis.containerIpAddress, redis.mappedPort(6379)) + } + + override protected def afterAll(): Unit = { + jedisPool.close() + super.afterAll() + } + + override protected def afterEach(): Unit = { + super.afterEach() + val jedis = jedisPool.getResource + try jedis.flushAll() + finally jedis.close() + } +} diff --git a/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisRateLimiterSpec.scala b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisRateLimiterSpec.scala new file mode 100644 index 0000000..0727235 --- /dev/null +++ b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisRateLimiterSpec.scala @@ -0,0 +1,29 @@ +package genkai.redis.jedis + +import genkai.redis.{RedisContainer, RedisRateLimiterSpecForAll} +import org.apache.commons.pool2.impl.GenericObjectPoolConfig +import redis.clients.jedis.{Jedis, JedisPool} + +trait JedisRateLimiterSpec[F[_]] extends RedisRateLimiterSpecForAll[F] { + var jedisPool: JedisPool = _ + + override def afterContainersStart(redis: RedisContainer): Unit = { + val poolConfig = new GenericObjectPoolConfig[Jedis]() + poolConfig.setMinIdle(1) + poolConfig.setMaxIdle(2) + poolConfig.setMaxTotal(2) + jedisPool = new JedisPool(poolConfig, redis.containerIpAddress, redis.mappedPort(6379)) + } + + override protected def afterAll(): Unit = { + jedisPool.close() + super.afterAll() + } + + override protected def afterEach(): Unit = { + super.afterEach() + val jedis = jedisPool.getResource + try jedis.flushAll() + finally jedis.close() + } +} diff --git a/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSpec.scala b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSpec.scala deleted file mode 100644 index af8bff7..0000000 --- a/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSpec.scala +++ /dev/null @@ -1,23 +0,0 @@ -package genkai.redis.jedis - -import genkai.redis.{RedisContainer, RedisSpecForAll} -import redis.clients.jedis.JedisPool - -trait JedisSpec[F[_]] extends RedisSpecForAll[F] { - var jedisPool: JedisPool = _ - - override def afterContainersStart(redis: RedisContainer): Unit = - jedisPool = new JedisPool(redis.containerIpAddress, redis.mappedPort(6379)) - - override protected def afterAll(): Unit = { - jedisPool.close() - super.afterAll() - } - - override protected def afterEach(): Unit = { - super.afterEach() - val jedis = jedisPool.getResource - try jedis.flushAll() - finally jedis.close() - } -} diff --git a/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSyncConcurrentRateLimiterSpec.scala b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSyncConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..edfb283 --- /dev/null +++ b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSyncConcurrentRateLimiterSpec.scala @@ -0,0 +1,15 @@ +package genkai.redis.jedis + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy, TryConcurrentRateLimiter} + +import scala.concurrent.Future +import scala.util.Try + +class JedisSyncConcurrentRateLimiterSpec extends JedisConcurrentRateLimiterSpec[Try] { + override def concurrentRateLimiter( + strategy: ConcurrentStrategy + ): ConcurrentRateLimiter[Try] = + new TryConcurrentRateLimiter(JedisSyncConcurrentRateLimiter(jedisPool, strategy)) + + override def toFuture[A](v: Try[A]): Future[A] = Future.fromTry(v) +} diff --git a/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSyncRateLimiterSpec.scala b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSyncRateLimiterSpec.scala index d16ba01..a6543c7 100644 --- a/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSyncRateLimiterSpec.scala +++ b/modules/redis/jedis/src/test/scala/genkai/redis/jedis/JedisSyncRateLimiterSpec.scala @@ -4,7 +4,7 @@ import genkai.{Identity, RateLimiter, Strategy} import scala.concurrent.Future -class JedisSyncRateLimiterSpec extends JedisSpec[Identity] { +class JedisSyncRateLimiterSpec extends JedisRateLimiterSpec[Identity] { override def rateLimiter(strategy: Strategy): RateLimiter[Identity] = JedisSyncRateLimiter(jedisPool, strategy) diff --git a/modules/redis/jedis/zio/src/main/scala/genkai/redis/jedis/zio/JedisZioConcurrentRateLimiter.scala b/modules/redis/jedis/zio/src/main/scala/genkai/redis/jedis/zio/JedisZioConcurrentRateLimiter.scala new file mode 100644 index 0000000..8865c67 --- /dev/null +++ b/modules/redis/jedis/zio/src/main/scala/genkai/redis/jedis/zio/JedisZioConcurrentRateLimiter.scala @@ -0,0 +1,105 @@ +package genkai.redis.jedis.zio + +import zio._ +import genkai.ConcurrentStrategy +import genkai.effect.zio.ZioMonadError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.jedis.JedisConcurrentRateLimiter +import redis.clients.jedis.JedisPool +import zio.blocking.{Blocking, blocking} + +class JedisZioConcurrentRateLimiter private ( + pool: JedisPool, + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String, + monad: ZioMonadError +) extends JedisConcurrentRateLimiter[Task]( + pool, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object JedisZioConcurrentRateLimiter { + def useClient( + pool: JedisPool, + strategy: ConcurrentStrategy + ): ZIO[Blocking, Throwable, JedisZioConcurrentRateLimiter] = for { + blocker <- ZIO.service[Blocking.Service] + monad = new ZioMonadError(blocker) + redisStrategy = RedisConcurrentStrategy(strategy) + sha <- monad.eval(pool.getResource).flatMap { client => + monad.guarantee { + monad.eval { + ( + client.scriptLoad(redisStrategy.acquireLuaScript), + client.scriptLoad(redisStrategy.releaseLuaScript), + client.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + }(monad.eval(client.close())) + } + } yield new JedisZioConcurrentRateLimiter( + pool = pool, + strategy = redisStrategy, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad = monad + ) + + def layerUsingClient( + pool: JedisPool, + strategy: ConcurrentStrategy + ): ZLayer[Blocking, Throwable, Has[JedisZioConcurrentRateLimiter]] = + useClient(pool, strategy).toLayer + + def managed( + host: String, + port: Int, + strategy: ConcurrentStrategy + ): ZManaged[Blocking, Throwable, JedisZioConcurrentRateLimiter] = + ZManaged.make { + for { + blocker <- ZIO.service[Blocking.Service] + monad = new ZioMonadError(blocker) + redisStrategy = RedisConcurrentStrategy(strategy) + pool <- monad.eval(new JedisPool(host, port)) + sha <- monad.eval(pool.getResource).flatMap { client => + monad.guarantee { + monad.eval { + ( + client.scriptLoad(redisStrategy.acquireLuaScript), + client.scriptLoad(redisStrategy.releaseLuaScript), + client.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + }(monad.eval(client.close())) + } + } yield new JedisZioConcurrentRateLimiter( + pool = pool, + strategy = redisStrategy, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad = monad + ) + } { limiter => + blocking(limiter.close().orDie) + } + + def layerFromManaged( + host: String, + port: Int, + strategy: ConcurrentStrategy + ): ZLayer[Blocking, Throwable, Has[JedisZioConcurrentRateLimiter]] = + ZLayer.fromManaged(managed(host, port, strategy)) +} diff --git a/modules/redis/jedis/zio/src/test/scala/genkai/redis/jedis/zio/JedisZioConcurrentRateLimiterSpec.scala b/modules/redis/jedis/zio/src/test/scala/genkai/redis/jedis/zio/JedisZioConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..e821a0c --- /dev/null +++ b/modules/redis/jedis/zio/src/test/scala/genkai/redis/jedis/zio/JedisZioConcurrentRateLimiterSpec.scala @@ -0,0 +1,18 @@ +package genkai.redis.jedis.zio + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.zio.ZioBaseSpec +import genkai.redis.jedis.JedisConcurrentRateLimiterSpec +import zio.Task + +import scala.concurrent.Future + +class JedisZioConcurrentRateLimiterSpec + extends JedisConcurrentRateLimiterSpec[Task] + with ZioBaseSpec { + + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[Task] = + runtime.unsafeRun(JedisZioConcurrentRateLimiter.useClient(jedisPool, strategy)) + + override def toFuture[A](v: Task[A]): Future[A] = runtime.unsafeRunToFuture(v) +} diff --git a/modules/redis/jedis/zio/src/test/scala/genkai/redis/jedis/zio/JedisZioRateLimiterSpec.scala b/modules/redis/jedis/zio/src/test/scala/genkai/redis/jedis/zio/JedisZioRateLimiterSpec.scala index 407617e..228d2a0 100644 --- a/modules/redis/jedis/zio/src/test/scala/genkai/redis/jedis/zio/JedisZioRateLimiterSpec.scala +++ b/modules/redis/jedis/zio/src/test/scala/genkai/redis/jedis/zio/JedisZioRateLimiterSpec.scala @@ -3,11 +3,11 @@ package genkai.redis.jedis.zio import genkai.effect.zio.ZioBaseSpec import genkai.{RateLimiter, Strategy} import zio._ -import genkai.redis.jedis.JedisSpec +import genkai.redis.jedis.JedisRateLimiterSpec import scala.concurrent.Future -class JedisZioRateLimiterSpec extends JedisSpec[Task] with ZioBaseSpec { +class JedisZioRateLimiterSpec extends JedisRateLimiterSpec[Task] with ZioBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[Task] = runtime.unsafeRun(JedisZioRateLimiter.useClient(jedisPool, strategy)) diff --git a/modules/redis/lettuce/cats/src/main/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncConcurrentRateLimiter.scala b/modules/redis/lettuce/cats/src/main/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..2fc585d --- /dev/null +++ b/modules/redis/lettuce/cats/src/main/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncConcurrentRateLimiter.scala @@ -0,0 +1,97 @@ +package genkai.redis.lettuce.cats + +import cats.effect.{Concurrent, Resource} +import genkai.ConcurrentStrategy +import genkai.monad.syntax._ +import genkai.effect.cats.CatsMonadAsyncError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.lettuce.LettuceAsyncConcurrentRateLimiter +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulRedisConnection + +class LettuceCatsAsyncConcurrentRateLimiter[F[_]: Concurrent] private ( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String, + monad: CatsMonadAsyncError[F] +) extends LettuceAsyncConcurrentRateLimiter[F]( + client = client, + connection = connection, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + +object LettuceCatsAsyncConcurrentRateLimiter { + // blocking script loading + def useClient[F[_]: Concurrent]( + client: RedisClient, + strategy: ConcurrentStrategy + ): F[LettuceCatsAsyncConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadAsyncError[F] = new CatsMonadAsyncError[F]() + + val redisStrategy = RedisConcurrentStrategy(strategy) + + for { + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceCatsAsyncConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad + ) + } + + // blocking script loading + def resource[F[_]: Concurrent]( + redisUri: String, + strategy: ConcurrentStrategy + ): Resource[F, LettuceCatsAsyncConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadAsyncError[F] = new CatsMonadAsyncError[F]() + + val redisStrategy = RedisConcurrentStrategy(strategy) + + Resource.make { + for { + client <- monad.eval(RedisClient.create(redisUri)) + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceCatsAsyncConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad + ) + }(_.close()) + } +} diff --git a/modules/redis/lettuce/cats/src/main/scala/genkai/redis/lettuce/cats/LettuceCatsConcurrentRateLimiter.scala b/modules/redis/lettuce/cats/src/main/scala/genkai/redis/lettuce/cats/LettuceCatsConcurrentRateLimiter.scala new file mode 100644 index 0000000..80de6cc --- /dev/null +++ b/modules/redis/lettuce/cats/src/main/scala/genkai/redis/lettuce/cats/LettuceCatsConcurrentRateLimiter.scala @@ -0,0 +1,97 @@ +package genkai.redis.lettuce.cats + +import cats.effect.{Blocker, ContextShift, Resource, Sync} +import genkai.ConcurrentStrategy +import genkai.monad.syntax._ +import genkai.effect.cats.CatsMonadError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.lettuce.LettuceConcurrentRateLimiter +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulRedisConnection + +class LettuceCatsConcurrentRateLimiter[F[_]: Sync: ContextShift] private ( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String, + monad: CatsMonadError[F] +) extends LettuceConcurrentRateLimiter[F]( + client = client, + connection = connection, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + +object LettuceCatsConcurrentRateLimiter { + def useClient[F[_]: Sync: ContextShift]( + client: RedisClient, + strategy: ConcurrentStrategy, + blocker: Blocker + ): F[LettuceCatsConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadError[F] = new CatsMonadError[F](blocker) + + val redisStrategy = RedisConcurrentStrategy(strategy) + + for { + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceCatsConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad + ) + } + + def resource[F[_]: Sync: ContextShift]( + redisUri: String, + strategy: ConcurrentStrategy, + blocker: Blocker + ): Resource[F, LettuceCatsConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadError[F] = new CatsMonadError[F](blocker) + + val redisStrategy = RedisConcurrentStrategy(strategy) + + Resource.make { + for { + client <- monad.eval(RedisClient.create(redisUri)) + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceCatsConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad + ) + }(_.close()) + } +} diff --git a/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncConcurrentRateLimiterSpec.scala b/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..0879733 --- /dev/null +++ b/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.lettuce.cats + +import cats.effect.IO +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.cats.CatsBaseSpec +import genkai.redis.lettuce.LettuceConcurrentRateLimiterSpec + +import scala.concurrent.Future + +class LettuceCatsAsyncConcurrentRateLimiterSpec + extends LettuceConcurrentRateLimiterSpec[IO] + with CatsBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[IO] = + LettuceCatsAsyncConcurrentRateLimiter.useClient[IO](redisClient, strategy).unsafeRunSync() + + override def toFuture[A](v: IO[A]): Future[A] = v.unsafeToFuture() +} diff --git a/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncRateLimiterSpec.scala b/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncRateLimiterSpec.scala index 2c55199..89790ce 100644 --- a/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncRateLimiterSpec.scala +++ b/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsAsyncRateLimiterSpec.scala @@ -3,11 +3,11 @@ package genkai.redis.lettuce.cats import cats.effect.IO import genkai.effect.cats.CatsBaseSpec import genkai.{RateLimiter, Strategy} -import genkai.redis.lettuce.LettuceSpec +import genkai.redis.lettuce.LettuceRateLimiterSpec import scala.concurrent.Future -class LettuceCatsAsyncRateLimiterSpec extends LettuceSpec[IO] with CatsBaseSpec { +class LettuceCatsAsyncRateLimiterSpec extends LettuceRateLimiterSpec[IO] with CatsBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[IO] = LettuceCatsAsyncRateLimiter.useClient[IO](redisClient, strategy).unsafeRunSync() diff --git a/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsConcurrentRateLimiterSpec.scala b/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..4dd75fe --- /dev/null +++ b/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.lettuce.cats + +import cats.effect.IO +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.cats.CatsBaseSpec +import genkai.redis.lettuce.LettuceConcurrentRateLimiterSpec + +import scala.concurrent.Future + +class LettuceCatsConcurrentRateLimiterSpec + extends LettuceConcurrentRateLimiterSpec[IO] + with CatsBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[IO] = + LettuceCatsAsyncConcurrentRateLimiter.useClient[IO](redisClient, strategy).unsafeRunSync() + + override def toFuture[A](v: IO[A]): Future[A] = v.unsafeToFuture() +} diff --git a/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsRateLimiterSpec.scala b/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsRateLimiterSpec.scala index 03185b4..9851daa 100644 --- a/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsRateLimiterSpec.scala +++ b/modules/redis/lettuce/cats/src/test/scala/genkai/redis/lettuce/cats/LettuceCatsRateLimiterSpec.scala @@ -3,11 +3,11 @@ package genkai.redis.lettuce.cats import cats.effect.IO import genkai.effect.cats.CatsBaseSpec import genkai.{RateLimiter, Strategy} -import genkai.redis.lettuce.LettuceSpec +import genkai.redis.lettuce.LettuceRateLimiterSpec import scala.concurrent.Future -class LettuceCatsRateLimiterSpec extends LettuceSpec[IO] with CatsBaseSpec { +class LettuceCatsRateLimiterSpec extends LettuceRateLimiterSpec[IO] with CatsBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[IO] = LettuceCatsRateLimiter.useClient[IO](redisClient, strategy, blocker).unsafeRunSync() diff --git a/modules/redis/lettuce/monix/src/main/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncConcurrentRateLimiter.scala b/modules/redis/lettuce/monix/src/main/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..ec0da37 --- /dev/null +++ b/modules/redis/lettuce/monix/src/main/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncConcurrentRateLimiter.scala @@ -0,0 +1,97 @@ +package genkai.redis.lettuce.monix + +import cats.effect.Resource +import genkai.ConcurrentStrategy +import genkai.effect.monix.MonixMonadAsyncError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.lettuce.LettuceAsyncConcurrentRateLimiter +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulRedisConnection +import monix.eval.Task + +class LettuceMonixAsyncConcurrentRateLimiter private ( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String, + monad: MonixMonadAsyncError +) extends LettuceAsyncConcurrentRateLimiter[Task]( + client = client, + connection = connection, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object LettuceMonixAsyncConcurrentRateLimiter { + // blocking script loading + def useClient( + client: RedisClient, + strategy: ConcurrentStrategy + ): Task[LettuceMonixAsyncConcurrentRateLimiter] = { + implicit val monad: MonixMonadAsyncError = new MonixMonadAsyncError + + val redisStrategy = RedisConcurrentStrategy(strategy) + + for { + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceMonixAsyncConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad + ) + } + + // blocking script loading + def resource( + redisUri: String, + strategy: ConcurrentStrategy + ): Resource[Task, LettuceMonixAsyncConcurrentRateLimiter] = { + implicit val monad: MonixMonadAsyncError = new MonixMonadAsyncError + + val redisStrategy = RedisConcurrentStrategy(strategy) + + Resource.make { + for { + client <- monad.eval(RedisClient.create(redisUri)) + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceMonixAsyncConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad + ) + }(_.close()) + } +} diff --git a/modules/redis/lettuce/monix/src/test/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncConcurrentRateLimiterSpec.scala b/modules/redis/lettuce/monix/src/test/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..f74cd08 --- /dev/null +++ b/modules/redis/lettuce/monix/src/test/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.lettuce.monix + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.monix.MonixBaseSpec +import genkai.redis.lettuce.LettuceConcurrentRateLimiterSpec +import monix.eval.Task + +import scala.concurrent.Future + +class LettuceMonixAsyncConcurrentRateLimiterSpec + extends LettuceConcurrentRateLimiterSpec[Task] + with MonixBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[Task] = + LettuceMonixAsyncConcurrentRateLimiter.useClient(redisClient, strategy).runSyncUnsafe() + + override def toFuture[A](v: Task[A]): Future[A] = v.runToFuture +} diff --git a/modules/redis/lettuce/monix/src/test/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncRateLimiterSpec.scala b/modules/redis/lettuce/monix/src/test/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncRateLimiterSpec.scala index de9a0f4..8504863 100644 --- a/modules/redis/lettuce/monix/src/test/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncRateLimiterSpec.scala +++ b/modules/redis/lettuce/monix/src/test/scala/genkai/redis/lettuce/monix/LettuceMonixAsyncRateLimiterSpec.scala @@ -2,12 +2,12 @@ package genkai.redis.lettuce.monix import genkai.effect.monix.MonixBaseSpec import genkai.{RateLimiter, Strategy} -import genkai.redis.lettuce.LettuceSpec +import genkai.redis.lettuce.LettuceRateLimiterSpec import monix.eval.Task import scala.concurrent.Future -class LettuceMonixAsyncRateLimiterSpec extends LettuceSpec[Task] with MonixBaseSpec { +class LettuceMonixAsyncRateLimiterSpec extends LettuceRateLimiterSpec[Task] with MonixBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[Task] = LettuceMonixAsyncRateLimiter.useClient(redisClient, strategy).runSyncUnsafe() diff --git a/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceAsyncConcurrentRateLimiter.scala b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceAsyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..f325515 --- /dev/null +++ b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceAsyncConcurrentRateLimiter.scala @@ -0,0 +1,140 @@ +package genkai.redis.lettuce + +import java.time.Instant + +import genkai.monad.syntax._ +import genkai.{ConcurrentLimitExhausted, ConcurrentRateLimiter, Key, Logging} +import genkai.monad.{MonadAsyncError, MonadError} +import genkai.redis.RedisConcurrentStrategy +import io.lettuce.core.{RedisClient, ScriptOutputType} +import io.lettuce.core.api.StatefulRedisConnection + +abstract class LettuceAsyncConcurrentRateLimiter[F[_]]( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + implicit val monad: MonadAsyncError[F], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends ConcurrentRateLimiter[F] + with Logging[F] { + private val asyncCommands = connection.async() + + override private[genkai] def permissions[A: Key](key: A, instant: Instant): F[Long] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.permissionsArgs(instant) + + debug(s"Permissions request ($keyStr): $args") *> + monad + .cancelable[Long] { cb => + val cf = asyncCommands + .evalsha[Long]( + permissionsSha, + ScriptOutputType.INTEGER, + keyStr.toArray, + args: _* + ) + .whenComplete { (result: Long, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(result)) + } + + () => monad.eval(cf.toCompletableFuture.cancel(true)) + } + .map(tokens => strategy.toPermissions(tokens)) + } + + override def reset[A: Key](key: A): F[Unit] = { + val now = Instant.now() + val keyStr = strategy.keys(key, now) + debug(s"Reset limits for: $keyStr") *> + monad.cancelable[Unit] { cb => + val cf = asyncCommands.unlink(keyStr: _*).whenComplete { (_, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(())) + } + + () => monad.eval(cf.toCompletableFuture.cancel(true)) + } + } + + override private[genkai] def use[A: Key, B](key: A, instant: Instant)( + f: => F[B] + ): F[Either[ConcurrentLimitExhausted[A], B]] = monad.ifM(acquire(key, instant))( + ifTrue = monad.guarantee(f)(release(key, instant).void).map(r => Right(r)), + ifFalse = monad.pure(Left(ConcurrentLimitExhausted(key))) + ) + + override private[genkai] def release[A: Key](key: A, instant: Instant): F[Boolean] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.releaseArgs(instant) + + debug(s"Release request ($keyStr): $args") *> + monad + .cancelable[Long] { cb => + val cf = asyncCommands + .evalsha[Long]( + releaseSha, + ScriptOutputType.INTEGER, + keyStr.toArray, + args: _* + ) + .whenComplete { (result: Long, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(result)) + } + + () => monad.eval(cf.toCompletableFuture.cancel(true)) + } + .map(tokens => strategy.isAllowed(tokens)) + } + + override private[genkai] def acquire[A: Key](key: A, instant: Instant): F[Boolean] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.acquireArgs(instant) + + debug(s"Acquire request ($keyStr): $args") *> + monad + .cancelable[Long] { cb => + val cf = asyncCommands + .evalsha[Long]( + acquireSha, + ScriptOutputType.INTEGER, + keyStr.toArray, + args: _* + ) + .whenComplete { (result: Long, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(result)) + } + + () => monad.eval(cf.toCompletableFuture.cancel(true)) + } + .map(tokens => strategy.isAllowed(tokens)) + } + + override def close(): F[Unit] = + monad.ifM(monad.pure(closeClient))( + monad.cancelable[Unit] { cb => + val cf = connection.closeAsync().thenCompose(_ => client.shutdownAsync()).whenComplete { + (_: Void, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(())) + } + + () => monad.eval(cf.toCompletableFuture.cancel(true)) + }, + monad.cancelable[Unit] { cb => + val cf = connection.closeAsync().whenComplete { (_: Void, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(())) + } + + () => monad.eval(cf.toCompletableFuture.cancel(true)) + } + ) + + override def monadError: MonadError[F] = monad +} diff --git a/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceAsyncRateLimiter.scala b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceAsyncRateLimiter.scala index 100e46c..28ad886 100644 --- a/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceAsyncRateLimiter.scala +++ b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceAsyncRateLimiter.scala @@ -84,7 +84,7 @@ abstract class LettuceAsyncRateLimiter[F[_]]( } override def close(): F[Unit] = - monad.ifA(monad.pure(closeClient))( + monad.ifM(monad.pure(closeClient))( monad.cancelable[Unit] { cb => val cf = connection.closeAsync().thenCompose(_ => client.shutdownAsync()).whenComplete { (_: Void, err: Throwable) => @@ -104,5 +104,5 @@ abstract class LettuceAsyncRateLimiter[F[_]]( } ) - override protected def monadError: MonadError[F] = monad + override def monadError: MonadError[F] = monad } diff --git a/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceConcurrentRateLimiter.scala b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceConcurrentRateLimiter.scala new file mode 100644 index 0000000..6c18244 --- /dev/null +++ b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceConcurrentRateLimiter.scala @@ -0,0 +1,97 @@ +package genkai.redis.lettuce + +import java.time.Instant + +import genkai.monad.syntax._ +import genkai.{ConcurrentLimitExhausted, ConcurrentRateLimiter, Key, Logging} +import genkai.monad.MonadError +import genkai.redis.RedisConcurrentStrategy +import io.lettuce.core.{RedisClient, ScriptOutputType} +import io.lettuce.core.api.StatefulRedisConnection + +abstract class LettuceConcurrentRateLimiter[F[_]]( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + implicit val monad: MonadError[F], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends ConcurrentRateLimiter[F] + with Logging[F] { + + private val syncCommands = connection.sync() + + override private[genkai] def permissions[A: Key](key: A, instant: Instant): F[Long] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.permissionsArgs(instant) + + debug(s"Permissions request ($keyStr): $args") *> monad + .eval( + syncCommands.evalsha[Long]( + permissionsSha, + ScriptOutputType.INTEGER, + keyStr.toArray, + args: _* + ) + ) + .map(tokens => strategy.toPermissions(tokens)) + } + + override def reset[A: Key](key: A): F[Unit] = { + val now = Instant.now() + val keyStr = strategy.keys(key, now) + debug(s"Reset limits for: $keyStr") *> + monad.eval(syncCommands.unlink(keyStr: _*)).void + } + + override private[genkai] def use[A: Key, B](key: A, instant: Instant)( + f: => F[B] + ): F[Either[ConcurrentLimitExhausted[A], B]] = + monad.ifM(acquire(key, instant))( + ifTrue = monad.guarantee(f)(release(key, instant).void).map(r => Right(r)), + ifFalse = monad.pure(Left(ConcurrentLimitExhausted(key))) + ) + + override private[genkai] def release[A: Key](key: A, instant: Instant): F[Boolean] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.releaseArgs(instant) + + debug(s"Release request ($keyStr): $args") *> monad + .eval( + syncCommands.evalsha[Long]( + releaseSha, + ScriptOutputType.INTEGER, + keyStr.toArray, + args: _* + ) + ) + .map(tokens => strategy.isReleased(tokens)) + } + + override private[genkai] def acquire[A: Key](key: A, instant: Instant): F[Boolean] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.acquireArgs(instant) + + debug(s"Acquire request ($keyStr): $args") *> monad + .eval( + syncCommands.evalsha[Long]( + acquireSha, + ScriptOutputType.INTEGER, + keyStr.toArray, + args: _* + ) + ) + .map(tokens => strategy.isAllowed(tokens)) + } + + override def close(): F[Unit] = + monad.ifM(monad.pure(closeClient))( + monad.eval(connection.close()) *> + monad.eval(client.shutdown()), + monad.eval(connection.close()) + ) + + override def monadError: MonadError[F] = monad +} diff --git a/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceFutureConcurrentRateLimiter.scala b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceFutureConcurrentRateLimiter.scala new file mode 100644 index 0000000..257f07d --- /dev/null +++ b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceFutureConcurrentRateLimiter.scala @@ -0,0 +1,92 @@ +package genkai.redis.lettuce + +import genkai.ConcurrentStrategy +import genkai.monad.{FutureMonadAsyncError, IdMonadError} +import genkai.redis.RedisConcurrentStrategy +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulRedisConnection + +import scala.concurrent.{ExecutionContext, Future} + +class LettuceFutureConcurrentRateLimiter private ( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +)(implicit ec: ExecutionContext) + extends LettuceAsyncConcurrentRateLimiter[Future]( + client = client, + connection = connection, + monad = new FutureMonadAsyncError(), + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + +object LettuceFutureConcurrentRateLimiter { + + /** client initialization is blocking */ + def apply( + client: RedisClient, + strategy: ConcurrentStrategy + )(implicit ec: ExecutionContext): LettuceFutureConcurrentRateLimiter = { + val monad = IdMonadError + val redisStrategy = RedisConcurrentStrategy(strategy) + + val connection = client.connect() + val command = connection.sync() + + val (acquireSha, releaseSha, permissionsSha) = monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + + new LettuceFutureConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = false, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + )(ec) + } + + /** client initialization is blocking */ + def apply(redisUri: String, strategy: ConcurrentStrategy)(implicit + ec: ExecutionContext + ): LettuceFutureConcurrentRateLimiter = { + val monad = IdMonadError + val redisStrategy = RedisConcurrentStrategy(strategy) + + val client = RedisClient.create(redisUri) + val connection = client.connect() + val command = connection.sync() + + val (acquireSha, releaseSha, permissionsSha) = monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + + new LettuceFutureConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = true, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + )(ec) + } +} diff --git a/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceRateLimiter.scala b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceRateLimiter.scala index 6932757..6139130 100644 --- a/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceRateLimiter.scala +++ b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceRateLimiter.scala @@ -63,11 +63,11 @@ abstract class LettuceRateLimiter[F[_]]( } override def close(): F[Unit] = - monad.ifA(monad.pure(closeClient))( + monad.ifM(monad.pure(closeClient))( monad.eval(connection.close()) *> monad.eval(client.shutdown()), monad.eval(connection.close()) ) - override protected def monadError: MonadError[F] = monad + override def monadError: MonadError[F] = monad } diff --git a/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceSyncConcurrentRateLimiter.scala b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceSyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..183991e --- /dev/null +++ b/modules/redis/lettuce/src/main/scala/genkai/redis/lettuce/LettuceSyncConcurrentRateLimiter.scala @@ -0,0 +1,87 @@ +package genkai.redis.lettuce + +import genkai.{ConcurrentStrategy, Identity} +import genkai.monad.IdMonadError +import genkai.redis.RedisConcurrentStrategy +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulRedisConnection + +class LettuceSyncConcurrentRateLimiter private ( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends LettuceConcurrentRateLimiter[Identity]( + client, + connection, + monad = IdMonadError, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + +object LettuceSyncConcurrentRateLimiter { + def apply( + client: RedisClient, + strategy: ConcurrentStrategy + ): LettuceSyncConcurrentRateLimiter = { + val monad = IdMonadError + val redisStrategy = RedisConcurrentStrategy(strategy) + + val connection = client.connect() + val command = connection.sync() + + val (acquireSha, releaseSha, permissionsSha) = monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + + new LettuceSyncConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = false, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + } + + def apply( + redisUri: String, + strategy: ConcurrentStrategy + ): LettuceSyncConcurrentRateLimiter = { + val monad = IdMonadError + val redisStrategy = RedisConcurrentStrategy(strategy) + + val client = RedisClient.create(redisUri) + val connection = client.connect() + val command = connection.sync() + + val (acquireSha, releaseSha, permissionsSha) = monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + + new LettuceSyncConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = true, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + } +} diff --git a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceConcurrentRateLimiterSpec.scala b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..75e0c5a --- /dev/null +++ b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceConcurrentRateLimiterSpec.scala @@ -0,0 +1,30 @@ +package genkai.redis.lettuce + +import genkai.redis.{RedisConcurrentRateLimiterSpecForAll, RedisContainer} +import io.lettuce.core.RedisClient +import io.lettuce.core.resource.DefaultClientResources + +trait LettuceConcurrentRateLimiterSpec[F[_]] extends RedisConcurrentRateLimiterSpecForAll[F] { + var redisClient: RedisClient = _ + + override def afterContainersStart(redis: RedisContainer): Unit = { + val clientResources = + DefaultClientResources.builder().ioThreadPoolSize(2).computationThreadPoolSize(2).build() + redisClient = RedisClient.create( + clientResources, + s"redis://${redis.containerIpAddress}:${redis.mappedPort(6379)}" + ) + } + + override protected def afterAll(): Unit = { + redisClient.shutdown() + super.afterAll() + } + + override protected def afterEach(): Unit = { + super.afterEach() + val connection = redisClient.connect() + try connection.sync().flushall() + finally connection.close() + } +} diff --git a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceFutureConcurrentRateLimiterSpec.scala b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceFutureConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..243a12e --- /dev/null +++ b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceFutureConcurrentRateLimiterSpec.scala @@ -0,0 +1,14 @@ +package genkai.redis.lettuce + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} + +import scala.concurrent.Future + +class LettuceFutureConcurrentRateLimiterSpec extends LettuceConcurrentRateLimiterSpec[Future] { + override def concurrentRateLimiter( + strategy: ConcurrentStrategy + ): ConcurrentRateLimiter[Future] = + LettuceFutureConcurrentRateLimiter(redisClient, strategy) + + override def toFuture[A](v: Future[A]): Future[A] = v +} diff --git a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceFutureRateLimiterSpec.scala b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceFutureRateLimiterSpec.scala index d081afa..49d986c 100644 --- a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceFutureRateLimiterSpec.scala +++ b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceFutureRateLimiterSpec.scala @@ -4,7 +4,7 @@ import genkai.{RateLimiter, Strategy} import scala.concurrent.Future -class LettuceFutureRateLimiterSpec extends LettuceSpec[Future] { +class LettuceFutureRateLimiterSpec extends LettuceRateLimiterSpec[Future] { override def rateLimiter(strategy: Strategy): RateLimiter[Future] = LettuceFutureRateLimiter(redisClient, strategy) diff --git a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceRateLimiterSpec.scala b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceRateLimiterSpec.scala new file mode 100644 index 0000000..ceb10b8 --- /dev/null +++ b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceRateLimiterSpec.scala @@ -0,0 +1,30 @@ +package genkai.redis.lettuce + +import genkai.redis.{RedisContainer, RedisRateLimiterSpecForAll} +import io.lettuce.core.RedisClient +import io.lettuce.core.resource.DefaultClientResources + +trait LettuceRateLimiterSpec[F[_]] extends RedisRateLimiterSpecForAll[F] { + var redisClient: RedisClient = _ + + override def afterContainersStart(redis: RedisContainer): Unit = { + val clientResources = + DefaultClientResources.builder().ioThreadPoolSize(2).computationThreadPoolSize(2).build() + redisClient = RedisClient.create( + clientResources, + s"redis://${redis.containerIpAddress}:${redis.mappedPort(6379)}" + ) + } + + override protected def afterAll(): Unit = { + redisClient.shutdown() + super.afterAll() + } + + override protected def afterEach(): Unit = { + super.afterEach() + val connection = redisClient.connect() + try connection.sync().flushall() + finally connection.close() + } +} diff --git a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSpec.scala b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSpec.scala deleted file mode 100644 index 1592feb..0000000 --- a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSpec.scala +++ /dev/null @@ -1,24 +0,0 @@ -package genkai.redis.lettuce - -import genkai.redis.{RedisContainer, RedisSpecForAll} -import io.lettuce.core.RedisClient - -trait LettuceSpec[F[_]] extends RedisSpecForAll[F] { - var redisClient: RedisClient = _ - - override def afterContainersStart(redis: RedisContainer): Unit = - redisClient = - RedisClient.create(s"redis://${redis.containerIpAddress}:${redis.mappedPort(6379)}") - - override protected def afterAll(): Unit = { - redisClient.shutdown() - super.afterAll() - } - - override protected def afterEach(): Unit = { - super.afterEach() - val connection = redisClient.connect() - try connection.sync().flushall() - finally connection.close() - } -} diff --git a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSyncConcurrentRateLimiterSpec.scala b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSyncConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..7283555 --- /dev/null +++ b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSyncConcurrentRateLimiterSpec.scala @@ -0,0 +1,14 @@ +package genkai.redis.lettuce + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy, Identity} + +import scala.concurrent.Future + +class LettuceSyncConcurrentRateLimiterSpec extends LettuceConcurrentRateLimiterSpec[Identity] { + override def concurrentRateLimiter( + strategy: ConcurrentStrategy + ): ConcurrentRateLimiter[Identity] = + LettuceSyncConcurrentRateLimiter(redisClient, strategy) + + override def toFuture[A](v: Identity[A]): Future[A] = Future.successful(v) +} diff --git a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSyncRateLimiterSpec.scala b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSyncRateLimiterSpec.scala index f2f26a4..6a992f1 100644 --- a/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSyncRateLimiterSpec.scala +++ b/modules/redis/lettuce/src/test/scala/genkai/redis/lettuce/LettuceSyncRateLimiterSpec.scala @@ -4,7 +4,7 @@ import genkai.{Identity, RateLimiter, Strategy} import scala.concurrent.Future -class LettuceSyncRateLimiterSpec extends LettuceSpec[Identity] { +class LettuceSyncRateLimiterSpec extends LettuceRateLimiterSpec[Identity] { override def rateLimiter(strategy: Strategy): RateLimiter[Identity] = LettuceSyncRateLimiter(redisClient, strategy) diff --git a/modules/redis/lettuce/zio/src/main/scala/genkai/redis/lettuce/zio/LettuceZioAsyncConcurrentRateLimiter.scala b/modules/redis/lettuce/zio/src/main/scala/genkai/redis/lettuce/zio/LettuceZioAsyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..12b6bc4 --- /dev/null +++ b/modules/redis/lettuce/zio/src/main/scala/genkai/redis/lettuce/zio/LettuceZioAsyncConcurrentRateLimiter.scala @@ -0,0 +1,104 @@ +package genkai.redis.lettuce.zio + +import genkai.ConcurrentStrategy +import genkai.effect.zio.ZioMonadAsyncError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.lettuce.LettuceAsyncConcurrentRateLimiter +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulRedisConnection +import zio.{Has, Task, ZIO, ZLayer, ZManaged} + +class LettuceZioAsyncConcurrentRateLimiter private ( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String, + monad: ZioMonadAsyncError +) extends LettuceAsyncConcurrentRateLimiter[Task]( + client = client, + connection = connection, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object LettuceZioAsyncConcurrentRateLimiter { + def useClient( + client: RedisClient, + strategy: ConcurrentStrategy + ): ZIO[Any, Throwable, LettuceZioAsyncConcurrentRateLimiter] = { + val monad = new ZioMonadAsyncError() + val redisStrategy = RedisConcurrentStrategy(strategy) + + for { + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceZioAsyncConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad = monad + ) + } + + def layerUsingClient( + client: RedisClient, + strategy: ConcurrentStrategy + ): ZLayer[Any, Throwable, Has[LettuceZioAsyncConcurrentRateLimiter]] = + useClient(client, strategy).toLayer + + def managed( + redisUri: String, + strategy: ConcurrentStrategy + ): ZManaged[Any, Throwable, LettuceZioAsyncConcurrentRateLimiter] = { + val monad = new ZioMonadAsyncError() + val redisStrategy = RedisConcurrentStrategy(strategy) + + ZManaged.make { + for { + client <- monad.eval(RedisClient.create(redisUri)) + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceZioAsyncConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad = monad + ) + }(limiter => limiter.close().orDie) + } + + def layerFromManaged( + redisUri: String, + strategy: ConcurrentStrategy + ): ZLayer[Any, Throwable, Has[LettuceZioAsyncConcurrentRateLimiter]] = + ZLayer.fromManaged(managed(redisUri, strategy)) +} diff --git a/modules/redis/lettuce/zio/src/main/scala/genkai/redis/lettuce/zio/LettuceZioConcurrentRateLimiter.scala b/modules/redis/lettuce/zio/src/main/scala/genkai/redis/lettuce/zio/LettuceZioConcurrentRateLimiter.scala new file mode 100644 index 0000000..f98003b --- /dev/null +++ b/modules/redis/lettuce/zio/src/main/scala/genkai/redis/lettuce/zio/LettuceZioConcurrentRateLimiter.scala @@ -0,0 +1,104 @@ +package genkai.redis.lettuce.zio + +import genkai.ConcurrentStrategy +import genkai.effect.zio.ZioMonadError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.lettuce.LettuceConcurrentRateLimiter +import io.lettuce.core.RedisClient +import io.lettuce.core.api.StatefulRedisConnection +import zio._ +import zio.blocking.{Blocking, blocking} + +class LettuceZioConcurrentRateLimiter private ( + client: RedisClient, + connection: StatefulRedisConnection[String, String], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String, + monad: ZioMonadError +) extends LettuceConcurrentRateLimiter[Task]( + client = client, + connection = connection, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object LettuceZioConcurrentRateLimiter { + def useClient( + client: RedisClient, + strategy: ConcurrentStrategy + ): ZIO[Blocking, Throwable, LettuceZioConcurrentRateLimiter] = for { + blocker <- ZIO.service[Blocking.Service] + monad = new ZioMonadError(blocker) + redisStrategy = RedisConcurrentStrategy(strategy) + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceZioConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad = monad + ) + + def layerUsingClient( + client: RedisClient, + strategy: ConcurrentStrategy + ): ZLayer[Blocking, Throwable, Has[LettuceZioConcurrentRateLimiter]] = + useClient(client, strategy).toLayer + + def managed( + redisUri: String, + strategy: ConcurrentStrategy + ): ZManaged[Blocking, Throwable, LettuceZioConcurrentRateLimiter] = + ZManaged.make { + for { + blocker <- ZIO.service[Blocking.Service] + monad = new ZioMonadError(blocker) + client <- monad.eval(RedisClient.create(redisUri)) + redisStrategy = RedisConcurrentStrategy(strategy) + connection <- monad.eval(client.connect()) + command = connection.sync() + sha <- monad.eval { + ( + command.scriptLoad(redisStrategy.acquireLuaScript), + command.scriptLoad(redisStrategy.releaseLuaScript), + command.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new LettuceZioConcurrentRateLimiter( + client = client, + connection = connection, + strategy = redisStrategy, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3, + monad = monad + ) + } { limiter => + blocking(limiter.close().ignore) + } + + def layerFromManaged( + redisUri: String, + strategy: ConcurrentStrategy + ): ZLayer[Blocking, Throwable, Has[LettuceZioConcurrentRateLimiter]] = + ZLayer.fromManaged(managed(redisUri, strategy)) +} diff --git a/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioAsyncConcurrentRateLimiterSpec.scala b/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioAsyncConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..df46b78 --- /dev/null +++ b/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioAsyncConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.lettuce.zio + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.zio.ZioBaseSpec +import genkai.redis.lettuce.LettuceConcurrentRateLimiterSpec +import zio.Task + +import scala.concurrent.Future + +class LettuceZioAsyncConcurrentRateLimiterSpec + extends LettuceConcurrentRateLimiterSpec[Task] + with ZioBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[Task] = + runtime.unsafeRun(LettuceZioAsyncConcurrentRateLimiter.useClient(redisClient, strategy)) + + override def toFuture[A](v: Task[A]): Future[A] = runtime.unsafeRunToFuture(v) +} diff --git a/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioAsyncRateLimiterSpec.scala b/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioAsyncRateLimiterSpec.scala index ef60952..199633d 100644 --- a/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioAsyncRateLimiterSpec.scala +++ b/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioAsyncRateLimiterSpec.scala @@ -2,12 +2,12 @@ package genkai.redis.lettuce.zio import genkai.{RateLimiter, Strategy} import genkai.effect.zio.ZioBaseSpec -import genkai.redis.lettuce.LettuceSpec +import genkai.redis.lettuce.LettuceRateLimiterSpec import zio._ import scala.concurrent.Future -class LettuceZioAsyncRateLimiterSpec extends LettuceSpec[Task] with ZioBaseSpec { +class LettuceZioAsyncRateLimiterSpec extends LettuceRateLimiterSpec[Task] with ZioBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[Task] = runtime.unsafeRun(LettuceZioAsyncRateLimiter.useClient(redisClient, strategy)) diff --git a/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioConcurrentRateLimiterSpec.scala b/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..5a542ae --- /dev/null +++ b/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.lettuce.zio + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.zio.ZioBaseSpec +import genkai.redis.lettuce.LettuceConcurrentRateLimiterSpec +import zio.Task + +import scala.concurrent.Future + +class LettuceZioConcurrentRateLimiterSpec + extends LettuceConcurrentRateLimiterSpec[Task] + with ZioBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[Task] = + runtime.unsafeRun(LettuceZioConcurrentRateLimiter.useClient(redisClient, strategy)) + + override def toFuture[A](v: Task[A]): Future[A] = runtime.unsafeRunToFuture(v) +} diff --git a/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioRateLimiterSpec.scala b/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioRateLimiterSpec.scala index 9b619a1..73d5a3a 100644 --- a/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioRateLimiterSpec.scala +++ b/modules/redis/lettuce/zio/src/test/scala/genkai/redis/lettuce/zio/LettuceZioRateLimiterSpec.scala @@ -2,12 +2,12 @@ package genkai.redis.lettuce.zio import genkai.{RateLimiter, Strategy} import genkai.effect.zio.ZioBaseSpec -import genkai.redis.lettuce.LettuceSpec +import genkai.redis.lettuce.LettuceRateLimiterSpec import zio.Task import scala.concurrent.Future -class LettuceZioRateLimiterSpec extends LettuceSpec[Task] with ZioBaseSpec { +class LettuceZioRateLimiterSpec extends LettuceRateLimiterSpec[Task] with ZioBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[Task] = runtime.unsafeRun(LettuceZioRateLimiter.useClient(redisClient, strategy)) diff --git a/modules/redis/redisson/cats/src/main/scala/genkai/redis/redisson/cats/RedissonCatsAsyncConcurrentRateLimiter.scala b/modules/redis/redisson/cats/src/main/scala/genkai/redis/redisson/cats/RedissonCatsAsyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..df91006 --- /dev/null +++ b/modules/redis/redisson/cats/src/main/scala/genkai/redis/redisson/cats/RedissonCatsAsyncConcurrentRateLimiter.scala @@ -0,0 +1,90 @@ +package genkai.redis.redisson.cats + +import cats.effect.{Concurrent, Resource} +import genkai.ConcurrentStrategy +import genkai.monad.syntax._ +import genkai.effect.cats.CatsMonadAsyncError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.redisson.RedissonAsyncConcurrentRateLimiter +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config + +class RedissonCatsAsyncConcurrentRateLimiter[F[_]: Concurrent] private ( + client: RedissonClient, + monad: CatsMonadAsyncError[F], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends RedissonAsyncConcurrentRateLimiter[F]( + client = client, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object RedissonCatsAsyncConcurrentRateLimiter { + // blocking script loading + def useClient[F[_]: Concurrent]( + client: RedissonClient, + strategy: ConcurrentStrategy + ): F[RedissonCatsAsyncConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadAsyncError[F] = new CatsMonadAsyncError[F]() + + val redisStrategy = RedisConcurrentStrategy(strategy) + + for { + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonCatsAsyncConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + } + + // blocking script loading + def resource[F[_]: Concurrent]( + config: Config, + strategy: ConcurrentStrategy + ): Resource[F, RedissonCatsAsyncConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadAsyncError[F] = new CatsMonadAsyncError[F]() + + val redisStrategy = RedisConcurrentStrategy(strategy) + + Resource.make { + for { + client <- monad.eval(Redisson.create(config)) + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonCatsAsyncConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + }(_.close()) + } +} diff --git a/modules/redis/redisson/cats/src/main/scala/genkai/redis/redisson/cats/RedissonCatsConcurrentRateLimiter.scala b/modules/redis/redisson/cats/src/main/scala/genkai/redis/redisson/cats/RedissonCatsConcurrentRateLimiter.scala new file mode 100644 index 0000000..e8fb9f5 --- /dev/null +++ b/modules/redis/redisson/cats/src/main/scala/genkai/redis/redisson/cats/RedissonCatsConcurrentRateLimiter.scala @@ -0,0 +1,90 @@ +package genkai.redis.redisson.cats + +import cats.effect.{Blocker, ContextShift, Resource, Sync} +import genkai.ConcurrentStrategy +import genkai.monad.syntax._ +import genkai.effect.cats.CatsMonadError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.redisson.RedissonConcurrentRateLimiter +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config + +class RedissonCatsConcurrentRateLimiter[F[_]: Sync: ContextShift] private ( + client: RedissonClient, + monad: CatsMonadError[F], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends RedissonConcurrentRateLimiter[F]( + client = client, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object RedissonCatsConcurrentRateLimiter { + def useClient[F[_]: Sync: ContextShift]( + client: RedissonClient, + strategy: ConcurrentStrategy, + blocker: Blocker + ): F[RedissonCatsConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadError[F] = new CatsMonadError[F](blocker) + + val redisStrategy = RedisConcurrentStrategy(strategy) + + for { + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonCatsConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + } + + def resource[F[_]: Sync: ContextShift]( + config: Config, + strategy: ConcurrentStrategy, + blocker: Blocker + ): Resource[F, RedissonCatsConcurrentRateLimiter[F]] = { + implicit val monad: CatsMonadError[F] = new CatsMonadError[F](blocker) + + val redisStrategy = RedisConcurrentStrategy(strategy) + + Resource.make { + for { + client <- monad.eval(Redisson.create(config)) + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonCatsConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + }(_.close()) + } +} diff --git a/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsAsyncConcurrentRateLimiterSpec.scala b/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsAsyncConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..9160c5d --- /dev/null +++ b/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsAsyncConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.redisson.cats + +import cats.effect.IO +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.cats.CatsBaseSpec +import genkai.redis.redisson.RedissonConcurrentRateLimiterSpec + +import scala.concurrent.Future + +class RedissonCatsAsyncConcurrentRateLimiterSpec + extends RedissonConcurrentRateLimiterSpec[IO] + with CatsBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[IO] = + RedissonCatsAsyncConcurrentRateLimiter.useClient[IO](redisClient, strategy).unsafeRunSync() + + override def toFuture[A](v: IO[A]): Future[A] = v.unsafeToFuture() +} diff --git a/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsAsyncRateLimiterSpec.scala b/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsAsyncRateLimiterSpec.scala index 034db6c..5a8ef96 100644 --- a/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsAsyncRateLimiterSpec.scala +++ b/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsAsyncRateLimiterSpec.scala @@ -3,11 +3,11 @@ package genkai.redis.redisson.cats import cats.effect.IO import genkai.{RateLimiter, Strategy} import genkai.effect.cats.CatsBaseSpec -import genkai.redis.redisson.RedissonSpec +import genkai.redis.redisson.RedissonRateLimiterSpec import scala.concurrent.Future -class RedissonCatsAsyncRateLimiterSpec extends RedissonSpec[IO] with CatsBaseSpec { +class RedissonCatsAsyncRateLimiterSpec extends RedissonRateLimiterSpec[IO] with CatsBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[IO] = RedissonCatsAsyncRateLimiter.useClient[IO](redisClient, strategy).unsafeRunSync() diff --git a/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsConcurrentRateLimiterSpec.scala b/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..8fad030 --- /dev/null +++ b/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.redisson.cats + +import cats.effect.IO +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.cats.CatsBaseSpec +import genkai.redis.redisson.RedissonConcurrentRateLimiterSpec + +import scala.concurrent.Future + +class RedissonCatsConcurrentRateLimiterSpec + extends RedissonConcurrentRateLimiterSpec[IO] + with CatsBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[IO] = + RedissonCatsConcurrentRateLimiter.useClient[IO](redisClient, strategy, blocker).unsafeRunSync() + + override def toFuture[A](v: IO[A]): Future[A] = v.unsafeToFuture() +} diff --git a/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsRateLimiterSpec.scala b/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsRateLimiterSpec.scala index e4f7e49..a5cf10f 100644 --- a/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsRateLimiterSpec.scala +++ b/modules/redis/redisson/cats/src/test/scala/genkai/redis/redisson/cats/RedissonCatsRateLimiterSpec.scala @@ -3,11 +3,11 @@ package genkai.redis.redisson.cats import cats.effect.IO import genkai.{RateLimiter, Strategy} import genkai.effect.cats.CatsBaseSpec -import genkai.redis.redisson.RedissonSpec +import genkai.redis.redisson.RedissonRateLimiterSpec import scala.concurrent.Future -class RedissonCatsRateLimiterSpec extends RedissonSpec[IO] with CatsBaseSpec { +class RedissonCatsRateLimiterSpec extends RedissonRateLimiterSpec[IO] with CatsBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[IO] = RedissonCatsRateLimiter.useClient[IO](redisClient, strategy, blocker).unsafeRunSync() diff --git a/modules/redis/redisson/monix/src/main/scala/genkai/redis/redisson/monix/RedissonMonixAsyncConcurrentRateLimiter.scala b/modules/redis/redisson/monix/src/main/scala/genkai/redis/redisson/monix/RedissonMonixAsyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..954dd6c --- /dev/null +++ b/modules/redis/redisson/monix/src/main/scala/genkai/redis/redisson/monix/RedissonMonixAsyncConcurrentRateLimiter.scala @@ -0,0 +1,90 @@ +package genkai.redis.redisson.monix + +import cats.effect.Resource +import genkai.ConcurrentStrategy +import genkai.effect.monix.MonixMonadAsyncError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.redisson.RedissonAsyncConcurrentRateLimiter +import monix.eval.Task +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config + +class RedissonMonixAsyncConcurrentRateLimiter private ( + client: RedissonClient, + monad: MonixMonadAsyncError, + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends RedissonAsyncConcurrentRateLimiter[Task]( + client = client, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object RedissonMonixAsyncConcurrentRateLimiter { + // blocking script loading + def useClient( + client: RedissonClient, + strategy: ConcurrentStrategy + ): Task[RedissonMonixAsyncConcurrentRateLimiter] = { + implicit val monad: MonixMonadAsyncError = new MonixMonadAsyncError() + + val redisStrategy = RedisConcurrentStrategy(strategy) + + for { + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonMonixAsyncConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + } + + // blocking script loading + def resource( + config: Config, + strategy: ConcurrentStrategy + ): Resource[Task, RedissonMonixAsyncConcurrentRateLimiter] = { + implicit val monad: MonixMonadAsyncError = new MonixMonadAsyncError() + + val redisStrategy = RedisConcurrentStrategy(strategy) + + Resource.make { + for { + client <- monad.eval(Redisson.create(config)) + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonMonixAsyncConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + }(_.close()) + } +} diff --git a/modules/redis/redisson/monix/src/test/scala/genkai/redis/redisson/monix/RedissonMonixAsyncRateLimiterSpec.scala b/modules/redis/redisson/monix/src/test/scala/genkai/redis/redisson/monix/RedissonMonixAsyncRateLimiterSpec.scala index ea259f6..99d32c8 100644 --- a/modules/redis/redisson/monix/src/test/scala/genkai/redis/redisson/monix/RedissonMonixAsyncRateLimiterSpec.scala +++ b/modules/redis/redisson/monix/src/test/scala/genkai/redis/redisson/monix/RedissonMonixAsyncRateLimiterSpec.scala @@ -2,12 +2,12 @@ package genkai.redis.redisson.monix import genkai.{RateLimiter, Strategy} import genkai.effect.monix.MonixBaseSpec -import genkai.redis.redisson.RedissonSpec +import genkai.redis.redisson.RedissonRateLimiterSpec import monix.eval.Task import scala.concurrent.Future -class RedissonMonixAsyncRateLimiterSpec extends RedissonSpec[Task] with MonixBaseSpec { +class RedissonMonixAsyncRateLimiterSpec extends RedissonRateLimiterSpec[Task] with MonixBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[Task] = RedissonMonixAsyncRateLimiter.useClient(redisClient, strategy).runSyncUnsafe() diff --git a/modules/redis/redisson/monix/src/test/scala/genkai/redis/redisson/monix/RedissonMonixConcurrentRateLimiterSpec.scala b/modules/redis/redisson/monix/src/test/scala/genkai/redis/redisson/monix/RedissonMonixConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..bf11fab --- /dev/null +++ b/modules/redis/redisson/monix/src/test/scala/genkai/redis/redisson/monix/RedissonMonixConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.redisson.monix + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.monix.MonixBaseSpec +import genkai.redis.redisson.RedissonConcurrentRateLimiterSpec +import monix.eval.Task + +import scala.concurrent.Future + +class RedissonMonixConcurrentRateLimiterSpec + extends RedissonConcurrentRateLimiterSpec[Task] + with MonixBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[Task] = + RedissonMonixAsyncConcurrentRateLimiter.useClient(redisClient, strategy).runSyncUnsafe() + + override def toFuture[A](v: Task[A]): Future[A] = v.runToFuture +} diff --git a/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonAsyncConcurrentRateLimiter.scala b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonAsyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..fe73952 --- /dev/null +++ b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonAsyncConcurrentRateLimiter.scala @@ -0,0 +1,142 @@ +package genkai.redis.redisson + +import java.time.Instant + +import genkai.monad.syntax._ +import genkai.{ConcurrentLimitExhausted, ConcurrentRateLimiter, Key, Logging} +import genkai.redis.RedisConcurrentStrategy +import genkai.monad.{MonadAsyncError, MonadError} +import org.redisson.api.{RFuture, RScript, RedissonClient} +import org.redisson.client.codec.StringCodec + +import scala.collection.JavaConverters._ + +abstract class RedissonAsyncConcurrentRateLimiter[F[_]]( + client: RedissonClient, + implicit val monad: MonadAsyncError[F], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends ConcurrentRateLimiter[F] + with Logging[F] { + /* to avoid unnecessary memory allocations */ + private val scriptCommand: RScript = client.getScript(new StringCodec) + + override private[genkai] def permissions[A: Key](key: A, instant: Instant): F[Long] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.permissionsArgs(instant) + + debug(s"Permissions request ($keyStr): $args") *> + monad + .cancelable[Long] { cb => + val cf = evalShaAsync( + permissionsSha, + new java.util.LinkedList[Object](keyStr.asJava), + args + ) + + cf.onComplete { (res: Long, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(res)) + } + + () => monad.eval(cf.cancel(true)) + } + .map(tokens => strategy.toPermissions(tokens)) + } + + override def reset[A: Key](key: A): F[Unit] = { + val now = Instant.now() + val keyStr = strategy.keys(key, now) + + debug(s"Reset limits for: $keyStr") *> + monad + .cancelable[Unit] { cb => + val cf = client.getKeys.unlinkAsync(keyStr: _*) + + cf.onComplete { (_, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(())) + } + + () => monad.eval(cf.cancel(true)) + } + .void + } + + override private[genkai] def use[A: Key, B](key: A, instant: Instant)( + f: => F[B] + ): F[Either[ConcurrentLimitExhausted[A], B]] = + monad.ifM(acquire(key, instant))( + ifTrue = monad.guarantee(f)(release(key, instant).void).map(r => Right(r)), + ifFalse = monad.pure(Left(ConcurrentLimitExhausted(key))) + ) + + override private[genkai] def release[A: Key](key: A, instant: Instant): F[Boolean] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.releaseArgs(instant) + + debug(s"Release request ($keyStr): $args") *> + monad + .cancelable[Long] { cb => + val cf = evalShaAsync( + releaseSha, + new java.util.LinkedList[Object](keyStr.asJava), + args + ) + + cf.onComplete { (res: Long, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(res)) + } + + () => monad.eval(cf.cancel(true)) + } + .map(tokens => strategy.isReleased(tokens)) + } + + override private[genkai] def acquire[A: Key](key: A, instant: Instant): F[Boolean] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.acquireArgs(instant) + + debug(s"Acquire request ($keyStr): $args") *> + monad + .cancelable[Long] { cb => + val cf = evalShaAsync( + acquireSha, + new java.util.LinkedList[Object](keyStr.asJava), + args + ) + + cf.onComplete { (res: Long, err: Throwable) => + if (err != null) cb(Left(err)) + else cb(Right(res)) + } + + () => monad.eval(cf.cancel(true)) + } + .map(tokens => strategy.isAllowed(tokens)) + } + + override def close(): F[Unit] = monad.ifM(monad.pure(closeClient))( + monad.eval(client.shutdown()), + monad.unit + ) + + override def monadError: MonadError[F] = monad + + private def evalShaAsync( + sha: String, + keys: java.util.List[Object], + args: Seq[String] + ): RFuture[Long] = + scriptCommand.evalShaAsync[Long]( + RScript.Mode.READ_WRITE, + sha, + RScript.ReturnType.INTEGER, + keys, + args: _* + ) +} diff --git a/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonAsyncRateLimiter.scala b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonAsyncRateLimiter.scala index 624ac74..efc21c2 100644 --- a/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonAsyncRateLimiter.scala +++ b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonAsyncRateLimiter.scala @@ -89,12 +89,12 @@ abstract class RedissonAsyncRateLimiter[F[_]]( .map(tokens => strategy.isAllowed(tokens)) } - override def close(): F[Unit] = monad.ifA(monad.pure(closeClient))( + override def close(): F[Unit] = monad.ifM(monad.pure(closeClient))( monad.eval(client.shutdown()), monad.unit ) - override protected def monadError: MonadError[F] = monad + override def monadError: MonadError[F] = monad private def evalShaAsync( sha: String, diff --git a/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonConcurrentRateLimiter.scala b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonConcurrentRateLimiter.scala new file mode 100644 index 0000000..727deb3 --- /dev/null +++ b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonConcurrentRateLimiter.scala @@ -0,0 +1,106 @@ +package genkai.redis.redisson + +import java.time.Instant + +import genkai.monad.syntax._ +import genkai.{ConcurrentLimitExhausted, ConcurrentRateLimiter, Key, Logging} +import genkai.monad.MonadError +import genkai.redis.RedisConcurrentStrategy +import org.redisson.api.{RScript, RedissonClient} +import org.redisson.client.codec.StringCodec + +import scala.collection.JavaConverters._ + +abstract class RedissonConcurrentRateLimiter[F[_]]( + client: RedissonClient, + implicit val monad: MonadError[F], + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends ConcurrentRateLimiter[F] + with Logging[F] { + /* to avoid unnecessary memory allocations */ + private val scriptCommand: RScript = client.getScript(new StringCodec) + + override private[genkai] def permissions[A: Key](key: A, instant: Instant): F[Long] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.permissionsArgs(instant) + + debug(s"Permissions request ($keyStr): $args") *> + monad + .eval( + evalSha( + permissionsSha, + new java.util.LinkedList[Object](keyStr.asJava), + args + ) + ) + .map(strategy.toPermissions) + } + + override def reset[A: Key](key: A): F[Unit] = { + val now = Instant.now() + val keyStr = strategy.keys(key, now) + debug(s"Reset limits for: $keyStr") *> + monad.eval(client.getKeys.unlink(keyStr: _*)).void + } + + override private[genkai] def use[A: Key, B](key: A, instant: Instant)( + f: => F[B] + ): F[Either[ConcurrentLimitExhausted[A], B]] = + monad.ifM(acquire(key, instant))( + ifTrue = monad.guarantee(f)(release(key, instant).void).map(r => Right(r)), + ifFalse = monad.pure(Left(ConcurrentLimitExhausted(key))) + ) + + override private[genkai] def release[A: Key](key: A, instant: Instant): F[Boolean] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.releaseArgs(instant) + + debug(s"Release request ($keyStr): $args") *> + monad + .eval( + evalSha( + releaseSha, + new java.util.LinkedList[Object](keyStr.asJava), + args + ) + ) + .map(strategy.isReleased) + } + + override def acquire[A: Key](key: A, instant: Instant): F[Boolean] = { + val keyStr = strategy.keys(key, instant) + val args = strategy.acquireArgs(instant) + + debug(s"Acquire request ($keyStr): $args") *> + monad + .eval( + evalSha( + acquireSha, + new java.util.LinkedList[Object](keyStr.asJava), + args + ) + ) + .map(strategy.isAllowed) + } + + override def close(): F[Unit] = + monad.ifM(monad.pure(closeClient))( + monad.eval(client.shutdown()), + monad.unit + ) + + override def monadError: MonadError[F] = monad + + private def evalSha(sha: String, keys: java.util.List[Object], args: Seq[String]): Long = + scriptCommand.evalSha[Long]( + RScript.Mode.READ_WRITE, + sha, + RScript.ReturnType.INTEGER, + keys, + args: _* + ) +} diff --git a/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonFutureConcurrentRateLimiter.scala b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonFutureConcurrentRateLimiter.scala new file mode 100644 index 0000000..4595484 --- /dev/null +++ b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonFutureConcurrentRateLimiter.scala @@ -0,0 +1,85 @@ +package genkai.redis.redisson + +import genkai.ConcurrentStrategy +import genkai.monad.{FutureMonadAsyncError, IdMonadError} +import genkai.redis.RedisConcurrentStrategy +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config + +import scala.concurrent.{ExecutionContext, Future} + +class RedissonFutureConcurrentRateLimiter private ( + client: RedissonClient, + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +)(implicit ec: ExecutionContext) + extends RedissonAsyncConcurrentRateLimiter[Future]( + client = client, + monad = new FutureMonadAsyncError(), + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + +object RedissonFutureConcurrentRateLimiter { + + /** blocking initialization */ + def apply( + client: RedissonClient, + strategy: ConcurrentStrategy + )(implicit ec: ExecutionContext): RedissonFutureConcurrentRateLimiter = { + val monad = IdMonadError + val redisStrategy = RedisConcurrentStrategy(strategy) + + val (acquireSha, releaseSha, permissionsSha) = monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + + new RedissonFutureConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + closeClient = false, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + } + + /** blocking initialization */ + def apply( + config: Config, + strategy: ConcurrentStrategy + )(implicit ec: ExecutionContext): RedissonFutureConcurrentRateLimiter = { + val monad = IdMonadError + + val client = Redisson.create(config) + val redisStrategy = RedisConcurrentStrategy(strategy) + + val (acquireSha, releaseSha, permissionsSha) = monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + + new RedissonFutureConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + closeClient = true, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + } +} diff --git a/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonRateLimiter.scala b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonRateLimiter.scala index 4adf7ba..e63b609 100644 --- a/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonRateLimiter.scala +++ b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonRateLimiter.scala @@ -64,12 +64,12 @@ abstract class RedissonRateLimiter[F[_]]( } override def close(): F[Unit] = - monad.ifA(monad.pure(closeClient))( + monad.ifM(monad.pure(closeClient))( monad.eval(client.shutdown()), monad.unit ) - override protected def monadError: MonadError[F] = monad + override def monadError: MonadError[F] = monad private def evalSha(sha: String, keys: java.util.List[Object], args: Seq[String]): Long = scriptCommand.evalSha[Long]( diff --git a/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonSyncConcurrentRateLimiter.scala b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonSyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..f2f6991 --- /dev/null +++ b/modules/redis/redisson/src/main/scala/genkai/redis/redisson/RedissonSyncConcurrentRateLimiter.scala @@ -0,0 +1,79 @@ +package genkai.redis.redisson + +import genkai.{ConcurrentStrategy, Identity} +import genkai.monad.IdMonadError +import genkai.redis.RedisConcurrentStrategy +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config + +class RedissonSyncConcurrentRateLimiter private ( + client: RedissonClient, + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends RedissonConcurrentRateLimiter[Identity]( + client = client, + monad = IdMonadError, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + +object RedissonSyncConcurrentRateLimiter { + def apply( + client: RedissonClient, + strategy: ConcurrentStrategy + ): RedissonSyncConcurrentRateLimiter = { + val monad = IdMonadError + val redisStrategy = RedisConcurrentStrategy(strategy) + + val (acquireSha, releaseSha, permissionsSha) = monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + + new RedissonSyncConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + closeClient = false, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + } + + def apply( + config: Config, + strategy: ConcurrentStrategy + ): RedissonSyncConcurrentRateLimiter = { + val monad = IdMonadError + + val client = Redisson.create(config) + val redisStrategy = RedisConcurrentStrategy(strategy) + + val (acquireSha, releaseSha, permissionsSha) = monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + + new RedissonSyncConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + closeClient = true, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) + } +} diff --git a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonConcurrentRateLimiterSpec.scala b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..db913c5 --- /dev/null +++ b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonConcurrentRateLimiterSpec.scala @@ -0,0 +1,32 @@ +package genkai.redis.redisson + +import genkai.redis.{RedisConcurrentRateLimiterSpecForAll, RedisContainer} +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config + +trait RedissonConcurrentRateLimiterSpec[F[_]] extends RedisConcurrentRateLimiterSpecForAll[F] { + var redisClient: RedissonClient = _ + + override def afterContainersStart(redis: RedisContainer): Unit = { + val config = new Config() + config + .useSingleServer() + .setTimeout(1000000) + .setConnectionMinimumIdleSize(1) + .setConnectionPoolSize(2) + .setAddress(s"redis://${redis.containerIpAddress}:${redis.mappedPort(6379)}") + + redisClient = Redisson.create(config) + } + + override protected def afterAll(): Unit = { + redisClient.shutdown() + super.afterAll() + } + + override protected def afterEach(): Unit = { + super.afterEach() + redisClient.getKeys.flushall() + } +} diff --git a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonFutureConcurrentRateLimiterSpec.scala b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonFutureConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..95fdbcf --- /dev/null +++ b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonFutureConcurrentRateLimiterSpec.scala @@ -0,0 +1,14 @@ +package genkai.redis.redisson + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} + +import scala.concurrent.Future + +class RedissonFutureConcurrentRateLimiterSpec extends RedissonConcurrentRateLimiterSpec[Future] { + override def concurrentRateLimiter( + strategy: ConcurrentStrategy + ): ConcurrentRateLimiter[Future] = + RedissonFutureConcurrentRateLimiter(redisClient, strategy) + + override def toFuture[A](v: Future[A]): Future[A] = v +} diff --git a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonFutureRateLimiterSpec.scala b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonFutureRateLimiterSpec.scala index 883ad15..e157d4d 100644 --- a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonFutureRateLimiterSpec.scala +++ b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonFutureRateLimiterSpec.scala @@ -4,7 +4,7 @@ import genkai.{RateLimiter, Strategy} import scala.concurrent.Future -class RedissonFutureRateLimiterSpec extends RedissonSpec[Future] { +class RedissonFutureRateLimiterSpec extends RedissonRateLimiterSpec[Future] { override def rateLimiter(strategy: Strategy): RateLimiter[Future] = RedissonFutureRateLimiter(redisClient, strategy) diff --git a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSpec.scala b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonRateLimiterSpec.scala similarity index 76% rename from modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSpec.scala rename to modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonRateLimiterSpec.scala index 1cde039..2eb1282 100644 --- a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSpec.scala +++ b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonRateLimiterSpec.scala @@ -1,11 +1,11 @@ package genkai.redis.redisson -import genkai.redis.{RedisContainer, RedisSpecForAll} +import genkai.redis.{RedisContainer, RedisRateLimiterSpecForAll} import org.redisson.Redisson import org.redisson.api.RedissonClient import org.redisson.config.Config -trait RedissonSpec[F[_]] extends RedisSpecForAll[F] { +trait RedissonRateLimiterSpec[F[_]] extends RedisRateLimiterSpecForAll[F] { var redisClient: RedissonClient = _ override def afterContainersStart(redis: RedisContainer): Unit = { @@ -13,6 +13,8 @@ trait RedissonSpec[F[_]] extends RedisSpecForAll[F] { config .useSingleServer() .setTimeout(1000000) + .setConnectionMinimumIdleSize(1) + .setConnectionPoolSize(2) .setAddress(s"redis://${redis.containerIpAddress}:${redis.mappedPort(6379)}") redisClient = Redisson.create(config) diff --git a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSyncConcurrentRateLimiterSpec.scala b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSyncConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..2683364 --- /dev/null +++ b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSyncConcurrentRateLimiterSpec.scala @@ -0,0 +1,14 @@ +package genkai.redis.redisson + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy, Identity} + +import scala.concurrent.Future + +class RedissonSyncConcurrentRateLimiterSpec extends RedissonConcurrentRateLimiterSpec[Identity] { + override def concurrentRateLimiter( + strategy: ConcurrentStrategy + ): ConcurrentRateLimiter[Identity] = + RedissonSyncConcurrentRateLimiter(redisClient, strategy) + + override def toFuture[A](v: Identity[A]): Future[A] = Future.successful(v) +} diff --git a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSyncRateLimiterSpec.scala b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSyncRateLimiterSpec.scala index 465b1b3..a652db0 100644 --- a/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSyncRateLimiterSpec.scala +++ b/modules/redis/redisson/src/test/scala/genkai/redis/redisson/RedissonSyncRateLimiterSpec.scala @@ -4,7 +4,7 @@ import genkai.{Identity, RateLimiter, Strategy} import scala.concurrent.Future -class RedissonSyncRateLimiterSpec extends RedissonSpec[Identity] { +class RedissonSyncRateLimiterSpec extends RedissonRateLimiterSpec[Identity] { override def rateLimiter(strategy: Strategy): RateLimiter[Identity] = RedissonSyncRateLimiter(redisClient, strategy) diff --git a/modules/redis/redisson/zio/src/main/scala/genkai/redis/redisson/zio/RedissonZioAsyncConcurrentRateLimiter.scala b/modules/redis/redisson/zio/src/main/scala/genkai/redis/redisson/zio/RedissonZioAsyncConcurrentRateLimiter.scala new file mode 100644 index 0000000..1b31b1c --- /dev/null +++ b/modules/redis/redisson/zio/src/main/scala/genkai/redis/redisson/zio/RedissonZioAsyncConcurrentRateLimiter.scala @@ -0,0 +1,97 @@ +package genkai.redis.redisson.zio + +import genkai.ConcurrentStrategy +import genkai.effect.zio.ZioMonadAsyncError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.redisson.RedissonAsyncConcurrentRateLimiter +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config +import zio.{Has, Task, ZIO, ZLayer, ZManaged} + +class RedissonZioAsyncConcurrentRateLimiter private ( + client: RedissonClient, + monad: ZioMonadAsyncError, + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends RedissonAsyncConcurrentRateLimiter[Task]( + client = client, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object RedissonZioAsyncConcurrentRateLimiter { + def useClient( + client: RedissonClient, + strategy: ConcurrentStrategy + ): ZIO[Any, Throwable, RedissonZioAsyncConcurrentRateLimiter] = { + val monad = new ZioMonadAsyncError() + val redisStrategy = RedisConcurrentStrategy(strategy) + + for { + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonZioAsyncConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + } + + def layerUsingClient( + client: RedissonClient, + strategy: ConcurrentStrategy + ): ZLayer[Any, Throwable, Has[RedissonZioAsyncConcurrentRateLimiter]] = + useClient(client, strategy).toLayer + + def managed( + config: Config, + strategy: ConcurrentStrategy + ): ZManaged[Any, Throwable, RedissonZioAsyncConcurrentRateLimiter] = { + val monad = new ZioMonadAsyncError() + val redisStrategy = RedisConcurrentStrategy(strategy) + + ZManaged.make { + for { + client <- monad.eval(Redisson.create(config)) + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonZioAsyncConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + }(limiter => limiter.close().orDie) + } + + def layerFromManaged( + config: Config, + strategy: ConcurrentStrategy + ): ZLayer[Any, Throwable, Has[RedissonZioAsyncConcurrentRateLimiter]] = + ZLayer.fromManaged(managed(config, strategy)) +} diff --git a/modules/redis/redisson/zio/src/main/scala/genkai/redis/redisson/zio/RedissonZioConcurrentRateLimiter.scala b/modules/redis/redisson/zio/src/main/scala/genkai/redis/redisson/zio/RedissonZioConcurrentRateLimiter.scala new file mode 100644 index 0000000..c6fee22 --- /dev/null +++ b/modules/redis/redisson/zio/src/main/scala/genkai/redis/redisson/zio/RedissonZioConcurrentRateLimiter.scala @@ -0,0 +1,97 @@ +package genkai.redis.redisson.zio + +import genkai.ConcurrentStrategy +import genkai.effect.zio.ZioMonadError +import genkai.redis.RedisConcurrentStrategy +import genkai.redis.redisson.RedissonConcurrentRateLimiter +import org.redisson.Redisson +import org.redisson.api.RedissonClient +import org.redisson.config.Config +import zio._ +import zio.blocking.{Blocking, blocking} + +class RedissonZioConcurrentRateLimiter private ( + client: RedissonClient, + monad: ZioMonadError, + strategy: RedisConcurrentStrategy, + closeClient: Boolean, + acquireSha: String, + releaseSha: String, + permissionsSha: String +) extends RedissonConcurrentRateLimiter[Task]( + client = client, + monad = monad, + strategy = strategy, + closeClient = closeClient, + acquireSha = acquireSha, + releaseSha = releaseSha, + permissionsSha = permissionsSha + ) {} + +object RedissonZioConcurrentRateLimiter { + def useClient( + client: RedissonClient, + strategy: ConcurrentStrategy + ): ZIO[Blocking, Throwable, RedissonZioConcurrentRateLimiter] = for { + blocker <- ZIO.service[Blocking.Service] + monad = new ZioMonadError(blocker) + redisStrategy = RedisConcurrentStrategy(strategy) + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonZioConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = false, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + + def layerUsingClient( + client: RedissonClient, + strategy: ConcurrentStrategy + ): ZLayer[Blocking, Throwable, Has[RedissonZioConcurrentRateLimiter]] = + useClient(client, strategy).toLayer + + def managed( + config: Config, + strategy: ConcurrentStrategy + ): ZManaged[Blocking, Throwable, RedissonZioConcurrentRateLimiter] = + ZManaged.make { + for { + blocker <- ZIO.service[Blocking.Service] + monad = new ZioMonadError(blocker) + client <- monad.eval(Redisson.create(config)) + redisStrategy = RedisConcurrentStrategy(strategy) + sha <- monad.eval { + ( + client.getScript.scriptLoad(redisStrategy.acquireLuaScript), + client.getScript.scriptLoad(redisStrategy.releaseLuaScript), + client.getScript.scriptLoad(redisStrategy.permissionsLuaScript) + ) + } + } yield new RedissonZioConcurrentRateLimiter( + client = client, + strategy = redisStrategy, + monad = monad, + closeClient = true, + acquireSha = sha._1, + releaseSha = sha._2, + permissionsSha = sha._3 + ) + } { limiter => + blocking(limiter.close().ignore) + } + + def layerFromManaged( + config: Config, + strategy: ConcurrentStrategy + ): ZLayer[Blocking, Throwable, Has[RedissonZioConcurrentRateLimiter]] = + ZLayer.fromManaged(managed(config, strategy)) +} diff --git a/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioAsyncConcurrentRateLimiterSpec.scala b/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioAsyncConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..9274e8d --- /dev/null +++ b/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioAsyncConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.redisson.zio + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.zio.ZioBaseSpec +import genkai.redis.redisson.RedissonConcurrentRateLimiterSpec +import zio.Task + +import scala.concurrent.Future + +class RedissonZioAsyncConcurrentRateLimiterSpec + extends RedissonConcurrentRateLimiterSpec[Task] + with ZioBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[Task] = + runtime.unsafeRun(RedissonZioAsyncConcurrentRateLimiter.useClient(redisClient, strategy)) + + override def toFuture[A](v: Task[A]): Future[A] = runtime.unsafeRunToFuture(v) +} diff --git a/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioAsyncRateLimiterSpec.scala b/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioAsyncRateLimiterSpec.scala index 2123162..5046e7e 100644 --- a/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioAsyncRateLimiterSpec.scala +++ b/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioAsyncRateLimiterSpec.scala @@ -2,12 +2,12 @@ package genkai.redis.redisson.zio import genkai.{RateLimiter, Strategy} import genkai.effect.zio.ZioBaseSpec -import genkai.redis.redisson.RedissonSpec +import genkai.redis.redisson.RedissonRateLimiterSpec import zio._ import scala.concurrent.Future -class RedissonZioAsyncRateLimiterSpec extends RedissonSpec[Task] with ZioBaseSpec { +class RedissonZioAsyncRateLimiterSpec extends RedissonRateLimiterSpec[Task] with ZioBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[Task] = runtime.unsafeRun(RedissonZioAsyncRateLimiter.useClient(redisClient, strategy)) diff --git a/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioConcurrentRateLimiterSpec.scala b/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioConcurrentRateLimiterSpec.scala new file mode 100644 index 0000000..b4f1254 --- /dev/null +++ b/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioConcurrentRateLimiterSpec.scala @@ -0,0 +1,17 @@ +package genkai.redis.redisson.zio + +import genkai.{ConcurrentRateLimiter, ConcurrentStrategy} +import genkai.effect.zio.ZioBaseSpec +import genkai.redis.redisson.RedissonConcurrentRateLimiterSpec +import zio.Task + +import scala.concurrent.Future + +class RedissonZioConcurrentRateLimiterSpec + extends RedissonConcurrentRateLimiterSpec[Task] + with ZioBaseSpec { + override def concurrentRateLimiter(strategy: ConcurrentStrategy): ConcurrentRateLimiter[Task] = + runtime.unsafeRun(RedissonZioConcurrentRateLimiter.useClient(redisClient, strategy)) + + override def toFuture[A](v: Task[A]): Future[A] = runtime.unsafeRunToFuture(v) +} diff --git a/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioRateLimiterSpec.scala b/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioRateLimiterSpec.scala index 4ce55be..205f152 100644 --- a/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioRateLimiterSpec.scala +++ b/modules/redis/redisson/zio/src/test/scala/genkai/redis/redisson/zio/RedissonZioRateLimiterSpec.scala @@ -2,12 +2,12 @@ package genkai.redis.redisson.zio import genkai.{RateLimiter, Strategy} import genkai.effect.zio.ZioBaseSpec -import genkai.redis.redisson.RedissonSpec +import genkai.redis.redisson.RedissonRateLimiterSpec import zio.Task import scala.concurrent.Future -class RedissonZioRateLimiterSpec extends RedissonSpec[Task] with ZioBaseSpec { +class RedissonZioRateLimiterSpec extends RedissonRateLimiterSpec[Task] with ZioBaseSpec { override def rateLimiter(strategy: Strategy): RateLimiter[Task] = runtime.unsafeRun(RedissonZioRateLimiter.useClient(redisClient, strategy))