Encore le débutant à Scala et je cherche maintenant un moyen d’implémenter le code suivant:
@Override public void store(InputStream source, Ssortingng 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); } }; }
Quel serait le meilleur moyen d’implémenter la même fonctionnalité que RetryableService implémente mais dans Scala?
Il appelle essentiellement la méthode d’ appel N fois, si toutes échouent, l’exception est alors levée si elle réussit. Celui-ci ne renvoie rien mais j’ai une autre version qui permet de renvoyer une valeur (j’ai donc deux classes en Java) et je crois pouvoir faire avec une seule classe / fonction dans Scala.
Des idées?
MODIFIER
L’implémentation actuelle en Java est la suivante:
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; }
Récursivité + fonctions de première classe parameters par nom == génial.
def retry[T](n: Int)(fn: => T): T = { try { fn } catch { case e => if (n > 1) retry(n - 1)(fn) else throw e } }
L’utilisation est comme ceci:
retry(3) { // insert code that may fail here }
Edit : légère variation inspirée de la réponse de @themel . Une ligne de code en moins 🙂
def retry[T](n: Int)(fn: => T): T = { try { fn } catch { case e if n > 1 => retry(n - 1)(fn) } }
Edit Again : La récursivité m’a dérangé en ajoutant plusieurs appels à la trace de la stack. Pour une raison quelconque, le compilateur n’a pas pu optimiser la récursion de la queue dans le gestionnaire de capture. La récursion de la queue n’est pas dans le gestionnaire de capture, cependant, optimise très 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) } }
Éditez encore : Apparemment, je vais faire un passe-temps pour continuer à revenir et append des alternatives à cette réponse. Voici une version récursive qui est un peu plus simple que l’utilisation de l’ Option
, mais l’utilisation d’une fonction de return
en court-circuit n’est pas idiomatique.
@annotation.tailrec def retry[T](n: Int)(fn: => T): T = { try { return fn } catch { case e if n > 1 => // ignore } retry(n - 1)(fn) }
Scala 2.10 mise à jour . Comme c’est mon passe-temps, je reviens sur cette réponse de temps en temps. Scala 2.10 telle qu’introduite Try , qui fournit un moyen propre d’implémenter la tentative de manière récursive.
// 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 } }
Il existe une méthode dans 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]
Étant donné une Task[T]
, vous pouvez créer une nouvelle Task[T]
qui réessayera un certain nombre de fois, le délai entre les tentatives étant défini par le paramètre de delays
. par exemple:
// Task.delay will lazily execute the supplied function when run val myTask: Task[Ssortingng] = Task.delay(???) // Retry four times if myTask throws java.lang.Exception when run val retryTask: Task[Ssortingng] = myTask.retry(Seq(20.millis, 50.millis, 100.millis, 5.seconds)) // Run the Task on the current thread to get the result val result: Ssortingng = retryTask.run
Voici une mise en œuvre possible:
def retry[T](times: Int)(fn: => T) = (1 to times).view flatMap (n => try Some(fn) catch {case e: Exception => None}) headOption
Vous pouvez l’utiliser comme ceci:
retry(3) { getClient.putObject(request) }
retry
renvoie également Some[T]
si le corps a été traité avec succès et None
si le corps ne lançait que des exceptions.
Si vous voulez remonter la dernière exception, vous pouvez adopter une approche très similaire, mais utilisez Either
au lieu de Option
:
def retry[T](times: Int)(fn: => T) = { val sortinges = (1 to times).toStream map (n => try Left(fn) catch {case e: Exception => Right(e)}) sortinges find (_ isLeft) match { case Some(Left(result)) => result case _ => throw sortinges.reverse.head.right.get } }
Aussi, comme vous pouvez le voir, à la fin, au lieu d’avoir que la dernière exception, je les ai toutes. Donc, vous pouvez également les envelopper dans une AggregatingException
si vous le souhaitez et ensuite le lancer. (pour simplifier, je jette juste la dernière exception)
Je suggère ceci –
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 }
Cela fait:
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
On peut probablement l’améliorer pour devenir une Scala plus idiomatique, mais je ne suis pas un grand fan des one-liners qui nécessitent de toute façon que le lecteur connaisse toute la bibliothèque standard par cœur.
Vous pouvez exprimer l’idée dans un style fonctionnel en utilisant 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); }
Comme nous pouvons le voir, la récursion de la queue peut être utilisée ici.
Cette approche vous offre l’avantage supplémentaire que vous pouvez paramétrer le conteneur catch, de sorte que vous ne pouvez réessayer qu’un certain sous-ensemble d’exceptions, append des finaliseurs, etc. Ainsi, la version finale de retry
pourrait ressembler à ceci:
/** Retry on any exception, no finalizers. */ def retry[T](n: Int)(fn: => T): T = retry(Exception.allCatch[T], n)(fn); /** Paramesortingzed 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); }
Avec cela, vous pouvez faire des choses complexes comme:
retry(Exception.allCatch andFinally { print("Finished.") }, 3) { // your scode }
Il existe une bibliothèque existante qui peut aider avec cela, appelée réessayer , et il existe également une bibliothèque Java, appelée guava-retrying .
Voici quelques exemples d’utilisation de réessayer :
// 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 }
J’aime la solution acceptée, mais suggère de vérifier que l’exception est 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 } }
Vous ne voulez pas réessayer une exception de stream de contrôle, et généralement pas pour les interruptions de thread …
Si vous souhaitez contrôler les exceptions que vous réessayez, vous pouvez utiliser des méthodes dans 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
(Comme écrit, il réessayera également la valeur null; c’est la partie Option(t)
. Si vous voulez que les valeurs NULL soient renvoyées, utilisez plutôt Some(t)
dans le remplissage de l’iterator).
Essayons ceci avec
class IoEx(var n: Int) { def get = if (n>0) { n -= 1; throw new IOException } else 5 } val ix = new IoEx(3)
Est-ce que ça marche?
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) ...
Cela semble bon!
J’ai fini par adapter une réponse précédente pour permettre le filtrage des exceptions sur lesquelles réessayer:
/** * 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 sortinges = (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) sortinges find (_ isLeft) match { case Some(Left(result)) => result case _ => throw sortinges.reverse.head.right.get } }
Vous pouvez appeler de deux manières:
val result = retry(4, _.isInstanceOf[SomeBadException]) { boom.doit() }
ou avec des fonctions partielles (montrant également la version où ne se soucient pas de la valeur de retour)
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) }} }
Ce projet semble fournir de belles implémentations pour différents mécanismes de nouvelle tentative 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) }
Cette solution n’est pas optimisée par le compilateur pour la récursion de la queue pour une raison quelconque (qui sait pourquoi?), Mais dans le cas de tentatives peu fréquentes serait une option:
def retry[T](n: Int)(f: => T): T = { Try { f } recover { case _ if n > 1 => retry(n - 1)(f) } get }
Usage:
val words: Ssortingng = retry(3) { whatDoesTheFoxSay() }
Fin de la réponse. Arrête de lire ici
def reTry[T](n: Int)(f: => T): Try[T] = { Try { f } recoverWith { case _ if n > 1 => reTry(n - 1)(f) } }
Usage:
// previous usage section will be identical to: val words: Ssortingng = reTry(3) { whatDoesTheFoxSay() } get // Try as a result: val words: Try[Ssortingng] = reTry(3) { whatDoesTheFoxSay() }
def retry[T](n: Int)(f: => Try[T]): Try[T] = { f recoverWith { case _ if n > 1 => reTry(n - 1)(f) } }
Usage:
// the first usage section will be identical to: val words: Ssortingng = retry(3) { Try(whatDoesTheFoxSay()) } get // if your function returns Try: def tryAskingFox(): Try = Failure(new IllegalStateException) val words: Try[Ssortingng] = retry(3) { tryAskingFox() }
Un object / méthode réutilisable avec une pause entre les tentatives:
Retry(3, 2 seconds) { /* some code */ }
Code:
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 } }