Skip to content

Commit f60279b

Browse files
authored
Deduplicate events happening in multiple threads simultaneously (#2845)
1 parent 288f538 commit f60279b

File tree

14 files changed

+325
-4
lines changed

14 files changed

+325
-4
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Deduplicate events happening in multiple threads simultaneously (e.g. `OutOfMemoryError`) ([#2845](https://github.com/getsentry/sentry-java/pull/2845))
8+
- This will improve Crash-Free Session Rate as we no longer will send multiple Session updates with `Crashed` status, but only the one that is relevant
9+
310
## 6.26.0
411

512
### Features

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import android.content.Context;
77
import android.content.pm.PackageInfo;
88
import android.os.Build;
9+
import io.sentry.DeduplicateMultithreadedEventProcessor;
910
import io.sentry.DefaultTransactionPerformanceCollector;
1011
import io.sentry.ILogger;
1112
import io.sentry.SendFireAndForgetEnvelopeSender;
@@ -125,6 +126,7 @@ static void initializeIntegrationsAndProcessors(
125126
options.setEnvelopeDiskCache(new AndroidEnvelopeCache(options));
126127
}
127128

129+
options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options));
128130
options.addEventProcessor(
129131
new DefaultAndroidEventProcessor(context, buildInfoProvider, options));
130132
options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker));

sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import java.io.InputStream;
2222
import java.io.OutputStreamWriter;
2323
import java.io.Writer;
24+
import java.util.ArrayList;
2425
import java.util.Calendar;
2526
import java.util.Collections;
27+
import java.util.List;
2628
import java.util.Locale;
29+
import java.util.concurrent.CountDownLatch;
2730
import timber.log.Timber;
2831

2932
public class MainActivity extends AppCompatActivity {
@@ -139,6 +142,28 @@ protected void onCreate(Bundle savedInstanceState) {
139142
Sentry.setUser(user);
140143
});
141144

145+
binding.outOfMemory.setOnClickListener(
146+
view -> {
147+
final CountDownLatch latch = new CountDownLatch(1);
148+
for (int i = 0; i < 20; i++) {
149+
new Thread(
150+
() -> {
151+
final List<String> data = new ArrayList<>();
152+
try {
153+
latch.await();
154+
for (int j = 0; j < 1_000_000; j++) {
155+
data.add(new String(new byte[1024 * 8]));
156+
}
157+
} catch (InterruptedException e) {
158+
e.printStackTrace();
159+
}
160+
})
161+
.start();
162+
}
163+
164+
latch.countDown();
165+
});
166+
142167
binding.nativeCrash.setOnClickListener(view -> NativeSample.crash());
143168

144169
binding.nativeCapture.setOnClickListener(view -> NativeSample.message());

sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@
6464
android:layout_height="wrap_content"
6565
android:text="@string/anr" />
6666

67+
<Button
68+
android:id="@+id/out_of_memory"
69+
android:layout_width="wrap_content"
70+
android:layout_height="wrap_content"
71+
android:text="@string/out_of_memory" />
72+
6773
<Button
6874
android:id="@+id/native_crash"
6975
android:layout_width="wrap_content"

sentry-samples/sentry-samples-android/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<resources>
22
<string name="app_name">Sentry sample</string>
33
<string name="crash_from_java">Crash from Java (UncaughtException)</string>
4+
<string name="out_of_memory">Out of Memory (Mulithreaded)</string>
45
<string name="send_message">Send Message</string>
56
<string name="send_message_from_inner_fragment">Send Message from inner fragment</string>
67
<string name="add_attachment">Add Attachment</string>

sentry/api/sentry.api

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ public final class io/sentry/DateUtils {
196196
public static fun toUtilDateNotNull (Lio/sentry/SentryDate;)Ljava/util/Date;
197197
}
198198

199+
public final class io/sentry/DeduplicateMultithreadedEventProcessor : io/sentry/EventProcessor {
200+
public fun <init> (Lio/sentry/SentryOptions;)V
201+
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
202+
}
203+
199204
public final class io/sentry/DefaultTransactionPerformanceCollector : io/sentry/TransactionPerformanceCollector {
200205
public fun <init> (Lio/sentry/SentryOptions;)V
201206
public fun close ()V
@@ -1564,6 +1569,7 @@ public final class io/sentry/SentryEvent : io/sentry/SentryBaseEvent, io/sentry/
15641569
public fun getThreads ()Ljava/util/List;
15651570
public fun getTimestamp ()Ljava/util/Date;
15661571
public fun getTransaction ()Ljava/lang/String;
1572+
public fun getUnhandledException ()Lio/sentry/protocol/SentryException;
15671573
public fun getUnknown ()Ljava/util/Map;
15681574
public fun isCrashed ()Z
15691575
public fun isErrored ()Z
@@ -2388,6 +2394,7 @@ public final class io/sentry/TypeCheckHint {
23882394
public static final field OPEN_FEIGN_RESPONSE Ljava/lang/String;
23892395
public static final field SENTRY_DART_SDK_NAME Ljava/lang/String;
23902396
public static final field SENTRY_DOTNET_SDK_NAME Ljava/lang/String;
2397+
public static final field SENTRY_EVENT_DROP_REASON Ljava/lang/String;
23912398
public static final field SENTRY_IS_FROM_HYBRID_SDK Ljava/lang/String;
23922399
public static final field SENTRY_JAVASCRIPT_SDK_NAME Ljava/lang/String;
23932400
public static final field SENTRY_SYNTHETIC_EXCEPTION Ljava/lang/String;
@@ -2671,6 +2678,12 @@ public abstract interface class io/sentry/hints/DiskFlushNotification {
26712678
public abstract fun markFlushed ()V
26722679
}
26732680

2681+
public final class io/sentry/hints/EventDropReason : java/lang/Enum {
2682+
public static final field MULTITHREADED_DEDUPLICATION Lio/sentry/hints/EventDropReason;
2683+
public static fun valueOf (Ljava/lang/String;)Lio/sentry/hints/EventDropReason;
2684+
public static fun values ()[Lio/sentry/hints/EventDropReason;
2685+
}
2686+
26742687
public abstract interface class io/sentry/hints/Flushable {
26752688
public abstract fun waitFlush ()Z
26762689
}
@@ -4154,13 +4167,15 @@ public final class io/sentry/util/FileUtils {
41544167

41554168
public final class io/sentry/util/HintUtils {
41564169
public static fun createWithTypeCheckHint (Ljava/lang/Object;)Lio/sentry/Hint;
4170+
public static fun getEventDropReason (Lio/sentry/Hint;)Lio/sentry/hints/EventDropReason;
41574171
public static fun getSentrySdkHint (Lio/sentry/Hint;)Ljava/lang/Object;
41584172
public static fun hasType (Lio/sentry/Hint;Ljava/lang/Class;)Z
41594173
public static fun isFromHybridSdk (Lio/sentry/Hint;)Z
41604174
public static fun runIfDoesNotHaveType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryNullableConsumer;)V
41614175
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;)V
41624176
public static fun runIfHasType (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/util/HintUtils$SentryConsumer;Lio/sentry/util/HintUtils$SentryHintFallback;)V
41634177
public static fun runIfHasTypeLogIfNot (Lio/sentry/Hint;Ljava/lang/Class;Lio/sentry/ILogger;Lio/sentry/util/HintUtils$SentryConsumer;)V
4178+
public static fun setEventDropReason (Lio/sentry/Hint;Lio/sentry/hints/EventDropReason;)V
41644179
public static fun setIsFromHybridSdk (Lio/sentry/Hint;Ljava/lang/String;)V
41654180
public static fun setTypeCheckHint (Lio/sentry/Hint;Ljava/lang/Object;)V
41664181
public static fun shouldApplyScopeData (Lio/sentry/Hint;)Z
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.sentry;
2+
3+
import io.sentry.hints.EventDropReason;
4+
import io.sentry.protocol.SentryException;
5+
import io.sentry.util.HintUtils;
6+
import java.util.Collections;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.jetbrains.annotations.Nullable;
11+
12+
/**
13+
* An event processor that deduplicates crash events of the same type that are simultaneously from
14+
* multiple threads. This can be the case for OutOfMemory errors or CursorWindowAllocationException,
15+
* basically any error related to allocating memory when it's low.
16+
*/
17+
public final class DeduplicateMultithreadedEventProcessor implements EventProcessor {
18+
19+
private final @NotNull Map<String, Long> processedEvents =
20+
Collections.synchronizedMap(new HashMap<>());
21+
22+
private final @NotNull SentryOptions options;
23+
24+
public DeduplicateMultithreadedEventProcessor(final @NotNull SentryOptions options) {
25+
this.options = options;
26+
}
27+
28+
@Override
29+
public @Nullable SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) {
30+
if (!HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class)) {
31+
// only dedupe crashes coming from our exception handler, because custom errors/crashes might
32+
// be sent on purpose
33+
return event;
34+
}
35+
36+
final SentryException exception = event.getUnhandledException();
37+
if (exception == null) {
38+
return event;
39+
}
40+
41+
final String type = exception.getType();
42+
if (type == null) {
43+
return event;
44+
}
45+
46+
final Long currentEventTid = exception.getThreadId();
47+
if (currentEventTid == null) {
48+
return event;
49+
}
50+
51+
final Long tid = processedEvents.get(type);
52+
if (tid != null && !tid.equals(currentEventTid)) {
53+
options
54+
.getLogger()
55+
.log(
56+
SentryLevel.INFO,
57+
"Event %s has been dropped due to multi-threaded deduplication",
58+
event.getEventId());
59+
HintUtils.setEventDropReason(hint, EventDropReason.MULTITHREADED_DEDUPLICATION);
60+
return null;
61+
}
62+
processedEvents.put(type, currentEventTid);
63+
return event;
64+
}
65+
}

sentry/src/main/java/io/sentry/SentryEvent.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,17 +211,20 @@ public void removeModule(final @NotNull String key) {
211211
* @return true if its crashed or false otherwise
212212
*/
213213
public boolean isCrashed() {
214+
return getUnhandledException() != null;
215+
}
216+
217+
public @Nullable SentryException getUnhandledException() {
214218
if (exception != null) {
215219
for (SentryException e : exception.getValues()) {
216220
if (e.getMechanism() != null
217221
&& e.getMechanism().isHandled() != null
218222
&& !e.getMechanism().isHandled()) {
219-
return true;
223+
return e;
220224
}
221225
}
222226
}
223-
224-
return false;
227+
return null;
225228
}
226229

227230
/**

sentry/src/main/java/io/sentry/TypeCheckHint.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ public final class TypeCheckHint {
1010
@ApiStatus.Internal
1111
public static final String SENTRY_IS_FROM_HYBRID_SDK = "sentry:isFromHybridSdk";
1212

13+
@ApiStatus.Internal
14+
public static final String SENTRY_EVENT_DROP_REASON = "sentry:eventDropReason";
15+
1316
@ApiStatus.Internal public static final String SENTRY_JAVASCRIPT_SDK_NAME = "sentry.javascript";
1417

1518
@ApiStatus.Internal public static final String SENTRY_DOTNET_SDK_NAME = "sentry.dotnet";

sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.jakewharton.nopen.annotation.Open;
44
import io.sentry.exception.ExceptionMechanismException;
55
import io.sentry.hints.BlockingFlushHint;
6+
import io.sentry.hints.EventDropReason;
67
import io.sentry.hints.SessionEnd;
78
import io.sentry.protocol.Mechanism;
89
import io.sentry.protocol.SentryId;
@@ -98,7 +99,11 @@ public void uncaughtException(Thread thread, Throwable thrown) {
9899

99100
final @NotNull SentryId sentryId = hub.captureEvent(event, hint);
100101
final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
101-
if (!isEventDropped) {
102+
final EventDropReason eventDropReason = HintUtils.getEventDropReason(hint);
103+
// in case the event has been dropped by multithreaded deduplicator, the other threads will
104+
// crash the app without a chance to persist the main event so we have to special-case this
105+
if (!isEventDropped
106+
|| EventDropReason.MULTITHREADED_DEDUPLICATION.equals(eventDropReason)) {
102107
// Block until the event is flushed to disk
103108
if (!exceptionHint.waitFlush()) {
104109
options
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.sentry.hints;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
5+
/** A reason for which an event was dropped, used for (not to confuse with ClientReports) */
6+
@ApiStatus.Internal
7+
public enum EventDropReason {
8+
MULTITHREADED_DEDUPLICATION
9+
}

sentry/src/main/java/io/sentry/util/HintUtils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static io.sentry.TypeCheckHint.SENTRY_DART_SDK_NAME;
44
import static io.sentry.TypeCheckHint.SENTRY_DOTNET_SDK_NAME;
5+
import static io.sentry.TypeCheckHint.SENTRY_EVENT_DROP_REASON;
56
import static io.sentry.TypeCheckHint.SENTRY_IS_FROM_HYBRID_SDK;
67
import static io.sentry.TypeCheckHint.SENTRY_JAVASCRIPT_SDK_NAME;
78
import static io.sentry.TypeCheckHint.SENTRY_TYPE_CHECK_HINT;
@@ -11,6 +12,7 @@
1112
import io.sentry.hints.ApplyScopeData;
1213
import io.sentry.hints.Backfillable;
1314
import io.sentry.hints.Cached;
15+
import io.sentry.hints.EventDropReason;
1416
import org.jetbrains.annotations.ApiStatus;
1517
import org.jetbrains.annotations.NotNull;
1618
import org.jetbrains.annotations.Nullable;
@@ -33,6 +35,16 @@ public static boolean isFromHybridSdk(final @NotNull Hint hint) {
3335
return Boolean.TRUE.equals(hint.getAs(SENTRY_IS_FROM_HYBRID_SDK, Boolean.class));
3436
}
3537

38+
public static void setEventDropReason(
39+
final @NotNull Hint hint, final @NotNull EventDropReason eventDropReason) {
40+
hint.set(SENTRY_EVENT_DROP_REASON, eventDropReason);
41+
}
42+
43+
@Nullable
44+
public static EventDropReason getEventDropReason(final @NotNull Hint hint) {
45+
return hint.getAs(SENTRY_EVENT_DROP_REASON, EventDropReason.class);
46+
}
47+
3648
public static Hint createWithTypeCheckHint(Object typeCheckHint) {
3749
Hint hint = new Hint();
3850
setTypeCheckHint(hint, typeCheckHint);

0 commit comments

Comments
 (0)