web-dev-qa-db-fra.com

Quelle est la méthode Scala pour implémenter un appel réessayable comme celui-ci?

Toujours le débutant à 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);
        }
    };

}

Quel serait le meilleur moyen d'implémenter la même fonctionnalité que RetryableService mais en Scala?

Il appelle essentiellement la méthode call N fois. Si elles échouent toutes, l'exception est levée, si elles réussissent, elles continuent. Celui-ci ne renvoie rien mais j'ai une autre version qui permet de retourner une valeur (donc, j'ai deux classes en Java) et je pense que je pourrais le 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;

}
47
Maurício Linhares

Récursion + fonctions de première classe par nom de paramètres == 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 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é car elle a ajouté plusieurs appels à la trace de pile. Pour une raison quelconque, le compilateur ne pouvait pas optimiser la récursion de la queue dans le gestionnaire de capture. La récursion de la queue pas dans le gestionnaire de prises, cependant, optimise parfaitement :-)

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

Modifier encore une fois: Apparemment, je vais en faire un loisir de continuer à revenir et d’ajouter des alternatives à cette réponse. Voici une version queue récursive qui est un peu plus simple que d'utiliser Option, mais utiliser return pour court-circuiter une fonction 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 revisite cette réponse de temps en temps. Scala 2.10 tel que présenté Try , qui fournit un moyen propre 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 util.Failure(_) if n > 1 => retry(n - 1)(fn)
    case fn => fn
  }
}
155
leedm777

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]

Avec un Task[T], vous pouvez créer un nouveau Task[T] qui réessayera un certain nombre de fois, le délai entre les tentatives étant 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
Gary Coady

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

En outre, comme vous pouvez le constater, à la fin, au lieu d’avoir la dernière exception, je les ai tous. Donc, vous pouvez aussi les envelopper dans une AggregatingException si vous le souhaitez et ensuite le jeter. (pour simplifier, je viens de lancer la dernière exception)

5
tenshi

Je suggérerais 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(<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 devenir plus idiomatique en Scala, mais je ne suis pas un grand fan des monolithes qui obligent le lecteur à connaître la bibliothèque standard complète par cœur de toute façon.

4
themel

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 de pouvoir paramétrer le conteneur de capture. Vous ne pouvez donc réessayer qu’un certain sous-ensemble d’exceptions, ajouter 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);

/** 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
Petr Pudlák

Il existe une bibliothèque existante, appelée retry , et une bibliothèque Java également appelée guava-retrying .

Voici quelques exemples d'utilisation 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 }
3
Hosam Aly

J'aime la solution acceptée, mais je 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 flux de contrôle, et généralement pas pour les interruptions de threads ...

2
srnm

J'ai fini par adapter une réponse précédente pour permettre le filtrage sur lequel les 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 manières:

val result = retry(4, _.isInstanceOf[SomeBadException])
{
   boom.doit()
}

ou avec des fonctions partielles (montrant aussi la version où la valeur de retour ne vous intéresse pas)

    def pf: PartialFunction[Throwable, Boolean] =
    {
      case x: SomeOtherException => true
      case _ => false
    }

   retry(4, pf)
   {
      boom.doit()
   }
1
Doug Donohoe

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 cela a été écrit, il faudra également essayer à nouveau null; c'est la partie Option(t). Si vous souhaitez que les null soient renvoyés, utilisez plutôt Some(t) dans le remplissage de l'itérateur.)

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

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

Cela semble bon!

1
Rex Kerr

Ce projet semble fournir certaines implémentations de Nice 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
hipjim

Un objet/une 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
Devis Lucato

Cette solution n'est pas optimisée par le compilateur pour réduire la récursivité pour une raison quelconque (qui sait pourquoi?), Mais dans le cas d'essais rares, une option serait:

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

Usage:

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

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


Version avec résultat en tant que Try:

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: 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)
  }
}

Usage:

// 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
Sergii Pogodin
//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
Santhosh Sath