Как я таймауты к фьючам прикручивал

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

Как я таймауты к фьючам прикручивал

  • 335