33import com .devcycle .sdk .server .common .api .IDevCycleApi ;
44import com .devcycle .sdk .server .common .exception .DevCycleException ;
55import com .devcycle .sdk .server .common .logging .DevCycleLogger ;
6- import com .devcycle .sdk .server .common .model .ErrorResponse ;
7- import com .devcycle .sdk .server .common .model .HttpResponseCode ;
8- import com .devcycle .sdk .server .common .model .ProjectConfig ;
6+ import com .devcycle .sdk .server .common .model .*;
97import com .devcycle .sdk .server .local .api .DevCycleLocalApiClient ;
108import com .devcycle .sdk .server .local .bucketing .LocalBucketing ;
119import com .devcycle .sdk .server .local .model .DevCycleLocalOptions ;
1210import com .fasterxml .jackson .core .JsonParseException ;
1311import com .fasterxml .jackson .core .JsonProcessingException ;
1412import com .fasterxml .jackson .databind .ObjectMapper ;
13+ import com .launchdarkly .eventsource .FaultEvent ;
14+ import com .launchdarkly .eventsource .MessageEvent ;
15+ import com .launchdarkly .eventsource .StartedEvent ;
1516import retrofit2 .Call ;
1617import retrofit2 .Response ;
1718
1819import java .io .IOException ;
20+ import java .net .URI ;
21+ import java .net .URISyntaxException ;
1922import java .time .ZonedDateTime ;
2023import java .time .format .DateTimeFormatter ;
2124import java .util .concurrent .Executors ;
@@ -26,46 +29,52 @@ public final class EnvironmentConfigManager {
2629 private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper ();
2730 private static final int DEFAULT_POLL_INTERVAL_MS = 30000 ;
2831 private static final int MIN_INTERVALS_MS = 1000 ;
29- private final ScheduledExecutorService scheduler = Executors . newScheduledThreadPool ( 1 , new DaemonThreadFactory ()) ;
32+ private ScheduledExecutorService scheduler ;
3033 private final IDevCycleApi configApiClient ;
3134 private final LocalBucketing localBucketing ;
35+ private SSEManager sseManager ;
36+ private boolean isSSEConnected = false ;
37+ private final DevCycleLocalOptions options ;
3238
3339 private ProjectConfig config ;
3440 private String configETag = "" ;
3541 private String configLastModified = "" ;
3642
3743 private final String sdkKey ;
3844 private final int pollingIntervalMS ;
45+ private static final int pollingIntervalSSEMS = 15 * 60 * 60 * 1000 ;
3946 private boolean pollingEnabled = true ;
4047
4148 public EnvironmentConfigManager (String sdkKey , LocalBucketing localBucketing , DevCycleLocalOptions options ) {
4249 this .sdkKey = sdkKey ;
4350 this .localBucketing = localBucketing ;
51+ this .options = options ;
4452
4553 configApiClient = new DevCycleLocalApiClient (sdkKey , options ).initialize ();
4654
4755 int configPollingIntervalMS = options .getConfigPollingIntervalMS ();
4856 pollingIntervalMS = configPollingIntervalMS >= MIN_INTERVALS_MS ? configPollingIntervalMS
4957 : DEFAULT_POLL_INTERVAL_MS ;
5058
51- setupScheduler ();
59+ scheduler = setupScheduler ();
60+ scheduler .scheduleAtFixedRate (getConfigRunnable , 0 , this .pollingIntervalMS , TimeUnit .MILLISECONDS );
5261 }
5362
54- private void setupScheduler () {
55- Runnable getConfigRunnable = new Runnable () {
56- public void run () {
57- try {
58- if ( pollingEnabled ) {
59- getConfig ();
60- }
61- } catch ( DevCycleException e ) {
62- DevCycleLogger . error ( "Failed to load config: " + e . getMessage () );
63+ private ScheduledExecutorService setupScheduler () {
64+ return Executors . newScheduledThreadPool ( 1 , new DaemonThreadFactory ());
65+ }
66+
67+ private final Runnable getConfigRunnable = new Runnable ( ) {
68+ public void run () {
69+ try {
70+ if ( pollingEnabled ) {
71+ getConfig ( );
6372 }
73+ } catch (DevCycleException e ) {
74+ DevCycleLogger .error ("Failed to load config: " + e .getMessage ());
6475 }
65- };
66-
67- scheduler .scheduleAtFixedRate (getConfigRunnable , 0 , this .pollingIntervalMS , TimeUnit .MILLISECONDS );
68- }
76+ }
77+ };
6978
7079 public boolean isConfigInitialized () {
7180 return config != null ;
@@ -74,9 +83,57 @@ public boolean isConfigInitialized() {
7483 private ProjectConfig getConfig () throws DevCycleException {
7584 Call <ProjectConfig > config = this .configApiClient .getConfig (this .sdkKey , this .configETag , this .configLastModified );
7685 this .config = getResponseWithRetries (config , 1 );
86+ if (this .options .isEnableBetaRealtimeUpdates ()) {
87+ try {
88+ URI uri = new URI (this .config .getSse ().getHostname () + this .config .getSse ().getPath ());
89+ if (sseManager == null ) {
90+ sseManager = new SSEManager (uri );
91+ }
92+ sseManager .restart (uri , this ::handleSSEMessage , this ::handleSSEError , this ::handleSSEStarted );
93+ } catch (URISyntaxException e ) {
94+ DevCycleLogger .warning ("Failed to create SSEManager: " + e .getMessage ());
95+ }
96+ }
7797 return this .config ;
7898 }
7999
100+ private Void handleSSEMessage (MessageEvent messageEvent ) {
101+ DevCycleLogger .debug ("Received message: " + messageEvent .getData ());
102+ if (!isSSEConnected )
103+ {
104+ handleSSEStarted (null );
105+ }
106+
107+ String data = messageEvent .getData ();
108+ if (data == null || data .isEmpty () || data .equals ("keepalive" )) {
109+ return null ;
110+ }
111+ try {
112+ SSEMessage message = OBJECT_MAPPER .readValue (data , SSEMessage .class );
113+ if (message .getType () == null || message .getType ().equals ("refetchConfig" ) || message .getType ().isEmpty ()) {
114+ DevCycleLogger .debug ("Received refetchConfig message, fetching new config" );
115+ getConfigRunnable .run ();
116+ }
117+ } catch (JsonProcessingException e ) {
118+ DevCycleLogger .warning ("Failed to parse SSE message: " + e .getMessage ());
119+ }
120+ return null ;
121+ }
122+
123+ private Void handleSSEError (FaultEvent faultEvent ) {
124+ DevCycleLogger .warning ("Received error: " + faultEvent .getCause ());
125+ return null ;
126+ }
127+
128+ private Void handleSSEStarted (StartedEvent startedEvent ) {
129+ isSSEConnected = true ;
130+ DevCycleLogger .debug ("SSE Connected - setting polling interval to " + pollingIntervalSSEMS );
131+ scheduler .shutdown ();
132+ scheduler = setupScheduler ();
133+ scheduler .scheduleAtFixedRate (getConfigRunnable , 0 , pollingIntervalSSEMS , TimeUnit .MILLISECONDS );
134+ return null ;
135+ }
136+
80137 private ProjectConfig getResponseWithRetries (Call <ProjectConfig > call , int maxRetries ) throws DevCycleException {
81138 // attempt 0 is the initial request, attempt > 0 are all retries
82139 int attempt = 0 ;
@@ -206,6 +263,9 @@ private void stopPolling() {
206263 }
207264
208265 public void cleanup () {
266+ if (sseManager != null ) {
267+ sseManager .close ();
268+ }
209269 stopPolling ();
210270 }
211271}
0 commit comments