Невозможно восстановить исключение в Future in Scala - PullRequest
0 голосов
/ 13 ноября 2018

Следующий код Scala использует кошки EitherT для переноса результатов в Future[Either[ServiceError, T]]:

package com.example

import com.example.AsyncResult.AsyncResult
import cats.implicits._

import scala.concurrent.ExecutionContext.Implicits.global

class ExternalService {
  def doAction(): AsyncResult[Int] = {
    AsyncResult.success(2)
  }

  def doException(): AsyncResult[Int] = {
    println("do exception")
    throw new NullPointerException("run time exception")
  }
}

class ExceptionExample {
  private val service = new ExternalService()

  def callService(): AsyncResult[Int] = {
    println("start callService")
    val result = for {
      num <- service.doException()
    } yield num

    result.recoverWith {
      case ex: Throwable =>
        println("recovered exception")
        AsyncResult.success(99)
    }
  }
}

object ExceptionExample extends App {
  private val me     = new ExceptionExample()
  private val result = me.callService()
  result.value.map {
    case Right(value) => println(value)
    case Left(error)  => println(error)
  }
}

AsyncResult.scala содержит:

package com.example

import cats.data.EitherT
import cats.implicits._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object AsyncResult {
  type AsyncResult[T] = EitherT[Future, ServiceError, T]

  def apply[T](fe: => Future[Either[ServiceError, T]]): AsyncResult[T]          = EitherT(fe)
  def apply[T](either: Either[ServiceError, T]): AsyncResult[T]                 = EitherT.fromEither[Future](either)
  def success[T](res: => T): AsyncResult[T]                                     = EitherT.rightT[Future, ServiceError](res)
  def error[T](error: ServiceError): AsyncResult[T]                             = EitherT.leftT[Future, T](error)
  def futureSuccess[T](fres: => Future[T]): AsyncResult[T]                      = AsyncResult.apply(fres.map(res => Right(res)))
  def expectTrue(cond: => Boolean, err: => ServiceError): AsyncResult[Boolean]  = EitherT.cond[Future](cond, true, err)
  def expectFalse(cond: => Boolean, err: => ServiceError): AsyncResult[Boolean] = EitherT.cond[Future](cond, false, err)
}

ServiceError.scala содержит:

package com.example

sealed trait ServiceError {
  val detail: String
}

В ExceptionExample, если он вызывает service.doAction(), он печатает 2, как и ожидалось, но если он вызывает service.doException(), он генерирует исключение, но я ожидал, что он напечатает «восстановленное исключение» и «99".

Как правильно восстановиться после исключения?

Ответы [ 2 ]

0 голосов
/ 13 ноября 2018

Я склонен согласиться с тем, что это кажется немного запутанным, но ради упражнения я считаю, что есть пара вещей, которые не совсем щелкают.

Первое - это то, чтовы бросаете исключение вместо того, чтобы захватить его как часть семантики будущего.то есть.Вы должны изменить свой метод doException с:

def doException(): AsyncResult[Int] = {
    println("do exception")
    throw new NullPointerException("run time exception")
}

На:

def doException(): AsyncResult[Int] = {
    println("do exception")
    AsyncResult(Future.failed(new NullPointerException("run time exception")))
}

Второй не совсем правильный бит - это восстановление исключения.Когда вы вызываете recoverWith на EitherT, вы определяете частичную функцию от Left из EitherT к другому EitherT.В вашем случае это будет:

ServiceError => AsyncResult[Int]

Если вы хотите восстановить несостоявшееся будущее, я думаю, вам нужно явно восстановить его.Что-то вроде:

AsyncResult {
  result.value.recover {
    case _: Throwable => {
      println("recovered exception")
      Right(99)
    }
  }
}

Если вы действительно хотите использовать recoverWith, тогда вы можете написать это вместо:

 AsyncResult {
    result.value.recoverWith {
      case _: Throwable =>
        println("recovered exception")
        Future.successful(Right(99))
    }
  }
0 голосов
/ 13 ноября 2018

Это потому, что doException создает исключение в строке. Если вы хотите использовать Either, вы должны вернуть Future(Left(exception)), а не бросать его.

Я думаю, вы вроде как обдумываете это. Не похоже, что вам нужно Either здесь ... или cats в этом отношении.

Почему бы не сделать что-то простое, как это:

 class ExternalService {
   def doAction(): Future[Int] = Future.successful(2)

   def doException(): AsyncResult[Int] = {
     println("do exception")
     Future.failed(NullPointerException("run time exception")) 
     // alternatively: Future { throw new NullPointerExceptioN() }
 }


 class ExceptionExample {
   private val service = new ExternalService()

   def callService(): AsyncResult[Int] = {
     println("start callService")
       val result = for {
         num <- service.doException()
     } yield num
     // Note: the aboive is equivalent to just
     // val result = service.doException
     // You can write it as a chain without even needing a variable:
     // service.doException.recover { ... }

     result.recover { case ex: Throwable =>
       println("recovered exception")
       Future.successful(99)
    }
 }
...