Skip to content

Commit ccc5c40

Browse files
committed
add uts
1 parent 868dc77 commit ccc5c40

File tree

10 files changed

+226
-15
lines changed

10 files changed

+226
-15
lines changed

client/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>io.split.client</groupId>
77
<artifactId>java-client-parent</artifactId>
8-
<version>4.1.7-rc3</version>
8+
<version>4.1.7-rc4</version>
99
</parent>
1010
<artifactId>java-client</artifactId>
1111
<packaging>jar</packaging>

client/src/main/java/io/split/engine/common/Backoff.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,26 @@
55
import static com.google.common.base.Preconditions.checkNotNull;
66

77
public class Backoff {
8-
private static final long BACKOFF_MAX_SECONDS_ALLOWED = 1800;
8+
private static final long BACKOFF_MAX_ALLOWED = 1800;
99

1010
private final long _backoffBase;
1111
private AtomicInteger _attempt;
12+
private final long _maxAllowed;
1213

1314
public Backoff(long backoffBase) {
15+
this(backoffBase, BACKOFF_MAX_ALLOWED);
16+
}
17+
18+
public Backoff(long backoffBase, long maxAllowed) {
1419
_backoffBase = checkNotNull(backoffBase);
1520
_attempt = new AtomicInteger(0);
21+
_maxAllowed = maxAllowed;
1622
}
1723

1824
public long interval() {
1925
long interval = _backoffBase * (long) Math.pow(2, _attempt.getAndIncrement());
2026

21-
return interval >= BACKOFF_MAX_SECONDS_ALLOWED ? BACKOFF_MAX_SECONDS_ALLOWED : interval;
27+
return interval >= _maxAllowed ? BACKOFF_MAX_ALLOWED : interval;
2228
}
2329

2430
public synchronized void reset() {

client/src/main/java/io/split/engine/common/SynchronizerImp.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222
public class SynchronizerImp implements Synchronizer {
2323

24-
private static final long ON_DEMAND_FETCH_BACKOFF_BASE_MS = 10000; //backoff base starting at 10 seconds (!)
24+
// The boxing here IS necessary, so that the constants are not inlined by the compiler
25+
// and can be modified for the test (we don't want to wait that much in an UT)
26+
private static final long ON_DEMAND_FETCH_BACKOFF_BASE_MS = new Long(10000); //backoff base starting at 10 seconds (!)
27+
private static final long ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_MS = new Long(60000); // don't sleep for more than 1 second
2528
private static final int ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES = 10;
2629

2730
private static final Logger _log = LoggerFactory.getLogger(Synchronizer.class);
@@ -52,7 +55,7 @@ public SynchronizerImp(SplitSynchronizationTask splitSynchronizationTask,
5255
_segmentSynchronizationTaskImp = checkNotNull(segmentSynchronizationTaskImp);
5356
_splitCache = checkNotNull(splitCache);
5457
_segmentCache = checkNotNull(segmentCache);
55-
_onDemandFetchRetryDelayMs = checkNotNull(onDemandFetchRetryDelayMs);
58+
_onDemandFetchRetryDelayMs = onDemandFetchRetryDelayMs;
5659
_cdnResponseHeadersLogging = cdnResponseHeadersLogging;
5760
_onDemandFetchMaxRetries = onDemandFetchMaxRetries;
5861
_failedAttemptsBeforeLogging = failedAttemptsBeforeLogging;
@@ -115,7 +118,8 @@ private SyncResult attemptSync(long targetChangeNumber,
115118
return new SyncResult(false, remainingAttempts);
116119
}
117120
try {
118-
Thread.sleep(nextWaitMs.apply(null));
121+
long howLong = nextWaitMs.apply(null);
122+
Thread.sleep(howLong);
119123
} catch (InterruptedException e) {
120124
Thread.currentThread().interrupt();
121125
_log.debug("Error trying to sleep current Thread.");
@@ -155,10 +159,10 @@ public void refreshSplits(long targetChangeNumber) {
155159
}
156160

157161
FetchOptions withCdnBypass = new FetchOptions.Builder(opts).cdnBypass(true).build();
158-
Backoff backoff = new Backoff(ON_DEMAND_FETCH_BACKOFF_BASE_MS);
162+
Backoff backoff = new Backoff(ON_DEMAND_FETCH_BACKOFF_BASE_MS, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_MS);
159163
SyncResult withCDNBypassed = attemptSync(targetChangeNumber, withCdnBypass,
160164
(discard) -> backoff.interval(), ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES);
161-
165+
162166
if (_cdnResponseHeadersLogging) {
163167
logCdnHeaders(_onDemandFetchMaxRetries + ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES,
164168
withCDNBypassed.remainingAttempts(), captor.get());

client/src/test/java/io/split/TestHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public static CloseableHttpClient mockHttpClient(String jsonName, int httpStatus
2626
return httpClientMock;
2727
}
2828

29-
private static CloseableHttpResponse classicResponseToCloseableMock(ClassicHttpResponse mocked) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
29+
public static CloseableHttpResponse classicResponseToCloseableMock(ClassicHttpResponse mocked) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
3030
Method adaptMethod = CloseableHttpResponse.class.getDeclaredMethod("adapt", ClassicHttpResponse.class);
3131
adaptMethod.setAccessible(true);
3232
return (CloseableHttpResponse) adaptMethod.invoke(null, mocked);

client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,30 @@
55
import io.split.client.dtos.SplitChange;
66
import io.split.engine.common.FetchOptions;
77
import io.split.engine.metrics.Metrics;
8+
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
89
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
10+
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
911
import org.apache.hc.client5.http.impl.classic.HttpClients;
10-
import org.apache.hc.core5.http.HttpStatus;
12+
import org.apache.hc.core5.http.*;
13+
import org.apache.hc.core5.http.io.entity.StringEntity;
14+
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
1115
import org.hamcrest.Matchers;
1216
import org.junit.Assert;
1317
import org.junit.Test;
18+
import org.mockito.ArgumentCaptor;
19+
import org.mockito.Mockito;
1420

21+
import java.io.Closeable;
1522
import java.io.IOException;
23+
import java.io.StringBufferInputStream;
1624
import java.lang.reflect.InvocationTargetException;
1725
import java.net.URI;
1826
import java.net.URISyntaxException;
27+
import java.util.List;
1928
import java.util.Map;
2029

30+
import static org.mockito.Mockito.when;
31+
2132
public class HttpSplitChangeFetcherTest {
2233
@Test
2334
public void testDefaultURL() throws URISyntaxException {
@@ -76,4 +87,44 @@ public void testFetcherWithSpecialCharacters() throws URISyntaxException, Invoca
7687
Assert.assertEquals("{\"test\": \"blue\",\"grüne Straße\": 13}", configs.get("on"));
7788
Assert.assertEquals("{\"test\": \"blue\",\"size\": 15}", configs.get("off"));
7889
}
90+
91+
@Test
92+
public void testFetcherWithCDNBypassOption() throws IOException, URISyntaxException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
93+
URI rootTarget = URI.create("https://api.split.io");
94+
95+
HttpEntity entityMock = Mockito.mock(HttpEntity.class);
96+
when(entityMock.getContent()).thenReturn(new StringBufferInputStream("{\"till\": 1}"));
97+
ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class);
98+
when(response.getCode()).thenReturn(200);
99+
when(response.getEntity()).thenReturn(entityMock);
100+
when(response.getHeaders()).thenReturn(new Header[0]);
101+
102+
ArgumentCaptor<ClassicHttpRequest> requestCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
103+
CloseableHttpClient httpClientMock = Mockito.mock(CloseableHttpClient.class);
104+
when(httpClientMock.execute(requestCaptor.capture())).thenReturn(TestHelper.classicResponseToCloseableMock(response));
105+
106+
Metrics.NoopMetrics metrics = new Metrics.NoopMetrics();
107+
HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClientMock, rootTarget, metrics);
108+
109+
fetcher.fetch(-1, new FetchOptions.Builder().cdnBypass(true).build());
110+
fetcher.fetch(-1, new FetchOptions.Builder().build());
111+
List<ClassicHttpRequest> captured = requestCaptor.getAllValues();
112+
Assert.assertEquals(captured.size(), 2);
113+
Assert.assertTrue(captured.get(0).getUri().toString().contains("till="));
114+
Assert.assertFalse(captured.get(1).getUri().toString().contains("till="));
115+
}
116+
117+
@Test
118+
public void testRandomNumberGeneration() throws URISyntaxException {
119+
URI rootTarget = URI.create("https://api.split.io");
120+
CloseableHttpClient httpClientMock = Mockito.mock(CloseableHttpClient.class);
121+
Metrics.NoopMetrics metrics = new Metrics.NoopMetrics();
122+
HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClientMock, rootTarget, metrics);
123+
124+
long min = (long)Math.pow(2, 63) * (-1);
125+
for (long x = 0; x < 100000000; x++) {
126+
long r = fetcher.makeRandomTill();
127+
Assert.assertTrue(r < 0 && r > min);
128+
}
129+
}
79130
}

client/src/test/java/io/split/engine/common/FetcherOptionsTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ public Void apply(Map<String, String> unused) {
2525
.cacheControlHeaders(true)
2626
.fastlyDebugHeader(true)
2727
.responseHeadersCallback(func)
28+
.cdnBypass(true)
2829
.build();
2930

3031
assertEquals(options.cacheControlHeadersEnabled(), true);
3132
assertEquals(options.fastlyDebugHeaderEnabled(), true);
33+
assertEquals(options.cdnBypass(), true);
3234
options.handleResponseHeaders(new HashMap<>());
3335
assertEquals(called[0], true);
3436
}

client/src/test/java/io/split/engine/common/SynchronizerTest.java

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
package io.split.engine.common;
22

3+
import io.split.cache.InMemoryCacheImp;
34
import io.split.cache.SegmentCache;
45
import io.split.cache.SplitCache;
56
import io.split.engine.experiments.SplitFetcherImp;
67
import io.split.engine.experiments.SplitSynchronizationTask;
78
import io.split.engine.segments.SegmentFetcher;
89
import io.split.engine.segments.SegmentSynchronizationTask;
10+
import org.junit.Assert;
911
import org.junit.Before;
1012
import org.junit.Test;
13+
import org.mockito.ArgumentCaptor;
1114
import org.mockito.Mockito;
1215

16+
import java.lang.reflect.Field;
17+
import java.lang.reflect.Modifier;
18+
import java.util.List;
19+
import java.util.concurrent.atomic.AtomicInteger;
20+
21+
import static org.mockito.Mockito.when;
22+
1323
public class SynchronizerTest {
1424
private SplitSynchronizationTask _refreshableSplitFetcherTask;
1525
private SegmentSynchronizationTask _segmentFetcher;
@@ -56,7 +66,7 @@ public void stopPeriodicFetching() {
5666

5767
@Test
5868
public void streamingRetryOnSplit() {
59-
Mockito.when(_splitCache.getChangeNumber()).thenReturn(0l).thenReturn(0l).thenReturn(1l);
69+
when(_splitCache.getChangeNumber()).thenReturn(0l).thenReturn(0l).thenReturn(1l);
6070
_synchronizer.refreshSplits(1l);
6171

6272
Mockito.verify(_splitCache, Mockito.times(3)).getChangeNumber();
@@ -65,11 +75,102 @@ public void streamingRetryOnSplit() {
6575
@Test
6676
public void streamingRetryOnSegment() {
6777
SegmentFetcher fetcher = Mockito.mock(SegmentFetcher.class);
68-
Mockito.when(_segmentFetcher.getFetcher(Mockito.anyString())).thenReturn(fetcher);
69-
Mockito.when(_segmentCache.getChangeNumber(Mockito.anyString())).thenReturn(0l).thenReturn(0l).thenReturn(1l);
78+
when(_segmentFetcher.getFetcher(Mockito.anyString())).thenReturn(fetcher);
79+
when(_segmentCache.getChangeNumber(Mockito.anyString())).thenReturn(0l).thenReturn(0l).thenReturn(1l);
7080
_synchronizer.refreshSegment("Segment",1l);
7181

7282
Mockito.verify(_segmentCache, Mockito.times(3)).getChangeNumber(Mockito.anyString());
7383
}
7484

85+
@Test
86+
public void testCDNBypassIsRequestedAfterNFailures() {
87+
88+
SplitCache cache = new InMemoryCacheImp();
89+
Synchronizer imp = new SynchronizerImp(_refreshableSplitFetcherTask,
90+
_splitFetcher,
91+
_segmentFetcher,
92+
cache,
93+
_segmentCache,
94+
50,
95+
3,
96+
1,
97+
true);
98+
99+
ArgumentCaptor<FetchOptions> optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class);
100+
AtomicInteger calls = new AtomicInteger();
101+
Mockito.doAnswer(invocationOnMock -> {
102+
calls.getAndIncrement();
103+
switch (calls.get()) {
104+
case 4: cache.setChangeNumber(1);
105+
}
106+
return null;
107+
}).when(_splitFetcher).forceRefresh(optionsCaptor.capture());
108+
109+
imp.refreshSplits(1);
110+
111+
List<FetchOptions> options = optionsCaptor.getAllValues();
112+
Assert.assertEquals(options.size(), 4);
113+
Assert.assertFalse(options.get(0).cdnBypass());
114+
Assert.assertFalse(options.get(1).cdnBypass());
115+
Assert.assertFalse(options.get(2).cdnBypass());
116+
Assert.assertTrue(options.get(3).cdnBypass());
117+
}
118+
119+
@Test
120+
public void testCDNBypassRequestLimitAndBackoff() throws NoSuchFieldException, IllegalAccessException {
121+
122+
SplitCache cache = new InMemoryCacheImp();
123+
Synchronizer imp = new SynchronizerImp(_refreshableSplitFetcherTask,
124+
_splitFetcher,
125+
_segmentFetcher,
126+
cache,
127+
_segmentCache,
128+
50,
129+
3,
130+
1,
131+
true);
132+
133+
ArgumentCaptor<FetchOptions> optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class);
134+
AtomicInteger calls = new AtomicInteger();
135+
Mockito.doAnswer(invocationOnMock -> {
136+
calls.getAndIncrement();
137+
switch (calls.get()) {
138+
case 14: Assert.assertTrue(false); // should never get here
139+
}
140+
return null;
141+
}).when(_splitFetcher).forceRefresh(optionsCaptor.capture());
142+
143+
// Before executing, we'll update the backoff via reflection, to avoid waiting minutes for the test to run.
144+
Field backoffBase = SynchronizerImp.class.getDeclaredField("ON_DEMAND_FETCH_BACKOFF_BASE_MS");
145+
backoffBase.setAccessible(true);
146+
Field modifiersField = Field.class.getDeclaredField("modifiers");
147+
modifiersField.setAccessible(true);
148+
modifiersField.setInt(backoffBase, backoffBase.getModifiers() & ~Modifier.FINAL);
149+
backoffBase.set(imp, 1); // 1ms
150+
151+
long before = System.currentTimeMillis();
152+
imp.refreshSplits(1);
153+
long after = System.currentTimeMillis();
154+
155+
List<FetchOptions> options = optionsCaptor.getAllValues();
156+
Assert.assertEquals(options.size(), 13);
157+
Assert.assertFalse(options.get(0).cdnBypass());
158+
Assert.assertFalse(options.get(1).cdnBypass());
159+
Assert.assertFalse(options.get(2).cdnBypass());
160+
Assert.assertTrue(options.get(3).cdnBypass());
161+
Assert.assertTrue(options.get(4).cdnBypass());
162+
Assert.assertTrue(options.get(5).cdnBypass());
163+
Assert.assertTrue(options.get(6).cdnBypass());
164+
Assert.assertTrue(options.get(7).cdnBypass());
165+
Assert.assertTrue(options.get(8).cdnBypass());
166+
Assert.assertTrue(options.get(9).cdnBypass());
167+
Assert.assertTrue(options.get(10).cdnBypass());
168+
Assert.assertTrue(options.get(11).cdnBypass());
169+
Assert.assertTrue(options.get(12).cdnBypass());
170+
171+
Assert.assertEquals(calls.get(), 13);
172+
long minDiffExpected = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 + 256;
173+
Assert.assertTrue((after - before) > minDiffExpected);
174+
}
175+
75176
}

client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
import io.split.engine.segments.SegmentSynchronizationTask;
1616
import io.split.engine.segments.SegmentSynchronizationTaskImp;
1717
import io.split.grammar.Treatments;
18+
import org.junit.Assert;
1819
import org.junit.Test;
20+
import org.mockito.ArgumentCaptor;
21+
import org.mockito.Mock;
1922
import org.mockito.Mockito;
2023
import org.mockito.internal.matchers.Any;
2124
import org.slf4j.Logger;
@@ -218,6 +221,50 @@ public void works_with_user_defined_segments() throws Exception {
218221
assertThat(gates.isSDKReady(0), is(equalTo(true)));
219222
}
220223

224+
@Test
225+
public void testBypassCdnClearedAfterFirstHit() {
226+
SplitChangeFetcher mockFetcher = Mockito.mock(SplitChangeFetcher.class);
227+
SegmentSynchronizationTask segmentSynchronizationTaskMock = Mockito.mock(SegmentSynchronizationTask.class);
228+
SegmentCache segmentCacheMock = Mockito.mock(SegmentCache.class);
229+
SplitParser mockParser = new SplitParser(segmentSynchronizationTaskMock, segmentCacheMock);
230+
SDKReadinessGates mockGates = Mockito.mock(SDKReadinessGates.class);
231+
SplitCache mockCache = new InMemoryCacheImp();
232+
SplitFetcherImp fetcher = new SplitFetcherImp(mockFetcher, mockParser, mockGates, mockCache);
233+
234+
235+
SplitChange response1 = new SplitChange();
236+
response1.splits = new ArrayList<>();
237+
response1.since = -1;
238+
response1.till = 1;
239+
240+
SplitChange response2 = new SplitChange();
241+
response2.splits = new ArrayList<>();
242+
response2.since = 1;
243+
response2.till = 1;
244+
245+
246+
ArgumentCaptor<FetchOptions> optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class);
247+
ArgumentCaptor<Long> cnCaptor = ArgumentCaptor.forClass(Long.class);
248+
when(mockFetcher.fetch(cnCaptor.capture(), optionsCaptor.capture())).thenReturn(response1, response2);
249+
250+
FetchOptions originalOptions = new FetchOptions.Builder().cdnBypass(true).build();
251+
fetcher.forceRefresh(originalOptions);
252+
List<Long> capturedCNs = cnCaptor.getAllValues();
253+
List<FetchOptions> capturedOptions = optionsCaptor.getAllValues();
254+
255+
Assert.assertEquals(capturedOptions.size(), 2);
256+
Assert.assertEquals(capturedCNs.size(), 2);
257+
258+
Assert.assertEquals(capturedCNs.get(0), Long.valueOf(-1));
259+
Assert.assertEquals(capturedCNs.get(1), Long.valueOf(1));
260+
261+
Assert.assertTrue(capturedOptions.get(0).cdnBypass());
262+
Assert.assertFalse(capturedOptions.get(1).cdnBypass());
263+
264+
// Ensure that the original value hasn't been modified
265+
Assert.assertTrue(originalOptions.cdnBypass());
266+
}
267+
221268
private SegmentChange getSegmentChange(long since, long till, String segmentName){
222269
SegmentChange segmentChange = new SegmentChange();
223270
segmentChange.name = segmentName;

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>io.split.client</groupId>
66
<artifactId>java-client-parent</artifactId>
7-
<version>4.1.7-rc3</version>
7+
<version>4.1.7-rc4</version>
88
<dependencyManagement>
99
<dependencies>
1010
<dependency>

0 commit comments

Comments
 (0)