diff --git a/lib/android_build/app/src/androidTest/java/com/microsoft/applications/events/maesdktest/OfflineRoomUnitTest.java b/lib/android_build/app/src/androidTest/java/com/microsoft/applications/events/maesdktest/OfflineRoomUnitTest.java index e83507dcf..3799a94e5 100644 --- a/lib/android_build/app/src/androidTest/java/com/microsoft/applications/events/maesdktest/OfflineRoomUnitTest.java +++ b/lib/android_build/app/src/androidTest/java/com/microsoft/applications/events/maesdktest/OfflineRoomUnitTest.java @@ -17,11 +17,20 @@ import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; @RunWith(AndroidJUnit4.class) public class OfflineRoomUnitTest { @@ -185,5 +194,94 @@ public void RetireRetries() { } } } + + @Test + public void ConcurrentPageSizeInitialization() throws InterruptedException { + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + try (OfflineRoom room = new OfflineRoom(appContext, "OfflineRoomConcurrent")) { + room.deleteAllRecords(); + + // Add a record to ensure the database is not empty + StorageRecord record = new StorageRecord( + 0, "Test", + StorageRecord.EventLatency_Normal, + StorageRecord.EventPersistence_Normal, + 32, + 1, + 0, + new byte[]{1, 2, 3}); + room.storeRecords(record); + + final int threadCount = 10; + final CountDownLatch startLatch = new CountDownLatch(1); + final CountDownLatch doneLatch = new CountDownLatch(threadCount); + final List pageSizes = new ArrayList<>(); + final List totalSizes = new ArrayList<>(); + final AtomicInteger errorCount = new AtomicInteger(0); + + // Use ExecutorService for proper thread management + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + try { + // Submit tasks that will call loadPageSize() and totalSize() concurrently + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + // Wait for all threads to be ready + startLatch.await(); + + // Call both methods to test concurrent initialization + long pageSize = room.loadPageSize(); + long totalSize = room.totalSize(); + + synchronized (pageSizes) { + pageSizes.add(pageSize); + totalSizes.add(totalSize); + } + } catch (Exception e) { + errorCount.incrementAndGet(); + e.printStackTrace(); + } finally { + doneLatch.countDown(); + } + }); + } + + // Start all threads simultaneously + startLatch.countDown(); + + // Wait for all threads to complete + doneLatch.await(); + } finally { + // Ensure executor is properly shut down + executor.shutdown(); + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } + + // Verify no errors occurred + assertEquals("No errors should occur during concurrent access", 0, errorCount.get()); + + // Verify all threads got results + assertEquals("All threads should complete", threadCount, pageSizes.size()); + assertEquals("All threads should complete", threadCount, totalSizes.size()); + + // Verify all threads got the same page size (no race condition) + long firstPageSize = pageSizes.get(0); + assertThat("Page size should be valid", firstPageSize, greaterThan(0L)); + assertThat("Page size should be a power of 2", firstPageSize & (firstPageSize - 1), is(0L)); + + for (long pageSize : pageSizes) { + assertEquals("All threads should get the same page size", firstPageSize, pageSize); + } + + // Verify all threads got a valid total size + for (long totalSize : totalSizes) { + assertThat("Total size should be valid", totalSize, greaterThan(0L)); + } + + room.deleteAllRecords(); + } + } } diff --git a/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/OfflineRoom.java b/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/OfflineRoom.java index 7f700c0b0..f4cce079a 100644 --- a/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/OfflineRoom.java +++ b/lib/android_build/maesdk/src/main/java/com/microsoft/applications/events/OfflineRoom.java @@ -89,8 +89,9 @@ public Long call() private OfflineRoomDatabase m_db; private StorageRecordDao m_srDao; private StorageSettingDao m_settingDao; - private long m_pageSize = -1; + private volatile long m_pageSize = -1; private static final long PAGE_SIZE_DEFAULT = 4096; + private static final Object PAGE_SIZE_LOCK = new Object(); public OfflineRoom(Context context, String name) { RoomDatabase.Builder builder; @@ -290,22 +291,26 @@ public void deleteSetting(String name) { private void initPageSize() { if (m_pageSize == -1) { - try { - try (Cursor c = m_db.query("PRAGMA page_size", null)) { - if (c.getCount() == 1 && c.getColumnCount() == 1) { - c.moveToFirst(); - m_pageSize = c.getLong(0); - } else { + synchronized (PAGE_SIZE_LOCK) { + if (m_pageSize == -1) { + try { + try (Cursor c = m_db.query("PRAGMA page_size", null)) { + if (c.getCount() == 1 && c.getColumnCount() == 1) { + c.moveToFirst(); + m_pageSize = c.getLong(0); + } else { + m_pageSize = PAGE_SIZE_DEFAULT; + Log.e("MAE", + String.format("Unexpected result from PRAGMA page_size: %d rows, %d columns", + c.getCount(), + c.getColumnCount())); + } + } + } catch (Exception e) { m_pageSize = PAGE_SIZE_DEFAULT; - Log.e("MAE", - String.format("Unexpected result from PRAGMA page_size: %d rows, %d columns", - c.getCount(), - c.getColumnCount())); + Log.e("MAE", "Failed to query PRAGMA page_size, using default page size.", e); } } - } catch (Exception e) { - m_pageSize = PAGE_SIZE_DEFAULT; - Log.e("MAE", "Failed to query PRAGMA page_size, using default page size.", e); } } }