Skip to content

Commit 89bd017

Browse files
committed
Polling tests and some fixes.
1 parent bba0cdc commit 89bd017

File tree

3 files changed

+489
-119
lines changed

3 files changed

+489
-119
lines changed

lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/PollingSynchronizerImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public PollingSynchronizerImpl(
3838

3939
private void doPoll() {
4040
try {
41-
FDv2SourceResult res = poll(selectorSource.getSelector(), true).get();
41+
FDv2SourceResult res = poll(selectorSource.getSelector(), false).get();
4242
switch(res.getResultType()) {
4343
case CHANGE_SET:
4444
break;
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
package com.launchdarkly.sdk.server;
2+
3+
import com.launchdarkly.sdk.internal.fdv2.sources.FDv2ChangeSet;
4+
import com.launchdarkly.sdk.internal.fdv2.sources.Selector;
5+
import com.launchdarkly.sdk.internal.http.HttpErrors;
6+
import com.launchdarkly.sdk.server.datasources.FDv2SourceResult;
7+
import com.launchdarkly.sdk.server.datasources.SelectorSource;
8+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
9+
import com.launchdarkly.sdk.server.subsystems.SerializationException;
10+
11+
import org.junit.Test;
12+
13+
import java.io.IOException;
14+
import java.util.concurrent.CompletableFuture;
15+
import java.util.concurrent.ExecutionException;
16+
import java.util.concurrent.TimeUnit;
17+
18+
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.assertNotNull;
20+
import static org.junit.Assert.assertNull;
21+
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.times;
24+
import static org.mockito.Mockito.verify;
25+
import static org.mockito.Mockito.when;
26+
27+
@SuppressWarnings("javadoc")
28+
public class PollingInitializerImplTest extends BaseTest {
29+
30+
private FDv2Requestor mockRequestor() {
31+
return mock(FDv2Requestor.class);
32+
}
33+
34+
private SelectorSource mockSelectorSource() {
35+
SelectorSource source = mock(SelectorSource.class);
36+
when(source.getSelector()).thenReturn(Selector.EMPTY);
37+
return source;
38+
}
39+
40+
// Helper for Java 8 compatibility - failedFuture() is Java 9+
41+
private <T> CompletableFuture<T> failedFuture(Throwable ex) {
42+
CompletableFuture<T> future = new CompletableFuture<>();
43+
future.completeExceptionally(ex);
44+
return future;
45+
}
46+
47+
private FDv2Requestor.FDv2PollingResponse makeSuccessResponse() {
48+
String json = "{\n" +
49+
" \"events\": [\n" +
50+
" {\n" +
51+
" \"event\": \"server-intent\",\n" +
52+
" \"data\": {\n" +
53+
" \"payloads\": [{\n" +
54+
" \"id\": \"payload-1\",\n" +
55+
" \"target\": 100,\n" +
56+
" \"intentCode\": \"xfer-full\",\n" +
57+
" \"reason\": \"payload-missing\"\n" +
58+
" }]\n" +
59+
" }\n" +
60+
" },\n" +
61+
" {\n" +
62+
" \"event\": \"payload-transferred\",\n" +
63+
" \"data\": {\n" +
64+
" \"state\": \"(p:payload-1:100)\",\n" +
65+
" \"version\": 100\n" +
66+
" }\n" +
67+
" }\n" +
68+
" ]\n" +
69+
"}";
70+
71+
try {
72+
return new FDv2Requestor.FDv2PollingResponse(
73+
com.launchdarkly.sdk.internal.fdv2.payloads.FDv2Event.parseEventsArray(json),
74+
okhttp3.Headers.of()
75+
);
76+
} catch (Exception e) {
77+
throw new RuntimeException(e);
78+
}
79+
}
80+
81+
@Test
82+
public void successfulInitialization() throws Exception {
83+
FDv2Requestor requestor = mockRequestor();
84+
SelectorSource selectorSource = mockSelectorSource();
85+
86+
FDv2Requestor.FDv2PollingResponse response = makeSuccessResponse();
87+
when(requestor.Poll(any(Selector.class)))
88+
.thenReturn(CompletableFuture.completedFuture(response));
89+
90+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
91+
92+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
93+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
94+
95+
assertNotNull(result);
96+
assertEquals(FDv2SourceResult.ResultType.CHANGE_SET, result.getResultType());
97+
assertNotNull(result.getChangeSet());
98+
99+
verify(requestor, times(1)).Poll(any(Selector.class));
100+
}
101+
102+
@Test
103+
public void httpRecoverableError() throws Exception {
104+
FDv2Requestor requestor = mockRequestor();
105+
SelectorSource selectorSource = mockSelectorSource();
106+
107+
when(requestor.Poll(any(Selector.class)))
108+
.thenReturn(failedFuture(new HttpErrors.HttpErrorException(503)));
109+
110+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
111+
112+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
113+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
114+
115+
assertNotNull(result);
116+
assertEquals(FDv2SourceResult.ResultType.STATUS, result.getResultType());
117+
assertEquals(FDv2SourceResult.State.TERMINAL_ERROR, result.getStatus().getState());
118+
assertNotNull(result.getStatus().getErrorInfo());
119+
assertEquals(DataSourceStatusProvider.ErrorKind.ERROR_RESPONSE, result.getStatus().getErrorInfo().getKind());
120+
121+
122+
}
123+
124+
@Test
125+
public void httpNonRecoverableError() throws Exception {
126+
FDv2Requestor requestor = mockRequestor();
127+
SelectorSource selectorSource = mockSelectorSource();
128+
129+
when(requestor.Poll(any(Selector.class)))
130+
.thenReturn(failedFuture(new HttpErrors.HttpErrorException(401)));
131+
132+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
133+
134+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
135+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
136+
137+
assertNotNull(result);
138+
assertEquals(FDv2SourceResult.ResultType.STATUS, result.getResultType());
139+
assertEquals(FDv2SourceResult.State.TERMINAL_ERROR, result.getStatus().getState());
140+
assertEquals(DataSourceStatusProvider.ErrorKind.ERROR_RESPONSE, result.getStatus().getErrorInfo().getKind());
141+
142+
143+
}
144+
145+
@Test
146+
public void networkError() throws Exception {
147+
FDv2Requestor requestor = mockRequestor();
148+
SelectorSource selectorSource = mockSelectorSource();
149+
150+
when(requestor.Poll(any(Selector.class)))
151+
.thenReturn(failedFuture(new IOException("Connection refused")));
152+
153+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
154+
155+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
156+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
157+
158+
assertNotNull(result);
159+
assertEquals(FDv2SourceResult.ResultType.STATUS, result.getResultType());
160+
assertEquals(FDv2SourceResult.State.TERMINAL_ERROR, result.getStatus().getState());
161+
assertEquals(DataSourceStatusProvider.ErrorKind.NETWORK_ERROR, result.getStatus().getErrorInfo().getKind());
162+
163+
164+
}
165+
166+
@Test
167+
public void serializationError() throws Exception {
168+
FDv2Requestor requestor = mockRequestor();
169+
SelectorSource selectorSource = mockSelectorSource();
170+
171+
when(requestor.Poll(any(Selector.class)))
172+
.thenReturn(failedFuture(new SerializationException(new Exception("Invalid JSON"))));
173+
174+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
175+
176+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
177+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
178+
179+
assertNotNull(result);
180+
assertEquals(FDv2SourceResult.ResultType.STATUS, result.getResultType());
181+
assertEquals(FDv2SourceResult.State.TERMINAL_ERROR, result.getStatus().getState());
182+
assertEquals(DataSourceStatusProvider.ErrorKind.INVALID_DATA, result.getStatus().getErrorInfo().getKind());
183+
184+
185+
}
186+
187+
@Test
188+
public void shutdownBeforePollCompletes() throws Exception {
189+
FDv2Requestor requestor = mockRequestor();
190+
SelectorSource selectorSource = mockSelectorSource();
191+
192+
CompletableFuture<FDv2Requestor.FDv2PollingResponse> delayedResponse = new CompletableFuture<>();
193+
when(requestor.Poll(any(Selector.class))).thenReturn(delayedResponse);
194+
195+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
196+
197+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
198+
199+
// Shutdown before poll completes
200+
Thread.sleep(100);
201+
initializer.shutdown();
202+
203+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
204+
205+
assertNotNull(result);
206+
assertEquals(FDv2SourceResult.ResultType.STATUS, result.getResultType());
207+
assertEquals(FDv2SourceResult.State.SHUTDOWN, result.getStatus().getState());
208+
assertNull(result.getStatus().getErrorInfo());
209+
210+
211+
}
212+
213+
@Test
214+
public void shutdownAfterPollCompletes() throws Exception {
215+
FDv2Requestor requestor = mockRequestor();
216+
SelectorSource selectorSource = mockSelectorSource();
217+
218+
FDv2Requestor.FDv2PollingResponse response = makeSuccessResponse();
219+
when(requestor.Poll(any(Selector.class)))
220+
.thenReturn(CompletableFuture.completedFuture(response));
221+
222+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
223+
224+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
225+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
226+
227+
assertNotNull(result);
228+
assertEquals(FDv2SourceResult.ResultType.CHANGE_SET, result.getResultType());
229+
230+
// Shutdown after completion should still work
231+
initializer.shutdown();
232+
233+
234+
}
235+
236+
@Test
237+
public void errorEventInResponse() throws Exception {
238+
FDv2Requestor requestor = mockRequestor();
239+
SelectorSource selectorSource = mockSelectorSource();
240+
241+
String errorJson = "{\n" +
242+
" \"events\": [\n" +
243+
" {\n" +
244+
" \"event\": \"error\",\n" +
245+
" \"data\": {\n" +
246+
" \"error\": \"invalid-request\",\n" +
247+
" \"reason\": \"bad request\"\n" +
248+
" }\n" +
249+
" }\n" +
250+
" ]\n" +
251+
"}";
252+
253+
FDv2Requestor.FDv2PollingResponse response = new FDv2Requestor.FDv2PollingResponse(
254+
com.launchdarkly.sdk.internal.fdv2.payloads.FDv2Event.parseEventsArray(errorJson),
255+
okhttp3.Headers.of()
256+
);
257+
258+
when(requestor.Poll(any(Selector.class)))
259+
.thenReturn(CompletableFuture.completedFuture(response));
260+
261+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
262+
263+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
264+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
265+
266+
assertNotNull(result);
267+
assertEquals(FDv2SourceResult.ResultType.STATUS, result.getResultType());
268+
assertEquals(FDv2SourceResult.State.TERMINAL_ERROR, result.getStatus().getState());
269+
270+
271+
}
272+
273+
@Test
274+
public void goodbyeEventInResponse() throws Exception {
275+
FDv2Requestor requestor = mockRequestor();
276+
SelectorSource selectorSource = mockSelectorSource();
277+
278+
String goodbyeJson = "{\n" +
279+
" \"events\": [\n" +
280+
" {\n" +
281+
" \"event\": \"goodbye\",\n" +
282+
" \"data\": {\n" +
283+
" \"reason\": \"service-unavailable\"\n" +
284+
" }\n" +
285+
" }\n" +
286+
" ]\n" +
287+
"}";
288+
289+
FDv2Requestor.FDv2PollingResponse response = new FDv2Requestor.FDv2PollingResponse(
290+
com.launchdarkly.sdk.internal.fdv2.payloads.FDv2Event.parseEventsArray(goodbyeJson),
291+
okhttp3.Headers.of()
292+
);
293+
294+
when(requestor.Poll(any(Selector.class)))
295+
.thenReturn(CompletableFuture.completedFuture(response));
296+
297+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
298+
299+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
300+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
301+
302+
assertNotNull(result);
303+
assertEquals(FDv2SourceResult.ResultType.STATUS, result.getResultType());
304+
assertEquals(FDv2SourceResult.State.GOODBYE, result.getStatus().getState());
305+
306+
307+
}
308+
309+
@Test
310+
public void emptyEventsArray() throws Exception {
311+
FDv2Requestor requestor = mockRequestor();
312+
SelectorSource selectorSource = mockSelectorSource();
313+
314+
String emptyJson = "{\"events\": []}";
315+
316+
FDv2Requestor.FDv2PollingResponse response = new FDv2Requestor.FDv2PollingResponse(
317+
com.launchdarkly.sdk.internal.fdv2.payloads.FDv2Event.parseEventsArray(emptyJson),
318+
okhttp3.Headers.of()
319+
);
320+
321+
when(requestor.Poll(any(Selector.class)))
322+
.thenReturn(CompletableFuture.completedFuture(response));
323+
324+
PollingInitializerImpl initializer = new PollingInitializerImpl(requestor, testLogger, selectorSource);
325+
326+
CompletableFuture<FDv2SourceResult> resultFuture = initializer.run();
327+
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
328+
329+
// Empty events array should result in terminal error
330+
assertNotNull(result);
331+
assertEquals(FDv2SourceResult.ResultType.STATUS, result.getResultType());
332+
assertEquals(FDv2SourceResult.State.TERMINAL_ERROR, result.getStatus().getState());
333+
334+
335+
}
336+
}

0 commit comments

Comments
 (0)