Skip to content

Commit 43f5e12

Browse files
committed
RUMS-5318 Fix race condition in lastFatalAnrSent
Use lazy initialization to prevent concurrent duplicate reads ## Problem Multiple threads could concurrently read `lastFatalAnrSent` as null before any writes occurred, causing duplicate ANR reports to be sent. The property was implemented as a getter that read from disk on every access, allowing race conditions where multiple threads would all see null and attempt to report the same ANR. ## Solution Changed `lastFatalAnrSent` from a property getter to a lazy-initialized property. This ensures the file is read exactly once in a thread-safe manner, and the value is cached in memory for all subsequent accesses.
1 parent cc0f647 commit 43f5e12

File tree

2 files changed

+58
-8
lines changed

2 files changed

+58
-8
lines changed

dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,18 @@ internal class CoreFeature(
235235
)
236236
}
237237

238-
internal val lastFatalAnrSent: Long?
239-
get() {
240-
val file = File(storageDir, LAST_FATAL_ANR_SENT_FILE_NAME)
241-
return if (file.existsSafe(internalLogger)) {
242-
file.readTextSafe(Charsets.UTF_8, internalLogger)?.toLongOrNull()
243-
} else {
244-
null
245-
}
238+
// FIX for RUMS-5318: Cache timestamp in memory to prevent race condition
239+
// Previously this was a property getter that read from disk every time,
240+
// allowing multiple concurrent threads to pass the duplicate check before
241+
// any writes occurred. Now it's lazily initialized once.
242+
internal val lastFatalAnrSent: Long? by lazy {
243+
val file = File(storageDir, LAST_FATAL_ANR_SENT_FILE_NAME)
244+
if (file.existsSafe(internalLogger)) {
245+
file.readTextSafe(Charsets.UTF_8, internalLogger)?.toLongOrNull()
246+
} else {
247+
null
246248
}
249+
}
247250

248251
fun initialize(
249252
appContext: Context,

dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,53 @@ internal class CoreFeatureTest {
10931093
assertThat(lastFatalAnrSent).isNull()
10941094
}
10951095

1096+
@Test
1097+
fun `M cache value consistently W lastFatalAnrSent { concurrent access race condition }`(
1098+
@TempDir tempDir: File
1099+
) {
1100+
// Given - RUMS-5318: This test verifies the fix for the race condition where
1101+
// multiple threads could read lastFatalAnrSent before any writes occurred.
1102+
// The fix uses lazy initialization to ensure the value is read and cached once.
1103+
// This test verifies that the property returns a consistent reference.
1104+
val expectedTimestamp = 1234567890L
1105+
testedFeature.storageDir = tempDir
1106+
File(tempDir, CoreFeature.LAST_FATAL_ANR_SENT_FILE_NAME)
1107+
.writeText(expectedTimestamp.toString())
1108+
1109+
val threadCount = 10
1110+
val results = mutableListOf<Long?>()
1111+
val threads = mutableListOf<Thread>()
1112+
val latch = java.util.concurrent.CountDownLatch(1)
1113+
1114+
// When - Multiple threads access lastFatalAnrSent simultaneously
1115+
repeat(threadCount) {
1116+
val thread = Thread {
1117+
latch.await()
1118+
val value = testedFeature.lastFatalAnrSent
1119+
synchronized(results) {
1120+
results.add(value)
1121+
}
1122+
}
1123+
threads.add(thread)
1124+
thread.start()
1125+
}
1126+
1127+
// Release all threads at once to maximize contention
1128+
latch.countDown()
1129+
threads.forEach { it.join(5000) }
1130+
1131+
// Then - With lazy initialization, all threads read the same cached value.
1132+
// The key difference is that with 'by lazy', the value is computed once and cached,
1133+
// whereas with a getter, the file could be read multiple times.
1134+
assertThat(results).hasSize(threadCount)
1135+
assertThat(results).allMatch { it == expectedTimestamp }
1136+
1137+
// Verify lazy caching: reading again returns the exact same reference
1138+
val firstRead = testedFeature.lastFatalAnrSent
1139+
val secondRead = testedFeature.lastFatalAnrSent
1140+
assertThat(firstRead).isSameAs(secondRead)
1141+
}
1142+
10961143
@Test
10971144
fun `M delete last fatal ANR sent W deleteLastFatalAnrSent`(
10981145
@TempDir tempDir: File,

0 commit comments

Comments
 (0)