Skip to content

Instantly share code, notes, and snippets.

@danilbykov
Last active October 11, 2019 12:15
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save danilbykov/957487c9463a66daa69290698a9320ac to your computer and use it in GitHub Desktop.
Save danilbykov/957487c9463a66daa69290698a9320ac to your computer and use it in GitHub Desktop.
import cats.effect.ExitCase._
import cats.effect.Sync
import cats.effect.concurrent.Ref
import cats.syntax.flatMap._
import cats.syntax.functor._
trait Tap[F[_]] {
def apply[A](effect: F[A]): F[A]
}
object Tap {
type Percentage = Double
def make[F[_]: Sync](errBound: Percentage,
qualified: Throwable => Boolean,
rejected: => Throwable): F[Tap[F]] = {
for {
ref <- Ref.of[F, TapState](TapState(Nil))
} yield new TapImpl(ref, errBound, qualified, rejected)
}
val tailSize = 100
private case class TapState(lastResults: List[Boolean])
private final class TapImpl[F[_]: Sync](ref: Ref[F, TapState],
errBound: Percentage,
qualified: Throwable => Boolean,
rejected: => Throwable) extends Tap[F] {
override def apply[A](effect: F[A]): F[A] =
for {
state <- ref.get
failed = state.lastResults.count(_ == false)
result <-
if (failed <= state.lastResults.size * errBound) {
Sync[F].guaranteeCase(effect) {
case Completed =>
ref.update(updateState(true))
case Error(e) =>
ref.update(updateState(qualified(e)))
case Canceled =>
Sync[F].unit
}
} else {
ref.update(updateState(true)) >> Sync[F].raiseError(rejected)
}
} yield result
private def updateState(newResult: Boolean)(state: TapState): TapState =
state.copy(lastResults = (newResult :: state.lastResults).take(tailSize))
}
}
@oleg-py
Copy link

oleg-py commented Mar 12, 2019

Man, I love your solution. It's so concise and straightforward.

@danilbykov
Copy link
Author

Man, I love your solution. It's so concise and straightforward.

Thank you.

@c0d3r85
Copy link

c0d3r85 commented Mar 15, 2019

I think, there is no need to store list of bools:

final case class ErrorStats(bufferSize: Int, errorCount: Int = 0) {

  def update(newResult: Boolean): ErrorStats = {
    val newErrorCount = {
      val delta = if (newResult) -1 else 1
      Math.min(Math.max(errorCount + delta, 0), bufferSize)
    }
    copy(bufferSize, newErrorCount)
  }

  def rate = errorCount.toDouble / bufferSize

}

@danilbykov
Copy link
Author

danilbykov commented Mar 16, 2019

Let's assume that every second request fails on our service so we get series of true, false, true, false and so on. We feed these results into ErrorStats(100):

(1 to 100).foldLeft(ErrorStats(100)) {
  case (stat, _) => stat.update(true).update(false)
} rate

Final rate is 0.01 which is too good where only half of requests is successful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment