3
3
import com .devcycle .sdk .server .common .api .IDevCycleApi ;
4
4
import com .devcycle .sdk .server .common .exception .DevCycleException ;
5
5
import 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 .*;
9
7
import com .devcycle .sdk .server .local .api .DevCycleLocalApiClient ;
10
8
import com .devcycle .sdk .server .local .bucketing .LocalBucketing ;
11
9
import com .devcycle .sdk .server .local .model .DevCycleLocalOptions ;
12
10
import com .fasterxml .jackson .core .JsonParseException ;
13
11
import com .fasterxml .jackson .core .JsonProcessingException ;
14
12
import com .fasterxml .jackson .databind .ObjectMapper ;
13
+ import com .launchdarkly .eventsource .FaultEvent ;
14
+ import com .launchdarkly .eventsource .MessageEvent ;
15
+ import com .launchdarkly .eventsource .StartedEvent ;
15
16
import retrofit2 .Call ;
16
17
import retrofit2 .Response ;
17
18
18
19
import java .io .IOException ;
20
+ import java .net .URI ;
21
+ import java .net .URISyntaxException ;
19
22
import java .time .ZonedDateTime ;
20
23
import java .time .format .DateTimeFormatter ;
21
24
import java .util .concurrent .Executors ;
@@ -26,46 +29,52 @@ public final class EnvironmentConfigManager {
26
29
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper ();
27
30
private static final int DEFAULT_POLL_INTERVAL_MS = 30000 ;
28
31
private static final int MIN_INTERVALS_MS = 1000 ;
29
- private final ScheduledExecutorService scheduler = Executors . newScheduledThreadPool ( 1 , new DaemonThreadFactory ()) ;
32
+ private ScheduledExecutorService scheduler ;
30
33
private final IDevCycleApi configApiClient ;
31
34
private final LocalBucketing localBucketing ;
35
+ private SSEManager sseManager ;
36
+ private boolean isSSEConnected = false ;
37
+ private final DevCycleLocalOptions options ;
32
38
33
39
private ProjectConfig config ;
34
40
private String configETag = "" ;
35
41
private String configLastModified = "" ;
36
42
37
43
private final String sdkKey ;
38
44
private final int pollingIntervalMS ;
45
+ private static final int pollingIntervalSSEMS = 15 * 60 * 60 * 1000 ;
39
46
private boolean pollingEnabled = true ;
40
47
41
48
public EnvironmentConfigManager (String sdkKey , LocalBucketing localBucketing , DevCycleLocalOptions options ) {
42
49
this .sdkKey = sdkKey ;
43
50
this .localBucketing = localBucketing ;
51
+ this .options = options ;
44
52
45
53
configApiClient = new DevCycleLocalApiClient (sdkKey , options ).initialize ();
46
54
47
55
int configPollingIntervalMS = options .getConfigPollingIntervalMS ();
48
56
pollingIntervalMS = configPollingIntervalMS >= MIN_INTERVALS_MS ? configPollingIntervalMS
49
57
: DEFAULT_POLL_INTERVAL_MS ;
50
58
51
- setupScheduler ();
59
+ scheduler = setupScheduler ();
60
+ scheduler .scheduleAtFixedRate (getConfigRunnable , 0 , this .pollingIntervalMS , TimeUnit .MILLISECONDS );
52
61
}
53
62
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 ( );
63
72
}
73
+ } catch (DevCycleException e ) {
74
+ DevCycleLogger .error ("Failed to load config: " + e .getMessage ());
64
75
}
65
- };
66
-
67
- scheduler .scheduleAtFixedRate (getConfigRunnable , 0 , this .pollingIntervalMS , TimeUnit .MILLISECONDS );
68
- }
76
+ }
77
+ };
69
78
70
79
public boolean isConfigInitialized () {
71
80
return config != null ;
@@ -74,9 +83,57 @@ public boolean isConfigInitialized() {
74
83
private ProjectConfig getConfig () throws DevCycleException {
75
84
Call <ProjectConfig > config = this .configApiClient .getConfig (this .sdkKey , this .configETag , this .configLastModified );
76
85
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
+ }
77
97
return this .config ;
78
98
}
79
99
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
+
80
137
private ProjectConfig getResponseWithRetries (Call <ProjectConfig > call , int maxRetries ) throws DevCycleException {
81
138
// attempt 0 is the initial request, attempt > 0 are all retries
82
139
int attempt = 0 ;
@@ -206,6 +263,9 @@ private void stopPolling() {
206
263
}
207
264
208
265
public void cleanup () {
266
+ if (sseManager != null ) {
267
+ sseManager .close ();
268
+ }
209
269
stopPolling ();
210
270
}
211
271
}
0 commit comments