Как я таймауты к фьючам прикручивал
val future: Future[String] = ???
val future: Future[String] = ???
future.withTimeout(500.millis).onComplete {
case Success(value) => println(s"Succeed with $value")
case Failure(e: TimeoutException) => println("Timed out")
case Failure(e) => println(s"Failed with other error: $e")
}
Проблема
implicit class FutureTimeoutOps[T](
val self: Future[T]
) extends AnyVal {
def withTimeout(
duration: FiniteDuration
)(implicit ec: ExecutionContext): Future[T] = {
???
}
}
implicit class FutureTimeoutOps[T](
val self: Future[T]
) extends AnyVal {
def withTimeout(
duration: FiniteDuration
)(implicit ec: ExecutionContext): Future[T] = {
Future.firstCompletedOf(Seq(
self,
???
))
}
}
implicit class FutureTimeoutOps[T](
val self: Future[T]
) extends AnyVal {
def withTimeout(
duration: FiniteDuration
)(implicit ec: ExecutionContext): Future[T] = {
Future.firstCompletedOf(Seq(
self,
delay(duration).map(_ => throw new TimeoutException)
))
}
}
Решение
val future: Future[String] = ???
val fallback: String = ???
future.withTimeout(500.millis).transform {
case Success(value) => Success(value)
case Failure(e: TimeoutException) => Success(fallback)
case Failure(e) => Failure(e)
}.map { x =>
???
}
Использование решения
future.withTimeoutTo(500.millis, "fallback")
Таймаут с фолбэком
def withTimeoutTo(
duration: FiniteDuration,
fallback: => T
)(implicit ec: ExecutionContext): Future[T] = {
Future.firstCompletedOf(Seq(
self,
delay(duration).map(_ => fallback)
))
}
Таймаут с фолбэком
fallback всегда срабатывает!
def withTimeoutTo(
duration: FiniteDuration,
fallback: => T
)(implicit ec: ExecutionContext): Future[T] = {
val promise = Promise[T]()
promise.completeWith(self)
delay(duration).onComplete { _ =>
if (!promise.isCompleted) {
promise.complete(Try(fallback))
}
}
promise.future
}
Обработка сайд-эффектов
Гонки!
def withTimeoutTo(
duration: FiniteDuration,
fallback: => T
)(implicit ec: ExecutionContext): Future[T] = {
val promise = Promise[Option[T]]()
promise.completeWith(self.map(Some(_)))
promise.completeWith(delay(duration).map(_ => None))
promise.future.map {
case Some(result) => result
case None => fallback
}
}
Обработка гонок
Утекание ресурсов!
def withTimeoutTo(
duration: FiniteDuration,
fallback: => T,
resourceClose: T => Future[Unit]
)(implicit ec: ExecutionContext): Future[T] = {
val promise = Promise[Option[T]]()
promise.completeWith(self.map(Some(_)))
promise.completeWith(delay(duration).map(_ => None))
promise.future.map {
case Some(result) => result
case None =>
self.flatMap { resource =>
resourceClose(resource)
}
fallback
}
}
Обработка ресурсов
Подводные камни
1. Реализация с фолбэком и без
2. Фолбэк должен срабатывать только при возникновении таймаута
3. Фолбэк должен учитывать гонки
4. Ресурсы должны закрываться
val io: IO[String] = ???
cats-effect
val io: IO[String] = ???
io.timeout(500.millis)
val io: IO[String] = ???
io.timeout(500.millis)
io.timeoutTo(500.millis, IO.pure("fallback"))
cats-effect
1. IO — ленивый, а значит нет проблем с сайд эффектами (см. мой доклад)
3. IO можно отменить
4. IO безопасно работает с ресурсами
2. Нет гонок — это учтено в либе
def readFirstLine(file: File): IO[String] = {
IO(new BufferedReader(new FileReader(file)))
.bracket { in =>
IO(in.readLine())
} { in =>
IO(in.close())
}
}
Работа с ресурсами
Bracket
IO(acquire1).bracket { r1 =>
IO(acquire2).bracket { r2 =>
IO(acquire3).bracket { r3 =>
r1 + r2 + r3
}(_.close())
}(_.close())
}(_.close())
Работа с ресурсами
Bracket
abstract class Resource[F[_], A] {
def use[B](
f: A => F[B]
)(implicit F: Bracket[F, Throwable]): F[B]
}
Работа с ресурсами
Resource
def make[F[_], A](
acquire: F[A]
)(
release: A => F[Unit]
): Resource[F, A]
val file: File = ???
Работа с ресурсами
Resource
val file: File = ???
val resource: Resource[IO, BufferedReader] = {
Resource.make(
IO(new BufferedReader(new FileReader(file)))
)(r => IO(r.close()))
}
val file: File = ???
val resource: Resource[IO, BufferedReader] = {
Resource.make(
IO(new BufferedReader(new FileReader(file)))
)(r => IO(r.close()))
}
val res: IO[String] = {
resource.use { reader =>
IO(reader.readLine())
}
}
case class AppResources(
actorSystem: ActorSystem,
threadPool: ExecutionContext,
connectionPool: HikariPool,
)
val appResources: Resource[IO, AppResources] = for {
actorSystem <- actorSystemResource
threadPool <- threadPoolResource
connectionPool <- connectionPoolResource
} yield {
AppResources(actorSystem, threadPool, connectionPool)
}
appResources.use { appResources =>
// usage
}
Работа с ресурсами
Resource
val io: IO[String] = ???
Cancellability
val io: IO[String] = ???
for {
fiber <- io.start
_ <- fiber.cancel
} yield println("Started and cancelled")
def start(implicit cs: ContextShift[IO]): IO[Fiber[IO, A]]
Cancellability
def start(implicit cs: ContextShift[IO]): IO[Fiber[IO, A]]
trait Fiber[F[_], A] {
def join: F[A]
def cancel: CancelToken[F]
}
def start(implicit cs: ContextShift[IO]): IO[Fiber[IO, A]]
trait Fiber[F[_], A] {
def join: F[A]
def cancel: CancelToken[F]
}
type CancelToken[F[_]] = F[Unit]
Cancellability + resources
def acquire: IO[String] = for {
_ <- IO(logger.info("Resource acquiring..."))
_ <- timer.sleep(5.seconds)
res <- IO {
logger.info("Resource acquired")
"some resource"
}
} yield res
def release(resource: String): IO[Unit] = {
IO(logger.info(s"Releasing $resource"))
}
val action: IO[Unit] = acquire.bracket { r =>
IO(logger.info(s"Using $r"))
}(release)
val result: IO[Unit] = action.timeoutTo(100.millis, IO {
logger.info("Resource acquiring timed out")
})
IO(logger.info("Started")) >> result.as(ExitCode.Success)
Cancellability + resources
10:34:46.760 Started
10:34:46.804 Resource acquiring...
10:34:46.917 Resource acquiring timed out
10:34:51.806 Resource acquired
10:34:51.814 Releasing some resource
10:34:46.760 Started
10:34:46.804 Resource acquiring...
10:34:51.806 Resource acquired
10:34:51.812 Using some resource
10:34:51.814 Releasing some resource
10:34:51.817 Resource acquiring timed out
Resource acquisition is uncancellable!
Monix
10:31:59.642 Started
10:31:59.715 Resource acquiring...
10:31:59.825 Resource acquiring timed out
10:32:04.736 Resource acquired
10:32:04.738 Releasing some resource
10:32:04.738 Using some resource
Monix
val action = acquire.bracket { r =>
Task(logger.info(s"Using $r"))
}(release)
val action = acquire.bracket { r =>
Task(logger.info(s"Using $r"))
}(release)
val action2 = acquire.bracket { r =>
Task.cancelBoundary >> Task(logger.info(s"Using $r"))
}(release)
Monix
10:34:24.758 Started
10:34:24.804 Resource acquiring...
10:34:24.910 Resource acquiring timed out
10:34:29.814 Resource acquired
10:34:29.816 Releasing some resource
ZIO
12:38:45.952 Started
12:38:46.254 Resource acquiring...
12:38:51.331 Resource acquired
12:38:51.349 Releasing some resource
12:38:51.377 Resource acquiring timed out
ZIO
def bracketInterruptAcquire[R <: Clock, E, A, B](
acquire: ZIO[R, E, A],
release: A => UIO[Any],
timeout: zio.duration.Duration,
)(
use: A => ZIO[R, E, B],
): ZIO[R, E, Option[B]] = {
ZIO.uninterruptibleMask { restore =>
for {
either <- acquire.uninterruptible.timeoutFork(timeout)
res <- either match {
case Left(fiber) =>
fiber.join.flatMap(release).fork *> ZIO.none
case Right(a) =>
restore(use(a).map(Some(_))).ensuring(release(a))
}
} yield res
}
}
ZIO
13:19:40.973 Started
13:19:41.270 Resource acquiring...
13:19:41.724 Resource acquiring timed out
13:19:46.393 Resource acquired
13:19:46.396 Releasing some resource
cats-effect 3
Выводы
1. Сделать правильно асинхронные таймауты ОЧЕНЬ сложно
3. Текущий cats-effect так не умеет by design
4. В Monix всё работает как нам надо, если не учитывать баг, который скоро доедет до мастера.
5. ZIO из коробки не содержит нужного комбинатора, но его можно сделать на существующих примитивах без проблем
6. В cats-effect 3, скорее всего, будет как в ZIO
2. C Future нужно пройти через кучу подводных камней
Спасибо!
Как я таймауты к фьючам прикручивал
By Yury Badalyants
Как я таймауты к фьючам прикручивал
- 326