eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

branchを切ったらすぐにpushしてCIだけは通しておこう

前提

  • masterやdevelpoブランチでビルドが通っている
  • その状態で作業branchを作成する
  • branchを作ったら、何も更新してなくてもすぐにpushしてCIは通しておく

理由

  • CIが壊れるのは自分の更新だけが理由ではない
  • CI設定の間違い
  • サーバーが更新されて動作しなくなった
  • ライブラリのバージョンがいつの間にか変わっていた
    • dependenciesのバージョン名に + とかつけるのはやめよう
  • キャッシュ有無で正常にCIが通らなくなった
  • mavenリポジトリが死んでしまった
    • bintray、おまえよく死ぬな

自分の作業によってCIが壊れたか 環境によって壊れたか の違いをちゃんと切り分けてから作業しないと、無駄な確認作業が発生するぞ

Kotlinでinterface定義モジュールと実装モジュールを分離した上で実装を隠蔽する

やりたいこと

  • interface定義と、その実装をなるべくキレイに切り分けたい

インターフェース定義用モジュール

// ":interface-module" に定義を書いていく...
interface AuthService {
  fun login(id: String, password: String)
}

実装用モジュール

// ":impl-module" に実装を書いていく...
internal class AuthServiceImpl {
   override fun login(id: String, password: String) {
       // ログイン処理する....
   }
}

コレの問題点

  • AuthServiceImplがinternalなので、他のモジュールから見えない
  • けれどAuthServiceImplをモジュールの外に開放したくない(後で入替えたりとかやりやすいように)

解決

  • interface側にcompanion objectを入れておく

interface定義を修正

// companionだけ書いておく
interface AuthService {
  fun login(id: String, password: String)

  companion object
}

実装用モジュールにExtensionも定義

// ":impl-module" に実装を書いていく...
internal class AuthServiceImpl {
   override fun login(id: String, password: String) {
       // ログイン処理する....
   }
}

// CompanionのExtensionとしてインスタンス生成メソッドを生やす
// これはinternalにしない。
// 利用側からは `AuthService.newInstance()` と呼ぶことができる
fun AuthService.Companion.newInstance() {
    return AuthServiceImpl()
}

Androidアプリビルドの並列最大化の限界値

ビルド速度に影響する要素

  • CPU論理コア数
  • シングルコアスペック
  • メモリ
  • プロジェクト自体の並列性

gradleのworker設定

  • gradleには --max-workers 設定がある
  • このオプションを闇雲に指定しても意味はない
  • デフォルト値はCPU論理プロセッサー
    public DefaultParallelismConfiguration() {
        maxWorkerCount = Runtime.getRuntime().availableProcessors();
    }

Workerが増える条件

  • 並列可能なタスクが発生すると、Workerが増える
  • Workerは予約されない。なので、 --max-workersオプションをいくら設定しても、不必要なworkerは起動しない
  • mavenのライブラリ取得とか、並列実行しやすいタスクが発生すると一気に増えるのが観測できる
    • ローカルキャッシュを全消しして、--max-workersをやたら増やすと壮観な図を観れる
   // DefaultBuildOperationQueue
    @Override
    public void add(final T operation) {
        lock.lock();
        try {
            if (queueState == QueueState.Done) {
                throw new IllegalStateException("BuildOperationQueue cannot be reused once it has completed.");
            }
            if (queueState == QueueState.Cancelled) {
                return;
            }
            workQueue.add(operation);
            pendingOperations++;
            workAvailable.signalAll();
            if (workerCount == 0 || workerCount < workerLeases.getMaxWorkerCount() - 1) {
                // `getMaxWorkerCount() - 1` because main thread executes work as well. See https://github.com/gradle/gradle/issues/3273
                // TODO This could be more efficient, so that we only start a worker when there are none idle _and_ there is a worker lease available
                executor.execute(new WorkerRunnable());
            }
        } finally {
            lock.unlock();
        }
    }

最終的に収束するWorker数

  • どんなに頑張っても、module同士の依存によって増えるworker数には限度がある
  • その状態ではCPUコア数よりもシングルスレッド性能のほうが重要となる

  • これらのビルド速度は最終的にほぼ同一に落ち着いた

快適性

  • ビルド速度は同一だが、快適性とは別問題
  • 次のマシンは何にするか

Firestoreのローカルエミュレーターは同時にトランザクション接続しすぎるとフリーズする

問題点

  • 表題のまま
  • 100個くらいのUnitTestがgoroutineで2〜3のトランザクションを並列発行した
  • フリーズした

回避

FirestoreとMemorystoreとDatastoreと

Google Cloud Platformのサーバーデータはどこに保存するか

  • SQL系は月額固定課金なので、気軽には使えない(お金持ちを除く)
  • なので従量課金制の保存先としてDatastoreを気に入っていた

DatastoreとMemcache

  • DatastoreはFirestoreと両立できない
  • アプリがFirestoreを使っており、同じGCPプロジェクトにGAEが同居するとDatastoreは虚空へと消える
  • MemcacheがGoogle App Engineは標準APIで使えた
    • GAEのRuntimeが刷新されたことでMemcacheアクセスが行えなくなった

Memorystore

  • Memcacheの後継のように見えて、違う
  • コイツは月額固定だ
  • 最低環境で$50/monthが虚空へと消える
  • Private IPだけが割り振られるので、ローカルマシンからの開発がやりづらそう
    • そのうち解決しそうではある
    • Compute Engineのインスタンスを立てて、SSH経由で頑張ればつながるらしい
      • f1-micro料金が$5 / month

Firestore無料枠 VS Memorystore最低課金額

  • Read/Write共に50,000の無料枠がある
  • 250,000 Read/Write per Dayくらい使うと$50課金が発生する
  • もう直接アクセスでいいんじゃね?
    • 1msでも早くレスポンスを返したい場合を除く

Firestoreは安い

  • 安い
  • ガンガン使おう

Google App Engineで香川県ゲーム条例に対応する

雑感

  • 条例が最短で春に施行される
  • なるべく簡単に香川県スマホ禁止条例に対応しなければならない

X-AppEngine-City, X-AppEngine-CityLatLong

  • Google App Engineはリクエスト元のIPからざっくりとした国・都市情報を取得できる
  • X-AppEngine-City ヘッダをチェックし、香川県の都市だったら 403 Forbidden を返してしまうのが一番楽ではないだろうか
  • 万全を期すなら、 X-AppEngine-CityLatLong ヘッダをチェックし、香川付近の位置であれば弾くのも良い
  • コレであればコストは最低限になるのではないだろうか

ちゃんと対応するなら?

  • 香川県の人口は100万人弱
    • 日本人1億人として、日本を対象にしたサービスであればユーザーはざっくり1%
    • 1%のユーザーが、コストを上回るリターンを見込めるのであれば対応するほうが良い
  • クライアントごとに X-Allow-Kagawa-Game-Law みたいな設定を入れさせるのは非常に面倒だ
    • Chromeとか主要なエンジンが対応してくれないかな?

LiveDataを複数回observeしても1個のObserverしか登録されない現象と解法

前提

  • LiveDataのonActive/onInactiveで処理を行わせていた
  • 強制的にLiveDataをactiveにするため、次のようなExtensionを作った
    • LiveDataをactiveにしたいだけなので
    • Observerはナニも処理していない
// 強制的にLiveDataをactiveにする
fun <T> LiveData<T>.forceActiveAlive(owner: LifecycleOwner) {
    observeForever(Observer { /* drop value. */ })
}

問題点

  • 2箇所以上から LiveData.forceActiveAlive() を呼び出す
  • 1箇所でもonDestroyが走ると、LiveDataがinactiveになる

理由

  • Kotlinのコンパイラが、 Observer { /* drop value. */ } をシングルトンとして最適化し、コンパイルしていた
  • そのため、2回め以降の LiveData.forceActiveAlive() で実行がキャンセルされていた

解法

  • 毎度newが走るようにしてあげれば想定通りに動く
private class ObserverWrapper<T>(private val observer: Observer<T>) : Observer<T> {
    override fun onChanged(t: T?) {
        observer.onChanged(t)
    }
}

fun <T> LiveData<T>.forceActiveAlive(owner: LifecycleOwner) {
    observeForever(ObserverWrapper(Observer { /* drop value. */ }))
}

コンパイラの気持ちをわかってあげよう

  • コンパイラの気持ちを考えれば、このバグは回避できたかもしれないんだ