Как в Scala реализовать такой повторный вызов? - PullRequest
49 голосов
/ 28 октября 2011

Все еще новичок в 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.

Любойидеи?

РЕДАКТИРОВАТЬ

Текущая реализация в 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;

}

Ответы [ 13 ]

160 голосов
/ 28 октября 2011

Рекурсия + функции первого класса параметры по имени == потрясающе.

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
}

Редактировать : небольшое изменение, вдохновленное ответом @ themel . На одну строку кода меньше: -)

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

Редактировать снова : рекурсия беспокоила меня тем, что она добавила несколько вызовов в трассировку стека. По какой-то причине компилятор не смог оптимизировать хвостовую рекурсию в обработчике catch. Хвостовая рекурсия не в обработчике catch, тем не менее, оптимизируется просто отлично: -)

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

Отредактируйте еще раз : Очевидно, я собираюсь сделать это хобби, чтобы продолжать возвращаться и добавлять альтернативы к этому ответу. Вот хвосто-рекурсивная версия, которая немного более проста, чем использование Option, но использование return для короткого замыкания функции не является идиоматическим 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 . Как и мое хобби, я иногда возвращаюсь к этому ответу. Scala 2.10 в представленном виде Попробуйте , который обеспечивает чистый способ реализации повторных попыток с хвостовой рекурсией.

// 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
  }
}
7 голосов
/ 15 февраля 2015

В 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], который будет повторяться определенное количество раз, где задержкамежду повторными попытками определяется параметром delays.например:

// 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 голосов
/ 28 октября 2011

Вот одна из возможных реализаций:

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 также возвращает Some[T], если тело было успешно обработано, и None если тело генерировало только исключения.


Обновление

Если вы хотите вспомнить последнее исключение, вы можете использовать очень похожий подход, но используйте Either вместо 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
    }
}

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

4 голосов
/ 28 октября 2011

Я бы предложил это -

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

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

3 голосов
/ 14 августа 2015

Существует существующая библиотека, которая может помочь с этим, которая называется retry , а также есть библиотека Java, которая называется guava-retry .

Вот несколько примеров использования 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 голосов
/ 16 января 2013

Вы можете выразить идею в функциональном стиле, используя 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
}
2 голосов
/ 13 марта 2015

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

Вы не хотите повторять исключение потока управления, и обычно не для прерываний потока ...

1 голос
/ 09 марта 2012

Я закончил тем, что адаптировал предыдущий ответ, чтобы разрешить фильтрацию, для которых нужно повторить исключения:

  /**
   * 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 голос
/ 28 октября 2011

Если вы хотите контролировать, какие исключения вы повторяете, вы можете использовать методы в 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

(Как написано, он также будет повторять при нулевом;вы хотите, чтобы возвращались значения null, вместо этого используйте Some(t) внутри заливки итератора.)

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

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

Это работает?* Хорошо выглядит!

0 голосов
/ 21 октября 2016

Многократно используемый объект / метод с паузой между попытками:

Retry(3, 2 seconds) { /* some 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
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...