Cybozu Inside Out | サイボウズエンジニアのブログ

 

QAエンジニアのAgile Testing vol.1

初めまして! 松山品質保証部の北地と申します。どうぞよろしくお願いします。

私が所属しているチーム、SPITz(Software Process Improvement in Test の略)は品質保証部内のQA全般のカイゼン支援を行っています。SPITzでは当面のテーマを「QAエンジニアのAgile Testing」として活動しています。この記事で、どういったカイゼン活動を進めているのかをご紹介します。

タイトルに「vol.1」とつけたのは、Agile Testingの深堀を継続し、定期的に社外へアウトプットしていこうというチームの総意です。 なお、SPITzの活動内容については別の記事でも紹介していますので、こちらも是非ご覧ください。 blog.cybozu.io

なぜAgile Testing?

サイボウズではAgile開発の導入がどんどん進んでいます。 Agile開発の浸透と共に、当社のQAエンジニアからはいくつかの意見が挙がるようになりました。

  • QAエンジニアとして、Agile開発にうまく適応できていない気がする
  • 開発プロセスはスプリントを重ねるごとに良くなっているものの、製品チームごとに最適化されていて、分断が進んでいる
  • 他のチームのやり方を知る機会が少ないが、上手いやり方があるなら取り入れたい
  • ある製品チームではAgile開発を取り入れることが難しいと感じ、採用していない

SPITzではチーム間の情報共有にカイゼンの余地があると考えました。

ゴール

短期目標はこうです。 f:id:cybozuinsideout:20180827164344j:plain

私たちの最終的なゴールは「すべての製品開発チームでAgile開発に適したテストを行う」です。 そのために必要な土台として、Agile Testingに関する情報を、チームを横断して共有する環境を作りたいと考えています。

具体的な活動内容

SPITzのメンバーで検討を重ねた結果、2つの案が出てきました。

LT/パネルディスカッションの開催

製品チームのQAエンジニアにLTやパネルディスカッションをしてもらう案です。SPITzが主催し、それぞれのチームのベストプラクティスを共有してもらうイメージです。LTが良いのか、パネルディスカッション形式にするのかは、まだ細かいところを詰めていません。いずれにしても、当人からノウハウを説明してもらうことで理解が進むと考えます。 リアルなイベント開催だと、参加者が刺激を受けやすく、カイゼンへのモチベーションにもつながりやすいことも期待しています。
反面、登壇者に事前準備をお願いすることになり、やや心苦しい気もします。 また、なるべく多くのQAエンジニアが参加できるような日程の調整は、早い時期の告知が肝になりそうです。

事例集作成

SPITzのメンバーが製品チームのQAエンジニアにヒアリングし、その内容を社内向けに事例集としてまとめる案です。記事の作成は業務の合間にコツコツとできそうです。QAエンジニアには、ヒアリングする際に時間をとってもらうことになりますが、LT用のスライドを作成してもらったりする工数よりは軽いと見込んでいます。また人前でしゃべるのが苦手な人からもノウハウを吸い上げやすいのでは、とも考えています。
ただしSPITzのメンバーの工数負担(ヒアリング → 記事作成 → 共有)と、そのスピード感が気になるところです。細かい点が漏れないように丁寧な記事を書く必要がありそうです。 情報をプッシュしていくイベント開催に比べると、QAエンジニアに対する影響力が少ないかも?


どちらを採用するかで、SPITzメンバーでの議論は紛糾しました。

  • どちらが高いコストパフォーマンスを見込めるか
  • 1回限りに終わらず継続できるか
  • できればQAエンジニアの負担は抑えたい

などなど。悩ましいところですね。

結局、私たちは「LT/パネルディスカッションの開催」を採用することにしました。チームを横断して情報を共有する(し続ける)ために、リアルなイベント開催は効果があるという判断です。

次回予告

「LT/パネルディスカッション形式で扱うテーマ」について記事を書きます! みなさんの周りでもAgile開発が徐々に浸透していると思いますし、そのやり方についての想いや悩みをお持ちなのではないでしょうか。私たちのカイゼン活動が、何かのヒントになると良いなぁと思っています。

We are hiring!!

サイボウズでは「日々の仕事をカイゼンしたい」「いろんな人と繋がって学び合いたい」というカイゼン意欲をもった方がチャレンジできる環境があります。

キャリア採用 QAエンジニア | サイボウズ 採用情報(新卒・キャリア)
キャリア採用 QAエンジニア(ミドルウェア) | サイボウズ 採用情報(新卒・キャリア)
キャリア採用 テストエンジニア | サイボウズ 採用情報(新卒・キャリア)

ぜひ私たちと一緒に働きましょう!

 

セキュリティキャンプ全国大会 2018 集中開発コース 「Linux開発者を目指そう! 」テーマのレポート

はじめに

こんにちはNecoチームのsatです。本日はNecoチームの話ではなく、私が先週講師として参加した「セキュリティキャンプ全国大会 2018」というイベントの参加報告をいたします。このイベントの中でもとくに私が受け持った集中開発コース「Linux開発者を目指そう!」テーマについて述べます。

セキュリティキャンプ全国大会とは、お盆の時期に全国の学生が一か所に集まって、5日間その道のプロと一緒にセキュリティ技術について学ぶイベントです。イベントそのものについての詳細は以下リンク先をご覧ください。

www.ipa.go.jp

「Linux開発者を目指そう!」コースの概要

「Linux開発者を目指そう!」コースは受講者が実際にLinuxカーネルの部品を開発、追加することによってLinuxカーネルの開発についての理解を深めてもらうためのものです*1。今回は2人の学生がこのテーマに取り組みました。

一人の方はLinuxのカーネルモジュール(後述)を独自開発しました。もう一人の方は独自のslabアロケータ(後述)を実装しました。では、お二方が具体的にどのようなことをしたかについて述べていきたいと思います。

独自カーネルモジュールの開発

カーネルモジュールとは

カーネルモジュールとはシステム起動時、およびその後の動作中にカーネルに機能を追加するための部品です。Webブラウザでいうプラグインを思い浮かべてもらえればいいかと思います。カーネルの機能のうちの多くの部分は最初からカーネルに組み込んでおくこともできますし、モジュールとして独立したファイルにしておいて必要になった時にカーネルに組み込むこともできます。

たとえばみなさんのPCに繋がっている各種デバイスを操作するデバイスドライバなどがそうです。モジュールはカーネル本体と同時にビルドできますし、後から別途個別にビルドもできます。本コースでは後者のアプローチをとりました。

開発の流れ

まずはカーネルモジュールのロード時にカーネルのログ*2に"hello world"と表示するだけの数行のプログラムを作るところから始めました。単純なプログラムなのですが、自分自身のコードがカーネルの一部として動くというのはなかなか体験できるものではありません。

続いてカーネルモジュールに問題があったらどうなるかというのを確認していただきました。具体的には通常のプロセス、およびカーネルモジュールの両方からNULLポインタにアクセスするプログラムを書きました。そうすると前者はNULLポインタにアクセスしたプログラムが異常終了するだけで終了しました。

f:id:cybozuinsideout:20180820181609j:plain

これに対して後者はシステム全体が動作しなくなってしまいました。Windowsでいうブルースクリーンが出た状態に相当します。

f:id:cybozuinsideout:20180820181623j:plain

実はこれ、実際のカーネルでこのような障害があれば立派な脆弱性になります*3。マルチユーザシステムで悪意のある一般ユーザのプログラムによってシステム全体がダウンしてしまうような場合を考えればわかっていただけるかと思います。こう書くだけでなんとなく理解はできるのですが、やはり聞くだけなのと実際にシステム全体を落としてみるのとでは理解の度合いが違います。

続いて彼は所定時間後に所定の処理を呼び出すカーネルタイマーというものについて学びました。最初はカーネルモジュールをロードしてから10秒後にメッセージを一回出力して終わり、というものから始まって、タイマーを二つ同時に起動するもの、二つのタイマーに別のメッセージを表示させるもの、などなど、多種多様なカーネルモジュールを作りました。

残念ながら本イベントの短い期間ではここで終わってしまいましたが、彼が具体的にどういうものを開発したのか、および、その先にどういうメニューが用意されていたのかについては下記の一連の中の文書とC言語ソースファイルをごらんください(独自開発ツールの使い方について述べている部分は読まなくていいです)。

qiita.com

講師の目から見て成長したところ

彼は低レイヤのプログラムに慣れていないことから最初はC言語の理解がおぼつかなかったのですが、最後には立派にカーネルモジュールが作れるまでに成長しました。初心者にとっての最難関であるポインタについてもプログラムのメモリマップを何度も図示しながら必死に学んだ結果、ある程度理解できるまでにこぎつけました。ポインタは本来一朝一夕で理解できるものではないので、これはすごいことです。

裏話: カーネル内インターフェースの変更

カーネルタイマーを使ったカーネルモジュールを作っていた時に思わぬ問題に遭遇しました。それは私が提供したサンプルソースが数年前に書いた古いものだったため、今回開発に使ったカーネルv4.18ではビルドできなかったことです。全然意図していなかったことながら、あるバージョンのカーネルに対して作ったカーネルモジュールが将来も修正なしに使える保証はLinuxカーネルにおいてはどこにもないことも学んでいただきました。この件についての詳細は下記の記事をごらんください。

qiita.com

独自slabアロケータの実装

slabアロケータとは

カーネルのメモリ管理サブシステムはハードウェアとの間でメモリをページという単位(通常4KB)でやりとりします。メモリ管理サブシステムはカーネルの他のコンポーネント、およびプロセスに対してはページを割り当てます。この割り当てプログラムのことをbuddyアロケータと呼びます。

しかし、バイト単位のメモリオブジェクトが欲しいカーネルサブシステムにとってこれは使いにくいです。たとえばメモリを8バイトだけほしいのにページ単位でしか要求できなければ、8バイトのために1ページを消費するという壮絶な無駄を発生させるか、あるいは自分自身でページを小分けして管理しなければなりません。

ここで「ページを小分けして管理」してくれるのがslabアロケータというしくみです。slabアロケータはbuddyアロケータからページを獲得した上で、ページをカーネル内サブシステムにバイト単位に小分けして渡します。図中にkmalloc()と書いてあるのは、ユーザ空間におけるmalloc()に相当するものであり、バイト単位のメモリ割り当てをする関数です。

f:id:cybozuinsideout:20180820181807j:plain

linuxにはslabの実装が複数個存在します。具体的にはSLOB, SLAB, SLUBという3つです。似たような名前がたくさんあってややこしいですが、気にしないでください。

開発の流れ

彼は自分のslabアロケータにSLOBA(そば)というかっこいい名前を付けました。名前さえ付ければ終わったようなものです…というのは冗談として、最終日までの目的は「特定用途ではSLOB(最も単純な実装。メモリが少ないような環境で使われる)とSLAB(汎用。エンタープライズサーバ向けにも長きにわたって使われてきた)に勝つ」という明確なものでした*4

フルスクラッチでslabアロケータを作るのはなかなか骨が折れるので、最初はSLOBをもとにして作り始めて、次第に独自のコードを増やしていくというアプローチをとりました。ベンチマークテストとして採用したのはkernelソースに対するdu -sコマンド発行の所要時間です。ディレクトリエントリごとにdentryと呼ばれるカーネル内のデータがslabアロケータから割り当てられるので、slabへの大量アクセスの所要時間が計測できるというわけです*5

開発の初期段階で「SLOBのメモリ獲得処理はSLOBが持っているページの量に比例して多くなる」という事実に気づいた彼は、さっそくデータ構造を工夫して、定数時間でメモリ獲得できるように工夫しました。さらにその後には「SLOBは複数CPUから同時アクセスされるとスケールしない」などのSLOBの問題を次々に見つけ出し、改善していきました。スケーラビリティを上げる改善においては、spinlockの使い方に難儀したために無数のバグを仕込んでいましたが、最終日にはなんとか全て解決したようです。

最終的にはdu -sについてはシングルコア性能、マルチコア(4コア)性能共にSLOBには10倍以上の大差をつけて圧勝、SLABとはほぼ互角、という結果を叩き出しました。その上、SLOBAのコード量はSLABの約1/6に過ぎないというのですから驚きです。

筆者の見立てでは、SLOBにはほとんどのワークロードでも圧勝すると思いますが、SLABに対しては勝ったり負けたり、といったところでしょう。ぜひ色々なシステム構成、ワークロードでデータをとっていただきたいところです。

これ以上技術的な詳細には踏み込みませんが、参考までにSLOBとSLAB、SLOBAの構造を表す概要図を載せておきます。kmalloc()で取得するサイズごとにslabアロケータが存在するというイメージで見ていただければと思います*6

  • SLOB

f:id:cybozuinsideout:20180820185057j:plain

  • SLAB

f:id:cybozuinsideout:20180820185107j:plain

  • SLOBA

f:id:cybozuinsideout:20180820185116j:plain

講師の目から見て成長したところ

彼は手を動かす速さについては最初から教えることは何もなかったです。今回はLinuxカーネル開発のお作法、効率的なソースコード解析およびトラブルシューティングの方法などを体験できたのがよかったのではないかと思います。今後はさらに高速に、かつ、高品質なコードを書けるエンジニアになってくれるのではないでしょうか。

裏話: 開発はインクリメンタルに

彼は二日目あたりで、とあるバグが取れない状態で悩んでいました。その際、「問題の無かったことがわかっているコミット」から「問題を検出したコミット」までの間のどのコミットが犯人かを二分探索によって見つけるbisectと呼ばれる方法*7を使おうとしましたが、それは不可能でした。なぜなら問題が起きなかった状態から起きるようになるまで一切コミットせずに数百行を変更してしまっていたからです。

彼の記憶にはバグに悩まされたことに加えて「次はインクリメンタルに開発しよう」という思いが刻まれたことでしょう。多分。

おわりに

本コースは「Linux開発者を目指そう!」と銘打っていますが、おふたりが今後Linux開発を続けるかどうかは彼ら次第です。本コースがLinuxという特定ソフトウェアの開発技術だけではなく、「トラブルに遭遇した時の対処方法」や「何かを変えるときは一回に一つだけという原則」、「ソースコードを読むコツ」など汎用的な知識を得るきっかけとなったのであれば幸いです。

*1:一見セキュリティには関係なさそうに見えますが、Linuxに限らずカーネルに関するセキュリティ知識を得るにはその基礎としてカーネルそのものの知識が必要なので、実は十分関係があるのです

*2:dmesgコマンドによって見られます

*3:たとえばCVE-2018-11232があります。興味のあるかたは読んでみてください。

*4:SLUBについては時間の都合上省略しました

*5:ファイルシステムをストレージデバイスではなくメモリ上に存在するtmpfs上に作ることによって、このコマンド発行に対するI/O処理の影響を無くしています

*6:slabアロケータを使うのはkmalloc()だけではないのですが、それについては割愛します

*7:たとえば前者から後者の間に16コミットあれば、最初は8個目のコミットで問題が起きるか確認して、問題が起きなければ次は12個目のコミットで確認…というように容疑者を一度に半分づつ減らしていく

 

さようなら ImageMagick

こんにちは、アプリケーション基盤チームの青木(@a_o_k_i_n_g)です。

一般的な Web アプリケーションがそうであるように、サイボウズのグループウェアにも画像をサムネイルで表示する機能があります。サイボウズでは日々数万件やそれ以上のサムネイルを生成しており、それらは全て ImageMagick によって生成されていました。

そこで得た知見はこちらの記事で公開されています。
blog.cybozu.io

しかし現在、サイボウズから ImageMagick は消え去りました。その理由と、我々が取った代替手段について紹介します。

ImageMagick を外した理由

言うまでもなく ImageMagick は優秀なツールで、画像変換に関する何らかのサービスやツールを作る場合には採用の第一候補になることでしょう。あらゆる画像フォーマットに対応し、出力画像をきめ細かに制御できる膨大なオプションがあるからです。

しかし一方で、 ImageMagick には脆弱性が大量に存在します。2017 年に報告された ImageMagick の脆弱性は 236 件 でした。大量にある上にリモートコード実行級の脆弱性もあり、安全性という観点ではかなり厳しい評価をしなければなりません。こちらに ImageMagick の脆弱性情報があり、その多さが伺えると思います。
Imagemagick : Security Vulnerabilities

もちろん、我々は AppArmor による厳重なアクセス制御やサムネイル作成専用の環境を使うなどの策は施しています。しかしそれでもこれだけ多数の脆弱性があるとなると、 ユーザーに安心安全なクラウドサービスを提供できているか? という点で疑問がありました。

また、脆弱性報告が来るたびにサイボウズに影響が無いか調査しており、そのコストが無視できないほど大きくなりました。脆弱性報告が来た際は否応無しに調査に時間を取られますし、意図しないタイミングでバージョンアップを迫られることもあります。スムーズな運用のためにも ImageMagick を採用し続けるのは得策ではないだろうという意見が出ていました。

検討を重ねた結果、我々は ImageMagick を捨てる決意をしました。

ImageMagick の代わりに

代替として使えそうなツールがいくつかありましたが、サイボウズのサムネイル事情は少々仕様が凝っている部分があり、自作することにしました。そして調査の結果、Go 言語と imaging パッケージが良さそうということでこれを用いて実装することにしました。

結論から言えば、ImageMagick に依存しないサムネイル作成ツールを実装し、移行も終えています。いくつか判明した点について記します。

PNG の変換画像が僅かに暗くなるケースがある

Go 版サムネイル生成ツールと ImageMagick では、PNG を変換した際に Go 版の方がわずかに色が暗くなるケースがあることが判明しました。

Wikipedia の PNG のページ にある PNG 画像を例に取って説明します。一枚目が ImageMagick で変換した画像、二枚目が Go 版サムネイル生成ツールで変換した画像です。

f:id:cybozuinsideout:20180817180448p:plain
ImageMagick で変換した画像
f:id:cybozuinsideout:20180817180534p:plain
Go 版で変換した画像
・・・というわけで、肉眼ではわかりませんね。ImageMagick の compare コマンドで画像の差分を出力すると差分が浮かび上がります。

f:id:cybozuinsideout:20180817175457p:plain

稀に肉眼でわかる程度の差が出る画像もあるにはありますが、実用では問題無いと判断しました。

変換できない画像がある

ImageMagick で変換出来ても Go 版サムネイル生成ツールでは変換出来ない画像があります。

まず、Go 版では BMP の一部は変換出来ません。BMP にはいくつか種類があり、その種類を表す値は Bitmap Information Header というヘッダ部分で指定されます。ここで指定される値と種類は下記の通りです。

意味
40 Windows V3
108 Windows V4
124 Windows V5
12 OS/2 V1
64 OS/2 V2

しかし Go の image パッケージの当該部分を見ると 40 という値が来ることを前提としており、Windows V3 以外の BMP のバージョンは未サポートでした。
https://github.com/golang/image/blob/c73c2afc3b812cdd6385de5a50616511c4a3d458/bmp/reader.go#L151

ImageMagick では Windows V3 以外の BMP も変換可能で、この点では Go 化して劣化した部分と言えます。とはいえ近年は BMP ファイルがアップロードされる回数はさほど多くなく、大きな問題には至ってません。

他にも、ImageMagick は壊れた画像も変換できる という謎機能があり、もちろん壊れ方にも依るのですが、Go 版では変換に失敗する画像でも ImageMagick だと変換できることがあります。なおそういったケースは全体から見れば極僅かなのでこのケースも実用では問題無いでしょう。

運用後

ImageMagick を廃止し Go 版サムネイル生成ツールを運用してからしばらく経ちましたが、大きな問題は出ていません。我々はサムネイル作成の成功率をモニタリングしており、 成功率は 99.9% 付近を保っています。巨大画像や壊れた画像が来るケースを考慮すると十分な成功率と言えるでしょう。

これで以前より一歩安全なクラウドサービスに近づけたかと思います。「現状動いているからそれでいい」というような安易な運用ではなく、安全性を確保するためなら作りなおしも厭わないという姿勢はサイボウズの誇れるところだと思います。

また、脆弱性対応の工数を減らせるという点でも目的を達成できました。脆弱性対応は工数だけでなく精神力も消耗するものなので、こういった部分の改善はサービスの安定運用に繋がると思います。もちろん Go 化したからといって脆弱性が無くなるわけではなく、関連する脆弱性情報は引き続き注視していきます。

サムネイル機能というものはあって当然で、あまり目立たない地味な機能かも知れません。でもその裏では様々な画像を変換してくれる賢いツールが活躍してくれているのです。ImageMagick は長期間にわたりサイボウズのサムネイル事情を支えてくれました。さようなら ImageMagick、いままでサムネイルをありがとう。

 

リモート開発をテーマにしたMeetupを開催しました──西日本開発部の活動紹介

こんにちは、西日本開発部の岡田(@y_okady)です。 2018年7月26日(木)にサイボウズ東京オフィスで「Cybozu Meetup 西日本でのリモート開発事情」を開催し、西日本開発部でどんな風にリモート開発してるのかをご紹介しました。 今回はMeetupのレポートと、Meetupではお話しできなかった西日本開発部の活動をちょこっとご紹介します。

f:id:cybozuinsideout:20180815144630j:plain

西日本でのリモート開発事情

Meetupでは、松山オフィス勤務で近々広島に移住予定のマネージャーの水戸、大阪オフィス勤務でkintoneの新機能開発を担当しているエンジニアの榎原、京都での在宅勤務中心でUS向けkintoneの開発を担当しているエンジニアの三苫の3名が、それぞれの立場でリモート開発についてお話ししました。

speakerdeck.com speakerdeck.com

www.slideshare.net

リモート開発でお悩みの方や将来東京以外で働きたいと考えている多くの方にご参加いただき、どのセッションもみなさん熱心に聞いてくださって質問が次から次へと出てきたのが印象的でした。懇親会では「働く場所の自由化」という考え方やリモートスクラム・リモートモブプロといった具体的な開発手法について、熱い議論が交わされていました。

f:id:cybozuinsideout:20180815145222j:plain f:id:cybozuinsideout:20180815145525j:plain

西日本開発部の活動紹介

西日本開発部大交流会

少し前になりますが、大阪と松山に勤務する西日本開発部のエンジニア全員が松山に集合し、交流会を開催しました。初対面のメンバーも多く、今回は自己紹介やチーム紹介が中心でした。担当製品は違うけど業務内容の近いチーム同士でランチに行ったり、テレビ会議やグループウェア上でしかやり取りしないメンバーと直接話したり、普段は離れていてもたまにこうやって集まるのは大事だなぁと感じました。またみんなで集まって開発合宿とかやりたいですね。

f:id:cybozuinsideout:20180815150244j:plain
前夜祭は松山で大人気の複合温浴施設「そらともり」にお邪魔しました。オシャレ!

f:id:cybozuinsideout:20180815151021j:plain
交流会本編は松山オフィスにて。ボウズマン存在感ある。

f:id:cybozuinsideout:20180815150903j:plain
松山オフィスのメンバーが四国の名産品を用意してくれました。

9月は西日本各地でイベントラッシュ!

東京でのMeetupでもご紹介したリモート開発について、9月開催の西日本各地のイベントで西日本開発部のメンバーがお話しします。上記の発表資料に載っていない内容もお話しする予定です。お楽しみに!

おわりに

サイボウズでは、「チームワークあふれる社会を創る」という理念に共感してくれる仲間を募集中です。住みたい場所に住んで、いろんな場所で働く仲間と力を合わせて、世界中のチームを支えるサービスを一緒に開発しませんか?西日本をはじめ、住みたい場所で面白い仕事がしたいエンジニアの方、お待ちしてます!

www.wantedly.com

 

【RxSwift】Singleton で DisposeBag を使うことの考察

こんにちは。モバイル開発チームに所属している小島です。

弊社のプロダクトでもようやく RxSwift を使い始めています。今回は RxSwift の Disposable について思うところがあったので、メモしておきます。

Disposable と DisposeBag

Observable を subscribe すると、Disposable を返してきます。 Disposable は、subscribe したものを unsubscribe するための仕組みで、これを無視すると subscribe した処理が永遠に解放されずにメモリリークやリソースリークに繋がる恐れがあります。 なので、とりあえず DisposeBag に入れておけばいいよというのはよく見かけます。

class SomeClass {
    private let disposeBag = DisposeBag()
    private let dependentObject: DependentClass

    func someMethod() {
        let disposable = dependentObject.getObservable().subscribe(onNext: {
            // do someting.
        })
        self.disposeBag.insert(disposable)
    }

    func someMethod2() {
        // 書き方が違うだけで↑と意味は同じ
        dependentObject.getObservable().subscribe(onNext: {
            // do someting.
        })
        .disposed(by: self.disposeBag)
    }
}

DisposeBag は、Disposable をためておいて、自身が解放 (deinit) されるときに保持している Disposable を dispose してくれます。上記の場合だと、SomeClass が解放されるときに disposeBag も解放されこれまで subscribe したものが unsubscribe されるという流れになります。

Singleton オブジェクトの場合も同じように考えていいのだろうか?

DisposeBag は、便利な仕組みではありますが、保持しているオブジェクトのライフサイクルと同一になりますので、Singleton オブジェクトの場合にはアプリのライフサイクルと同一ということになります。

class SingletonClass {
    private let disposeBag = DisposeBag()
    private let dependentObject: DependentClass

    func calledFrequently() {
        dependentObject.getObservable().subscribe(onNext: {
            // do someting.
        })
        .disposed(by: self.disposeBag)
        // disposeBag が解放されることはない
    }
}

例えば上記のような場合、calledFrequently が呼ばれるたびに disposeBag に Disposable が追加されますが、disposeBag は開放されないので、subscribe された Observable は unsubscribe されることはありません。

チーム内で改善策を決めたけど、期待通りに unsubscribe されないことがありました

チームメンバーとも相談し Singleton オブジェクトの場合は以下のような形にしようと決めました。

class SingletonClass {
    private let dependentObject: DependentClass

    func calledFrequently() {
        var disposable: Disposable?
        disposable = dependentObject.getObservable().subscribe(onNext: {
            // do someting.
            disposable?.dispose()
        })
    }
}

ところが上記の書き方の場合、とあるケースで期待通りの動作になりませんでした。 onNext で渡しているクロージャの実行タイミングは Observable によって異なっているのです。

class SingletonClass {
    private let dependentObject: DependentClass

    func method1() {
        var disposable: Disposable?
        disposable = dependentObject.getAsyncSubjectAsObservable().subscribe(onNext: {
            // do someting.
            print(disposable?)
        })
    }

    func method2() {
        var disposable: Disposable?
        disposable = dependentObject.getBeheiviorSubjectAsObservable().subscribe(onNext: {
            // do someting.
            print(disposable?)
        })
    }
}

上記のコードでは、method1 は AsyncSubject を Observable として受け取り、method2 は BeheiviorSubject を Observable で受け取る想定です。BeheiviorSubject を subscribe した時の onNext は同期的に呼び出されるので、method1 は disposable オブジェクトが出力されますが、method2 は nil が出力されます。 つまり method2 の場合では、想定通りに unsubscribe されず getBeheiviorSubjectAsObservable の中の BeheiviorSubject の値が変わるたびにクロージャが実行されてしまいました。

(正確には、method1 の場合でも getAsyncSubjectAsObservable の中で実行している AsyncSubject が同期的に onNext された場合は nil が出力されます)

問題点のまとめ

問題は onNext が同期的に呼び出されるのか、非同期で呼び出されるのか、呼び出し元にはわからないということです。都度、中身の実装を確認しに行くのは辛いですし、メソッド名を工夫してわかるようにするということも考えましたが、なんとなく微妙だなぁとも思いました。

今のところの改善案

チームメンバーと再度検討し、とりあえず以下のような形に落ち着きました。

class SingletonClass {
    private let dependentObject: DependentClass

    func useTake() {
        _ = dependentObject.getObservable().take(1).subscribe(onNext: {
            // do someting.
        })
    }

    func useCompositeDisposable() {
        let compositeDisposable = CompositeDisposable()
        let disposable = dependentObject.getObservable().subscribe(onNext: {
            // do someting.
            if (conditionalExpression) {
                compositeDisposable.dispose()
            }
        })
        _ = compositeDisposable.insert(diposable)
    }
}

そもそも onNext の処理が1回きりで良い場合は、take(1) を使って確実に1回で onCompolete が呼ばれるようにすることにしました。onCompolete が呼ばれれば unsubscribe の必要はないので、戻り値の Disposable は無視しても問題ありません。

onNext 内で dispose する必要がある場合は CompositeDisposable を使うようにしました。CompositeDisposable は、dispose を呼び出したときに insert してあった diposable を dispose してくれます。予め dispose を呼び出していた場合には、あとから insert した diposable は、即 dispose してくれます。これにより、onNext の実行が同期的か非同期的かは気にしなくても良くなります。

これがベストかはもう少し使ってみないとなんとも言えませんが、RxSwift は非常に強力なツールであると感じるとともに難しさを感じる部分もあります。