From e44695145d2d8802f9050c57f7e0d42d07a69e8b Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Mon, 28 Apr 2025 21:04:09 +0200 Subject: [PATCH 01/16] Make int fields atomic --- .../services/BacktraceDatabaseContext.java | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java index 4424ab9c..e37940dd 100644 --- a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java +++ b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java @@ -8,6 +8,8 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import backtraceio.library.enums.database.RetryOrder; import backtraceio.library.interfaces.DatabaseContext; @@ -30,7 +32,6 @@ public class BacktraceDatabaseContext implements DatabaseContext { */ private final int _retryNumber; - /** * Database cache */ @@ -39,12 +40,12 @@ public class BacktraceDatabaseContext implements DatabaseContext { /** * Total database size on hard drive */ - private long totalSize = 0; + private final AtomicLong totalSize = new AtomicLong(0); /** * Total records in BacktraceDatabase */ - private int totalRecords = 0; + private final AtomicInteger totalRecords = new AtomicInteger(0); /** * Record order @@ -135,9 +136,9 @@ public BacktraceDatabaseRecord add(BacktraceDatabaseRecord backtraceDatabaseReco throw new NullPointerException("BacktraceDatabaseRecord"); } backtraceDatabaseRecord.locked = true; - this.totalSize += backtraceDatabaseRecord.getSize(); + this.totalSize.addAndGet(backtraceDatabaseRecord.getSize()); this.addToFirstBatch(backtraceDatabaseRecord); - this.totalRecords++; + this.totalRecords.incrementAndGet(); return backtraceDatabaseRecord; } @@ -202,8 +203,8 @@ public boolean delete(BacktraceDatabaseRecord record) { databaseRecord.delete(); try { iterator.remove(); - this.totalRecords--; - this.totalSize -= databaseRecord.getSize(); + this.totalRecords.decrementAndGet(); + this.totalSize.addAndGet(-databaseRecord.getSize()); return true; } catch (Exception e) { BacktraceLogger.d(LOG_TAG, "Exception on removing record " @@ -242,7 +243,7 @@ public boolean contains(BacktraceDatabaseRecord record) { * @return is database empty */ public boolean isEmpty() { - return totalRecords == 0; + return totalRecords.get() == 0; } /** @@ -251,7 +252,7 @@ public boolean isEmpty() { * @return number of records in database */ public int count() { - return totalRecords; + return totalRecords.get(); } /** @@ -267,32 +268,15 @@ public void clear() { } } - this.totalRecords = 0; - this.totalSize = 0; + this.totalRecords.set(0); + this.totalSize.set(0); for (Map.Entry> entry : this.batchRetry.entrySet()) { entry.getValue().clear(); } } - /** - * Increment retry time for current record - */ - public void incrementBatchRetry() { - removeMaxRetries(); - incrementBatches(); - } - - /** - * Get database size - * - * @return database size - */ - public long getDatabaseSize() { - return this.totalSize; - } - - /** + /** * Delete the oldest file * * @return is deletion was successful @@ -330,11 +314,18 @@ private void removeMaxRetries() { continue; } record.delete(); - this.totalRecords--; - totalSize -= record.getSize(); + this.totalRecords.decrementAndGet(); + totalSize.addAndGet(-record.getSize()); } } + /** + * Increment retry time for current record + */ + public void incrementBatchRetry() { + removeMaxRetries(); + incrementBatches(); + } /** * Get first record in in-cache BacktraceDatabase From 73da3ed73f24bc7498922be1864ef3f72fa91022 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Mon, 28 Apr 2025 21:47:29 +0200 Subject: [PATCH 02/16] Replace ArrayList with ConcurrentLinkedQueue --- .../services/BacktraceDatabaseContext.java | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java index e37940dd..c2fe4d06 100644 --- a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java +++ b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java @@ -4,10 +4,12 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -35,7 +37,7 @@ public class BacktraceDatabaseContext implements DatabaseContext { /** * Database cache */ - private final Map> batchRetry = new HashMap<>(); + private final Map> batchRetry = new ConcurrentHashMap<>(); /** * Total database size on hard drive @@ -99,7 +101,7 @@ private void setupBatch() { } for (int i = 0; i < _retryNumber; i++) { - this.batchRetry.put(i, new ArrayList<>()); + this.batchRetry.put(i, new ConcurrentLinkedQueue<>()); } } @@ -170,12 +172,22 @@ public BacktraceDatabaseRecord last() { */ public Iterable get() { List allRecords = new ArrayList<>(); - for (Map.Entry> entry : batchRetry.entrySet()) { + for (Map.Entry> entry : batchRetry.entrySet()) { allRecords.addAll(entry.getValue()); } return allRecords; } + /** + * Get database size + * + * @return database size + */ + public long getDatabaseSize() { + return this.totalSize.get(); + } + + /** * Delete existing record from database * @@ -187,7 +199,7 @@ public boolean delete(BacktraceDatabaseRecord record) { } for (int key : batchRetry.keySet()) { - List records = batchRetry.get(key); + Queue records = batchRetry.get(key); if (records == null) { continue; @@ -225,8 +237,8 @@ public boolean contains(BacktraceDatabaseRecord record) { if (record == null) { throw new NullPointerException("BacktraceDatabaseRecord"); } - for (Map.Entry> entry : this.batchRetry.entrySet()) { - List records = entry.getValue(); + for (Map.Entry> entry : this.batchRetry.entrySet()) { + Queue records = entry.getValue(); for (BacktraceDatabaseRecord databaseRecord : records) { if (databaseRecord != null && databaseRecord.id == record.id) { @@ -260,8 +272,8 @@ public int count() { */ public void clear() { BacktraceLogger.d(LOG_TAG, "Deleting all records from database context"); - for (Map.Entry> entry : this.batchRetry.entrySet()) { - List records = entry.getValue(); + for (Map.Entry> entry : this.batchRetry.entrySet()) { + Queue records = entry.getValue(); for (BacktraceDatabaseRecord databaseRecord : records) { databaseRecord.delete(); @@ -271,7 +283,7 @@ public void clear() { this.totalRecords.set(0); this.totalSize.set(0); - for (Map.Entry> entry : this.batchRetry.entrySet()) { + for (Map.Entry> entry : this.batchRetry.entrySet()) { entry.getValue().clear(); } } @@ -297,8 +309,8 @@ public boolean removeOldestRecord() { */ private void incrementBatches() { for (int i = this._retryNumber - 2; i >= 0; i--) { - List currentBatch = this.batchRetry.get(i); - batchRetry.put(i, new ArrayList<>()); + Queue currentBatch = this.batchRetry.get(i); + batchRetry.put(i, new ConcurrentLinkedQueue<>()); batchRetry.put(i + 1, currentBatch); } } @@ -307,7 +319,7 @@ private void incrementBatches() { * Remove last batch */ private void removeMaxRetries() { - List currentBatch = this.batchRetry.get(_retryNumber - 1); + Queue currentBatch = this.batchRetry.get(_retryNumber - 1); for (BacktraceDatabaseRecord record : currentBatch) { if (!record.valid()) { @@ -353,17 +365,18 @@ private BacktraceDatabaseRecord getLastRecord() { */ private BacktraceDatabaseRecord getRecordFromCache(boolean reverse) { for (int i = _retryNumber - 1; i >= 0; i--) { - List reverseRecords = batchRetry.get(i); + Queue reverseRecords = batchRetry.get(i); if (reverseRecords == null) { continue; } + List tempList = new ArrayList<>(reverseRecords); if (reverse) { - Collections.reverse(reverseRecords); + Collections.reverse(tempList); } - for (BacktraceDatabaseRecord record : reverseRecords) { + for (BacktraceDatabaseRecord record : tempList) { if (record != null && !record.locked) { record.locked = true; return record; @@ -377,13 +390,13 @@ private void addToFirstBatch(BacktraceDatabaseRecord backtraceDatabaseRecord) { final int firstBatch = 0; if (this.batchRetry.isEmpty()) { - this.batchRetry.put(firstBatch, new ArrayList<>()); + this.batchRetry.put(firstBatch, new ConcurrentLinkedQueue<>()); } - List batch = this.batchRetry.get(firstBatch); + Queue batch = this.batchRetry.get(firstBatch); if (batch == null) { - batch = new ArrayList<>(); + batch = new ConcurrentLinkedQueue<>(); this.batchRetry.put(firstBatch, batch); } From a5cdc8580380a55e75b08ae70564ab84202a5823 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Tue, 29 Apr 2025 17:21:48 +0200 Subject: [PATCH 03/16] Add Multithreaded tests --- ...traceDatabaseContextMultithreadedTest.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java new file mode 100644 index 00000000..f005f59d --- /dev/null +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -0,0 +1,145 @@ +package backtraceio.library.database; + +import static org.junit.Assert.assertTrue; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import backtraceio.library.models.BacktraceData; +import backtraceio.library.models.database.BacktraceDatabaseRecord; +import backtraceio.library.models.database.BacktraceDatabaseSettings; +import backtraceio.library.models.json.BacktraceReport; +import backtraceio.library.services.BacktraceDatabaseContext; + +public class BacktraceDatabaseContextMultithreadedTest { + private BacktraceDatabaseContext databaseContext; + + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + @Before + public void setUp() { + BacktraceDatabaseSettings settings = new BacktraceDatabaseSettings("test-path"); + settings.setRetryLimit(3); + databaseContext = new BacktraceDatabaseContext(settings); + } + + @Test + public void testConcurrentModification() throws InterruptedException { + // First populate the database with some records + int recordCount = 1000; + List records = new ArrayList<>(); + + for (int i = 0; i < recordCount; i++) { + BacktraceData data = createMockBacktraceData(); + BacktraceDatabaseRecord record = databaseContext.add(data); + records.add(record); + } + + // Create a latch to synchronize threads + CountDownLatch latch = new CountDownLatch(1); + List caughtExceptions = new ArrayList<>(); + + List deleted = new ArrayList<>(); + // Thread 1: Continuously deleting records + Thread deleteThread = new Thread(() -> { + try { + latch.await(); // Wait for signal + for (int i = 0; i < recordCount; i++) { + databaseContext.delete(records.get(i)); + deleted.add(1); + } +// for (BacktraceDatabaseRecord record : databaseContext.get()) { +// databaseContext.delete(record); +// } + } catch (Exception e) { + synchronized (caughtExceptions) { + caughtExceptions.add(e); + } + } + }); + + List added = new ArrayList<>(); + // Thread 2: Continuously adding new records + Thread addThread = new Thread(() -> { + try { + latch.await(); // Wait for signal + for (int i = 0; i < recordCount; i++) { + BacktraceData data = createMockBacktraceData(); + databaseContext.add(data); + added.add(1); + } + } catch (Exception e) { + synchronized (caughtExceptions) { + caughtExceptions.add(e); + } + } + }); + + // Thread 3: Continuously getting records + Thread getThread = new Thread(() -> { + try { + latch.await(); // Wait for signal + for (int i = 0; i < recordCount; i++) { + for (BacktraceDatabaseRecord record : databaseContext.get()) { + // Force iteration + record.toString(); + } + } + } catch (Exception e) { + synchronized (caughtExceptions) { + caughtExceptions.add(e); + } + } + }); + + // Start all threads + deleteThread.start(); + addThread.start(); + getThread.start(); + + // Release all threads simultaneously + latch.countDown(); + + // Wait for threads to complete + deleteThread.join(5000); + addThread.join(5000); + getThread.join(5000); + + // Print all caught exceptions + for (Exception e : caughtExceptions) { + e.printStackTrace(); + } + + // Assert that we caught a ConcurrentModificationException + assertTrue("Expected ConcurrentModificationException", + caughtExceptions.stream() + .anyMatch(e -> e instanceof ConcurrentModificationException || + (e.getCause() != null && e.getCause() instanceof ConcurrentModificationException))); + } + + private BacktraceData createMockBacktraceData() { + // Create a mock exception for the test + Exception testException = new Exception("Test exception"); + + // Create attributes map if needed + Map attributes = new HashMap<>(); + attributes.put("test_attribute", "test_value"); + + // Create BacktraceData with the exception + return new BacktraceData( + context, + new BacktraceReport(testException), + attributes + ); + } +} From a58a09cd3490e52eeedb0c3d75ca11cac1f9d055 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Tue, 29 Apr 2025 22:09:20 +0200 Subject: [PATCH 04/16] Improve unit-tests --- ...traceDatabaseContextMultithreadedTest.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index f005f59d..87a870b4 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -1,6 +1,6 @@ package backtraceio.library.database; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; import android.content.Context; @@ -10,7 +10,6 @@ import org.junit.Test; import java.util.ArrayList; -import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,13 +49,16 @@ public void testConcurrentModification() throws InterruptedException { List caughtExceptions = new ArrayList<>(); List deleted = new ArrayList<>(); + // Thread 1: Continuously deleting records + int recordsToDelete = 750; Thread deleteThread = new Thread(() -> { try { latch.await(); // Wait for signal - for (int i = 0; i < recordCount; i++) { + for (int i = 0; i < recordsToDelete; i++) { databaseContext.delete(records.get(i)); deleted.add(1); +// Thread.sleep(20); } // for (BacktraceDatabaseRecord record : databaseContext.get()) { // databaseContext.delete(record); @@ -68,15 +70,17 @@ public void testConcurrentModification() throws InterruptedException { } }); + int recordsToAdd = 500; List added = new ArrayList<>(); // Thread 2: Continuously adding new records Thread addThread = new Thread(() -> { try { latch.await(); // Wait for signal - for (int i = 0; i < recordCount; i++) { + for (int i = 0; i < recordsToAdd; i++) { BacktraceData data = createMockBacktraceData(); databaseContext.add(data); added.add(1); +// Thread.sleep(5); } } catch (Exception e) { synchronized (caughtExceptions) { @@ -89,10 +93,10 @@ public void testConcurrentModification() throws InterruptedException { Thread getThread = new Thread(() -> { try { latch.await(); // Wait for signal - for (int i = 0; i < recordCount; i++) { + String result; + while(true) { for (BacktraceDatabaseRecord record : databaseContext.get()) { - // Force iteration - record.toString(); + result = record.toString(); } } } catch (Exception e) { @@ -120,11 +124,13 @@ public void testConcurrentModification() throws InterruptedException { e.printStackTrace(); } + assertEquals(0, caughtExceptions.size()); + assertEquals(750, recordCount + added.size() - deleted.size()); // Assert that we caught a ConcurrentModificationException - assertTrue("Expected ConcurrentModificationException", - caughtExceptions.stream() - .anyMatch(e -> e instanceof ConcurrentModificationException || - (e.getCause() != null && e.getCause() instanceof ConcurrentModificationException))); +// assertTrue("Expected ConcurrentModificationException", +// caughtExceptions.stream() +// .anyMatch(e -> e instanceof ConcurrentModificationException || +// (e.getCause() != null && e.getCause() instanceof ConcurrentModificationException))); } private BacktraceData createMockBacktraceData() { From d2321c257704501b4e29b253ad861b00deec0df7 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Sun, 4 May 2025 21:40:58 +0200 Subject: [PATCH 05/16] Refactor unit-test --- ...traceDatabaseContextMultithreadedTest.java | 106 ++++++++---------- 1 file changed, 46 insertions(+), 60 deletions(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index 87a870b4..77560007 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -2,9 +2,7 @@ import static org.junit.Assert.assertEquals; -import android.content.Context; - -import androidx.test.platform.app.InstrumentationRegistry; +import androidx.annotation.NonNull; import org.junit.Before; import org.junit.Test; @@ -24,7 +22,6 @@ public class BacktraceDatabaseContextMultithreadedTest { private BacktraceDatabaseContext databaseContext; - Context context = InstrumentationRegistry.getInstrumentation().getContext(); @Before public void setUp() { BacktraceDatabaseSettings settings = new BacktraceDatabaseSettings("test-path"); @@ -34,35 +31,27 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { - // First populate the database with some records - int recordCount = 1000; - List records = new ArrayList<>(); - - for (int i = 0; i < recordCount; i++) { - BacktraceData data = createMockBacktraceData(); - BacktraceDatabaseRecord record = databaseContext.add(data); - records.add(record); - } + // GIVEN + final int recordsState = 1000; + final int recordsToAdd = 500; + final int recordsToDelete = 750; + final int threadWaitTimeMs = 5000; + final List records = generateMockRecords(recordsState); - // Create a latch to synchronize threads - CountDownLatch latch = new CountDownLatch(1); - List caughtExceptions = new ArrayList<>(); + final CountDownLatch latch = new CountDownLatch(1); - List deleted = new ArrayList<>(); + final List caughtExceptions = new ArrayList<>(); + final List deletedRecords = new ArrayList<>(); + final List addedRecords = new ArrayList<>(); - // Thread 1: Continuously deleting records - int recordsToDelete = 750; - Thread deleteThread = new Thread(() -> { + // GIVEN threads + final Thread deleteThread = new Thread(() -> { try { - latch.await(); // Wait for signal + latch.await(); for (int i = 0; i < recordsToDelete; i++) { databaseContext.delete(records.get(i)); - deleted.add(1); -// Thread.sleep(20); + deletedRecords.add(1); } -// for (BacktraceDatabaseRecord record : databaseContext.get()) { -// databaseContext.delete(record); -// } } catch (Exception e) { synchronized (caughtExceptions) { caughtExceptions.add(e); @@ -70,17 +59,13 @@ public void testConcurrentModification() throws InterruptedException { } }); - int recordsToAdd = 500; - List added = new ArrayList<>(); - // Thread 2: Continuously adding new records - Thread addThread = new Thread(() -> { + final Thread addThread = new Thread(() -> { try { - latch.await(); // Wait for signal + latch.await(); for (int i = 0; i < recordsToAdd; i++) { BacktraceData data = createMockBacktraceData(); databaseContext.add(data); - added.add(1); -// Thread.sleep(5); + addedRecords.add(1); } } catch (Exception e) { synchronized (caughtExceptions) { @@ -89,12 +74,11 @@ public void testConcurrentModification() throws InterruptedException { } }); - // Thread 3: Continuously getting records - Thread getThread = new Thread(() -> { + final Thread readThread = new Thread(() -> { try { - latch.await(); // Wait for signal + latch.await(); String result; - while(true) { + while (true) { for (BacktraceDatabaseRecord record : databaseContext.get()) { result = record.toString(); } @@ -106,46 +90,48 @@ public void testConcurrentModification() throws InterruptedException { } }); + // WHEN // Start all threads deleteThread.start(); addThread.start(); - getThread.start(); + readThread.start(); // Release all threads simultaneously latch.countDown(); // Wait for threads to complete - deleteThread.join(5000); - addThread.join(5000); - getThread.join(5000); + deleteThread.join(threadWaitTimeMs); + addThread.join(threadWaitTimeMs); + readThread.join(threadWaitTimeMs); // Print all caught exceptions for (Exception e : caughtExceptions) { e.printStackTrace(); } + // THEN assertEquals(0, caughtExceptions.size()); - assertEquals(750, recordCount + added.size() - deleted.size()); - // Assert that we caught a ConcurrentModificationException -// assertTrue("Expected ConcurrentModificationException", -// caughtExceptions.stream() -// .anyMatch(e -> e instanceof ConcurrentModificationException || -// (e.getCause() != null && e.getCause() instanceof ConcurrentModificationException))); + assertEquals(recordsState + recordsToAdd - recordsToDelete, recordsState + addedRecords.size() - deletedRecords.size()); + } + + @NonNull + private List generateMockRecords(int recordCount) { + final List records = new ArrayList<>(); + for (int i = 0; i < recordCount; i++) { + BacktraceData data = createMockBacktraceData(); + BacktraceDatabaseRecord record = databaseContext.add(data); + records.add(record); + } + return records; } private BacktraceData createMockBacktraceData() { - // Create a mock exception for the test - Exception testException = new Exception("Test exception"); - - // Create attributes map if needed - Map attributes = new HashMap<>(); - attributes.put("test_attribute", "test_value"); - - // Create BacktraceData with the exception - return new BacktraceData( - context, - new BacktraceReport(testException), - attributes - ); + final Exception testException = new Exception("Test exception"); + + final Map attributes = new HashMap() {{ + put("test_attribute", "test_value"); + }}; + + return new BacktraceData.Builder(new BacktraceReport(testException, attributes)).build(); } } From cf4f358b1a8a94f852d68e75f7d394835647ba0f Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Mon, 5 May 2025 16:51:47 +0200 Subject: [PATCH 06/16] Refactor unit-tests to use state instead of list of variables --- ...traceDatabaseContextMultithreadedTest.java | 125 ++++++++++++------ 1 file changed, 81 insertions(+), 44 deletions(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index 77560007..5f5222cc 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -20,6 +20,35 @@ import backtraceio.library.services.BacktraceDatabaseContext; public class BacktraceDatabaseContextMultithreadedTest { + private static class TestConfig { + final int recordsState; + final int recordsToAdd; + final int recordsToDelete; + final int threadWaitTimeMs; + + TestConfig(int recordsState, int recordsToAdd, int recordsToDelete, int threadWaitTimeMs) { + this.recordsState = recordsState; + this.recordsToAdd = recordsToAdd; + this.recordsToDelete = recordsToDelete; + this.threadWaitTimeMs = threadWaitTimeMs; + } + } + + private static class ConcurrentTestState { + final List caughtExceptions = new ArrayList<>(); + final List deletedRecords = new ArrayList<>(); + final List addedRecords = new ArrayList<>(); + + synchronized void handleException(Exception e) { + caughtExceptions.add(e); + } + + void printExceptions() { + for (Exception e : caughtExceptions) { + e.printStackTrace(); + } + } + } private BacktraceDatabaseContext databaseContext; @Before @@ -32,49 +61,61 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final int recordsState = 1000; - final int recordsToAdd = 500; - final int recordsToDelete = 750; - final int threadWaitTimeMs = 5000; - final List records = generateMockRecords(recordsState); + final TestConfig config = new TestConfig(1000, 500, 750, 5000); + final List initialRecords = generateMockRecords(config.recordsState); + + final CountDownLatch startLatch = new CountDownLatch(1); + final ConcurrentTestState testState = new ConcurrentTestState(); - final CountDownLatch latch = new CountDownLatch(1); + // Create and start test threads + Thread deleteThread = createDeleteThread(startLatch, initialRecords, config.recordsToDelete, testState); + Thread addThread = createAddThread(startLatch, config.recordsToAdd, testState); + Thread readThread = createReadThread(startLatch, testState); - final List caughtExceptions = new ArrayList<>(); - final List deletedRecords = new ArrayList<>(); - final List addedRecords = new ArrayList<>(); + // WHEN + startThreads(deleteThread, addThread, readThread); + startLatch.countDown(); + waitForThreads(deleteThread, addThread, readThread, config.threadWaitTimeMs); - // GIVEN threads - final Thread deleteThread = new Thread(() -> { + // Print any exceptions that occurred + testState.printExceptions(); + + // THEN + assertTestResults(config, testState); + } + + private Thread createDeleteThread(CountDownLatch latch, List records, + int recordsToDelete, ConcurrentTestState state) { + return new Thread(() -> { try { latch.await(); for (int i = 0; i < recordsToDelete; i++) { databaseContext.delete(records.get(i)); - deletedRecords.add(1); + state.deletedRecords.add(1); } } catch (Exception e) { - synchronized (caughtExceptions) { - caughtExceptions.add(e); - } + state.handleException(e); } }); + } - final Thread addThread = new Thread(() -> { + private Thread createAddThread(CountDownLatch latch, int recordsToAdd, ConcurrentTestState state) { + return new Thread(() -> { try { latch.await(); for (int i = 0; i < recordsToAdd; i++) { BacktraceData data = createMockBacktraceData(); databaseContext.add(data); - addedRecords.add(1); + state.addedRecords.add(1); } } catch (Exception e) { - synchronized (caughtExceptions) { - caughtExceptions.add(e); - } + state.handleException(e); } }); + } - final Thread readThread = new Thread(() -> { + private Thread createReadThread(CountDownLatch latch, ConcurrentTestState state) { + return new Thread(() -> { try { latch.await(); String result; @@ -84,34 +125,30 @@ public void testConcurrentModification() throws InterruptedException { } } } catch (Exception e) { - synchronized (caughtExceptions) { - caughtExceptions.add(e); - } + state.handleException(e); } }); + } - // WHEN - // Start all threads - deleteThread.start(); - addThread.start(); - readThread.start(); - - // Release all threads simultaneously - latch.countDown(); - - // Wait for threads to complete - deleteThread.join(threadWaitTimeMs); - addThread.join(threadWaitTimeMs); - readThread.join(threadWaitTimeMs); - - // Print all caught exceptions - for (Exception e : caughtExceptions) { - e.printStackTrace(); + private void startThreads(Thread... threads) { + for (Thread thread : threads) { + thread.start(); } + } - // THEN - assertEquals(0, caughtExceptions.size()); - assertEquals(recordsState + recordsToAdd - recordsToDelete, recordsState + addedRecords.size() - deletedRecords.size()); + private void waitForThreads(Thread deleteThread, Thread addThread, Thread readThread, int waitTimeMs) + throws InterruptedException { + deleteThread.join(waitTimeMs); + addThread.join(waitTimeMs); + readThread.join(waitTimeMs); + } + + private void assertTestResults(TestConfig config, ConcurrentTestState state) { + assertEquals(0, state.caughtExceptions.size()); + assertEquals( + config.recordsState + config.recordsToAdd - config.recordsToDelete, + config.recordsState + state.addedRecords.size() - state.deletedRecords.size() + ); } @NonNull From a02bf943d90534b5903cbfbddfa58e909e08e18b Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Mon, 5 May 2025 16:57:49 +0200 Subject: [PATCH 07/16] Refactor list nam --- .../library/services/BacktraceDatabaseContext.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java index c2fe4d06..abf90f5b 100644 --- a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java +++ b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java @@ -365,18 +365,18 @@ private BacktraceDatabaseRecord getLastRecord() { */ private BacktraceDatabaseRecord getRecordFromCache(boolean reverse) { for (int i = _retryNumber - 1; i >= 0; i--) { - Queue reverseRecords = batchRetry.get(i); + Queue batchRecords = batchRetry.get(i); - if (reverseRecords == null) { + if (batchRecords == null) { continue; } - - List tempList = new ArrayList<>(reverseRecords); + + List recordsList = new ArrayList<>(batchRecords); if (reverse) { - Collections.reverse(tempList); + Collections.reverse(recordsList); } - for (BacktraceDatabaseRecord record : tempList) { + for (BacktraceDatabaseRecord record : recordsList) { if (record != null && !record.locked) { record.locked = true; return record; From 93ea62cff4927bc5bcd0716540bffbe88803d76b Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Mon, 5 May 2025 20:18:15 +0200 Subject: [PATCH 08/16] Use iterable interface --- .../library/services/BacktraceDatabaseContext.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java index abf90f5b..046afd1a 100644 --- a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java +++ b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java @@ -370,13 +370,15 @@ private BacktraceDatabaseRecord getRecordFromCache(boolean reverse) { if (batchRecords == null) { continue; } - - List recordsList = new ArrayList<>(batchRecords); + + Iterable records = batchRecords; if (reverse) { + List recordsList = new ArrayList<>(batchRecords); Collections.reverse(recordsList); + records = recordsList; } - for (BacktraceDatabaseRecord record : recordsList) { + for (BacktraceDatabaseRecord record : records) { if (record != null && !record.locked) { record.locked = true; return record; From 6186aa92805e94df066563f4af71596d261c98f0 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Mon, 5 May 2025 23:04:10 +0200 Subject: [PATCH 09/16] Increase thread waiting --- .../database/BacktraceDatabaseContextMultithreadedTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index 5f5222cc..26e813ac 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -61,15 +61,15 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final TestConfig config = new TestConfig(1000, 500, 750, 5000); + final TestConfig config = new TestConfig(1000, 500, 750, 10000); final List initialRecords = generateMockRecords(config.recordsState); final CountDownLatch startLatch = new CountDownLatch(1); final ConcurrentTestState testState = new ConcurrentTestState(); // Create and start test threads - Thread deleteThread = createDeleteThread(startLatch, initialRecords, config.recordsToDelete, testState); Thread addThread = createAddThread(startLatch, config.recordsToAdd, testState); + Thread deleteThread = createDeleteThread(startLatch, initialRecords, config.recordsToDelete, testState); Thread readThread = createReadThread(startLatch, testState); // WHEN From 6bb1aebb1433de27b31dd4fd78281407768573f9 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Mon, 5 May 2025 23:25:43 +0200 Subject: [PATCH 10/16] Increase timeout --- .../database/BacktraceDatabaseContextMultithreadedTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index 26e813ac..6c648f56 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -61,7 +61,7 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final TestConfig config = new TestConfig(1000, 500, 750, 10000); + final TestConfig config = new TestConfig(1000, 500, 750, 500000); final List initialRecords = generateMockRecords(config.recordsState); final CountDownLatch startLatch = new CountDownLatch(1); From d72974e5d15de2a7302732aa169a9960c3ea1eb5 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Tue, 6 May 2025 22:08:12 +0200 Subject: [PATCH 11/16] Fix values --- .../database/BacktraceDatabaseContextMultithreadedTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index 6c648f56..ecf41d1b 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -61,7 +61,7 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final TestConfig config = new TestConfig(1000, 500, 750, 500000); + final TestConfig config = new TestConfig(100, 50, 75, 2000); final List initialRecords = generateMockRecords(config.recordsState); final CountDownLatch startLatch = new CountDownLatch(1); From 20e3ec00eb8ef532a0464fc122ed07e7400bcb12 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Tue, 6 May 2025 22:25:09 +0200 Subject: [PATCH 12/16] Improve unit-tests --- .../database/BacktraceDatabaseContextMultithreadedTest.java | 2 +- .../backtraceio/library/services/BacktraceDatabaseContext.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index ecf41d1b..3d87a50e 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -61,7 +61,7 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final TestConfig config = new TestConfig(100, 50, 75, 2000); + final TestConfig config = new TestConfig(500, 250, 150, 2000); final List initialRecords = generateMockRecords(config.recordsState); final CountDownLatch startLatch = new CountDownLatch(1); diff --git a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java index 046afd1a..499720bf 100644 --- a/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java +++ b/backtrace-library/src/main/java/backtraceio/library/services/BacktraceDatabaseContext.java @@ -220,7 +220,7 @@ public boolean delete(BacktraceDatabaseRecord record) { return true; } catch (Exception e) { BacktraceLogger.d(LOG_TAG, "Exception on removing record " - + databaseRecord.id.toString() + "from db context: " + e.getMessage()); + + databaseRecord.id + " from db context: " + e.getMessage()); } } } From 5c5b59e6760e3cf7f5c70b00cfd9c095f1055139 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Tue, 6 May 2025 22:42:29 +0200 Subject: [PATCH 13/16] Increase timeout --- .../database/BacktraceDatabaseContextMultithreadedTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index 3d87a50e..18f3b044 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -61,7 +61,7 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final TestConfig config = new TestConfig(500, 250, 150, 2000); + final TestConfig config = new TestConfig(500, 250, 150, 5000); final List initialRecords = generateMockRecords(config.recordsState); final CountDownLatch startLatch = new CountDownLatch(1); From c9d60c9694aa417da0523c974a2251bc4bb09d22 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Tue, 6 May 2025 22:57:47 +0200 Subject: [PATCH 14/16] Increase timeout --- .../database/BacktraceDatabaseContextMultithreadedTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index 18f3b044..34500cc3 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -61,7 +61,7 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final TestConfig config = new TestConfig(500, 250, 150, 5000); + final TestConfig config = new TestConfig(500, 250, 150, 10000); final List initialRecords = generateMockRecords(config.recordsState); final CountDownLatch startLatch = new CountDownLatch(1); From a13435bc08224b36bcc8365195f248d17a886778 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Tue, 6 May 2025 23:24:31 +0200 Subject: [PATCH 15/16] Increase timeout --- .../database/BacktraceDatabaseContextMultithreadedTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index 34500cc3..f4fc27ac 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -61,7 +61,7 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final TestConfig config = new TestConfig(500, 250, 150, 10000); + final TestConfig config = new TestConfig(500, 250, 150, 3000); // 30s final List initialRecords = generateMockRecords(config.recordsState); final CountDownLatch startLatch = new CountDownLatch(1); From cff7c5aa25e3c63b3f703e78db6eabf3e9bd28e8 Mon Sep 17 00:00:00 2001 From: Bartosz Litwiniuk <> Date: Tue, 6 May 2025 23:24:59 +0200 Subject: [PATCH 16/16] Increase timeout --- .../database/BacktraceDatabaseContextMultithreadedTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java index f4fc27ac..05de2902 100644 --- a/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java +++ b/backtrace-library/src/androidTest/java/backtraceio/library/database/BacktraceDatabaseContextMultithreadedTest.java @@ -61,7 +61,7 @@ public void setUp() { @Test public void testConcurrentModification() throws InterruptedException { // GIVEN - final TestConfig config = new TestConfig(500, 250, 150, 3000); // 30s + final TestConfig config = new TestConfig(500, 250, 150, 30000); // 30s final List initialRecords = generateMockRecords(config.recordsState); final CountDownLatch startLatch = new CountDownLatch(1);