From 09513ceacbc7e72a0111783f92c2aeddf4665852 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Mon, 17 Aug 2026 13:55:56 -0700 Subject: [PATCH 1/3] chore: stop unit test OOM from a spinning synchronizer loop recoveryResetsToFirstAvailableSynchronizer handed the same mock synchronizer back from its factory on every build, but SourceManager closes the synchronizer it switches away from, and a closed one reports SHUTDOWN immediately. Once both mocks were closed the round-robin loop in runSynchronizers re-entered with no delay, logging on each pass into a LogCapture that retains every message, so the live heap climbed until it exhausted the test JVM. Building a fresh synchronizer per call, as the real factories do, keeps the live set flat. Also size the forked test JVM, which Gradle otherwise leaves at 512m (org.gradle.jvmargs only sizes the daemon). The suite ran against that ceiling, and the extra headroom cuts full GCs during the run from ~1050 to ~240. Co-authored-by: Cursor --- build.gradle | 3 +++ .../sdk/android/FDv2DataSourceTest.java | 22 ++++++++++++------- .../launchdarkly/sdk/android/FlagTest.java | 6 ++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 057f8bcb..c406ace4 100644 --- a/build.gradle +++ b/build.gradle @@ -77,5 +77,8 @@ subprojects { subproject -> tasks.withType(Test).configureEach { // Allow EasyMock/CGLIB to use reflection on Java base classes when running on newer JDKs. jvmArgs += ["--add-opens", "java.base/java.lang=ALL-UNNAMED"] + // org.gradle.jvmargs sizes the daemon, not the forked test JVM, which Gradle otherwise + // leaves at 512m. The suite runs against that ceiling and intermittently exhausts it. + maxHeapSize = "2g" } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2DataSourceTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2DataSourceTest.java index 5b2fde21..75156e95 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2DataSourceTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2DataSourceTest.java @@ -663,17 +663,23 @@ public void recoveryResetsToFirstAvailableSynchronizer() throws Exception { AtomicInteger firstCallCount = new AtomicInteger(0); AtomicInteger secondCallCount = new AtomicInteger(0); - MockQueuedSynchronizer firstSync = new MockQueuedSynchronizer( - FDv2SourceResult.changeSet(makeChangeSet(false), false), - interrupted()); - MockQueuedSynchronizer secondSync = new MockQueuedSynchronizer( - FDv2SourceResult.changeSet(makeChangeSet(false), false)); - + // Each factory must build a fresh synchronizer, as the real ones do. SourceManager closes + // the synchronizer it is switching away from, and a closed one reports SHUTDOWN + // immediately, so handing the same instance back on every recovery would spin this loop. FDv2DataSource dataSource = buildDataSource(sink, Collections.emptyList(), Arrays.asList( - () -> { firstCallCount.incrementAndGet(); return firstSync; }, - () -> { secondCallCount.incrementAndGet(); return secondSync; }), + () -> { + firstCallCount.incrementAndGet(); + return new MockQueuedSynchronizer( + FDv2SourceResult.changeSet(makeChangeSet(false), false), + interrupted()); + }, + () -> { + secondCallCount.incrementAndGet(); + return new MockQueuedSynchronizer( + FDv2SourceResult.changeSet(makeChangeSet(false), false)); + }), 1, 2); AwaitableCallback startCallback = startDataSource(dataSource); diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java index 36360754..63d7bb57 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java @@ -156,7 +156,7 @@ public void variationIsSerialized() { public void variationIsDeserialized() { final String jsonStr = "{\"version\": 99, \"variation\": 2}"; final Flag r = gson.fromJson(jsonStr, Flag.class); - assertEquals(new Integer(2), r.getVariation()); + assertEquals(Integer.valueOf(2), r.getVariation()); } @Test @@ -243,12 +243,12 @@ public void debugEventsUntilDateIsSerialized() { public void debugEventsUntilDateIsDeserialized() { final String jsonStr = "{\"version\": 99, \"debugEventsUntilDate\": 12345}"; final Flag r = gson.fromJson(jsonStr, Flag.class); - assertEquals(new Long(12345L), r.getDebugEventsUntilDate()); + assertEquals(Long.valueOf(12345L), r.getDebugEventsUntilDate()); // Test long sized number final String jsonStrl = "{\"version\": 99, \"debugEventsUntilDate\": 2500000000}"; final Flag rl = gson.fromJson(jsonStrl, Flag.class); - assertEquals(new Long(2500000000L), rl.getDebugEventsUntilDate()); + assertEquals(Long.valueOf(2500000000L), rl.getDebugEventsUntilDate()); } @Test From 7b0f1b9521619929f05c7b933eb490670fb2f224 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Mon, 17 Aug 2026 14:08:19 -0700 Subject: [PATCH 2/3] chore: drop unrelated deprecation cleanup from this branch Keeps this branch dedicated to the unit test memory problem. The new Integer/new Long warnings in FlagTest are unrelated noise and belong on their own branch. Co-authored-by: Cursor --- .../test/java/com/launchdarkly/sdk/android/FlagTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java index 63d7bb57..36360754 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java @@ -156,7 +156,7 @@ public void variationIsSerialized() { public void variationIsDeserialized() { final String jsonStr = "{\"version\": 99, \"variation\": 2}"; final Flag r = gson.fromJson(jsonStr, Flag.class); - assertEquals(Integer.valueOf(2), r.getVariation()); + assertEquals(new Integer(2), r.getVariation()); } @Test @@ -243,12 +243,12 @@ public void debugEventsUntilDateIsSerialized() { public void debugEventsUntilDateIsDeserialized() { final String jsonStr = "{\"version\": 99, \"debugEventsUntilDate\": 12345}"; final Flag r = gson.fromJson(jsonStr, Flag.class); - assertEquals(Long.valueOf(12345L), r.getDebugEventsUntilDate()); + assertEquals(new Long(12345L), r.getDebugEventsUntilDate()); // Test long sized number final String jsonStrl = "{\"version\": 99, \"debugEventsUntilDate\": 2500000000}"; final Flag rl = gson.fromJson(jsonStrl, Flag.class); - assertEquals(Long.valueOf(2500000000L), rl.getDebugEventsUntilDate()); + assertEquals(new Long(2500000000L), rl.getDebugEventsUntilDate()); } @Test From 3fd6c46ffe5acb078279bcb1dbdb306da60a14b8 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Mon, 17 Aug 2026 14:12:52 -0700 Subject: [PATCH 3/3] chore: move test JVM heap size to its own branch Leaves this branch as only the fix for the spinning synchronizer loop. The heap size is headroom, not the fix, and is easier to review alone. Co-authored-by: Cursor --- build.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/build.gradle b/build.gradle index c406ace4..057f8bcb 100644 --- a/build.gradle +++ b/build.gradle @@ -77,8 +77,5 @@ subprojects { subproject -> tasks.withType(Test).configureEach { // Allow EasyMock/CGLIB to use reflection on Java base classes when running on newer JDKs. jvmArgs += ["--add-opens", "java.base/java.lang=ALL-UNNAMED"] - // org.gradle.jvmargs sizes the daemon, not the forked test JVM, which Gradle otherwise - // leaves at 512m. The suite runs against that ceiling and intermittently exhausts it. - maxHeapSize = "2g" } }