Skip to content

Commit bdb94fc

Browse files
committed
fix: force streaming to reconnect if a critical error is encountered
1 parent 9095eff commit bdb94fc

4 files changed

Lines changed: 76 additions & 10 deletions

File tree

examples/streaming-example/src/main/java/io/getunleash/example/StreamingExample.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ public static void main(String[] args) {
3434
UnleashConfig.builder()
3535
.appName("streaming-example")
3636
.instanceId("streaming-example-instance")
37-
.unleashAPI(getOrElse("UNLEASH_API_URL", "https://app.unleash-hosted.com/demo/api"))
37+
.unleashAPI(getOrElse("UNLEASH_API_URL", "http://localhost:4242/api"))
3838
.customHttpHeader(
3939
"Authorization",
4040
getOrElse("UNLEASH_API_TOKEN",
41-
"*:development.25a06b75248528f8ca93ce179dcdd141aedfb632231e0d21fd8ff349"))
41+
"*:development.0e244a346fec7bca236182d59ffa232baa16c14c68acb2ec21b2c1d3"))
4242
.experimentalStreamingMode()
4343
.subscriber(subscriber)
4444
.build();

src/main/java/io/getunleash/repository/PollingFeatureFetcher.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import io.getunleash.util.Throttler;
99
import io.getunleash.util.UnleashConfig;
1010
import io.getunleash.util.UnleashScheduledExecutor;
11-
1211
import java.util.concurrent.atomic.AtomicBoolean;
1312
import java.util.function.Consumer;
1413
import org.slf4j.Logger;
@@ -58,10 +57,10 @@ public void start() {
5857
runInitialFetch(this.unleashConfig.getStartupExceptionHandler()).run();
5958
} else {
6059
runInitialFetch(
61-
// just throw exception handler
62-
e -> {
63-
throw e;
64-
})
60+
// just throw exception handler
61+
e -> {
62+
throw e;
63+
})
6564
.run();
6665
}
6766
} else if (!unleashConfig.isDisablePolling()) {

src/main/java/io/getunleash/repository/StreamingFeatureFetcherImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class StreamingFeatureFetcherImpl implements FetchWorker {
6464
this.modeController = modeController;
6565
}
6666

67-
public void start() {
67+
public synchronized void start() {
6868
try {
6969
URI streamingUri = config.getUnleashURLs().getStreamingURL().toURI();
7070

@@ -104,7 +104,7 @@ public void start() {
104104
}
105105
}
106106

107-
public void stop() {
107+
public synchronized void stop() {
108108
try {
109109
BackgroundEventSource currentEventSource = eventSource;
110110
if (currentEventSource != null) {
@@ -116,7 +116,7 @@ public void stop() {
116116
}
117117
}
118118

119-
private void reconnect() {
119+
void reconnect() {
120120
stop();
121121
start();
122122
}

src/test/java/io/getunleash/repository/StreamingFeatureFetchingTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
import static com.github.tomakehurst.wiremock.client.WireMock.*;
44
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
55
import static org.assertj.core.api.Assertions.assertThat;
6+
import static org.mockito.Mockito.doThrow;
67
import static org.mockito.Mockito.mock;
78
import static org.mockito.Mockito.times;
89

910
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
11+
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
1012
import io.getunleash.DefaultUnleash;
1113
import io.getunleash.SynchronousTestExecutor;
1214
import io.getunleash.Unleash;
1315
import io.getunleash.engine.UnleashEngine;
16+
import io.getunleash.engine.YggdrasilInvalidInputException;
1417
import io.getunleash.event.ClientFeaturesResponse;
1518
import io.getunleash.event.EventDispatcher;
1619
import io.getunleash.event.GatedEventEmitter;
@@ -22,6 +25,7 @@
2225
import java.util.List;
2326
import java.util.concurrent.CountDownLatch;
2427
import java.util.concurrent.TimeUnit;
28+
import java.util.stream.Collectors;
2529
import org.junit.jupiter.api.BeforeEach;
2630
import org.junit.jupiter.api.Test;
2731
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -178,6 +182,69 @@ public void should_write_backup_file_when_streaming_update_received() throws Exc
178182
assertThat(savedBackupContent).doesNotContain("\"events\""); // store state
179183
}
180184

185+
@Test
186+
void should_reconnect_without_last_event_id_header() throws Exception {
187+
String hydration =
188+
"{\"events\":[{\"type\":\"hydration\",\"eventId\":1,\"features\":[{\"name\":\"deltaFeature\",\"enabled\":true,\"strategies\":[],\"variants\":[]}],\"segments\":[]}]}";
189+
190+
String badUpdate = "{not-json";
191+
192+
stubFor(
193+
get(urlEqualTo("/api/client/streaming"))
194+
.willReturn(
195+
aResponse()
196+
.withStatus(200)
197+
.withHeader("Content-Type", "text/event-stream")
198+
.withBody(
199+
"event: unleash-connected\n"
200+
+ "data: "
201+
+ hydration
202+
+ "\n\n"
203+
+ "event: unleash-updated\n"
204+
+ "data: "
205+
+ badUpdate
206+
+ "\n\n")));
207+
208+
FeatureBackupHandlerFile backupHandler = mock(FeatureBackupHandlerFile.class);
209+
UnleashEngine mockEngine = mock(UnleashEngine.class);
210+
211+
doThrow(new YggdrasilInvalidInputException("Invalid input"))
212+
.when(mockEngine)
213+
.takeState(badUpdate);
214+
215+
StreamingFeatureFetcherImpl streamingFetcher =
216+
new StreamingFeatureFetcherImpl(
217+
config,
218+
new GatedEventEmitter(new EventDispatcher(config)),
219+
mockEngine,
220+
backupHandler,
221+
null);
222+
streamingFetcher.start();
223+
224+
// Wait for reconnect to happen by polling WireMock's request log
225+
long deadline = System.currentTimeMillis() + 5_000;
226+
while (System.currentTimeMillis() < deadline) {
227+
int count = serverMock.getAllServeEvents().size();
228+
if (count >= 2) break;
229+
Thread.sleep(50);
230+
}
231+
232+
var serveEvents = serverMock.getAllServeEvents();
233+
assertThat(serveEvents.size()).isGreaterThanOrEqualTo(2);
234+
235+
List<LoggedRequest> requests =
236+
serveEvents.stream()
237+
.map(se -> se.getRequest())
238+
.filter(req -> req.getMethod().getName().equals("GET"))
239+
.filter(req -> req.getUrl().equals("/api/client/streaming"))
240+
.collect(Collectors.toList());
241+
242+
LoggedRequest reconnectionRequest = requests.get(1);
243+
// The important part - a reconnection should never include the last-event-id header so that
244+
// we get a fresh hydration
245+
assertThat(reconnectionRequest.getHeader("Last-Event-ID")).isNull();
246+
}
247+
181248
private static class TestSubscriber implements UnleashSubscriber {
182249
private final CountDownLatch togglesFetchedLatch = new CountDownLatch(1);
183250
private final CountDownLatch multipleTogglesFetchedLatch = new CountDownLatch(2);

0 commit comments

Comments
 (0)