All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.paoloboni.http.ratelimit.RateLimiter.scala Maven / Gradle / Ivy

/*
 * Copyright (c) 2021 Paolo Boni
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package io.github.paoloboni.http.ratelimit

import cats.Applicative
import cats.effect.kernel.{Async, Deferred}
import cats.effect.std.Queue
import cats.effect.syntax.spawn._
import cats.implicits._
import fs2.Stream
import io.github.paoloboni.binance.common.response.RateLimitType

import scala.concurrent.duration.{DurationInt, FiniteDuration}

object RateLimiter {
  def make[F[_]](
      perSecond: Double,
      bufferSize: Int,
      `type`: RateLimitType
  )(implicit F: Async[F]): F[RateLimiter[F]] = {
    F.unlessA(perSecond > 0 && bufferSize > 0)(
      F.raiseError(new IllegalArgumentException("perSecond and buffer size must be strictly positive"))
    ) *>
      (for {
        queue <- Queue.bounded[F, F[Unit]](bufferSize)
        period = periodFrom(perSecond)
        _ <- Stream.awakeDelay(period).evalMap(_ => queue.take.flatten).repeat.compile.drain.start.void
      } yield {
        new RateLimiter[F] {
          override def rateLimit[T](effect: => F[T]): F[T] = Deferred[F, T].flatMap { p =>
            queue.offer(F.defer(effect.flatTap(p.complete).void)) *> p.get
          }
          override val limitType: RateLimitType = `type`
        }
      })
  }
  private def periodFrom(perSecond: Double) =
    (1.second.toNanos.toDouble / perSecond).toInt.nanos

  def noOp[F[+_]: Applicative]: F[RateLimiter[F]] =
    new RateLimiter[F] {
      override def rateLimit[T](effect: => F[T]): F[T] = effect
      override val limitType: RateLimitType            = RateLimitType.NONE
    }.pure[F]
}

sealed trait RateLimiter[F[_]] {
  def rateLimit[T](effect: => F[T]): F[T]
  def limitType: RateLimitType
}

case class Rate(n: Int, t: FiniteDuration, limitType: RateLimitType) {
  def perSecond: Double = n.toDouble / t.toSeconds.toDouble
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy