ratelimter4s is lightweight rate limiter library designed for Scala. It provides wrappers to enhance any Function
with rate limiting capabilities. The wrappers are available in 3 flavours
- FRateLimiter : Rate limited method returns a
Either
type - CatsRateLimiter : Rate limited method returns a
EitherT
type - ZIORateLimiter : Rate limited method returns a
Task
type
ratelimiter4s uses resilience4j rate limiter to create the underlying rate limiting policies. Resilience4j is a lightweight fault tolerance library inspired by Netflix Hystrix, but designed for Java 8.
The heroes of our story are FRateLimiter
, CatsRateLimiter
and ZIORateLimiter
classes which provide limit
method to rate limit.
Any scala Function
can be rate limited.
Lets check out FRateLimiter
in action :
Consider a public service which takes a character name and returns an Artist
.
trait ArtistService {
def getArtist(character: String): Artist
}
Rules dictate that calling rate for getArtist
to be not higher than 3 requests/second.
We can define a RateLimiter
config defined like this
val threePerSecondConfig: RateLimiterConfig = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(3)
.timeoutDuration(Duration.ofMillis(25))
.build()
val threePerSecondLimiter = RateLimiter.of("3/second", threePerSecondConfig)
The details of the limiter config are described in detail here
We can use this config and add rate-limiting capability to our service. We would need to import the FRateLimiter
class that provides the method limit
.
import ratelimiter4s.core.FRateLimiter._
val rateLimitedService = limit(getArtist _, threePerSecondLimiter)
we can also say
import ratelimiter4s.core.FRateLimiter._
def rateLimitedService = limit(getArtist _, threePerSecondLimiter)
Lets checkout a rate limited service in action
//first attempt
rateLimitedService("Sheldon") == Right(Artist("Jim Parsons"))
rateLimitedService("Penny") == Right(Artist("Kaley Cuoco"))
rateLimitedService("Leonard") == Right(Artist("Johnny Galecki"))
//fourth attempt will fail with a RequestNotPermitted
rateLimitedService("Kripke") == Left(RequestNotPermitted)
And that is it! We have achieved the objective of limiting the requests to 3/second.
limit
methods return a ratelimiter4s.core.FunctionNLimiter
instance. To be more accurate:
- limiting a
scala.Function0
returns aFunction0Limiter
- limiting a
scala.Function1
returns aFunction1Limiter
- and so on for N till 22
A pure scala function is side-effect free, plus the result does not depend on anything other than its inputs.
FunctionNLimiter
provides a pure apply for every rate-limited function. The return type of pure
is Either[RequestNotPermitted,A]
.
def greeter(guest: String) : String = s"Hello $guest"
Decorating greeter
with a rate limiter
val rateLimitedGreeter = limit(getArtist _, rateLimiter)
We can call pure
on the rate limited method.
val result: Either[RequestNotPermitted, String] = rateLimitedGreeter.pure("World")
Any exceptions thrown by the original greeter
method are unhandled. So any instances of Left
can only have a RequestNotPermitted
type inside.
However many useful methods that we would like to rate limit interact with outside world, mutate state and would not qualify as pure functions. Such a rate limited method could throw exceptions which are caused by the actual business logic.
Consider the following example as a tweak to the original greeter
.
def greeter(guest: String) : String = {
if(!guestList.contains(guest)) throw new UninvitedGuestException(guest)
s"Hello $guest"
}
Decorating the impure greeter
with a rate limiter would remain the same.
val rateLimitedGreeter = limit(getArtist _, rateLimiter)
We should call an apply
on the rate limited method instead of pure. This means that we are also expecting greeter
to fail for reasons other than rate limitations.
val result: Either[Throwable, String] = rateLimitedGreeter("World")
The Left
is of Throwable
type in this case which is self-explanatory.
We can also capture the result of a rate limited method using cats and its monad transformer instance EitherT
Consider an image recognition service.
trait RecognitionService {
def recognizeImage(image: URL): ImageType
}
Lets rate limit the recognizeImage
method using CatsFunctionLimiter
instances available in the CatsRateLimiter
class
import ratelimiter4s.cats.CatsRateLimiter._
val rateLimitedService = limit(getArtist _, rateLimiter)
Calling this rate limited return an EitherT
bounded to an IO
effect type
val result : EitherT[IO, Throwable, ImageType] = rateLimitedService("https://samples.clarifai.com/metro-north.jpg")
The result is an EitherT[IO, Throwable, ImageType
] . The left
is a throwable because the method can fail due to other reasons apart from RequestNotPermitted
. If that is not the case, we can use pure
.
val result : EitherT[IO, RequestNotPermitted, ImageType] = rateLimitedService.pure("https://samples.clarifai.com/metro-north.jpg")
the type clearly indicates that the failure can only be caused by a RequestNotPermitted
error.
We can also capture the result of a rate limited method using zio types.
Lets consider the image recognition service again.
trait RecognitionService {
def recognizeImage(image: URL): ImageType
}
We rate limit this time using the ZFunctionLimiter
instances available inside the ZIORateLimiter
class.
import ratelimiter4s.zio.ZIORateLimiter._
val rateLimitedService = limit(getArtist _, rateLimiter)
Calling this rate limited return an Task[ImageType]
result which is shorthand for ZIO[Any, Throwable, ImageType]
val result : Task[ImageType] = rateLimitedService("https://samples.clarifai.com/metro-north.jpg")
The result is Task
type because the method can fail due to other reasons apart from RequestNotPermitted
. If that is not the case , we can use pure
.
val result : IO[RequestNotPermitted, ImageType] = rateLimitedService.pure("https://samples.clarifai.com/metro-north.jpg")
IO[RequestNotPermitted, ImageType]
which is a shorthand for ZIO[Any, RequestNotPermitted, ImageType]
clearly indicates that the failure can only be caused by a RequestNotPermitted
error.