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))