¿Cuál es la forma de Scala de implementar una llamada reintentable como esta?

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; } 

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 stack. Por algún motivo, el comstackdor 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 rebash 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 } } 

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 rebashs 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 

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)

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(:7) at $anonfun$1.apply(:7) at .retry(:11) at .(:7) at .() at RequestResult$.(:9) at RequestResult$.() at RequestResult$scala_repl_result() 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.

Puede express 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 } 

Hay una biblioteca existente que puede ayudar con eso, llamada reintentar , y también hay una biblioteca Java, llamada guava-retrying .

Estos son algunos ejemplos del uso de rebashs :

 // 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 …

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(:20) ... scala> ioretry(4) { throw new Exception } java.lang.Exception at $anonfun$1.apply(:21) ... 

¡Se ve bien!

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 (eg, 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() } 
 //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) }} } 

Este proyecto parece proporcionar algunas implementaciones agradables para diferentes mecanismos de rebash 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) } 

Esta solución no está optimizada por el comstackdor para ralentizar la recursividad por alguna razón (¿quién sabe por qué?), Pero en el caso de rebashs 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() } 

Un objeto / método reutilizable con una pausa entre bashs:

 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 } } 
Intereting Posts