ロコガイド テックブログ

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの社員がいろいろな記事を書いています。

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの社員がいろいろな記事を書いています。

CIがまれにコケていたのを一年越しに解決した

Androidアプリエンジニアの石原(iaia)です。 DroidKaigi 2021 がもうすぐですね!

弊社ロコガイドも、スポンサーとして協賛しております。

さて、私が入社したのが2020年1月で、その頃からずっとCIがたまにコケる、という問題が発生していて、 原因、解決策が分からず、ずっと放置していたのを一年越しに解決した、という話をします。

※ 解決したのが今年の1月で、思い出しながら書いています...

どんなエラーだったか

※ 注意 本記事には Orma の名前が何度も出てきますが、 Orma の問題では一切ありません
Ormaとは

> Task :data:kaptStagingDebugKotlin
Note: [OrmaProcessor] built 11 of schema models in 45msNote: [OrmaProcessor] process classes in 103ms/tmp/android-bargain/data/src/main/java/jp/co/tokubai/android/bargain/data/di/provider/OrmaDatabaseProvider.kt:6: error: cannot find symbol
    val database: OrmaDatabase
    ^
  symbol:   class OrmaDatabase
  location: interface OrmaDatabaseProvider/tmp/android-bargain/data/src/main/java/jp/co/tokubai/android/bargain/data/source/example/ExampleLocalDataSource.kt:16: error: cannot find symbol
    private val database: OrmaDatabase = databaseProvider.database
    ^
  symbol:   class OrmaDatabase
  location: class ExampleLocalDataSource/tmp/android-bargain/data/src/main/java/jp/co/tokubai/android/bargain/data/di/provider/OrmaDatabaseProviderImpl.kt:12: error: cannot find symbol
    override val database: OrmaDatabase = OrmaDatabase
    ^
  symbol:   class OrmaDatabase
  location: class OrmaDatabaseProviderImpl/tmp/android-bargain/data/build/tmp/kapt3/stubs/stagingDebug/jp/co/tokubai/android/bargain/data/source/example/ExampleLocalDataSource.java:11: error: [ComponentProcessor:MiscError] dagger.internal.codegen.ComponentProcessor was unable to process this class because not all of its dependencies could be resolved. Check for compilation errors or a circular dependency with generated code.
public final class ExampleLocalDataSource implements jp.co.tokubai.android.bargain.data.source.example.ExampleDataSource {
             ^/tmp/android-bargain/data/build/tmp/kapt3/stubs/stagingDebug/jp/co/tokubai/android/bargain/data/di/provider/OrmaDatabaseProviderImpl.java:11: error: [ComponentProcessor:MiscError] dagger.internal.codegen.ComponentProcessor was unable to process this class because not all of its dependencies could be resolved. Check for compilation errors or a circular dependency with generated code.
public final class OrmaDatabaseProviderImpl implements jp.co.tokubai.android.bargain.data.di.provider.OrmaDatabaseProvider {
             ^[WARN] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: com.github.gfx.android.orma.processor.OrmaProcessor (NON_INCREMENTAL).

> Task :data:kaptStagingDebugKotlin FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':data:kaptStagingDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution
   > java.lang.reflect.InvocationTargetException (no error message)

どういう問題があるか

  • すべてのビルド(staging/product, debug/release)時に発生する可能性がある
    • Pull Request時のビルド(stagingDebugビルド)でもコケる
      • CIが通るまでレビューお願いできないので時間がかかる
    • Google Play Storeへのsubmitのビルド(productReleaseビルド)でもコケることがある
      • HotFixを早く出したいときなどにも時間がかかってしまう可能性
  • CircleCIに行って、原因確認して、re-runボタン押す、としないといけなくて面倒
    • 発生頻度は1週間に1回くらいで、CIをre-runすればとりあえず通るから

一年前の原因調査

原因?

  • OrmaDatabase.java はあるはず...
    • このとき実は出来ていないのだがちゃんと調査していなかった...
  • 成功するときと失敗するときがあるということはコード的には問題ない
    • 実際ローカルでも成功しているし
  • 多分ビルドのタスク順?
    • 成功時と失敗時のdataモジュールのタスク順を眺めてみる

f:id:iaiaie:20211008105120p:plain
diff

  • 確かに差分はある
  • 何のタスクが先にあると良くて、どのタスクが後にあると良いのか? を調べようとしていた

諦め

ってところまで進めてはいたものの以下の理由で諦めました。

  • 原因が分かったところで、ではどうすれば解消できるのか? が思いつかない
    • タスクの順番を固定化させるのか?
  • この問題を調べ始めたときはコケることがそんなに頻繁じゃなくなった
    • dataモジュールを変更しない場合、成功時のキャッシュが利用されて問題が発生しない
  • 隙間時間で調べていたが、他のタスクが積まれていた

それから一年後...

  • 突然当該エラーが頻発し始める
    • 3回に2回くらいの頻度でコケはじめる

このままだとまともに開発が出来ないので、本腰入れて原因調査することになりました。

一年越しの原因調査

OrmaDatabasae.java をkaptで生成し、それを参照する OrmaDatabaseProvider.kt をDIして XxxxLocalDataSource.kt で使う、という構造だった。

以下コード例

import com.example.data.OrmaDatabase

interface OrmaDatabaseProvider {
    val database: OrmaDatabase
}
class ExampleLocalDataSource @Inject constructor(
    databaseProvider: OrmaDatabaseProvider
) {
    private val database: OrmaDatabase = databaseProvider.database
}

もう一度エラーコードを振り返ると

OrmaDatabaseProvider.kt:6: error: cannot find symbol
    val database: OrmaDatabase

OrmaDatabase が見つからないと言っている。

本当にOrmaDatabaseは存在しないのか?

  • Circle CIにsshして OrmaDatabase.java が存在するか確認すると良さそう
  • まずローカルの OrmaDatabase.java はどこにあるのか? を調べる
    • build/generated/source/kapt/debug/com/example/data/OrmaDatabase.java に作られている
  • sshして該当の場所を探す
    • 確かにない!

OrmaDatabase.javaが存在しない原因

  1. 生成されたが削除されてしまった
    • わざわざそんなことはしないはず...
    • 成功しているときは存在していることからもこの説はなさそう
  2. 設定に不備がある
    • 同じような問題に当たっている人が多分いるのでは?
    • 「こういう条件のとき生成されない可能性があるのでxxxを設定してください」みたいなワークアラウンドがあるのでは?
  3. ビルドエラーになった時点では生成されない (タイミングの問題)
    • これは原因調査時に気づいてなかった仮説

設定に不備があるのか?

  • OrmaDatabase.java を生成する条件は何なのか?
    • 必要なライブラリは全部入っている
      • README と見比べても問題なさそう
    • @Table が指定されているclassがある
      • 設定されている
  • 他に OrmaDatabase.java を生成させるオプションはないのか?

タイミングの確認

  • そもそも OrmaDatabase.java は最初存在しておらず cannot find symbol であるのは正しい
  • OrmaDatabase.java が必要となるタイミング」までに作られていれば良い
  • OrmaDatabase.java が必要となるタイミング」とは?
  • もう一度エラーメッセージを見てみる
             ^/tmp/android-bargain/data/build/tmp/kapt3/stubs/stagingDebug/jp/co/tokubai/android/bargain/data/di/provider/OrmaDatabaseProviderImpl.java:11: error: [ComponentProcessor:MiscError] dagger.internal.codegen.ComponentProcessor was unable to process this class because not all of its dependencies could be resolved. Check for compilation errors or a circular dependency with generated code.
  • Dagger2 がjavaファイル生成しているところで必要になっているっぽい
  • そういえばdaggerもkaptだ
  • orma -> dagger の順でアノテーションを処理していれば成功するし、 dagger -> ormaの順であれば失敗する のでは?

kaptの処理順が問題ならどうするか?

  • kaptの処理順は明示的に指定できるのか?
  • モジュール分割してあげればいいじゃん!ということに気づく
    • 1個のモジュールの中での処理順が決められないなら、そもそも別のモジュールにして、参照するようにする
    • すると参照されているモジュールは先にビルドされる必要があるので、そのモジュール内のkaptが必ず先に処理されるはず
  • Ormaのアノテーションの @Database とか @Table とかすべてdataモジュールの中に存在している
    • daggerのアノテーションもdataモジュールにある
    • これをOrmaのアノテーション使っているようなものをすべて別のモジュール(database)に切り出してしまう

結果

おれはやったぜ

f:id:iaiaie:20211008105210p:plain
pr

当該エラーでCIがコケることはなくなってみんなはっぴー

振り返り

  • まず最初の原因調査の仕方がまずかった
    • 原因調査になっていない、推測しかしてない
    • まずCIにsshして「本当にjavaファイルがないのか?」は絶対にやるべきだった
      • 多分、Rerun with SSH しても同じエラーが起きるとは限らなくて断念してた気がする
    • 文脈がちょっと違うが「推測するな、計測せよ」を肝に銘じた
  • 調査したら必ずメモを取る
    • 本記事は2020年4月頃と、2021年1月頃に調査したときのメモがGitHubのissue上に残っていたものを参考に書いている
    • メモってなかったらここまで書けなかったと思う
    • それでもメモが足りていない部分がある
      • sshしてファイルが無かったってことをメモっていない
      • kaptの処理順がランダムなことのエビデンス
    • 将来、この直し方はやっぱり間違えていて問題が起きたときに、どうしてこの直し方にしたのか? が分かるはず
    • 自分の調査の仕方が悪い、ということも一年越しにメモを見ることで気づけた
  • kaptの処理順とかあまり意識しなかったが調べるいいきっかけになった
  • マルチモジュール化進んでないなという実感
    • 進んでいればこういう問題にぶち当たることもなかったかも
  • そもそもまだOrma使っているのか?
    • Room に移行したいが...

おわり

今回みたいなCIの安定化や、マルチモジュール化を進めてくれるようなAndroidアプリエンジニアを積極採用中です! 最近はJetpack Composeも取り入れはじめているので、腕力で既存のコードを書き換えてくれるような方もお待ちしております!

https://hrmos.co/pages/locoguide/jobs/BAAA110