Skip to content

Outdated proxy handling #764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
May 12, 2025
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ apply plugin: 'kotlin-android'
apply from: 'spec.gradle'

ext {
splitVersion = '5.2.0-alpha.2'
splitVersion = '5.3.0-alpha.1'
}

android {
Expand Down
121 changes: 121 additions & 0 deletions src/androidTest/assets/split_changes_legacy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
{
"splits": [
{
"trafficTypeName": "account",
"name": "FACUNDO_TEST",
"trafficAllocation": 59,
"trafficAllocationSeed": -2108186082,
"seed": -1947050785,
"status": "ACTIVE",
"killed": false,
"defaultTreatment": "off",
"changeNumber": 1506703262916,
"algo": 2,
"conditions": [
{
"conditionType": "WHITELIST",
"matcherGroup": {
"combiner": "AND",
"matchers": [
{
"keySelector": null,
"matcherType": "WHITELIST",
"negate": false,
"userDefinedSegmentMatcherData": null,
"whitelistMatcherData": {
"whitelist": [
"nico_test",
"othertest"
]
},
"unaryNumericMatcherData": null,
"betweenMatcherData": null,
"booleanMatcherData": null,
"dependencyMatcherData": null,
"stringMatcherData": null
}
]
},
"partitions": [
{
"treatment": "on",
"size": 100
}
],
"label": "whitelisted"
},
{
"conditionType": "WHITELIST",
"matcherGroup": {
"combiner": "AND",
"matchers": [
{
"keySelector": null,
"matcherType": "WHITELIST",
"negate": false,
"userDefinedSegmentMatcherData": null,
"whitelistMatcherData": {
"whitelist": [
"bla"
]
},
"unaryNumericMatcherData": null,
"betweenMatcherData": null,
"booleanMatcherData": null,
"dependencyMatcherData": null,
"stringMatcherData": null
}
]
},
"partitions": [
{
"treatment": "off",
"size": 100
}
],
"label": "whitelisted"
},
{
"conditionType": "ROLLOUT",
"matcherGroup": {
"combiner": "AND",
"matchers": [
{
"keySelector": {
"trafficType": "account",
"attribute": null
},
"matcherType": "ALL_KEYS",
"negate": false,
"userDefinedSegmentMatcherData": null,
"whitelistMatcherData": null,
"unaryNumericMatcherData": null,
"betweenMatcherData": null,
"booleanMatcherData": null,
"dependencyMatcherData": null,
"stringMatcherData": null
}
]
},
"partitions": [
{
"treatment": "on",
"size": 0
},
{
"treatment": "off",
"size": 100
},
{
"treatment": "visa",
"size": 0
}
],
"label": "in segment all"
}
]
}
],
"since": -1,
"till": 1506703262916
}
20 changes: 18 additions & 2 deletions src/androidTest/java/helper/IntegrationHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,22 @@ public static String rbsChange(String changeNumber, String previousChangeNumber,
}

public static String loadSplitChanges(Context context, String fileName) {
FileHelper fileHelper = new FileHelper();
String change = fileHelper.loadFileContent(context, fileName);
String change = getFileContentsAsString(context, fileName);
TargetingRulesChange targetingRulesChange = Json.fromJson(change, TargetingRulesChange.class);
SplitChange parsedChange = targetingRulesChange.getFeatureFlagsChange();
parsedChange.since = parsedChange.till;
return Json.toJson(TargetingRulesChange.create(parsedChange, targetingRulesChange.getRuleBasedSegmentsChange()));
}

public static String loadLegacySplitChanges(Context context, String fileName) {
return getFileContentsAsString(context, fileName);
}

private static String getFileContentsAsString(Context context, String fileName) {
FileHelper fileHelper = new FileHelper();
return fileHelper.loadFileContent(context, fileName);
}

/**
* Builds a dispatcher with the given responses.
*
Expand Down Expand Up @@ -442,6 +450,14 @@ public static String getRbSinceFromUri(URI uri) {
}
}

public static String getSpecFromUri(URI uri) {
try {
return parse(uri.getQuery()).get("s");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}

static Map<String, String> parse(String query) throws UnsupportedEncodingException {
Map<String, String> queryPairs = new HashMap<>();
String[] pairs = query.split("&");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio
return new MockResponse().setResponseCode(500);
}

if (request.getRequestUrl().encodedPathSegments().contains("splitChanges")) {
updateEndpointHit("splitChanges");
if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.SPLIT_CHANGES)) {
updateEndpointHit(IntegrationHelper.ServicePath.SPLIT_CHANGES);
return new MockResponse().setResponseCode(200).setBody(splitChangesLargeSegments(1602796638344L, 1602796638344L));
} else if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.MEMBERSHIPS)) {
Thread.sleep(mMySegmentsDelay.get());
Expand All @@ -93,7 +93,7 @@ public void tearDown() throws IOException {

private void initializeLatches() {
mLatches = new ConcurrentHashMap<>();
mLatches.put("splitChanges", new CountDownLatch(1));
mLatches.put(IntegrationHelper.ServicePath.SPLIT_CHANGES, new CountDownLatch(1));
mLatches.put(IntegrationHelper.ServicePath.MEMBERSHIPS, new CountDownLatch(1));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package tests.integration.rbs;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static helper.IntegrationHelper.emptyTargetingRulesChanges;
import static helper.IntegrationHelper.getSinceFromUri;
import static helper.IntegrationHelper.getSpecFromUri;

import android.content.Context;

import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import helper.DatabaseHelper;
import helper.IntegrationHelper;
import helper.TestableSplitConfigBuilder;
import io.split.android.client.ServiceEndpoints;
import io.split.android.client.SplitClient;
import io.split.android.client.SplitFactory;
import io.split.android.client.TestingConfig;
import io.split.android.client.events.SplitEvent;
import io.split.android.client.storage.db.GeneralInfoEntity;
import io.split.android.client.storage.db.SplitRoomDatabase;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import tests.integration.shared.TestingHelper;

public class OutdatedProxyIntegrationTest {

private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();

private MockWebServer mWebServer;
private Map<String, AtomicInteger> mEndpointHits;
private Map<String, CountDownLatch> mLatches;
private final AtomicBoolean mOutdatedProxy = new AtomicBoolean(false);
private final AtomicBoolean mSimulatedProxyError = new AtomicBoolean(false);
private final AtomicBoolean mRecoveryHit = new AtomicBoolean(false);

@Before
public void setUp() throws IOException {
mEndpointHits = new ConcurrentHashMap<>();
mOutdatedProxy.set(false);
initializeLatches();

mWebServer = new MockWebServer();
mWebServer.setDispatcher(new Dispatcher() {
@NonNull
@Override
public MockResponse dispatch(@NonNull RecordedRequest request) {
if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.SPLIT_CHANGES)) {
updateEndpointHit(IntegrationHelper.ServicePath.SPLIT_CHANGES);
float specFromUri = Float.parseFloat(getSpecFromUri(request.getRequestUrl().uri()));
if (mOutdatedProxy.get() && specFromUri > 1.2f) {
mSimulatedProxyError.set(true);
return new MockResponse().setResponseCode(400);
} else if (mOutdatedProxy.get()) {
String body = (getSinceFromUri(request.getRequestUrl().uri()).equals("-1")) ?
IntegrationHelper.loadLegacySplitChanges(mContext, "split_changes_legacy.json") :
emptyTargetingRulesChanges(1506703262916L, -1L);
return new MockResponse().setResponseCode(200)
.setBody(body);
}

if (!mOutdatedProxy.get()) {
if (request.getRequestUrl().uri().toString().contains("?s=1.3&since=-1&rbSince=-1")) {
mRecoveryHit.set(true);
}
}

return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.loadSplitChanges(mContext, "split_changes_rbs.json"));
} else if (request.getRequestUrl().encodedPathSegments().contains(IntegrationHelper.ServicePath.MEMBERSHIPS)) {
updateEndpointHit(IntegrationHelper.ServicePath.MEMBERSHIPS);

return new MockResponse().setResponseCode(200).setBody(IntegrationHelper.emptyAllSegments());
} else {
return new MockResponse().setResponseCode(404);
}
}
});
mWebServer.start();
}

@After
public void tearDown() throws IOException {
mWebServer.shutdown();
}

@Test
public void clientIsReadyEvenWhenUsingOutdatedProxy() {
mOutdatedProxy.set(true);
SplitClient readyClient = getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), getFactory());

assertNotNull(readyClient);
assertFalse(mRecoveryHit.get());
assertTrue(mSimulatedProxyError.get());
}

@Test
public void clientIsReadyWithLatestProxy() {
mOutdatedProxy.set(false);
SplitClient readyClient = getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), getFactory());

assertNotNull(readyClient);
assertFalse(mRecoveryHit.get() && mOutdatedProxy.get());
assertFalse(mSimulatedProxyError.get());
}

@Test
public void clientRecoversFromOutdatedProxy() {
mOutdatedProxy.set(false);
SplitRoomDatabase database = DatabaseHelper.getTestDatabase(mContext);
database.generalInfoDao().update(new GeneralInfoEntity("lastProxyCheckTimestamp", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(62)));
SplitClient readyClient = getReadyClient(IntegrationHelper.dummyUserKey().matchingKey(), getFactory(database));

assertNotNull(readyClient);
assertTrue(mRecoveryHit.get() && !mOutdatedProxy.get());
assertFalse(mSimulatedProxyError.get());
}

private void initializeLatches() {
mLatches = new ConcurrentHashMap<>();
mLatches.put(IntegrationHelper.ServicePath.SPLIT_CHANGES, new CountDownLatch(1));
mLatches.put(IntegrationHelper.ServicePath.MEMBERSHIPS, new CountDownLatch(1));
}

private void updateEndpointHit(String splitChanges) {
if (mEndpointHits.containsKey(splitChanges)) {
mEndpointHits.get(splitChanges).getAndIncrement();
} else {
mEndpointHits.put(splitChanges, new AtomicInteger(1));
}

if (mLatches.containsKey(splitChanges)) {
mLatches.get(splitChanges).countDown();
}
}

protected SplitFactory getFactory() {
return getFactory(null);
}

protected SplitFactory getFactory(SplitRoomDatabase database) {
TestableSplitConfigBuilder configBuilder = new TestableSplitConfigBuilder()
.enableDebug()
.serviceEndpoints(ServiceEndpoints.builder()
.apiEndpoint("http://" + mWebServer.getHostName() + ":" + mWebServer.getPort())
.build());

configBuilder.streamingEnabled(false);
configBuilder.ready(10000);
TestingConfig testingConfig = new TestingConfig();
testingConfig.setFlagsSpec("1.3");
return IntegrationHelper.buildFactory(
IntegrationHelper.dummyApiKey(),
IntegrationHelper.dummyUserKey(),
configBuilder.build(),
mContext,
null, database, null, testingConfig, null);
}

protected SplitClient getReadyClient(String matchingKey, SplitFactory factory) {
CountDownLatch countDownLatch = new CountDownLatch(1);

SplitClient client = factory.client(matchingKey);
boolean await;
client.on(SplitEvent.SDK_READY, TestingHelper.testTask(countDownLatch));
try {
await = countDownLatch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (!await) {
fail("Client is not ready");
}

return client;
}
}
Loading
Loading