Skip to content

Commit add01eb

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 56fb8ca commit add01eb

5 files changed

Lines changed: 101 additions & 11 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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.sentry.SentryOptions;
2121
import io.sentry.android.core.ApplicationExitInfoHistoryDispatcher.ApplicationExitInfoPolicy;
2222
import io.sentry.android.core.cache.AndroidEnvelopeCache;
23+
import io.sentry.hints.AbnormalExit;
2324
import io.sentry.hints.Backfillable;
2425
import io.sentry.hints.BlockingFlushHint;
2526
import io.sentry.protocol.Mechanism;
@@ -71,6 +72,7 @@ public final class MemoryLimiterIntegration implements Integration, Closeable {
7172
static final @NotNull String MEMORY_LIMITER_DESCRIPTION =
7273
MEMORY_LIMITER_DESCRIPTION_PREFIX + "AnonSwap";
7374
static final @NotNull String MEMORY_LIMITER_FINGERPRINT = "memory-limiter";
75+
static final @NotNull String MEMORY_LIMITER_MECHANISM = "memory_limiter";
7476
static final @NotNull String MEMORY_LIMITER_MESSAGE_PREFIX =
7577
"Android process killed by MemoryLimiter";
7678

@@ -368,7 +370,8 @@ public void markReported(final long timestamp) {
368370
* backfilled with persisted launch state or kept as a lighter historical record.
369371
*/
370372
@ApiStatus.Internal
371-
public static final class MemoryLimiterHint extends BlockingFlushHint implements Backfillable {
373+
public static final class MemoryLimiterHint extends BlockingFlushHint
374+
implements Backfillable, AbnormalExit {
372375

373376
private final long epochTimestampMs;
374377
private final boolean shouldEnrich;
@@ -383,11 +386,21 @@ public MemoryLimiterHint(
383386
this.shouldEnrich = shouldEnrich;
384387
}
385388

386-
/** Returns epoch wall-clock time, in milliseconds. */
387-
public long timestamp() {
389+
@Override
390+
public @NotNull Long timestamp() {
388391
return epochTimestampMs;
389392
}
390393

394+
@Override
395+
public @NotNull String mechanism() {
396+
return MEMORY_LIMITER_MECHANISM;
397+
}
398+
399+
@Override
400+
public boolean ignoreCurrentThread() {
401+
return true;
402+
}
403+
391404
@Override
392405
public boolean shouldEnrich() {
393406
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
@@ -276,7 +276,10 @@ class MemoryLimiterIntegrationTest {
276276
},
277277
argThat<Hint> {
278278
val hint = HintUtils.getSentrySdkHint(this) as MemoryLimiterHint
279-
hint.shouldEnrich() && hint.timestamp() == newTimestamp
279+
hint.shouldEnrich() &&
280+
hint.timestamp() == newTimestamp &&
281+
hint.mechanism() == MemoryLimiterIntegration.MEMORY_LIMITER_MECHANISM &&
282+
!hint.ignoreCurrentThread()
280283
},
281284
)
282285
}

‎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)