java - ¿Cuál es la forma de Scala de implementar una llamada reintentable como esta?
design coding-style (13)
Todavía soy un novato en Scala y ahora estoy buscando una forma de implementar el siguiente código en él:
@Override
public void store(InputStream source, String destination, long size) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(size);
final PutObjectRequest request = new PutObjectRequest(
this.configuration.getBucket(), destination, source, metadata);
new RetryableService(3) {
@Override
public void call() throws Exception {
getClient().putObject(request);
}
};
}
¿Cuál sería la mejor manera de implementar la misma funcionalidad que RetryableService implementa pero en Scala?
Básicamente, llama al método de llamada N veces; si todas fallan, la excepción se genera y, si tienen éxito, se mueve. Éste no devuelve nada, pero luego tengo otra versión que permite devolver un valor (entonces, tengo dos clases en Java) y creo que podría hacerlo con una sola clase / función en Scala.
¿Algunas ideas?
EDITAR
La implementación actual en Java es la siguiente:
public abstract class RetryableService {
private static final JobsLogger log = JobsLogger
.getLogger(RetryableService.class);
private int times;
public RetryableService() {
this(3);
}
public RetryableService(int times) {
this.times = times;
this.run();
}
private void run() {
RuntimeException lastExceptionParent = null;
int x = 0;
for (; x < this.times; x++) {
try {
this.call();
lastExceptionParent = null;
break;
} catch (Exception e) {
lastExceptionParent = new RuntimeException(e);
log.errorWithoutNotice( e, "Try %d caused exception %s", x, e.getMessage() );
try {
Thread.sleep( 5000 );
} catch (InterruptedException e1) {
log.errorWithoutNotice( e1, "Sleep inside try %d caused exception %s", x, e1.getMessage() );
}
}
}
try {
this.ensure();
} catch (Exception e) {
log.error(e, "Failed while ensure inside RetryableService");
}
if ( lastExceptionParent != null ) {
throw new IllegalStateException( String.format( "Failed on try %d of %s", x, this ), lastExceptionParent);
}
}
public void ensure() throws Exception {
// blank implementation
}
public abstract void call() throws Exception;
}
Aquí hay una posible implementación:
def retry[T](times: Int)(fn: => T) =
(1 to times).view flatMap (n => try Some(fn) catch {case e: Exception => None}) headOption
Puedes usarlo así:
retry(3) {
getClient.putObject(request)
}
retry
también devuelve Some[T]
si body se procesó correctamente y None
si body solo lanzó excepciones.
Actualizar
Si desea bobblear la última excepción, puede adoptar un enfoque muy similar, pero use Either
lugar de Option
:
def retry[T](times: Int)(fn: => T) = {
val tries = (1 to times).toStream map (n => try Left(fn) catch {case e: Exception => Right(e)})
tries find (_ isLeft) match {
case Some(Left(result)) => result
case _ => throw tries.reverse.head.right.get
}
}
Además, como puede ver, al final, en lugar de tener la última excepción, los tengo todos. De modo que también puede envolverlos en alguna AggregatingException
si lo desea y luego lanzarlos. (por simplicidad, acabo de arrojar la última excepción)
Esta solución no está optimizada por el compilador para ralentizar la recursividad por alguna razón (¿quién sabe por qué?), Pero en el caso de reintentos raros sería una opción:
def retry[T](n: Int)(f: => T): T = {
Try { f } recover {
case _ if n > 1 => retry(n - 1)(f)
} get
}
Uso:
val words: String = retry(3) {
whatDoesTheFoxSay()
}
Fin de la respuesta Deja de leer aquí
Versión con resultado como prueba:
def reTry[T](n: Int)(f: => T): Try[T] = {
Try { f } recoverWith {
case _ if n > 1 => reTry(n - 1)(f)
}
}
Uso:
// previous usage section will be identical to:
val words: String = reTry(3) {
whatDoesTheFoxSay()
} get
// Try as a result:
val words: Try[String] = reTry(3) {
whatDoesTheFoxSay()
}
Versión con una función que regresa Prueba
def retry[T](n: Int)(f: => Try[T]): Try[T] = {
f recoverWith {
case _ if n > 1 => reTry(n - 1)(f)
}
}
Uso:
// the first usage section will be identical to:
val words: String = retry(3) {
Try(whatDoesTheFoxSay())
} get
// if your function returns Try:
def tryAskingFox(): Try = Failure(new IllegalStateException)
val words: Try[String] = retry(3) {
tryAskingFox()
}
Este proyecto parece proporcionar algunas implementaciones agradables para diferentes mecanismos de reintento https://github.com/hipjim/scala-retry
// define the retry strategy
implicit val retryStrategy =
RetryStrategy.fixedBackOff(retryDuration = 1.seconds, maxAttempts = 2)
// pattern match the result
val r = Retry(1 / 1) match {
case Success(x) => x
case Failure(t) => log("I got 99 problems but you won''t be one", t)
}
Hay un método en scalaz.concurrent.Task[T]
: http://docs.typelevel.org/api/scalaz/nightly/#scalaz.concurrent.Task
def retry(delays: Seq[Duration], p: (Throwable) ⇒ Boolean = _.isInstanceOf[Exception]): Task[T]
Dada una Task[T]
, puede crear una Task[T]
nueva que volverá a intentar una cierta cantidad de veces, donde el retardo entre reintentos se define mediante el parámetro de delays
. p.ej:
// Task.delay will lazily execute the supplied function when run
val myTask: Task[String] =
Task.delay(???)
// Retry four times if myTask throws java.lang.Exception when run
val retryTask: Task[String] =
myTask.retry(Seq(20.millis, 50.millis, 100.millis, 5.seconds))
// Run the Task on the current thread to get the result
val result: String = retryTask.run
Hay una biblioteca existente que puede ayudar con eso, llamada retry , y también hay una biblioteca Java, llamada guava-retrying .
Estos son algunos ejemplos del uso de retry :
// retry 4 times
val future = retry.Directly(4) { () => doSomething }
// retry 3 times pausing 30 seconds in between attempts
val future = retry.Pause(3, 30.seconds) { () => doSomething }
// retry 4 times with a delay of 1 second which will be multipled
// by 2 on every attempt
val future = retry.Backoff(4, 1.second) { () => doSomething }
Me gusta la solución aceptada, pero sugiero que compruebe que la excepción sea NonFatal:
// Returning T, throwing the exception on failure
@annotation.tailrec
def retry[T](n: Int)(fn: => T): T = {
Try { fn } match {
case Success(x) => x
case _ if n > 1 && NonFatal(e) => retry(n - 1)(fn)
case Failure(e) => throw e
}
}
No desea volver a intentar una excepción de flujo de control, y generalmente no para las interrupciones de hilo ...
Puede expresar la idea en estilo funcional utilizando scala.util.control.Exception :
@annotation.tailrec
def retry[T](n: Int)(fn: => T): T =
Exception.allCatch.either(fn) match {
case Right(v) => v;
case Left(e) if (n <= 1) => throw e;
case _ => retry(n - 1)(fn);
}
Como podemos ver, la recursión de cola se puede usar aquí.
Este enfoque le brinda la ventaja adicional de que puede parametrizar el contenedor de capturas, por lo que solo puede volver a intentar un cierto subconjunto de excepciones, agregar finalizadores, etc. Así que la versión final del retry
podría verse así:
/** Retry on any exception, no finalizers. */
def retry[T](n: Int)(fn: => T): T =
retry(Exception.allCatch[T], n)(fn);
/** Parametrized retry. */
@annotation.tailrec
def retry[T](theCatch: Exception.Catch[T], n: Int)(fn: => T): T =
theCatch.either(fn) match {
case Right(v) => v;
case Left(e) if (n <= 1) => throw e;
case _ => retry(theCatch, n - 1)(fn);
}
Con esto, puedes hacer cosas complejas como:
retry(Exception.allCatch andFinally { print("Finished.") }, 3) {
// your scode
}
Recursion + funciones de primera clase parámetros de nombre == impresionante.
def retry[T](n: Int)(fn: => T): T = {
try {
fn
} catch {
case e =>
if (n > 1) retry(n - 1)(fn)
else throw e
}
}
El uso es así:
retry(3) {
// insert code that may fail here
}
Editar : ligera variación inspirada en la respuesta de @themel . Una menos línea de código :-)
def retry[T](n: Int)(fn: => T): T = {
try {
fn
} catch {
case e if n > 1 =>
retry(n - 1)(fn)
}
}
Editar nuevamente : La recursión me molestó porque agregó varias llamadas al seguimiento de la pila. Por algún motivo, el compilador no pudo optimizar la recursividad final en el controlador catch. Sin embargo, la recursión de cola que no está en el controlador catch se optimiza muy bien :-)
@annotation.tailrec
def retry[T](n: Int)(fn: => T): T = {
val r = try { Some(fn) } catch { case e: Exception if n > 1 => None }
r match {
case Some(x) => x
case None => retry(n - 1)(fn)
}
}
Edita una vez más : Aparentemente voy a convertirlo en un hobby para seguir volviendo y agregando alternativas a esta respuesta. Aquí hay una versión recursiva de la cola que es un poco más directa que usar la Option
, pero usar el return
al cortocircuito de una función no es idiomático de Scala.
@annotation.tailrec
def retry[T](n: Int)(fn: => T): T = {
try {
return fn
} catch {
case e if n > 1 => // ignore
}
retry(n - 1)(fn)
}
Actualización de Scala 2.10 . Como es mi hobby, revisito esta respuesta de vez en cuando. Scala 2.10 como Try introducido, que proporciona una forma limpia de implementar el reintento de una manera recursiva.
// Returning T, throwing the exception on failure
@annotation.tailrec
def retry[T](n: Int)(fn: => T): T = {
util.Try { fn } match {
case util.Success(x) => x
case _ if n > 1 => retry(n - 1)(fn)
case util.Failure(e) => throw e
}
}
// Returning a Try[T] wrapper
@annotation.tailrec
def retry[T](n: Int)(fn: => T): util.Try[T] = {
util.Try { fn } match {
case x: util.Success[T] => x
case _ if n > 1 => retry(n - 1)(fn)
case fn => fn
}
}
Si desea controlar las excepciones que vuelve a intentar, puede usar métodos en scala.util.control.Exception
:
import java.io._
import scala.util.control.Exception._
def ioretry[T](n: Int)(t: => T) = (
Iterator.fill(n){ failing[T](classOf[IOException]){ Option(t) } } ++
Iterator(Some(t))
).dropWhile(_.isEmpty).next.get
(Tal como está escrito, también volverá a intentarlo en nulo; esa es la parte Option(t)
. Si desea que se devuelvan nulos, use Some(t)
dentro del relleno del iterador en su lugar).
Probemos esto con
class IoEx(var n: Int) {
def get = if (n>0) { n -= 1; throw new IOException } else 5
}
val ix = new IoEx(3)
¿Funciona?
scala> ioretry(4) { ix.get }
res0: Int = 5
scala> ix.n = 3
scala> ioretry(2) { ix.get }
java.io.IOException
at IoEx.get(<console>:20)
...
scala> ioretry(4) { throw new Exception }
java.lang.Exception
at $anonfun$1.apply(<console>:21)
...
¡Se ve bien!
Sugeriría esto -
def retry[T](n: Int)(code: => T) : T = {
var res : Option[T] = None
var left = n
while(!res.isDefined) {
left = left - 1
try {
res = Some(code)
} catch {
case t: Throwable if left > 0 =>
}
}
res.get
}
Lo hace:
scala> retry(3) { println("foo"); }
foo
scala> retry(4) { throw new RuntimeException("nope"); }
java.lang.RuntimeException: nope
at $anonfun$1.apply(<console>:7)
at $anonfun$1.apply(<console>:7)
at .retry(<console>:11)
at .<init>(<console>:7)
at .<clinit>(<console>)
at RequestResult$.<init>(<console>:9)
at RequestResult$.<clinit>(<console>)
at RequestResult$scala_repl_result(<console>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at scala.tools.nsc.Interpreter$Request$$anonfun$loadAndRun$1$$anonfun$apply$17.apply(Interpreter.scala:988)
at scala.tools.nsc.Interpreter$Request$$anonfun$loadAndRun$1$$anonfun$apply$17.apply(Interpreter....
scala> var i = 0 ;
i: Int = 0
scala> retry(3) { i = i + 1; if(i < 3) throw new RuntimeException("meh");}
scala> i
res3: Int = 3
Probablemente se pueda mejorar para ser Scala más idiomático, pero no soy un gran fanático de las frases ingeniosas que requieren que el lector sepa de memoria toda la biblioteca estándar de todos modos.
Terminé adaptando una respuesta anterior para permitir el filtrado sobre qué excepciones volver a intentar:
/**
* Attempt ''fn'' up to ''attempts'' times, retrying only if ''forExceptions'' returns true for retry-able exceptions.
*/
def retry[T](attempts: Int, forExceptions: (Throwable) => Boolean)(fn: => T): T =
{
// toStream creates a lazily evaluated list, which we map to a try/catch block resulting in an Either
val tries = (1 to attempts).toStream map
{
n =>
try
Left(fn)
catch
{
case e if forExceptions(e) => Right(e)
}
}
// find the first ''Either'' where left is defined and return that, or if not found, return last
// exception thrown (stored as ''right''). The cool thing is that because of lazy evaluation, ''fn'' is only
// evaluated until it success (e.g., until Left is found)
tries find (_ isLeft) match
{
case Some(Left(result)) => result
case _ => throw tries.reverse.head.right.get
}
}
Puedes llamar de dos maneras:
val result = retry(4, _.isInstanceOf[SomeBadException])
{
boom.doit()
}
o con funciones parciales (también muestra una versión donde no importa el valor de retorno)
def pf: PartialFunction[Throwable, Boolean] =
{
case x: SomeOtherException => true
case _ => false
}
retry(4, pf)
{
boom.doit()
}
Un objeto / método reutilizable con una pausa entre intentos:
Retry(3, 2 seconds) { /* some code */ }
Código:
object Retry {
def apply[A](times: Int, pause: Duration)(code: ⇒ A): A = {
var result: Option[A] = None
var remaining = times
while (remaining > 0) {
remaining -= 1
try {
result = Some(code)
remaining = 0
} catch {
case _ if remaining > 0 ⇒ Thread.sleep(pause.toMillis)
}
}
result.get
}
}
//Here is one using Play framework
def retry[T](times:Int)(block: => Future[T])(implicit ctx: ExecutionContext):Future[T] = {
type V = Either[Throwable,T]
val i:Iterator[Future[Option[V]]] =
Iterator.continually(block.map(t => Right(t)).recover { case e => Left(e) }.map(t => Some(t)))
def _retry:Iteratee[V,V] = {
def step(ctr:Int)(i:Input[V]):Iteratee[V,V] = i match {
case Input.El(e) if (e.isRight) => Done(e,Input.EOF)
case _ if (ctr < times) => Cont[V,V](i => step(ctr + 1)(i))
case Input.El(e) => Done(e,Input.EOF)
}
Cont[V,V](i => step(0)(i))
}
Enumerator.generateM(i.next).run(_retry).flatMap { _ match {
case Right(t) => future(t)
case Left(e) => Future.failed(e)
}}
}