WorkManagerの闇 〜永遠に走らないワーカーとの戦い〜

※ChatGPTを使用して記事を作成しています。

「バックグラウンド処理?今どきはWorkManager一択でしょ!」

そんな甘い気持ちで手を出したのが運の尽きだった。

確かにドキュメントには「信頼性の高いスケジューラ」と書いてある。

だが、「スケジュールされる」と「実行される」は全然別の話だったのだ──。

今回は、WorkManagerを導入したはずが「永遠に実行されないワーカー」に悩まされた数ヶ月を振り返ります。

導入の背景:バッテリーセーバー時も処理したい

とあるクライアントアプリで、ユーザーの利用ログを定期的に送信する要件があった。

  • バックグラウンドでも動作する必要がある
  • OSバージョンによらず安定して送信したい
  • ネットワーク接続時のみ実行すれば良い

この要件にぴったりだと思ったのが、Jetpack WorkManagerだった。

WorkManagerは、OSの制限下でもバックグラウンドジョブを安定的に実行できるJetpackライブラリです。

この文句を信じて、導入を決定。

実装:定期ログ送信ワーカーを作った

class LogUploadWorker(
    context: Context,
    params: WorkerParameters
) : Worker(context, params) {
    override fun doWork(): Result {
        val logs = fetchPendingLogs()
        return if (uploadLogs(logs)) {
            Result.success()
        } else {
            Result.retry()
        }
    }
}

そして、以下のように8時間ごとのPeriodicWorkを登録。

val request = PeriodicWorkRequestBuilder<LogUploadWorker>(8, TimeUnit.HOURS)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "LogUpload",
    ExistingPeriodicWorkPolicy.KEEP,
    request
)

これで完璧だと思った。

でも、全然動かない。

何が起きたか:登録はされてる。けど…実行されない!

Enqueued work: LogUpload
State: ENQUEUED

だが、アプリを放置しても、ネット接続があっても、バッテリーが十分でも、ログ送信が一度も実行されない

当然、doWork()も一切呼ばれない。

しかも、デバイスを変えると実行されたりする。

条件を変えても実行されたりされなかったり、挙動が安定しない

本当の原因たち:WorkManagerの仕様に翻弄される

後からわかったのは、WorkManagerには複数の罠があるということ。

⚠️ 罠①:PeriodicWorkは最低15分間隔、かつ正確な間隔は保証されない

公式ドキュメントにも小さく書いてある:

「The minimum repeat interval that can be defined is 15 minutes. WorkManager does not guarantee exact timing.」

つまり、8時間ごとに実行されるとは限らない。

次回実行はシステムが決める。バッテリーや他の制約次第で、1日遅れることもある。

⚠️ 罠②:バッテリーセーバー中はWorkManagerが一切実行されないことがある(OS依存)

特にAndroid 9以降、バッテリーセーバー中はDozeモードによってジョブが制限される。

この時、Constraintsを満たしていてもジョブは実行されない

私の検証端末(Pixel 3a)は、夜間にバッテリーセーバーが自動ONになっていたため、WorkManagerが一切動かなくなっていた。

⚠️ 罠③:デバイスメーカーの独自実装で勝手にスケジューラが殺される

特に中華系メーカー(Xiaomi、Huawei、OPPOなど)の端末では、WorkManagerのバックエンド(JobSchedulerやAlarmManager)がOSによって止められることがある。

ログにはエラーも出ず、「実行されない」だけ。

問題の再現と原因特定にかかった日数:30日以上

WorkManagerの「動くかもしれない」「動かないかもしれない」という非決定的な挙動が、原因の特定を非常に難しくした。

  • ある端末では毎回実行される
  • 別の端末では一度も動かない
  • 機内モードON→OFFで動くこともある
  • OSアップデートで挙動が変わる

正直、呪われているとしか思えなかった。

対処法:WorkManagerを使いこなすための鉄則

この地獄を抜け出すために、以下の対応を取った。

✅ 対策①:Constraintsとバックエンド挙動の理解

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)
    .setRequiresBatteryNotLow(true)
    .build()

上記のようなConstraintsは便利だが、複雑にしすぎると一生満たされない

可能な限りシンプルに、かつOSごとの動作確認を行う。

✅ 対策②:テスト用にOneTimeWorkRequestを使って挙動を確認

開発時は以下のような一時的なWorkRequestを使って、必ずdoWork()が実行されるか確認。

val request = OneTimeWorkRequestBuilder<LogUploadWorker>().build()
WorkManager.getInstance(context).enqueue(request)

これでワーカーの登録や処理が正しく動くかどうかを明示的に検証できる。

✅ 対策③:永続ログを取り、doWork()の呼び出し回数を分析

端末内に「いつワーカーが実行されたか」のログを記録。

開発チーム全員で実機テストを行い、環境ごとの実行傾向を把握。

補足:JobSchedulerに切り替えようとしたら?

「WorkManager面倒だから、JobSchedulerに戻したら?」という意見も出た。

だがJobSchedulerはAPI21以降限定で、他の制約も多い。

また、WorkManagerは内部でJobSchedulerやAlarmManagerを自動で使い分けてくれるため、
「動かない原因を避けられる」とは限らない。

教訓:ワーカーは登録して終わりじゃない。走るまでが勝負

今回の事件を通じて学んだのは、WorkManagerは非常に強力な反面、環境に大きく依存するライブラリであるということ。

  • スマホの設定
  • OSバージョン
  • メーカーの独自カスタマイズ
  • ユーザーが気づかずONにしたバッテリーセーバー

「なぜ動かないか」よりも「どう動かすか」の工夫が必要だった。

まとめ:信頼性の裏には複雑な現実がある

Jetpackライブラリは便利で安全。

だが、その「安全」はOSやメーカーの協力あってのもの。

WorkManagerは、信頼性を「目指す」ライブラリであって、何もしなくても絶対動くわけではない

結局、バッテリーやネットワークやスケジューラなど、システムの状態に強く依存するのがAndroid。

その上に構築される非同期処理には、動作保証よりも“監視と補完”が求められる

タイトルとURLをコピーしました