Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update synchronous crash reporting logic #1188

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
[#1185](https://github.com/bugsnag/bugsnag-android/pull/1185)
[#1186](https://github.com/bugsnag/bugsnag-android/pull/1186)
[#1180](https://github.com/bugsnag/bugsnag-android/pull/1180)
[#1188](https://github.com/bugsnag/bugsnag-android/pull/1188)

## 5.7.1 (2021-03-03)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class EventStoreConfinementTest {
@Before
fun setUp() {
// setup delivery for interception
retainingDelivery = RetainingDelivery()
retainingDelivery = RetainingDelivery(EVENT_CONFINEMENT_ATTEMPTS)
val cfg = BugsnagTestUtils.generateConfiguration().apply {
autoTrackSessions = false
delivery = retainingDelivery
Expand All @@ -39,7 +39,7 @@ internal class EventStoreConfinementTest {
repeat(EVENT_CONFINEMENT_ATTEMPTS) { count ->
client.notify(RuntimeException("$count"))
}
retainingDelivery.latch.await(1, TimeUnit.SECONDS)
retainingDelivery.latch.await(10, TimeUnit.SECONDS)

// confirm that no dupe requests are sent and that the request order is deterministic
val payloads = retainingDelivery.payloads
Expand Down Expand Up @@ -80,41 +80,13 @@ internal class EventStoreConfinementTest {
}
}

/**
* Calling flushOnLaunch() is confined to a single thread
*/
@Test
fun flushOnLaunchIsThreadConfined() {
val eventStore = client.eventStore

// send 20 errors
repeat(EVENT_CONFINEMENT_ATTEMPTS) { count ->
val event = BugsnagTestUtils.generateEvent().apply {
apiKey = "$count"
}
eventStore.write(event)
eventStore.flushOnLaunch()
}
retainingDelivery.latch.await(5, TimeUnit.SECONDS)

// confirm that no dupe requests are sent
val filenames = retainingDelivery.files
assertEquals(EVENT_CONFINEMENT_ATTEMPTS, filenames.size)
assertEquals(EVENT_CONFINEMENT_ATTEMPTS, filenames.toSet().size)

retainingDelivery.files.forEachIndexed { index, file ->
val eventInfo = EventFilenameInfo.fromFile(file, client.immutableConfig)
assertEquals("$index", eventInfo.apiKey)
}
}

/**
* Retains all the sent error payloads
*/
private class RetainingDelivery : Delivery {
private class RetainingDelivery(attempts: Int) : Delivery {
val files = mutableListOf<File>()
val payloads = mutableListOf<Event>()
val latch = CountDownLatch(EVENT_CONFINEMENT_ATTEMPTS)
val latch = CountDownLatch(attempts)

override fun deliver(payload: Session, deliveryParams: DeliveryParams) =
DeliveryStatus.DELIVERED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ public Unit invoke(String activity, Map<String, ?> metadata) {

// Flush any on-disk errors and sessions
eventStore.flushOnLaunch();
eventStore.flushAsync();
sessionTracker.flushAsync();

lastRunInfoStore = new LastRunInfoStore(immutableConfig);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ internal data class EventFilenameInfo(
sanitizedApiKey,
uuid,
timestamp,
findSuffixForEvent(obj, config),
findSuffixForEvent(obj),
findErrorTypesForEvent(obj)
)
}
Expand Down Expand Up @@ -140,24 +140,18 @@ internal data class EventFilenameInfo(
/**
* Calculates the suffix for the given event
*/
private fun findSuffixForEvent(obj: Any, config: ImmutableConfig): String {
private fun findSuffixForEvent(obj: Any): String {
return when (obj) {
is Event -> {
val duration = obj.app.duration
if (duration != null && isStartupCrash(duration.toLong(), config)) {
STARTUP_CRASH
} else {
""
when (obj.app.isLaunching) {
true -> STARTUP_CRASH
else -> ""
}
}
else -> {
NON_JVM_CRASH
}
}
}

private fun isStartupCrash(durationMs: Long, config: ImmutableConfig): Boolean {
return durationMs < config.launchDurationMillis
}
}
}
131 changes: 68 additions & 63 deletions bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.bugsnag.android;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.File;
import java.lang.Thread;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
* Store and flush Event reports which couldn't be sent immediately due to
Expand All @@ -19,13 +23,11 @@
class EventStore extends FileStore {

private static final long LAUNCH_CRASH_TIMEOUT_MS = 2000;
private static final int LAUNCH_CRASH_POLL_MS = 50;

volatile boolean flushOnLaunchCompleted = false;
private final ImmutableConfig config;
private final Delegate delegate;
private final Notifier notifier;
private final BackgroundTaskService backgroundTaskService;
private final BackgroundTaskService bgTaskSevice;
final Logger logger;

static final Comparator<File> EVENT_COMPARATOR = new Comparator<File>() {
Expand All @@ -47,7 +49,7 @@ public int compare(File lhs, File rhs) {
EventStore(@NonNull ImmutableConfig config,
@NonNull Logger logger,
Notifier notifier,
BackgroundTaskService backgroundTaskService,
BackgroundTaskService bgTaskSevice,
Delegate delegate) {
super(new File(config.getPersistenceDirectory(), "bugsnag-errors"),
config.getMaxPersistedEvents(),
Expand All @@ -58,64 +60,78 @@ public int compare(File lhs, File rhs) {
this.logger = logger;
this.delegate = delegate;
this.notifier = notifier;
this.backgroundTaskService = backgroundTaskService;
this.bgTaskSevice = bgTaskSevice;
}

/**
* Flush startup crashes synchronously on the main thread
*/
void flushOnLaunch() {
if (config.getLaunchDurationMillis() != 0) {
List<File> storedFiles = findStoredFiles();
final List<File> crashReports = findLaunchCrashReports(storedFiles);

// cancel non-launch crash reports
storedFiles.removeAll(crashReports);
cancelQueuedFiles(storedFiles);

if (!crashReports.isEmpty()) {

// Block the main thread for a 2 second interval as the app may crash very soon.
// The request itself will run in a background thread and will continue after the 2
// second period until the request completes, or the app crashes.
flushOnLaunchCompleted = false;
logger.i("Attempting to send launch crash reports");

try {
backgroundTaskService.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
@Override
public void run() {
flushReports(crashReports);
flushOnLaunchCompleted = true;
}
});
} catch (RejectedExecutionException ex) {
logger.w("Failed to flush launch crash reports", ex);
flushOnLaunchCompleted = true;
if (!config.getSendLaunchCrashesSynchronously()) {
return;
}
Future<?> future = null;
try {
future = bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
@Override
public void run() {
flushLaunchCrashReport();
}
});
} catch (RejectedExecutionException exc) {
logger.d("Failed to flush launch crash reports, continuing.", exc);
}

try {
if (future != null) {
future.get(LAUNCH_CRASH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException | ExecutionException | TimeoutException exc) {
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc);
}
}

long waitMs = 0;
void flushLaunchCrashReport() {
List<File> storedFiles = findStoredFiles();
File launchCrashReport = findLaunchCrashReport(storedFiles);

while (!flushOnLaunchCompleted && waitMs < LAUNCH_CRASH_TIMEOUT_MS) {
try {
Thread.sleep(LAUNCH_CRASH_POLL_MS);
waitMs += LAUNCH_CRASH_POLL_MS;
} catch (InterruptedException exception) {
logger.w("Interrupted while waiting for launch crash report request");
}
}
logger.i("Continuing with Bugsnag initialisation");
} else {
logger.d("No startupcrash events to flush to Bugsnag.");
// cancel non-launch crash reports
if (launchCrashReport != null) {
storedFiles.remove(launchCrashReport);
}
cancelQueuedFiles(storedFiles);

if (launchCrashReport != null) {
logger.i("Attempting to send the most recent launch crash report");
flushReports(Collections.singletonList(launchCrashReport));
logger.i("Continuing with Bugsnag initialisation");
} else {
logger.d("No startupcrash events to flush to Bugsnag.");
}
}

@Nullable
File findLaunchCrashReport(Collection<File> storedFiles) {
List<File> launchCrashes = new ArrayList<>();

for (File file : storedFiles) {
EventFilenameInfo filenameInfo = EventFilenameInfo.Companion.fromFile(file, config);
if (filenameInfo.isLaunchCrashReport()) {
launchCrashes.add(file);
}
}

flushAsync(); // flush any remaining errors async that weren't delivered
// sort to get most recent timestamp
Collections.sort(launchCrashes, EVENT_COMPARATOR);
return launchCrashes.isEmpty() ? null : launchCrashes.get(launchCrashes.size() - 1);
}

/**
* Flush any on-disk errors to Bugsnag
*/
void flushAsync() {
try {
backgroundTaskService.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
@Override
public void run() {
List<File> storedFiles = findStoredFiles();
Expand All @@ -133,7 +149,7 @@ public void run() {
void flushReports(Collection<File> storedReports) {
if (!storedReports.isEmpty()) {
logger.i(String.format(Locale.US,
"Sending %d saved error(s) to Bugsnag", storedReports.size()));
"Sending %d saved error(s) to Bugsnag", storedReports.size()));

for (File eventFile : storedReports) {
flushEventFile(eventFile);
Expand All @@ -147,7 +163,8 @@ private void flushEventFile(File eventFile) {
String apiKey = eventInfo.getApiKey();
EventPayload payload = new EventPayload(apiKey, null, eventFile, notifier, config);
DeliveryParams deliveryParams = config.getErrorApiDeliveryParams(payload);
DeliveryStatus deliveryStatus = config.getDelivery().deliver(payload, deliveryParams);
Delivery delivery = config.getDelivery();
DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams);

switch (deliveryStatus) {
case DELIVERED:
Expand Down Expand Up @@ -178,31 +195,19 @@ private void handleEventFlushFailure(Exception exc, File eventFile) {
deleteStoredFiles(Collections.singleton(eventFile));
}

private List<File> findLaunchCrashReports(Collection<File> storedFiles) {
List<File> launchCrashes = new ArrayList<>();

for (File file : storedFiles) {
EventFilenameInfo filenameInfo = EventFilenameInfo.Companion.fromFile(file, config);
if (filenameInfo.isLaunchCrashReport()) {
launchCrashes.add(file);
}
}
return launchCrashes;
}

@NonNull
@Override
String getFilename(Object object) {
EventFilenameInfo eventInfo
= EventFilenameInfo.Companion.fromEvent(object, null, config);
String encodedInfo = eventInfo.encode();
return String.format(Locale.US, "%s.json", encodedInfo);
return String.format(Locale.US, "%s", encodedInfo);
}

String getNdkFilename(Object object, String apiKey) {
EventFilenameInfo eventInfo
= EventFilenameInfo.Companion.fromEvent(object, apiKey, config);
String encodedInfo = eventInfo.encode();
return String.format(Locale.US, "%s.json", encodedInfo);
return String.format(Locale.US, "%s", encodedInfo);
}
}
Loading