什么是Scala实现像这样的可重试调用的方式?

时间:2011-10-28 14:45:34

标签: java scala functional-programming

仍然是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);
        }
    };

}

在Scala中实现RetryableService实现的相同功能的最佳方法是什么?

它基本上调用调用方法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;

}

14 个答案:

答案 0 :(得分:158)

递归+ 第一类函数按名称参数==真棒。

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引入了Try,它提供了一种以尾递归方式实现重试的简洁方法。

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

答案 1 :(得分:7)

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参数定义。 e.g:

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

答案 2 :(得分: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也会返回Some[T],如果正在抛出异常,则None也会返回。{/ p>


更新

如果您想要填补上一个例外,那么您可以采用非常类似的方法,但使用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中,然后将其抛出。 (为简单起见,我只抛出最后一个例外)

答案 3 :(得分:4)

我建议这个 -

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

它可能会被改进为更加惯用的Scala,但我不是那种需要读者全心全意了解整个标准库的单行的粉丝。

答案 4 :(得分: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);
  }

正如我们所看到的,尾递归可以在这里使用。

这种方法为您提供了参数化catch容器的额外好处,因此您只能重试某个异常子集,添加终结器等。因此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
}

答案 5 :(得分:3)

现有的库可以提供帮助,称为retry,并且还有一个Java库,名为guava-retrying

以下是使用retry的一些示例:

#include <math.h>
#include <inttypes.h>
#include <stdio.h>

#define safe_abs(n) _Generic((n), \
    signed char: abs(n), short: abs(n), int: abs(n), long: labs(n), long long: llabs(n))

int main(void)
{
    int8_t n1 = -123;
    printf("n1 = %" PRId8 "\n", safe_abs(n1));

    int16_t n2 = -1234;
    printf("n2 = %" PRId16 "\n", safe_abs(n2));

    int32_t n3 = -123456;
    printf("n3 = %" PRId32 "\n", safe_abs(n3));

    int64_t n4 = -12345678910;
    printf("n4 = %" PRId64 "\n", safe_abs(n4));

    return 0;
}

答案 6 :(得分:2)

我喜欢接受的解决方案,但建议检查异常是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
  }
}

您不想重试控制流异常,通常不会重置线程中断......

答案 7 :(得分:1)

如果您想控制您重试的异常,可以使用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上重试;那是Option(t)部分。如果要返回空值,请在迭代器填充内使用Some(t)。)

让我们试试

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

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

看起来不错!

答案 8 :(得分: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()
   }

答案 9 :(得分:0)

这个项目似乎为不同的重试机制提供了一些很好的实现 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)
}

答案 10 :(得分:0)

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

答案 11 :(得分: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()
}

答案结束。停止阅读


版本,结果为Try:

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

答案 12 :(得分:0)

尝试之间暂停的可重用对象/方法:

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

答案 13 :(得分:0)

打印输出略有改进尝试使用x的N

// Returning T, throwing the exception on failure
      @annotation.tailrec
      final def retry[T](n: Int, name: String ="", attemptCount:Int = 1)(fn: => T): T = {
        logger.info(s"retry count: attempt $attemptCount of $n ....... function: $name")
        try {
          val result = fn
          logger.info(s"Succeeded: attempt $attemptCount of $n ....... function: $name")
          result
        } catch {
          case e: Throwable =>
            if (n < attemptCount) { Thread.sleep(5000 * attemptCount); retry(n, name, attemptCount+1)(fn) }
            else throw e 
        }
      }