Skip to content

Commit e3d925c

Browse files
committed
chore(android): Mark MemoryLimiter sessions as having exited abnormally (JAVA-687)
Mark recovered MemoryLimiter exits as abnormal session endings so release health no longer treats the terminated process as a healthy exit. Reuse the existing AbnormalExit cache repair path with a stable memory_limiter mechanism and the OS exit timestamp.
1 parent cc0773d commit e3d925c

5 files changed

Lines changed: 101 additions & 10 deletions

File tree

‎sentry-android-core/api/sentry-android-core.api‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,12 +340,14 @@ public final class io/sentry/android/core/MemoryLimiterIntegration : io/sentry/I
340340
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
341341
}
342342

343-
public final class io/sentry/android/core/MemoryLimiterIntegration$MemoryLimiterHint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/Backfillable {
343+
public final class io/sentry/android/core/MemoryLimiterIntegration$MemoryLimiterHint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/AbnormalExit, io/sentry/hints/Backfillable {
344344
public fun <init> (JLio/sentry/ILogger;JZ)V
345+
public fun ignoreCurrentThread ()Z
345346
public fun isFlushable (Lio/sentry/protocol/SentryId;)Z
347+
public fun mechanism ()Ljava/lang/String;
346348
public fun setFlushable (Lio/sentry/protocol/SentryId;)V
347349
public fun shouldEnrich ()Z
348-
public fun timestamp ()J
350+
public fun timestamp ()Ljava/lang/Long;
349351
}
350352

351353
public final class io/sentry/android/core/NativeEventCollector {

‎sentry-android-core/src/main/java/io/sentry/android/core/MemoryLimiterIntegration.java‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.sentry.SentryOptions;
2222
import io.sentry.android.core.ApplicationExitInfoHistoryDispatcher.ApplicationExitInfoPolicy;
2323
import io.sentry.android.core.cache.AndroidEnvelopeCache;
24+
import io.sentry.hints.AbnormalExit;
2425
import io.sentry.hints.Backfillable;
2526
import io.sentry.hints.BlockingFlushHint;
2627
import io.sentry.protocol.Mechanism;
@@ -76,6 +77,7 @@ public final class MemoryLimiterIntegration implements Integration, Closeable {
7677
static final @NotNull String MEMORY_LIMITER_DESCRIPTION =
7778
MEMORY_LIMITER_DESCRIPTION_PREFIX + "AnonSwap";
7879

80+
static final @NotNull String MEMORY_LIMITER_MECHANISM = "memory_limiter";
7981
static final @NotNull String MEMORY_LIMITER_MESSAGE = "Android process killed by MemoryLimiter";
8082

8183
static final @NotNull String IMPORTANCE_DATA_KEY = "importance";
@@ -318,7 +320,8 @@ public void markReported(final long timestamp) {
318320
* backfilled with persisted launch state or kept as a lighter historical record.
319321
*/
320322
@ApiStatus.Internal
321-
public static final class MemoryLimiterHint extends BlockingFlushHint implements Backfillable {
323+
public static final class MemoryLimiterHint extends BlockingFlushHint
324+
implements Backfillable, AbnormalExit {
322325

323326
private final long timestamp;
324327
private final boolean shouldEnrich;
@@ -333,10 +336,21 @@ public MemoryLimiterHint(
333336
this.shouldEnrich = shouldEnrich;
334337
}
335338

336-
public long timestamp() {
339+
@Override
340+
public @NotNull Long timestamp() {
337341
return timestamp;
338342
}
339343

344+
@Override
345+
public @NotNull String mechanism() {
346+
return MEMORY_LIMITER_MECHANISM;
347+
}
348+
349+
@Override
350+
public boolean ignoreCurrentThread() {
351+
return true;
352+
}
353+
340354
@Override
341355
public boolean shouldEnrich() {
342356
return shouldEnrich;

‎sentry-android-core/src/test/java/io/sentry/android/core/MemoryLimiterIntegrationTest.kt‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,10 @@ class MemoryLimiterIntegrationTest {
266266
},
267267
argThat<Hint> {
268268
val hint = HintUtils.getSentrySdkHint(this) as MemoryLimiterHint
269-
hint.shouldEnrich() && hint.timestamp() == newTimestamp
269+
hint.shouldEnrich() &&
270+
hint.timestamp() == newTimestamp &&
271+
hint.mechanism() == MemoryLimiterIntegration.MEMORY_LIMITER_MECHANISM &&
272+
!hint.ignoreCurrentThread()
270273
},
271274
)
272275
}

‎sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package io.sentry.android.core.cache
22

3+
import com.google.common.truth.Truth.assertThat
4+
import io.sentry.DateUtils
35
import io.sentry.ISerializer
46
import io.sentry.NoOpLogger
57
import io.sentry.SentryEnvelope
8+
import io.sentry.SentryEvent
69
import io.sentry.SentryOptions
10+
import io.sentry.SentryUUID
11+
import io.sentry.Session
12+
import io.sentry.Session.State.Abnormal
13+
import io.sentry.Session.State.Ok
714
import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint
815
import io.sentry.android.core.AnrV2Integration.AnrV2Hint
916
import io.sentry.android.core.MemoryLimiterIntegration.MemoryLimiterHint
@@ -14,6 +21,7 @@ import io.sentry.transport.ICurrentDateProvider
1421
import io.sentry.util.HintUtils
1522
import java.io.File
1623
import java.lang.IllegalArgumentException
24+
import java.util.Date
1725
import kotlin.test.BeforeTest
1826
import kotlin.test.Test
1927
import kotlin.test.assertEquals
@@ -227,6 +235,51 @@ class AndroidEnvelopeCacheTest {
227235
assertEquals("23456789", fixture.lastReportedMemoryLimiterFile.readText())
228236
}
229237

238+
@Test
239+
fun `memory limiter hint marks previous session abnormal at exit timestamp`() {
240+
val cache = fixture.getSut(tmpDir)
241+
val sessionStart = DateUtils.getCurrentDateTime()
242+
val exitTimestamp = sessionStart.time + 1_000
243+
val previousSessionFile = EnvelopeCache.getPreviousSessionFile(fixture.options.cacheDirPath!!)
244+
fixture.options.serializer.serialize(createSession(sessionStart), previousSessionFile.writer())
245+
val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null)
246+
247+
cache.storeEnvelope(
248+
envelope,
249+
HintUtils.createWithTypeCheckHint(
250+
MemoryLimiterHint(0, NoOpLogger.getInstance(), exitTimestamp, true)
251+
),
252+
)
253+
254+
val updatedSession =
255+
fixture.options.serializer.deserialize(previousSessionFile.reader(), Session::class.java)!!
256+
assertThat(updatedSession.status).isEqualTo(Abnormal)
257+
assertThat(updatedSession.timestamp!!.time).isEqualTo(exitTimestamp)
258+
assertThat(updatedSession.abnormalMechanism).isEqualTo("memory_limiter")
259+
}
260+
261+
// Protects against misbehaved clocks, stale cached state, or mismatched recovery data.
262+
@Test
263+
fun `memory limiter exit before previous session start does not mark session abnormal`() {
264+
val cache = fixture.getSut(tmpDir)
265+
val sessionStart = DateUtils.getCurrentDateTime()
266+
val previousSessionFile = EnvelopeCache.getPreviousSessionFile(fixture.options.cacheDirPath!!)
267+
fixture.options.serializer.serialize(createSession(sessionStart), previousSessionFile.writer())
268+
val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null)
269+
270+
cache.storeEnvelope(
271+
envelope,
272+
HintUtils.createWithTypeCheckHint(
273+
MemoryLimiterHint(0, NoOpLogger.getInstance(), sessionStart.time - 1_000, true)
274+
),
275+
)
276+
277+
val updatedSession =
278+
fixture.options.serializer.deserialize(previousSessionFile.reader(), Session::class.java)!!
279+
assertThat(updatedSession.status).isEqualTo(Ok)
280+
assertThat(updatedSession.abnormalMechanism).isNull()
281+
}
282+
230283
@Test
231284
fun `memory limiter and anr markers are stored independently`() {
232285
val cache = fixture.getSut(tmpDir)
@@ -260,5 +313,23 @@ class AndroidEnvelopeCacheTest {
260313
assertFalse(didStore)
261314
}
262315

316+
private fun createSession(started: Date): Session =
317+
Session(
318+
Ok,
319+
started,
320+
started,
321+
0,
322+
"distinct-id",
323+
SentryUUID.generateSentryId(),
324+
true,
325+
null,
326+
null,
327+
null,
328+
null,
329+
"environment",
330+
"release",
331+
null,
332+
)
333+
263334
internal class UncaughtHint : UncaughtExceptionHint(0, NoOpLogger.getInstance())
264335
}

‎sentry/src/main/java/io/sentry/hints/AbnormalExit.java‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
/**
66
* Marker interface for Sessions experiencing abnormal status.
77
*
8-
* <p><b>Note:</b> While this interface applies to the broad category of abnormal exits (meaning any
9-
* exits that weren't classified as normal terminations or crashes) it currently is exclusively used
10-
* as a hint marker for Android ANRs (both watchdog and ApplicationExitInfo based). If additional
11-
* categories of abnormal exits were introduced, all instances of discriminator code (`instanceof
12-
* AbnormalExit`) should be carefully reviewed for ANR specifics accidentally being applied.
8+
* <p>This includes exits that were not classified as normal terminations or crashes, such as
9+
* Android ANRs and MemoryLimiter process deaths.
10+
*
11+
* <p><b>Note:</b> Some existing discriminator code (`instanceof AbnormalExit`) is shaped by the
12+
* historical ANR-only usage of this interface. New implementations should review all of those call
13+
* sites carefully to ensure ANR-specific behavior isn't applied accidentally.
1314
*/
1415
public interface AbnormalExit {
1516

0 commit comments

Comments
 (0)