From 7cbbb7d793852b2446078ab9394d920799115cd4 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Wed, 23 Sep 2026 11:13:45 -0700 Subject: [PATCH 1/2] test(fdv2): build a new synchronizer per factory call in the recovery tests fallbackAndRecoveryTasksWellBehaved handed the same MockQueuedSynchronizer back from each factory call. Recovery closes the active synchronizer and builds the primary again; the closed mock answers every next() with SHUTDOWN immediately, which advances to the other closed mock, and the orchestrator spun between the two until the test stopped it. The third changeset the test waits for could never arrive, so it spun for the whole ten-second await: locally about 630,000 factory calls and 3.8 million captured log lines, and on CI an OutOfMemoryError. Each factory call now builds a new synchronizer, as a real factory does, and the test asserts the third changeset arrives rather than letting the await time out silently. It finishes in about three seconds. orchestrationLogging_recovery_logsInfo had the same shape and gets the same change. Co-authored-by: Cursor --- .../sdk/android/FDv2DataSourceTest.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) 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 75156e95..33ecfc61 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 @@ -517,13 +517,10 @@ public void emptyInitializerListSkipsToSynchronizers() throws Exception { @Test public void fallbackAndRecoveryTasksWellBehaved() throws Exception { - // First sync: changeset then INTERRUPTED; second sync: changeset; recovery brings back first - MockQueuedSynchronizer firstSync = new MockQueuedSynchronizer( - FDv2SourceResult.changeSet(makeChangeSet(false), false), - interrupted()); - MockQueuedSynchronizer secondSync = new MockQueuedSynchronizer( - FDv2SourceResult.changeSet(makeChangeSet(false), false)); - + // First sync: changeset then INTERRUPTED; second sync: changeset; recovery brings back first. + // Each factory call builds a new synchronizer, as a real factory does. Recovery closes the + // active one and builds the primary again, and a closed mock answers SHUTDOWN at once, so + // handing the same instance back would spin the orchestrator between two closed mocks. AtomicInteger firstCallCount = new AtomicInteger(0); AtomicInteger secondCallCount = new AtomicInteger(0); MockComponents.MockDataSourceUpdateSink sink = new MockComponents.MockDataSourceUpdateSink(); @@ -531,8 +528,17 @@ public void fallbackAndRecoveryTasksWellBehaved() throws Exception { 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); @@ -541,6 +547,7 @@ public void fallbackAndRecoveryTasksWellBehaved() throws Exception { // Wait for fallback + recovery: ~1s fallback + ~2s recovery. // Use generous timeouts for Android where thread scheduling can delay timer delivery. sink.awaitApplyCount(3, AWAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertTrue(sink.getApplyCount() >= 3); List statuses = sink.awaitStatuses(3, AWAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertTrue(statuses.size() >= 3); @@ -2030,16 +2037,15 @@ public void orchestrationLogging_fallback_logsInfo() throws Exception { @Test public void orchestrationLogging_recovery_logsInfo() throws Exception { MockComponents.MockDataSourceUpdateSink sink = new MockComponents.MockDataSourceUpdateSink(); - MockQueuedSynchronizer firstSync = new MockQueuedSynchronizer( - FDv2SourceResult.changeSet(makeChangeSet(false), false), - interrupted()); - MockQueuedSynchronizer secondSync = new MockQueuedSynchronizer( - FDv2SourceResult.changeSet(makeChangeSet(false), false)); + // A new synchronizer per build, for the reason fallbackAndRecoveryTasksWellBehaved gives. FDv2DataSource dataSource = buildDataSource(sink, Collections.emptyList(), Arrays.asList( - () -> firstSync, - () -> secondSync), + () -> new MockQueuedSynchronizer( + FDv2SourceResult.changeSet(makeChangeSet(false), false), + interrupted()), + () -> new MockQueuedSynchronizer( + FDv2SourceResult.changeSet(makeChangeSet(false), false))), 1, 2); AwaitableCallback startCallback = startDataSource(dataSource); try { From e3f1ef9df8c744dec419f676a8ff8494b28d17c8 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Wed, 23 Sep 2026 11:13:45 -0700 Subject: [PATCH 2/2] test: use valueOf instead of the deprecated boxing constructors in FlagTest 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 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