11package com .launchdarkly .openfeature .serverprovider ;
22
3+ import com .launchdarkly .sdk .server .Components ;
34import com .launchdarkly .sdk .server .LDConfig ;
45import com .launchdarkly .sdk .server .interfaces .DataSourceStatusProvider ;
6+ import com .launchdarkly .sdk .server .subsystems .ClientContext ;
7+ import com .launchdarkly .sdk .server .subsystems .ComponentConfigurer ;
8+ import com .launchdarkly .sdk .server .subsystems .DataSource ;
9+ import com .launchdarkly .sdk .server .subsystems .DataSourceUpdateSink ;
510import dev .openfeature .sdk .ImmutableContext ;
611import dev .openfeature .sdk .OpenFeatureAPI ;
712import dev .openfeature .sdk .ProviderEvent ;
813import dev .openfeature .sdk .ProviderState ;
14+ import dev .openfeature .sdk .exceptions .GeneralError ;
915import org .junit .jupiter .api .Test ;
1016
17+ import java .io .IOException ;
18+ import java .time .Duration ;
19+ import java .time .LocalDateTime ;
20+ import java .time .ZoneOffset ;
21+ import java .time .temporal .ChronoUnit ;
22+ import java .util .Timer ;
23+ import java .util .TimerTask ;
24+ import java .util .concurrent .CompletableFuture ;
25+ import java .util .concurrent .ExecutionException ;
26+ import java .util .concurrent .Future ;
27+ import java .util .concurrent .TimeUnit ;
28+ import java .util .concurrent .TimeoutException ;
1129import java .util .concurrent .atomic .AtomicInteger ;
1230
1331import static org .junit .jupiter .api .Assertions .assertDoesNotThrow ;
1432import static org .junit .jupiter .api .Assertions .assertEquals ;
33+ import static org .junit .jupiter .api .Assertions .assertNotNull ;
34+ import static org .junit .jupiter .api .Assertions .assertTrue ;
35+
36+ class DelayedDataSource implements DataSource {
37+ private Duration startDelay ;
38+ private boolean willError ;
39+ private boolean initialized = false ;
40+ private Object lock = new Object ();
41+ DataSourceUpdateSink sink ;
42+
43+ DelayedDataSource (Duration delay , boolean error , DataSourceUpdateSink sink ) {
44+ startDelay = delay ;
45+ willError = error ;
46+ this .sink = sink ;
47+ }
48+
49+ public Future <Void > start () {
50+ var future = new CompletableFuture <Void >();
51+ var timer = new Timer ();
52+ timer .schedule (new TimerTask () {
53+ @ Override
54+ public void run () {
55+ if (!willError ) {
56+ sink .updateStatus (DataSourceStatusProvider .State .VALID , null );
57+ synchronized (lock ) {
58+ initialized = true ;
59+ }
60+ } else {
61+ sink .updateStatus (DataSourceStatusProvider .State .OFF ,
62+ new DataSourceStatusProvider .ErrorInfo (
63+ DataSourceStatusProvider .ErrorKind .NETWORK_ERROR ,
64+ 404 ,
65+ "bad" ,
66+ LocalDateTime .now ().toInstant (ZoneOffset .UTC )));
67+ }
68+ future .complete (null );
69+ }
70+ }, startDelay .toMillis ());
71+
72+ return future ;
73+ }
74+
75+ public boolean isInitialized () {
76+ synchronized (lock ) {
77+ return initialized ;
78+ }
79+ }
80+
81+ public void close () throws IOException {
82+ }
83+ }
84+
85+ class DelayedDataSourceFactory implements ComponentConfigurer <DataSource > {
86+ private Duration startDelay ;
87+ private boolean willError ;
88+
89+ DelayedDataSourceFactory (Duration delay , boolean error ) {
90+ startDelay = delay ;
91+ willError = error ;
92+ }
93+
94+ @ Override
95+ public DataSource build (ClientContext clientContext ) {
96+ return new DelayedDataSource (startDelay , willError , clientContext .getDataSourceUpdateSink ());
97+ }
98+ }
1599
16100/**
17101 * Tests in this suite use a real client instance and the public constructor.
@@ -54,16 +138,18 @@ public void canShutdownAnOfflineClient() {
54138 }
55139
56140 @ Test
57- public void itEmitsReadyEvents () {
141+ public void itEmitsReadyEvents () throws ExecutionException , InterruptedException , TimeoutException {
58142 var provider = new Provider ("fake-key" , new LDConfig .Builder ()
59143 .offline (true ).build ());
60144
61145 var readyCount = new AtomicInteger ();
62146 var errorCount = new AtomicInteger ();
63147 var staleCount = new AtomicInteger ();
148+ CompletableFuture <Boolean > gotReadyEvent = new CompletableFuture <>();
64149
65150 OpenFeatureAPI .getInstance ().on (ProviderEvent .PROVIDER_READY , (detail ) -> {
66151 readyCount .getAndIncrement ();
152+ gotReadyEvent .complete (true );
67153 });
68154
69155 OpenFeatureAPI .getInstance ().on (ProviderEvent .PROVIDER_STALE , (detail ) -> {
@@ -76,10 +162,67 @@ public void itEmitsReadyEvents() {
76162
77163 OpenFeatureAPI .getInstance ().setProviderAndWait (provider );
78164
79- OpenFeatureAPI .getInstance ().shutdown ();
80-
165+ assertTrue (gotReadyEvent .get (1000 , TimeUnit .MILLISECONDS ));
81166 assertEquals (1 , readyCount .get ());
82167 assertEquals (0 , staleCount .get ());
83168 assertEquals (0 , errorCount .get ());
169+
170+ OpenFeatureAPI .getInstance ().shutdown ();
171+ }
172+
173+ @ Test
174+ public void itCanHandleClientThatIsNotInitializedImmediately () throws Exception {
175+ var config = new LDConfig .Builder ()
176+ .startWait (Duration .ZERO )
177+ .dataSource (new DelayedDataSourceFactory (Duration .ofMillis (100 ), false ))
178+ .events (Components .noEvents ())
179+ .build ();
180+ var provider = new Provider ("fake-key" , config );
181+ assertEquals (ProviderState .NOT_READY , provider .getState ());
182+
183+ var readyCount = new AtomicInteger ();
184+
185+ OpenFeatureAPI .getInstance ().on (ProviderEvent .PROVIDER_READY , (detail ) -> {
186+ readyCount .getAndIncrement ();
187+ });
188+
189+ OpenFeatureAPI .getInstance ().setProviderAndWait (provider );
190+
191+ OpenFeatureAPI .getInstance ().shutdown ();
192+
193+ assertEquals (ProviderState .READY , provider .getState ());
194+ assertEquals (1 , readyCount .get ());
195+ }
196+
197+ @ Test
198+ public void itCanHandleClientThatIsNotInitializedImmediatelyAndErrors () throws Exception {
199+ var config = new LDConfig .Builder ()
200+ .startWait (Duration .ZERO )
201+ .dataSource (new DelayedDataSourceFactory (Duration .ofMillis (100 ), true ))
202+ .events (Components .noEvents ())
203+ .build ();
204+ var provider = new Provider ("fake-key" , config );
205+ assertEquals (ProviderState .NOT_READY , provider .getState ());
206+
207+ CompletableFuture <Boolean > gotErrorEvent = new CompletableFuture <>();
208+
209+ OpenFeatureAPI .getInstance ().on (ProviderEvent .PROVIDER_ERROR , (detail ) -> {
210+ gotErrorEvent .complete (true );
211+ });
212+
213+ GeneralError error = null ;
214+ try {
215+ OpenFeatureAPI .getInstance ().setProviderAndWait (provider );
216+ } catch (GeneralError e ) {
217+ error = e ;
218+ }
219+
220+ assertNotNull (error );
221+
222+ assertEquals (ProviderState .ERROR , provider .getState ());
223+
224+ assertTrue (gotErrorEvent .get (1000 , TimeUnit .MILLISECONDS ));
225+
226+ OpenFeatureAPI .getInstance ().shutdown ();
84227 }
85228}
0 commit comments