Start Debugging

修正: Flutter Android ビルドでの Timeout waiting to lock journal cache

別の Gradle プロセスが ~/.gradle/caches/journal-1 を保持したまま手放せない状態です。Owner PID を特定してそのデーモンを終了し、.lock ファイルの削除は避けてください。エラーの場所が移るだけです。

メッセージ内の “Owner PID” が示す別の Gradle プロセスが ~/.gradle/caches/journal-1 のロックを保持しており、ビルドからの解放要求に対して Gradle 固定の 60 秒のタイムアウト内に応答しませんでした。対処法は、そのプロセスを見つけて終了することです。jps -l (または ps) で GradleDaemon であることを確認し、kill -9 <Owner PID> (Windows では Stop-Process -Id <Owner PID> -Force) を実行して、flutter run をやり直してください。ここでは ./gradlew --stop が何もしないことがよくあります。このコマンドは同じ Gradle バージョンのデーモンしか停止しないためです。journal-1.lock を削除しても解決しません。私の再現環境では、次のビルドが今度はファイルハッシュキャッシュでタイムアウトしただけでした。

以下はすべて、macOS、Flutter 3.44.8 (テンプレートは Gradle 9.1.0 と AGP 9.0.1 を固定しています)、Gradle 9.3.1 と 8.14 のディストリビューション、OpenJDK 17、使い捨ての GRADLE_USER_HOME で再現したものです。ロックのコードは、現行リリースであるタグ v9.8.0 の Gradle ソースで確認しました。

エラーの全体像

私の再現環境での実際の出力です (パスは ~ に短縮しています)。

FAILURE: Build failed with an exception.

* What went wrong:
Gradle could not start your build.
> Cannot create service of type BuildSessionActionExecutor using method LauncherServices$ToolingBuildSessionScopeServices.createActionExecutor() as there is a problem with parameter #21 of type BuildLifecycleAwareVirtualFileSystem.
   ...
            > Could not create service of type FileAccessTimeJournal using GradleUserHomeScopeServices.createFileAccessTimeJournal().
               > Timeout waiting to lock journal cache (~/.gradle/caches/journal-1). It is currently in use by another process.
                 Owner PID: 52907
                 Our PID: 52963
                 Owner Operation: 
                 Our operation: 
                 Lock file: ~/.gradle/caches/journal-1/journal-1.lock

BUILD FAILED in 1m 1s

Flutter がビルドを実行している場合も、FAILURE: Build failed with an exception. の下に同じブロックが表示され、続いて Flutter 自身の Gradle task assembleDebug failed with exit code 1 が出ます。この最後の行は知らせ役であって、原因ではありません。

どの Gradle を使っているかは、2 つの点でわかります。Gradle 8.x 以前では “It is currently in use by another Gradle instance” と表示されます。Gradle 9.0 からは “in use by another process” という表記になります。Owner Operation と Our operation の行は journal cache ではほとんど常に空なので、無視して構いません。重要なのは Owner PID の行です。

Gradle がロックを取得できない理由

~/.gradle/caches/journal-1 は、共有キャッシュ内の各ファイルが最後に使われた時刻を記録しており、Gradle のキャッシュクリーンアップが安全に削除できるものを判断するために使われます。caches/9.1.0/ や caches/8.14/ と違ってバージョン別ではないため、マシン上のすべての Gradle バージョン、すべてのデーモン、すべての IDE 同期が 1 つのディレクトリを共有します。そのため、最も頻繁にぶつかるロックになっています。

Gradle はビルドの間ずっとこれらのロックを保持するわけではありません。“オンデマンド” で取得し、他のプロセスから求められたら手放します。DefaultFileLockManager のアルゴリズムは次のとおりです。

  1. ロックファイルの state 領域に対して OS のファイルロックを取得しようとします。
  2. 失敗した場合、ロックファイルの info 領域から所有者の PID と UDP ポートを読み取り、ループバック経由で所有者に ping を送ります。
  3. 所有者がキャッシュを使用中でなければ、ロックを解放して応答します。
  4. ロックを取得できるか DEFAULT_LOCK_TIMEOUT (60,000 ms) が尽きるまで、指数バックオフで再試行します。
// Gradle v9.8.0, DefaultFileLockManager.java
public static final int DEFAULT_LOCK_TIMEOUT = 60000;

タイムアウトは BasicGlobalScopeServices から渡される定数です。この値を引き上げる Gradle プロパティもシステムプロパティもないため、“ロックのタイムアウトを延ばす” という選択肢はありません。

つまり、このエラーが “古いロックファイルが残っている” という意味になることはありません。所有者のプロセスが終了していれば、オペレーティングシステムがそのファイルロックを解放し、ビルドは即座にロックを取得できるはずです。このエラーは、生きているプロセスがロックを保持していて、ping に応答しなかったことを意味します。現実的な理由は 4 つあります。

Flutter の開発マシンでは最初の原因が大半を占めます。Flutter プロジェクトには複数の Gradle クライアントが向きがちだからです。ターミナルや VS Code からの flutter run、Android Studio の Gradle 同期、スクリプトからの flutter build apk などです。これらが異なる JDK (Android Studio 同梱の JBR と JAVA_HOME) や異なる Gradle バージョン (別の Flutter リリースで作成された 2 つのプロジェクト) を使っていると、それぞれが独自のデーモンを持ち、すべてが journal-1 を共有することになります。

最小再現

再現するのに Flutter も実機も必要ありません。1 タスクだけの Gradle プロジェクト、使い捨ての Gradle ユーザーホーム、そしてハングしたデーモンを模す kill -STOP があれば十分です。

# macOS 26, OpenJDK 17, Gradle 9.3.1 (same 9.x error wording as Gradle 9.1.0 in the Flutter 3.44 template)
export JAVA_HOME=/opt/homebrew/opt/openjdk@17
export GRADLE_USER_HOME=/tmp/guh          # keep your real ~/.gradle out of it
mkdir -p /tmp/plain && cd /tmp/plain
echo "rootProject.name = 'plain'" > settings.gradle
echo "tasks.register('hello') { doLast { println 'hello' } }" > build.gradle

gradle hello -q                           # starts a daemon, which keeps journal-1 on demand
PID=$(jps -l | awk '/GradleDaemon/{print $1}')
kill -STOP "$PID"                         # the daemon is alive but cannot answer pings

gradle hello --no-daemon                  # fails after ~60 s: Timeout waiting to lock journal cache
kill -CONT "$PID"

同じ使い捨てユーザーホームで測定した結果は次のとおりです。

シナリオ結果
正常でアイドル状態の 9.3.1 デーモン、2 つ目の 9.3.1 ビルド2 秒で成功 (デーモンがロックを引き渡します)
フリーズした 9.3.1 デーモン、2 つ目の 9.3.1 ビルド62 秒後に journal cache で失敗、Owner PID はフリーズしたデーモン
フリーズした 9.3.1 デーモン、journal-1.lock を削除して 2 つ目のビルド62 秒後に今度は file hash cache (caches/9.3.1/fileHashes) で失敗
フリーズした 9.3.1 デーモン、gradle --stop (9.3.1)“Stopping Daemon(s)” でハング (25 秒後もまだ待機中)
正常な 9.3.1 デーモン、Gradle 8.14 からの gradle --stop9.3.1 のデーモンは動き続けているのに “No Gradle daemons are running.” と表示
フリーズした 9.3.1 デーモン、Gradle 8.14 のビルド64 秒後に journal cache で失敗、古い “another Gradle instance” の表記
フリーズしたデーモンに kill -9 をして、そのあとビルド2 秒で成功

3 行目と 5 行目が、このエラーが厄介だと言われる理由を説明しています。

修正手順

1. Owner PID を特定する

何かを終了する前に、エラーに出ている PID を確認します。

# macOS / Linux, any Gradle version
ps -o pid,stat,etime,command -p 52907
jps -l                                    # lists every JVM; Gradle daemons show as org.gradle.launcher.daemon.bootstrap.GradleDaemon
# Windows PowerShell, any Gradle version
Get-CimInstance Win32_Process -Filter "ProcessId = 52907" | Select-Object ProcessId, CommandLine
Get-CimInstance Win32_Process -Filter "Name = 'java.exe'" |
  Where-Object CommandLine -match 'GradleDaemon' |
  Select-Object ProcessId, CommandLine

コマンドラインには、デーモンの Gradle バージョンと実行している JDK が含まれているので、誰が起動したのかがわかります。Android Studio の jbr フォルダー配下のデーモンは、IDE の同期から来たものです。macOS や Linux の stat 列にある T は、プロセスが停止していることを意味します。その PID が Gradle デーモンではないプロセス (Kotlin コンパイルデーモンやテスト用の JVM) だった場合は、メモしておいてください。ビルドがあなたの Gradle ユーザーホームを使って長時間動作する JVM をフォークしていることを示しているからです。

その PID がマシン上に存在しない場合、所有者は別の場所、つまり同じ .gradle ディレクトリを共有している別のコンテナや VM にいます。下の CI のセクションに進んでください。

2. ロックファイルではなく、そのプロセスを終了する

# macOS / Linux (a stopped or hung JVM ignores a plain kill, so use -9)
kill -9 52907
# Windows
Stop-Process -Id 52907 -Force

所有者が終了すると、オペレーティングシステムがそのロックを解放します。flutter run を再実行すれば、ビルドは通常どおり始まります。削除するものはなく、flutter clean も不要です。そもそも flutter clean は ~/.gradle には触れません。

すべてのバージョンのすべてのデーモンを一度に片付けるには次のようにします (1 日中プロジェクトを切り替えたあとに便利です)。

# macOS / Linux: stops all Gradle daemons regardless of version
jps -l | awk '/GradleDaemon/{print $1}' | xargs kill

android/ 内での ./gradlew --stop は、所有者がラッパーと同じ Gradle バージョンの正常なデーモンである場合には問題ありません。Gradle のドキュメントには、“terminates all Daemon processes started with the same version of Gradle used to execute the command” とはっきり書かれています。私の再現では両方の失敗パターンが出ました。別バージョンの --stop は所有者を認識すらせず、同じバージョンの --stop はハングしたデーモンを終了させずに待ち続けます。

3. ハングの原因を取り除く

同じデーモンがハングし続ける場合は、再発する前に理由を調べてください。

CI ランナーと Docker

CI では、Owner PID は通常、別のジョブにあります。有効な構成は 3 つです。

  1. ジョブごとに Gradle ユーザーホームを分ける。 GRADLE_USER_HOME をジョブのワークスペース内のパスに設定し、CI のキャッシュステップで実行間の永続化を行います。同時に動作する 2 つのプロセスがロックファイルを共有することはなくなります。
  2. 共有の読み取り専用依存関係キャッシュ。 Gradle の GRADLE_RO_DEP_CACHE は、事前に作成した modules-2 ディレクトリを指します。Gradle はこれをロックなしで読み取り、各コンテナは書き込み可能な自分用のユーザーホームを持ちます。この機能は Gradle のドキュメントではまだ incubating とされており、ビルドが読み取っている間は共有コピーに書き込んではいけません。
  3. デーモンを残さない。 flutter build apk --no-android-gradle-daemon (このフラグは Flutter 3.44.8 では既定で true です) はラッパーに --no-daemon を渡すので、ビルドの JVM はビルドの終了とともに終了します。同時に動作する 2 つのジョブ間の競合は防げませんが、最もよくある所有者、つまり再利用されるランナーで前のジョブが残したデーモンはなくなります。
# GitHub Actions, Flutter 3.44.8: per-job Gradle home, cached between runs
env:
  GRADLE_USER_HOME: ${{ github.workspace }}/.gradle-home
steps:
  - uses: actions/cache@v4
    with:
      path: ${{ github.workspace }}/.gradle-home/caches
      key: gradle-${{ runner.os }}-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
  - run: flutter build apk --release --no-android-gradle-daemon

コンテナをホストネットワークで実行しても ping は届くようになりますが、コンテナに求めていた分離を手放すことになります。そのため、私ならまずジョブごとのホームを検討します。

“.lock ファイルを削除する” という助言がなくならない理由

このエラーで最も支持されている助言は find ~/.gradle -type f -name "*.lock" -delete です。所有者がまだ生きている場合にこれが何をするかは、私の再現で確認できます。2 つ目のビルドは新しい journal-1.lock を作成してロックしたものの、60 秒後に caches/9.3.1/fileHashes/fileHashes.lock でタイムアウトしました。フリーズしたデーモンがそちらのロックも保持していたからです。すべてのロックファイルを削除すれば新しいビルドは先に進めますが、今度は 2 つのプロセスが同じキャッシュファイルを自分のものだと思い込むことになります。その結果が CorruptedCacheException と ~/.gradle/caches の全消去で、gradle/gradle#30135 で報告されているとおりです。

うまくいったように見える場合は、たまたまその間に所有者が終了したか、死んだからです。所有者を終了すれば、破損のリスクなしに同じ結果が得られます。

よく似たエラー

関連記事

参考資料

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る