対象となるシーケンス
次のような、3回やりなおさないと完了しないシーケンス (Observable) があるとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 } |
これを完了させるようにするには、retry
やretryWhen
を使います。
retry
単純に失敗したら何回でもリトライしてよいなら、retry
を使います。
1 2 3 4 5 | let disposeBag = DisposeBag() sequence .retry() .subscribe { print($ 0 ) } .addDisposableTo(disposeBag) |
実行結果は次のようになります。
1 2 3 4 5 6 7 8 9 | Next( 1 ) Next( 2 ) Next( 1 ) Next( 2 ) Next( 1 ) Next( 2 ) Next( 3 ) Next( 4 ) Completed |
retry(n)
リトライする回数を指定することもできます。
1 2 3 4 | sequence .retry( 2 ) .subscribe { print($ 0 ) } .addDisposableTo(disposeBag) |
実行結果は次の通り。retry(2)
なので、2回目の失敗で諦めてエラーを出していることがわかります。
1 2 3 4 5 | Next( 1 ) Next( 2 ) Next( 1 ) Next( 2 ) Error(Error Domain=Test Code= 402 "(null)" ) |
retryWhen
エラーコードが400番台のエラーはリトライしたいが、それ以外ではすぐに諦めてエラーを出したい、という状況を考えてみます。
このようなときはretryWhen
を使います。
1 2 3 4 5 6 7 8 9 10 11 12 | 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
にエラーが渡ってきます。
よって、エラーのシーケンスerrors
をflatMap
などで処理し、そのときにエラーコードを見て適切な処理をするようにします (ちなみに、ここでflatMap
の代わりに、map
を使うとなぜか上手くいきませんでした)。
なお、retryWhen
が実際にどのように処理をするのかは、次に示すように返すイベントの種類によって変わります (ここでは型は関係なく、Int64
以外のものでも構いません)。
.Next
のとき、sequence
をリトライする.Complete
のとき、sequence
をリトライさせずに、完了させる.Error
のとき、sequence
をリトライさせずに、エラーにする
これを上のコードに当てはめると、次のようになります。
- エラーコードが400番台のときは
.Next(0)
になるのでリトライ - それ以外は
.Error(err)
になるのでエラー
retryWhen
でリトライ回数制限
先ほどのコードにリトライの回数制限を設けてみます。つまり、次のような感じです。
- エラーコードが400番台のエラーは、2回までリトライするが、3回めには諦めてエラーにする
- エラーコードが400番台のエラーではないなら、すぐにエラーにする
これを実現する、とりあえずさっと思いつくコードは次のようになります。
外にretryCount
があって汚ないですが、ともかくちゃんと動きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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
です。前の値と今の値が与えられるので、次の値を返していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
という、シーケンスの要素と添字をタプルとして与えてくれるメソッドを使えばよいのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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でretry
、retryWhen
を利用してエラー時の再実行を試してみました。
なお、リトライしないのであれば、catchErrorJustReturn()
、catchError()
というメソッドも利用できます。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。