Що таке метод Scala для реалізації спроб повторного спроби, як цей?

Ще новачок у Scala та я зараз шукаю шлях для реалізації на нього наступного коду:

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

}

Який найкращий спосіб реалізувати ту ж функціональність, яку реалізує RetryableService, але в Scala?

Він в основному називає метод call N разів, якщо всі вони не виконуються, то виняток потім піднімається, якщо вони досягають успіху. Це не повертає нічого, крім цього, у мене є інша версія, яка дозволяє повернути значення (так, у мене є два класи в Java), і я вважаю, що зможу це зробити з єдиним класом/функцією в Scala.

Будь-які ідеї?

EDIT

Поточна реалізація в java така:

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;

}
41

13 Відповіді

Рекурсія + <�страйк> функції першого класу за іменем параметрів == дивовижний.

def retry[T](n: Int)(fn: => T): T = {
  try {
    fn
  } catch {
    case e =>
      if (n > 1) retry(n - 1)(fn)
      else throw e
  }
}

Використання виглядає так:

retry(3) {
 //insert code that may fail here
}

Edit: slight variation inspired by @themel's answer. One fewer line of code :-)

def retry[T](n: Int)(fn: => T): T = {
  try {
    fn
  } catch {
    case e if n > 1 =>
      retry(n - 1)(fn)
  }
}

Edit Again: The recursion bothered me in that it added several calls to the stack trace. For some reason, the compiler couldn't optimize tail recursion in the catch handler. Tail recursion not in the catch handler, though, optimizes just fine :-)

@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 yet again: Apparently I'm going to make it a hobby to keep coming back and adding alternatives to this answer. Here's a tail-recursive version that's a bit more straightforward than using Option, but using return to short-circuit a function isn't idiomatic 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)
}

Scala 2.10 update. As is my hobby, I revisit this answer occasionally. Scala 2.10 as introduced Try, which provides a clean way of implementing retry in a tail-recursive way.

// 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
  }
}
139
додано
Чому scala.util.control.Exception.catching (classOf [E1], classOf [E2 & zwnj;]) типу Catch [Нічого] ? Наприклад, Exception.catching (classOf [PSQLException]) стає типом : Catch [Nothing] . Чи має бути такий спосіб? Я не знаю, чому вони додали тип-параметризацію, якщо він завжди буде Nothing у будь-якому випадку ...? : /
додано Автор kornfridge, джерело
Це краса.
додано Автор Maurício Linhares, джерело
Чи є спосіб зробити tailrec працювати з recoverWith так що я тільки треба повторити спробу за певним винятком?
додано Автор Minh Thai, джерело
Насправді, за іменами параметрів, хоча мене турбує, може бути щось незручне, коли відбувається рекурсивний пропуск за іменем параметра.
додано Автор Daniel C. Sobral, джерело
@ annotation.tailrec def retry [T] (n: Int) (fn: => T): Спробуйте [T] = {Спробуйте {fn} match {case x: Success [T] => x case Failure NonFatal (_)) if n> 1 => retry (n - 1) (fn) case f => f}}
додано Автор piotr, джерело
@ MaurícioLinhares Дякую :-D
додано Автор leedm777, джерело
@IshankGulati Змініть сповіщення про повторення в пункті catch, щоб вказати виняток, яке ви хочете повторити. Подібно до case util.Failure (e: RuntimeException), якщо n> 1 => retry (n - 1) (fn) в прикладі Scala 2.10.
додано Автор leedm777, джерело
@fricadelle Я думаю, що просто викликати Thread.sleep (waitMillis) , перш ніж retry (n-1) (fn) буде робити трюк.
додано Автор leedm777, джерело
Потрібно перевірити NonFatal
додано Автор smartnut007, джерело
Я читаю всі ваші рішення, і ви дійсно можете помітити, як кожен покращує попередній. Ось чому Scala - це любов. Це те, що робить скалу чудовою. Гарний. Я пристосовую останню і додаю ескалації тайм-аутів між кожним повторним спробом. Я постараюся зберігати красу наскільки це можливо.
додано Автор Rafael Saraiva, джерело
Я просто вказую, що іноді ви захочете повторити спроби лише за деякими винятками, одночасно кидаючи інших. Ви можете використовувати scala.util.control.Exception.catching (classOf [E1], classOf [E2 & zwnj;]). MatchTry (fn) match ...
додано Автор Cristian Vrabie, джерело
Якщо ви все ще маєте звичку @dave, перегляньте об'єкт scala.util.Try ! Відмінні відповіді!
додано Автор tysonjh, джерело
Як щодо трохи менших рядків :) @ annotation.tailrec def retry [T] (n: Int) (fn: => T): Спробуйте [T] = Спробувати (fn) match {case x: util.Success [T] => х випадку _ якщо n> 1 => повторити спробу (n - 1) (fn) випадку f => f}
додано Автор Gaurav Abbi, джерело
Чи можна це змінити, щоб спробувати повторно лише за певних винятків?
додано Автор Ishank Gulati, джерело
@ leedm777, але функція retry буде специфічною для конкретного запиту. Я думав про видалення списку винятків у методі.
додано Автор Ishank Gulati, джерело
Як це можна покращити, щоб додати час очікування між кожним повторним спробом?
додано Автор fricadelle, джерело
одне питання тут. Як змінити його, якщо моя функція повертає майбутнє?
додано Автор asdasdsdf, джерело

There is a method in 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]

Задано Task [T] , ви можете створити новий Task [T] , який буде спробувати певну кількість разів, де затримка між спробами визначається затримка . напр .:

// 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
додано

Ось одна можлива реалізація:

def retry[T](times: Int)(fn: => T) = 
    (1 to times).view flatMap (n => try Some(fn) catch {case e: Exception => None}) headOption

Ви можете використовувати його таким чином:

retry(3) {
    getClient.putObject(request)
}

retry also returns Some[T] if body was processed successfully and None if body was only throwing exceptions.


Оновити

Якщо ви хочете вичерпати останнє виняток, то ви можете прийняти дуже схожий підхід, але замість Option скористайтеся командою Anywhere :

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

Крім того, як ви бачите, наприкінці, замість того, щоб мати лише останнє виключення, у мене їх все. Таким чином, ви можете також загорнути їх у деякий AggregatingException , якщо хочете, а потім викинути. (для простоти я просто викидаю останнє виняток)

5
додано
Дякую @ thenshi :)
додано Автор Maurício Linhares, джерело
Якщо воно постійно зазнає невдачі, то виключення повинне перевищуватись, я додам повну реалізацію в питанні.
додано Автор Maurício Linhares, джерело
Зверніть увагу, що це, мабуть, не те, що OP має намір відновити: побічні ефекти - повторити спробу (3) {println ("foo")} буде друкувати три рядки.
додано Автор themel, джерело
Ти спробував? Це не для мене в Scala 2.8.1.
додано Автор themel, джерело
@tenshi: Правильно, я можу відтворити - працює в 2.9.1, але не в 2.8.1. Я цього не очікував.
додано Автор themel, джерело
@ MaurícioLinhares: Я оновив мою відповідь
додано Автор tenshi, джерело
@ themel: після моєї реалізації retry , retry (3) {println ("foo")) буде друкувати тільки один раз
додано Автор tenshi, джерело
@ themel: я використовую 2.9.1.final , і він друкує foo лише один раз
додано Автор tenshi, джерело
Відмінна відповідь - дізнайтеся про .view() і .toStream() і, нарешті, зрозумійте, чому Option ефективно Traverable/Iterable настільки потужна.
додано Автор Doug Donohoe, джерело

Існує існуюча бібліотека, яка може допомогти з цим, що називається повторити спробу , і теж є бібліотека Java, яка викликається повторити гуава .

Ось кілька прикладів використання повторити спробу .

// 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
додано

Ви можете виразити ідею у функціональному стилі за допомогою 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);
  }

Як бачимо, тут можна використати хвостову рекурсію.

Цей підхід дає вам додаткову перевагу, яку можна параметризувати контейнер зловмисників, тому ви можете тільки спробувати певне підмножина винятків, додати фіналізатори тощо. Тож остаточна версія retry може виглядати так:

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

За допомогою цього ви можете виконати складні речі:

retry(Exception.allCatch andFinally { print("Finished.") }, 3) {
 //your scode
}
3
додано

Я б запропонував це -

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
} 

Це робить:

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

Можливо, це може бути покращено, щоб бути більш ідіоматичною Scala, але я не великий шанувальник однозарядних, які вимагають від читача знати всю стандартну бібліотеку серцем у будь-якому випадку.

3
додано
Це також відмінне рішення.
додано Автор Maurício Linhares, джерело

Мені подобається прийняте рішення, але рекомендую перевірити виключення 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
  }
}

Ви не бажаєте повторювати виняток із керуючим потоком, і зазвичай не для переривань потоку ...

2
додано
Лінія збігу NonFatal не буде компілюватися, якщо не виконаю: case Несправність (e), якщо n> 1 && NonFatal (e) => повторити спробу (n - 1) (fn) .
додано Автор Taylor R, джерело

Якщо ви хочете контролювати, які виключення ви повторите, ви можете використовувати методи 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

(Як і написано, це також буде повторитись на нуль, це частина Option (t) . Якщо ви хочете повернути нульові значення, використовуйте Some (t) усередині ітератора fill замість цього.)

Давайте спробуємо це

class IoEx(var n: Int) {
  def get = if (n>0) { n -= 1; throw new IOException } else 5
}
val ix = new IoEx(3)

Це працює?

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)
    ...

Виглядає добре!

1
додано

Я нарешті адаптував попередню відповідь, щоб дозволити фільтрувати, які виключення потрібно повторити:

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

  }

Ви можете зателефонувати двома способами:

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

або з частковими функціями (також показує версію, де не хвилює повернення значення)

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

   retry(4, pf)
   {
      boom.doit()
   }
1
додано
//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
додано

Багаторазовий об'єкт/метод з паузою між спробами:

Retry(3, 2 seconds) { /* some code */ }

Код:

object Retry {
  def apply[A](times: Int, pause: Duration)(Код: ⇒ 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
додано

This project seems to provide some nice implementations for different retry mechanisms 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
додано

Це рішення не є оптимізованим компілятором до рекурсії хвоста з якоїсь причини (хто знає чому?), Але у випадку рідкісних спроб буде варіант:

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

Використання:

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

Кінець відповіді. Припиніть читати тут


Версія з результатом як спробувати:

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

Використання:

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

Версія з функцією, що повертає спробу

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

Використання:

// 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
додано
ІТ КПІ - Java
ІТ КПІ - Java
436 учасників

ІТ КПІ - Scala
ІТ КПІ - Scala
45 учасників