Start Debugging

Fix: Timeout waiting to lock journal cache in einem Flutter-Android-Build

Ein anderer Gradle-Prozess hält ~/.gradle/caches/journal-1 und gibt die Sperre nicht frei. Finden Sie die Owner PID, beenden Sie diesen Daemon und löschen Sie keine .lock-Dateien mehr: Das verschiebt den Fehler nur.

Ein anderer Gradle-Prozess, die “Owner PID” in der Meldung, hält die Sperre auf ~/.gradle/caches/journal-1 und hat auf die Anfrage Ihres Builds, sie freizugeben, innerhalb des festen 60-Sekunden-Timeouts von Gradle nicht geantwortet. Die Lösung besteht darin, diesen Prozess zu finden und zu beenden: mit jps -l (oder ps) prüfen, ob es ein GradleDaemon ist, dann kill -9 <Owner PID> bzw. unter Windows Stop-Process -Id <Owner PID> -Force ausführen und flutter run erneut starten. ./gradlew --stop bewirkt hier oft nichts, weil es nur Daemons der eigenen Gradle-Version beendet. Auch das Löschen von journal-1.lock hilft nicht: In meiner Reproduktion lief der nächste Build stattdessen beim File-Hash-Cache in den Timeout.

Alles Folgende habe ich unter macOS mit Flutter 3.44.8 (dessen Template Gradle 9.1.0 und AGP 9.0.1 festlegt), den Gradle-Distributionen 9.3.1 und 8.14 sowie OpenJDK 17 reproduziert, mit einem Wegwerf-GRADLE_USER_HOME. Den Sperrcode habe ich im Gradle-Quelltext am Tag v9.8.0 gelesen, dem aktuellen Release.

Der Fehler im Kontext

Dies ist die exakte Ausgabe aus meiner Reproduktion (Pfade zu ~ gekürzt):

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

Wenn Flutter den Build ausführt, sehen Sie denselben Block unter FAILURE: Build failed with an exception., gefolgt von Flutters eigenem Gradle task assembleDebug failed with exit code 1. Diese letzte Zeile ist nur der Bote, nicht die Ursache.

Zwei Details verraten, welche Gradle-Version Sie verwenden. Gradle 8.x und älter schreiben “It is currently in use by another Gradle instance”. Ab Gradle 9.0 lautet die Formulierung “in use by another process”. Die Zeilen Owner Operation und Our operation sind beim Journal-Cache fast immer leer, ignorieren Sie sie also. Entscheidend ist die Zeile Owner PID.

Warum Gradle die Sperre nicht bekommt

~/.gradle/caches/journal-1 hält fest, wann jede Datei in den gemeinsamen Caches zuletzt verwendet wurde, damit die Cache-Bereinigung von Gradle weiß, was sich gefahrlos löschen lässt. Anders als caches/9.1.0/ oder caches/8.14/ ist es nicht versioniert: Jede Gradle-Version auf dem Rechner, jeder Daemon und jede IDE-Synchronisierung teilen sich dieses eine Verzeichnis. Deshalb ist es die Sperre, auf die man am häufigsten stößt.

Gradle hält diese Sperren nicht für die Dauer eines Builds. Es nimmt sie “bei Bedarf” und gibt sie weiter, wenn jemand anderes danach fragt. Der Algorithmus in DefaultFileLockManager sieht so aus:

  1. Versuchen, eine Betriebssystem-Dateisperre auf den Zustandsbereich der Sperrdatei zu legen.
  2. Schlägt das fehl, die PID und den UDP-Port des Besitzers aus dem Info-Bereich der Sperrdatei lesen und den Besitzer über Loopback anpingen.
  3. Der Besitzer gibt die Sperre frei und bestätigt dies, sofern er den Cache gerade nicht benutzt.
  4. Mit exponentiellem Backoff wiederholen, bis die Sperre erworben wurde oder DEFAULT_LOCK_TIMEOUT (60.000 ms) abgelaufen ist.
// Gradle v9.8.0, DefaultFileLockManager.java
public static final int DEFAULT_LOCK_TIMEOUT = 60000;

Der Timeout ist eine Konstante, die von BasicGlobalScopeServices übergeben wird. Es gibt keine Gradle-Eigenschaft und keine Systemeigenschaft, um ihn zu erhöhen, “den Lock-Timeout erhöhen” ist also keine Option.

Der Fehler bedeutet also nie, dass irgendwo eine veraltete Sperrdatei herumliegt. Wäre der Besitzerprozess gestorben, hätte das Betriebssystem seine Dateisperre freigegeben und Ihr Build hätte sie sofort bekommen. Er bedeutet: Ein lebender Prozess besitzt die Sperre und hat den Ping nicht beantwortet. Dafür gibt es vier realistische Gründe:

Auf einem Flutter-Entwicklungsrechner dominiert die erste Ursache, weil auf ein Flutter-Projekt meist mehrere Gradle-Clients zugreifen: flutter run im Terminal oder in VS Code, die Gradle-Synchronisierung von Android Studio und flutter build apk aus einem Skript. Verwenden sie unterschiedliche JDKs (das mitgelieferte JBR von Android Studio gegenüber JAVA_HOME) oder unterschiedliche Gradle-Versionen (zwei Projekte, die mit verschiedenen Flutter-Releases erstellt wurden), bekommt jeder seinen eigenen Daemon, und alle teilen sich journal-1.

Minimale Reproduktion

Sie brauchen weder Flutter noch ein Telefon, um das zu sehen. Ein Gradle-Projekt mit einer Aufgabe, ein separates Gradle-Benutzerverzeichnis und kill -STOP, um einen hängenden Daemon zu simulieren, genügen:

# 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"

Dies sind die gemessenen Ergebnisse, jeweils mit demselben separaten Benutzerverzeichnis:

SzenarioErgebnis
Gesunder, inaktiver 9.3.1-Daemon, zweiter 9.3.1-BuildErfolgreich in 2 s (der Daemon gibt die Sperre weiter)
Eingefrorener 9.3.1-Daemon, zweiter 9.3.1-BuildSchlägt nach 62 s bei journal cache fehl, Owner PID = der eingefrorene Daemon
Eingefrorener 9.3.1-Daemon, journal-1.lock gelöscht, zweiter BuildSchlägt nach 62 s stattdessen bei file hash cache (caches/9.3.1/fileHashes) fehl
Eingefrorener 9.3.1-Daemon, gradle --stop (9.3.1)Hängt bei “Stopping Daemon(s)” (wartet nach 25 s immer noch)
Gesunder 9.3.1-Daemon, gradle --stop von Gradle 8.14Gibt “No Gradle daemons are running.” aus, während der 9.3.1-Daemon weiterläuft
Eingefrorener 9.3.1-Daemon, Gradle-8.14-BuildSchlägt nach 64 s bei journal cache fehl, mit der älteren Formulierung “another Gradle instance”
kill -9 auf den eingefrorenen Daemon, danach ein BuildErfolgreich in 2 s

Die dritte und die fünfte Zeile erklären, warum dieser Fehler so einen frustrierenden Ruf hat.

Die Lösung, Schritt für Schritt

1. Die Owner PID identifizieren

Nehmen Sie die PID aus dem Fehler und sehen Sie sich den Prozess an, bevor Sie etwas beenden:

# 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

Die Befehlszeile enthält die Gradle-Version des Daemons und das JDK, auf dem er läuft, und verrät damit, wer ihn gestartet hat. Ein Daemon im jbr-Ordner von Android Studio stammt aus einer IDE-Synchronisierung. Ein T in der Spalte stat unter macOS oder Linux bedeutet, dass der Prozess angehalten ist. Gehört die PID zu einem Prozess, der gar kein Gradle-Daemon ist (ein Kotlin-Compile-Daemon, eine Test-JVM), notieren Sie das, denn es deutet auf einen Build hin, der mit Ihrem Gradle-Benutzerverzeichnis eine langlebige JVM abgespalten hat.

Existiert die PID auf Ihrem Rechner nicht, liegt der Besitzer woanders: in einem anderen Container oder einer VM, die dasselbe .gradle-Verzeichnis teilt. Springen Sie dann zum Abschnitt über CI weiter unten.

2. Den Prozess beenden, nicht die Sperrdatei

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

Wenn der Besitzer stirbt, gibt das Betriebssystem seine Sperre frei. Starten Sie flutter run erneut, und der Build läuft normal an. Es gibt nichts zu löschen und flutter clean ist nicht nötig, das ~/.gradle ohnehin nicht anfasst.

So beenden Sie alle Daemons aller Versionen auf einmal (nützlich nach einem langen Tag mit Wechseln zwischen Projekten):

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

./gradlew --stop im Verzeichnis android/ genügt, wenn der Besitzer ein gesunder Daemon derselben Gradle-Version wie Ihr Wrapper ist. Die Gradle-Dokumentation ist eindeutig: Der Befehl beende alle Daemon-Prozesse, die mit derselben Gradle-Version wie der ausführende Befehl gestartet wurden (englisch: “terminates all Daemon processes started with the same version of Gradle used to execute the command”). Meine Reproduktion zeigt beide Fehlerfälle: Ein --stop einer anderen Version sieht den Besitzer gar nicht, und ein --stop derselben Version wartet auf einen hängenden Daemon, statt ihn zu beenden.

3. Die Ursache fürs Hängen beseitigen

Wenn derselbe Daemon immer wieder hängt, finden Sie heraus, warum, bevor es erneut passiert:

CI-Runner und Docker

In CI lebt die Owner PID meist in einem anderen Job. Drei Setups funktionieren:

  1. Ein Gradle-Benutzerverzeichnis pro Job. Setzen Sie GRADLE_USER_HOME auf einen Pfad im Workspace des Jobs und nutzen Sie den CI-Cache-Schritt, um es zwischen Läufen zu erhalten. So teilen nie zwei lebende Prozesse eine Sperrdatei.
  2. Ein gemeinsamer schreibgeschützter Abhängigkeits-Cache. GRADLE_RO_DEP_CACHE von Gradle verweist auf ein vorab befülltes modules-2-Verzeichnis, das Gradle ohne Sperren liest, während jeder Container sein eigenes beschreibbares Benutzerverzeichnis behält. Die Funktion ist in der Gradle-Dokumentation noch als incubating markiert, und die gemeinsame Kopie darf nie beschrieben werden, solange Builds sie lesen.
  3. Keine Daemons zurücklassen. flutter build apk --no-android-gradle-daemon (das Flag ist in Flutter 3.44.8 standardmäßig true) übergibt --no-daemon an den Wrapper, sodass die JVM des Builds mit dem Build endet. Das verhindert keine Konkurrenz zwischen zwei gleichzeitig laufenden Jobs, beseitigt aber den häufigsten Besitzer: einen Daemon, der vom vorherigen Job auf einem wiederverwendeten Runner übrig geblieben ist.
# 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

Container mit Host-Networking lassen die Pings ebenfalls funktionieren, geben dafür aber die Isolation auf, die man von Containern wollte. Ich würde daher zuerst zu einem Benutzerverzeichnis pro Job greifen.

Warum der Tipp “die .lock-Dateien löschen” immer wiederkehrt

Der meistbewertete Rat zu diesem Fehler lautet find ~/.gradle -type f -name "*.lock" -delete. Meine Reproduktion zeigt, was das bewirkt, solange der Besitzer noch lebt. Der zweite Build legte eine frische journal-1.lock an, sperrte sie und lief 60 Sekunden später bei caches/9.3.1/fileHashes/fileHashes.lock in den Timeout, weil der eingefrorene Daemon auch diese Sperre hielt. Werden alle Sperrdateien gelöscht, kann der neue Build zwar weiterlaufen, aber nun glauben zwei Prozesse, dieselben Cache-Dateien zu besitzen. So landen Leute bei CorruptedCacheException und einem vollständigen Leeren von ~/.gradle/caches, wie in gradle/gradle#30135 berichtet.

Wenn es scheinbar funktioniert, liegt das daran, dass der Besitzer zufällig zwischenzeitlich fertig wurde oder gestorben ist. Den Besitzer zu beenden führt zum selben Ergebnis, ohne das Risiko einer Beschädigung.

Ähnliche Fehler

Verwandte Artikel

Quellen

Comments

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

< Zurück