33import com .launchdarkly .eventsource .ConnectStrategy ;
44import com .launchdarkly .eventsource .EventSource ;
55import com .launchdarkly .eventsource .MessageEvent ;
6+ import com .launchdarkly .eventsource .StreamHttpErrorException ;
67import com .launchdarkly .eventsource .background .BackgroundEventHandler ;
78import com .launchdarkly .eventsource .background .BackgroundEventSource ;
89import io .getunleash .UnleashException ;
1516import io .getunleash .util .UnleashConfig ;
1617import java .net .URI ;
1718import java .time .Duration ;
19+ import java .time .Instant ;
1820import okhttp3 .Headers ;
1921import okhttp3 .OkHttpClient ;
2022import org .slf4j .Logger ;
2123import org .slf4j .LoggerFactory ;
2224
2325public class StreamingFeatureFetcherImpl implements FetchWorker {
2426 private static final Logger LOGGER = LoggerFactory .getLogger (StreamingFeatureFetcherImpl .class );
27+ private static final int DEFAULT_MAX_FAILURES = 5 ;
28+ private static final long DEFAULT_FAIL_WINDOW_MS = 60_000L ;
2529
2630 private final UnleashConfig config ;
2731 private final EventDispatcher eventDispatcher ;
2832 private final UnleashEngine engine ;
2933 private final BackupHandler featureBackupHandler ;
34+ private final FailoverStrategy failoverStrategy ;
3035 private boolean ready ;
3136
3237 private volatile BackgroundEventSource eventSource ;
@@ -36,10 +41,25 @@ public StreamingFeatureFetcherImpl(
3641 EventDispatcher eventDispatcher ,
3742 UnleashEngine engine ,
3843 BackupHandler featureBackupHandler ) {
44+ this (
45+ config ,
46+ eventDispatcher ,
47+ engine ,
48+ featureBackupHandler ,
49+ new FailoverStrategy (DEFAULT_MAX_FAILURES , DEFAULT_FAIL_WINDOW_MS ));
50+ }
51+
52+ StreamingFeatureFetcherImpl (
53+ UnleashConfig config ,
54+ EventDispatcher eventDispatcher ,
55+ UnleashEngine engine ,
56+ BackupHandler featureBackupHandler ,
57+ FailoverStrategy failoverStrategy ) {
3958 this .config = config ;
4059 this .eventDispatcher = eventDispatcher ;
4160 this .engine = engine ;
4261 this .featureBackupHandler = featureBackupHandler ;
62+ this .failoverStrategy = failoverStrategy ;
4363 }
4464
4565 public void start () {
@@ -116,6 +136,61 @@ synchronized void handleStreamingUpdate(String data) {
116136 }
117137 }
118138
139+ void handleStreamingError (Throwable throwable ) {
140+ handleFailoverDecision (toFailEvent (throwable ));
141+ }
142+
143+ void handleModeChange (String eventData ) {
144+ if (eventData .equals ("polling" )) {
145+ FailoverStrategy .ServerEvent failEvent =
146+ new FailoverStrategy .ServerEvent (
147+ Instant .now (),
148+ "Server has explicitly requested switching to polling mode" ,
149+ eventData );
150+ handleFailoverDecision (failEvent );
151+ } else {
152+ LOGGER .debug ("Ignoring an unrecognized fetch mode change to {}" , eventData );
153+ }
154+ }
155+
156+ void handleServerDisconnect () {
157+ FailoverStrategy .NetworkEventError failEvent =
158+ new FailoverStrategy .NetworkEventError (
159+ Instant .now (), "Server closed the streaming connection" );
160+ handleFailoverDecision (failEvent );
161+ }
162+
163+ private FailoverStrategy .FailEvent toFailEvent (Throwable throwable ) {
164+ Instant now = Instant .now ();
165+ if (throwable instanceof StreamHttpErrorException ) {
166+ int statusCode = ((StreamHttpErrorException ) throwable ).getCode ();
167+ String message =
168+ throwable .getMessage () != null
169+ ? throwable .getMessage ()
170+ : String .format (
171+ "Streaming failed with http status code %d" , statusCode );
172+ return new FailoverStrategy .HttpStatusError (now , message , statusCode );
173+ }
174+
175+ // Not an HTTP problem so something has likely gone wrong on the network layer
176+ String message =
177+ (throwable != null && throwable .getMessage () != null )
178+ ? throwable .getMessage ()
179+ : "Network error occurred in streaming" ;
180+ return new FailoverStrategy .NetworkEventError (now , message );
181+ }
182+
183+ private void handleFailoverDecision (FailoverStrategy .FailEvent failEvent ) {
184+ boolean shouldFail = failoverStrategy .shouldFailover (failEvent , Instant .now ());
185+ if (shouldFail ) {
186+ LOGGER .warn (
187+ "Streaming failover triggered: {}. Client is switching over to polling mode." ,
188+ failEvent .getMessage ());
189+
190+ // changeToPolling()
191+ }
192+ }
193+
119194 private class UnleashEventHandler implements BackgroundEventHandler {
120195
121196 @ Override
@@ -126,6 +201,7 @@ public void onOpen() throws Exception {
126201 @ Override
127202 public void onClosed () throws Exception {
128203 LOGGER .info ("Streaming connection to Unleash server closed" );
204+ handleServerDisconnect ();
129205 }
130206
131207 @ Override
@@ -141,6 +217,9 @@ public void onMessage(String event, MessageEvent messageEvent) throws Exception
141217 case "unleash-updated" :
142218 handleStreamingUpdate (messageEvent .getData ());
143219 break ;
220+ case "fetch-mode" :
221+ handleModeChange (messageEvent .getData ());
222+ break ;
144223 default :
145224 LOGGER .debug ("Ignoring unknown event type: {}" , event );
146225 }
@@ -154,13 +233,13 @@ public void onMessage(String event, MessageEvent messageEvent) throws Exception
154233 }
155234
156235 @ Override
157- public void onComment (String comment ) throws Exception {}
236+ public void onComment (String comment ) throws Exception {
237+ // gotta implement this because inheritance reasons but we don't care about it
238+ }
158239
159240 @ Override
160241 public void onError (Throwable t ) {
161- UnleashException unleashException =
162- new UnleashException ("Streaming connection error" , t );
163- eventDispatcher .dispatch (unleashException );
242+ handleStreamingError (t );
164243 }
165244 }
166245}
0 commit comments