Skip to content

Commit 2057840

Browse files
authored
Fix breadcrumb race condition: log() drops entry before logException(… (#8076)
…) reads it CrashlyticsCore.log() used common.submit() which completed as soon as the diskWrite task was enqueued, not when it finished. This allowed the subsequent logException() common task to call logFileManager.getLogString() before writeToLog() had run on the diskWrite worker, silently dropping the breadcrumb from the non-fatal report. Fix: change to common.submitTask() so the common worker suspends until the diskWrite task resolves before dispatching the next item (e.g. logException). Adds a regression test that calls log() immediately before logException(), awaits only the common worker, and asserts the breadcrumb is present on disk. Fixes #8034
1 parent 433f564 commit 2057840

4 files changed

Lines changed: 40 additions & 5 deletions

File tree

firebase-crashlytics/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22

3+
- [fixed] Fixed race condition that caused logs from background threads to not be attached to
4+
reports in some cases [#8034]
5+
36
# 20.0.5
47

58
- [fixed] Fixed a runtime crash that could occur in minified native apps when using the Crashlytics

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static org.junit.Assert.assertEquals;
1818
import static org.junit.Assert.assertFalse;
19+
import static org.junit.Assert.assertNotNull;
1920
import static org.junit.Assert.assertNull;
2021
import static org.junit.Assert.assertTrue;
2122
import static org.mockito.Mockito.any;
@@ -458,6 +459,31 @@ public void testBreadcrumbSourceIsRegistered() {
458459
Mockito.verify(mockBreadcrumbSource).registerBreadcrumbHandler(any(BreadcrumbHandler.class));
459460
}
460461

462+
/**
463+
* Regression test for https://github.com/firebase/firebase-android-sdk/issues/8034.
464+
*
465+
* <p>Verifies that a breadcrumb logged immediately before {@code logException} is written to disk
466+
* before the non-fatal event snapshots the log. Prior to the fix, {@code log()} used
467+
* {@code common.submit()} which completed as soon as the disk-write was enqueued, allowing the
468+
* subsequent {@code logException()} task to read an empty log. The fix uses {@code submitTask()}
469+
* so the common worker suspends until the disk write finishes.
470+
*/
471+
@Test
472+
public void testLog_breadcrumbIsWrittenBeforeLogExceptionReadsIt() throws Exception {
473+
final String breadcrumb = "test breadcrumb";
474+
475+
crashlyticsCore.log(breadcrumb);
476+
crashlyticsCore.logException(new RuntimeException("non-fatal"), Map.of());
477+
478+
// Awaiting common is sufficient: submitTask makes common suspend until diskWrite completes,
479+
// so when this returns the log entry is guaranteed to be on disk.
480+
crashlyticsWorkers.common.await();
481+
482+
final String logString = crashlyticsCore.getController().getLogString();
483+
assertNotNull("Log should have been written before logException read it", logString);
484+
assertTrue("Log should contain the breadcrumb", logString.contains(breadcrumb));
485+
}
486+
461487
@Test
462488
public void testOnPreExecute_nativeDidCrashOnPreviousExecution() throws Exception {
463489
appRestart(); // Create a previous execution

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import android.util.Base64;
2626
import androidx.annotation.NonNull;
2727
import androidx.annotation.Nullable;
28+
import androidx.annotation.VisibleForTesting;
2829
import com.google.android.gms.tasks.SuccessContinuation;
2930
import com.google.android.gms.tasks.Task;
3031
import com.google.android.gms.tasks.TaskCompletionSource;
@@ -793,6 +794,12 @@ UserMetadata getUserMetadata() {
793794
return userMetadata;
794795
}
795796

797+
@VisibleForTesting
798+
@Nullable
799+
String getLogString() {
800+
return logFileManager.getLogString();
801+
}
802+
796803
boolean isHandlingException() {
797804
return crashHandler != null && crashHandler.isHandlingException();
798805
}

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,10 @@ public void logException(@NonNull Throwable throwable, @NonNull Map<String, Stri
330330
*/
331331
public void log(final String msg) {
332332
final long timestamp = System.currentTimeMillis() - startTime;
333-
// queuing up on common worker to maintain the order
334-
crashlyticsWorkers.common.submit(
335-
() -> {
336-
crashlyticsWorkers.diskWrite.submit(() -> controller.writeToLog(timestamp, msg));
337-
});
333+
// submitTask ensures the common worker waits for the diskWrite task to complete, ensuring
334+
// that subsequent tasks on the common worker (e.g. logException) see this log entry.
335+
crashlyticsWorkers.common.submitTask(
336+
() -> crashlyticsWorkers.diskWrite.submit(() -> controller.writeToLog(timestamp, msg)));
338337
}
339338

340339
public void setUserId(String identifier) {

0 commit comments

Comments
 (0)