Quelle est la façon Scala d'implémenter un appel réessayé comme celui-ci?

Toujours le débutant dans Scala et je cherche maintenant un moyen d'implémenter le code suivant:

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

}

Quelle serait la meilleure façon d'implémenter la même fonctionnalité que RetryableService implémente mais dans Scala?

Il appelle essentiellement la méthodecall N fois, si toutes échouent, l'exception est alors déclenchée, si elles réussissent, elle continue. Celui-ci ne retourne rien mais alors j'ai une autre version qui permet de retourner une valeur (donc, j'ai deux classes en Java) et je crois que je pourrais 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;

}
43
demandé sur Maurício Linhares 2011-10-28 18:45:33

13 réponses

Recursion +fonctions de première classe paramètres 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 par la réponse de @themel. Une ligne de code de 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 ce sens qu'elle a ajouté plusieurs appels à la trace de la pile. Pour une raison quelconque, le compilateur n'a pas pu optimiser la récursivité de la queue dans le gestionnaire de capture. Récursion de queue 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)
  }
}

Edit encore une fois : apparemment, je vais en faire un passe-temps pour continuer à revenir et ajouter des alternatives à cette réponse. Voici une version récursive qui est un peu plus simple que d'utiliser Option, mais utiliser return pour court-circuiter une fonction n'est pas une Scala 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)
}

Mise à jour Scala 2.10 . Comme c'est mon passe-temps, je revisite cette réponse de temps en temps. Scala 2.10 comme introduit Try , qui fournit un nettoyage façon d'implémenter une nouvelle 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
  }
}
141
répondu leedm777 2017-05-23 12:02:46

Il y a 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é un Task[T] , vous pouvez créer un nouveau Task[T] qui réessaiera un certain nombre de fois, où le délai entre les nouvelles tentatives est défini par le paramètre delays. par exemple:

// 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
6
répondu Gary Coady 2015-02-15 10:27:46

Voici une implémentation 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.


Mettre à jour

Si vous voulez lancer la dernière exception, vous pouvez adopter une approche très similaire mais utiliser Either au lieu 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
    }
}

Aussi, comme vous pouvez le voir, à la fin, au lieu d'avoir uniquement la dernière exception, je les ai tous. Ainsi, vous pouvez également envelopper les dans certains AggregatingException Si vous voulez, puis le jeter. (pour simplifier, je viens de lancer la dernière exception)

5
répondu tenshi 2011-10-28 16:13:40

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
} 

Il fait:

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

Il peut probablement être amélioré pour être plus idiomatique Scala, mais je ne suis pas un grand fan de one-liners qui exigent que le lecteur connaisse toute la bibliothèque standard par cœur de toute façon.

3
répondu themel 2011-10-28 15:35:17

Vous pouvez exprimer l'idée dans un style fonctionnel en utilisant scala.util.contrôle.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écursivité de la queue peut être utilisée ici.

Cette approche vous donne 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, ajouter des finaliseurs, etc. Donc, la version finale de retry pourrait ressembler à:

/** 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);
  }

Avec cela, vous pouvez faire des choses complexes comme:

retry(Exception.allCatch andFinally { print("Finished.") }, 3) {
  // your scode
}
3
répondu Petr Pudlák 2013-01-16 16:42:48

Il existe une bibliothèque qui peut vous aider, appelé réessayer, et il y a une bibliothèque Java trop, appelé goyave-nouvelle tentative de.

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 }
3
répondu Hosam Aly 2015-08-14 10:41:14

J'aime la solution acceptée, mais suggère de vérifier que l'exception est non fatale:

// 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 flux de contrôle, et généralement pas pour les interruptions de thread...

2
répondu srnm 2015-03-13 03:35:31

Si vous voulez 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 va également réessayer sur 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'itérateur.)

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)

Ç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(<console>:20)
    ...

scala> ioretry(4) { throw new Exception }
java.lang.Exception
    at $anonfun$1.apply(<console>:21)
    ...

Ça a l'air bien!

1
répondu Rex Kerr 2011-10-28 18:32:44

J'ai fini par adapter une réponse précédente pour permettre le filtrage sur quelles exceptions 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 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
    }

  }

, Vous pouvez appeler de deux façons:

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()
   }
1
répondu Doug Donohoe 2012-03-08 23:06:25
//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)
}}
}
0
répondu Santhosh Sath 2015-01-08 10:13:31

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)
}
0
répondu hipjim 2016-03-24 10:11:11

Cette solution n'est pas optimisée par le compilateur pour tail récursivité pour une raison quelconque (qui sait pourquoi?), mais en cas de nouvelles tentatives rares serait une option:

def retry[T](n: Int)(f: => T): T = {
  Try { f } recover {
    case _ if n > 1 => retry(n - 1)(f)
  } get
}

Utilisation:

val words: String = retry(3) {
  whatDoesTheFoxSay()
}

Fin de la réponse. Arrêtez de lire ici


Version avec le résultat comme un essai:

def reTry[T](n: Int)(f: => T): Try[T] = {
  Try { f } recoverWith {
    case _ if n > 1 => reTry(n - 1)(f)
  }
}

Utilisation:

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

Version avec une fonction retournant Try

def retry[T](n: Int)(f: => Try[T]): Try[T] = {
  f recoverWith {
    case _ if n > 1 => reTry(n - 1)(f)
  }
}

Utilisation:

// 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()
}
0
répondu Sergii Pogodin 2016-04-12 21:35:44

Un objet/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
  }
}
0
répondu Devis Lucato 2016-10-22 06:39:42