diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 3031d61..b54d895 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -210,7 +210,10 @@ private void handleDataSourceStatus(DataSourceStatusProvider.Status res, Complet // Our client/provider cannot be restarted, so we just go to error. setState(ProviderState.ERROR); completer.complete(false); - emitProviderError(ProviderEventDetails.builder().message("Provider shutdown").build()); + var message = res.getLastError() != null + ? res.getLastError().toString() + : "the provider has encountered a permanent error or has been shutdown"; + emitProviderError(ProviderEventDetails.builder().message(message).build()); } } } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java index 5f4304f..fafd372 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java @@ -37,13 +37,26 @@ class DelayedDataSource implements DataSource { private Duration startDelay; private boolean willError; + private boolean errorAfterInitialization; + private boolean useHttpError; private boolean initialized = false; private Object lock = new Object(); DataSourceUpdateSink sink; DelayedDataSource(Duration delay, boolean error, DataSourceUpdateSink sink) { + this(delay, error, false, false, sink); + } + + DelayedDataSource(Duration delay, boolean error, boolean errorAfterInitialization, DataSourceUpdateSink sink) { + this(delay, error, errorAfterInitialization, false, sink); + } + + DelayedDataSource(Duration delay, boolean error, boolean errorAfterInitialization, boolean useHttpError, + DataSourceUpdateSink sink) { startDelay = delay; willError = error; + this.errorAfterInitialization = errorAfterInitialization; + this.useHttpError = useHttpError; this.sink = sink; } @@ -58,13 +71,11 @@ public void run() { synchronized (lock) { initialized = true; } + if (errorAfterInitialization) { + sink.updateStatus(DataSourceStatusProvider.State.OFF, errorInfo()); + } } else { - sink.updateStatus(DataSourceStatusProvider.State.OFF, - new DataSourceStatusProvider.ErrorInfo( - DataSourceStatusProvider.ErrorKind.NETWORK_ERROR, - 404, - "bad", - LocalDateTime.now().toInstant(ZoneOffset.UTC))); + sink.updateStatus(DataSourceStatusProvider.State.OFF, errorInfo()); } future.complete(null); } @@ -73,6 +84,16 @@ public void run() { return future; } + private DataSourceStatusProvider.ErrorInfo errorInfo() { + return useHttpError + ? DataSourceStatusProvider.ErrorInfo.fromHttpError(401) + : new DataSourceStatusProvider.ErrorInfo( + DataSourceStatusProvider.ErrorKind.NETWORK_ERROR, + 404, + "bad", + LocalDateTime.now().toInstant(ZoneOffset.UTC)); + } + public boolean isInitialized() { synchronized (lock) { return initialized; @@ -86,15 +107,32 @@ public void close() throws IOException { class DelayedDataSourceFactory implements ComponentConfigurer { private Duration startDelay; private boolean willError; + private boolean errorAfterInitialization; + private boolean useHttpError; DelayedDataSourceFactory(Duration delay, boolean error) { + this(delay, error, false); + } + + DelayedDataSourceFactory(Duration delay, boolean error, boolean errorAfterInitialization) { + this(delay, error, errorAfterInitialization, false); + } + + DelayedDataSourceFactory(Duration delay, boolean error, boolean errorAfterInitialization, boolean useHttpError) { startDelay = delay; willError = error; + this.errorAfterInitialization = errorAfterInitialization; + this.useHttpError = useHttpError; } @Override public DataSource build(ClientContext clientContext) { - return new DelayedDataSource(startDelay, willError, clientContext.getDataSourceUpdateSink()); + return new DelayedDataSource( + startDelay, + willError, + errorAfterInitialization, + useHttpError, + clientContext.getDataSourceUpdateSink()); } } @@ -221,4 +259,47 @@ public void itCanHandleClientThatIsNotInitializedImmediatelyAndErrors() throws E assertTrue(gotErrorEvent.get(1000, TimeUnit.MILLISECONDS)); } + + @Test + public void itIncludesTheDataSourceErrorInErrorEvents() throws Exception { + var config = new LDConfig.Builder() + .startWait(Duration.ZERO) + .dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), false, true)) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config); + CompletableFuture errorMessage = new CompletableFuture<>(); + + OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> { + errorMessage.complete(detail.getMessage()); + }); + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + + var message = errorMessage.get(1000, TimeUnit.MILLISECONDS); + assertTrue(message.contains("404")); + assertTrue(message.contains("bad")); + } + + @Test + public void itIncludesHttpDataSourceErrorInErrorEvents() throws Exception { + var config = new LDConfig.Builder() + .startWait(Duration.ZERO) + .dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), false, true, true)) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config); + CompletableFuture errorMessage = new CompletableFuture<>(); + + OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> { + errorMessage.complete(detail.getMessage()); + }); + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + + var message = errorMessage.get(1000, TimeUnit.MILLISECONDS); + assertNotNull(message); + assertTrue(!message.isEmpty()); + assertTrue(message.contains("401")); + } }