2015/12/25

RxSwiftでのretry、retryWhenを利用したエラー時の再実行について

RxSwiftで`retry`、`retryWhen`を利用すると、処理の再実行やエラー処理がすっきり書けるようになるみたいなので試してみました。

対象となるシーケンス

次のような、3回やりなおさないと完了しないシーケンス (Observable) があるとします。

var count = 1
let sequence: Observable<Int> = create { observer in
    let error = NSError(domain: "Test", code: 400 + count, userInfo: nil)
    observer.on(.Next(1))
    observer.on(.Next(2))
    if count < 3 {
        observer.on(.Error(error))
        count++
    }
    observer.on(.Next(3))
    observer.on(.Next(4))
    observer.on(.Completed)

    return NopDisposable.instance
}

これを完了させるようにするには、retryretryWhenを使います。

retry

単純に失敗したら何回でもリトライしてよいなら、retryを使います。

let disposeBag = DisposeBag()
sequence
    .retry()
    .subscribe { print($0) }
    .addDisposableTo(disposeBag)

実行結果は次のようになります。

Next(1)
Next(2)
Next(1)
Next(2)
Next(1)
Next(2)
Next(3)
Next(4)
Completed

retry(n)

リトライする回数を指定することもできます。

sequence
    .retry(2)
    .subscribe { print($0) }
    .addDisposableTo(disposeBag)

実行結果は次の通り。retry(2)なので、2回目の失敗で諦めてエラーを出していることがわかります。

Next(1)
Next(2)
Next(1)
Next(2)
Error(Error Domain=Test Code=402 "(null)")

retryWhen

エラーコードが400番台のエラーはリトライしたいが、それ以外ではすぐに諦めてエラーを出したい、という状況を考えてみます。 このようなときはretryWhenを使います。

sequence
    .retryWhen { (errors: Observable<ErrorType>) in
        return errors.flatMap { err -> Observable<Int64> in
            let e = err as NSError
            if 400..<500 ~= e.code {
                return just(0)
            }
            return failWith(err)
        }
    }
    .subscribe { print($0) }
    .addDisposableTo(disposeBag)

sequenceがエラーになる度に、retryWhenのクロージャ引数のerrorsにエラーが渡ってきます。 よって、エラーのシーケンスerrorsflatMapなどで処理し、そのときにエラーコードを見て適切な処理をするようにします (ちなみに、ここでflatMapの代わりに、mapを使うとなぜか上手くいきませんでした)。

なお、retryWhenが実際にどのように処理をするのかは、次に示すように返すイベントの種類によって変わります (ここでは型は関係なく、Int64以外のものでも構いません)。

  • .Nextのとき、sequenceをリトライする
  • .Completeのとき、sequenceをリトライさせずに、完了させる
  • .Errorのとき、sequenceをリトライさせずに、エラーにする

これを上のコードに当てはめると、次のようになります。

  • エラーコードが400番台のときは.Next(0)になるのでリトライ
  • それ以外は.Error(err)になるのでエラー

retryWhenでリトライ回数制限

先ほどのコードにリトライの回数制限を設けてみます。つまり、次のような感じです。

  • エラーコードが400番台のエラーは、2回までリトライするが、3回めには諦めてエラーにする
  • エラーコードが400番台のエラーではないなら、すぐにエラーにする

これを実現する、とりあえずさっと思いつくコードは次のようになります。 外にretryCountがあって汚ないですが、ともかくちゃんと動きます。

var retryCount = 0
let scheduler = SerialDispatchQueueScheduler(globalConcurrentQueuePriority: .Low)
sequence
    .retryWhen { (errors: Observable<ErrorType>) in
        return errors.flatMap { err -> Observable<Int64> in
            let e = err as NSError
            retryCount += 1

            if 400..<500 ~= e.code && retryCount < 3 {
                return timer(5, scheduler)
            }
            return failWith(err)
        }
    }
    .subscribe { print($0) }
    .addDisposableTo(disposeBag)

ちなみに、このコードではjust(0)を返す代わりに、timer(5, scheduler)を返すようにしています。 これによってリトライ前に5秒待つようになります。

retryWhenでリトライ回数制限 (2)

先ほどのコードをscanで書き直してみます。 簡単に言うとscanはシーケンス向けreduceです。前の値と今の値が与えられるので、次の値を返していきます。

sequence
    .retryWhen { (errors: Observable<ErrorType>) in
        return errors.scan((0, nil)) { (a: (Int, ErrorType!), e) in
                return (a.0 + 1, e)
            }
            .flatMap { retryCount, err -> Observable<Int64> in
                let e = err as NSError
                if 400..<500 ~= e.code && retryCount < 3 {
                    return timer(5, scheduler)
                }
                return failWith(err)
            }
    }
    .subscribe { print($0) }
    .addDisposableTo(disposeBag)

上では、初期値にタプル(0, nil)を与えています。これは「0回のエラー、最新のエラーは空」を意味します。 そして、エラーになる度にタプルの0番めの値を1増加させ、タプル1番めの値は最新のエラーに置き替えるようにしています。

多少変則的なscanの使いかたですが、外に変数がなくなった分すっきりしました。

retryWhenでリトライ回数制限 (3)

実はscanを使わなくても、もっと綺麗に書く方法があります。 flatMapWithIndexという、シーケンスの要素と添字をタプルとして与えてくれるメソッドを使えばよいのです。

sequence
    .retryWhen { (errors: Observable<ErrorType>) in
        return errors
            .flatMapWithIndex{ err, retryCount -> Observable<Int64> in
                let e = err as NSError
                if 400..<500 ~= e.code && retryCount < 3 {
                    return timer(5, scheduler)
                }
                return failWith(err)
        }
    }
    .subscribe { print($0) }
    .addDisposableTo(disposeBag)

これですっきりとわかりやすいコードになりました。

おわりに

RxSwiftでretryretryWhenを利用してエラー時の再実行を試してみました。

なお、リトライしないのであれば、catchErrorJustReturn()catchError()というメソッドも利用できます。

関連項目

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。