From d06ba4d04f31e656aa6b41d10b9fa4cc47c36197 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:13:05 +0000 Subject: [PATCH 1/5] Initial plan From 9e4680e7acc863e3135c8c5cd2367ea17bf23b97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:17:53 +0000 Subject: [PATCH 2/5] Add thread-safety to initPageSize() using double-checked locking Co-authored-by: anod <171704+anod@users.noreply.github.com> --- .../applications/events/OfflineRoom.java | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) 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..388388f8f 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,7 +89,7 @@ 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; public OfflineRoom(Context context, String name) { @@ -290,22 +290,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 (this) { + 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); } } } From b28b8fb1b12d9a973a802b264ac2cbfcb0e0d9cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:24:42 +0000 Subject: [PATCH 3/5] Use static lock object for thread-safety in initPageSize() Co-authored-by: anod <171704+anod@users.noreply.github.com> --- .../java/com/microsoft/applications/events/OfflineRoom.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 388388f8f..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 @@ -91,6 +91,7 @@ public Long call() private StorageSettingDao m_settingDao; 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,7 +291,7 @@ public void deleteSetting(String name) { private void initPageSize() { if (m_pageSize == -1) { - synchronized (this) { + synchronized (PAGE_SIZE_LOCK) { if (m_pageSize == -1) { try { try (Cursor c = m_db.query("PRAGMA page_size", null)) { From b140ab0d5f7f4a0d0a5488e667bac601830f7f98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:30:39 +0000 Subject: [PATCH 4/5] Add concurrent test for thread-safe page size initialization Co-authored-by: anod <171704+anod@users.noreply.github.com> --- .../maesdktest/OfflineRoomUnitTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) 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..f53f99531 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,17 @@ 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.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 +191,85 @@ 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); + + // Create multiple threads that will call loadPageSize() and totalSize() concurrently + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + new Thread(() -> { + 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(); + } + + // Start all threads simultaneously + startLatch.countDown(); + + // Wait for all threads to complete + doneLatch.await(); + + // 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(); + } + } } From 1a28a950f82dd71f464d7ed6b569a6929cf5a92d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:26:20 +0000 Subject: [PATCH 5/5] Use ExecutorService instead of raw threads in concurrent test Co-authored-by: anod <171704+anod@users.noreply.github.com> --- .../maesdktest/OfflineRoomUnitTest.java | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) 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 f53f99531..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 @@ -20,6 +20,9 @@ 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.*; @@ -216,37 +219,46 @@ public void ConcurrentPageSizeInitialization() throws InterruptedException { final List totalSizes = new ArrayList<>(); final AtomicInteger errorCount = new AtomicInteger(0); - // Create multiple threads that will call loadPageSize() and totalSize() concurrently - for (int i = 0; i < threadCount; i++) { - final int threadId = i; - new Thread(() -> { - 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); + // 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(); } - } catch (Exception e) { - errorCount.incrementAndGet(); - e.printStackTrace(); - } finally { - doneLatch.countDown(); - } - }).start(); + }); + } + + // 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(); + } } - // Start all threads simultaneously - startLatch.countDown(); - - // Wait for all threads to complete - doneLatch.await(); - // Verify no errors occurred assertEquals("No errors should occur during concurrent access", 0, errorCount.get());