Skip to content

Commit ab7c178

Browse files
authored
fix: make empty config default instead of null (#51)
* make default configuration empty set.
1 parent 2d4ae83 commit ab7c178

File tree

4 files changed

+40
-29
lines changed

4 files changed

+40
-29
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group = 'cloud.eppo'
9-
version = '3.3.3-SNAPSHOT'
9+
version = '3.3.4'
1010
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
1111

1212
java {

src/main/java/cloud/eppo/ConfigurationStore.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,17 @@
77
/** Memory-only configuration store. */
88
public class ConfigurationStore implements IConfigurationStore {
99

10-
private volatile Configuration configuration;
10+
// this is the fallback value if no configuration is provided (i.e. by fetch or initial config).
11+
@NotNull private volatile Configuration configuration = Configuration.emptyConfig();
1112

12-
public ConfigurationStore() {
13-
configuration = null;
14-
}
13+
public ConfigurationStore() {}
1514

1615
public CompletableFuture<Void> saveConfiguration(@NotNull final Configuration configuration) {
1716
this.configuration = configuration;
1817
return CompletableFuture.completedFuture(null);
1918
}
2019

21-
public Configuration getConfiguration() {
20+
@NotNull public Configuration getConfiguration() {
2221
return configuration;
2322
}
2423
}

src/main/java/cloud/eppo/IConfigurationStore.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import cloud.eppo.api.Configuration;
44
import java.util.concurrent.CompletableFuture;
5+
import org.jetbrains.annotations.NotNull;
56

67
/**
78
* Common interface for extensions of this SDK to support caching and other strategies for
89
* persisting configuration data across sessions.
910
*/
1011
public interface IConfigurationStore {
11-
Configuration getConfiguration();
12+
@NotNull Configuration getConfiguration();
1213

1314
CompletableFuture<Void> saveConfiguration(Configuration configuration);
1415
}

src/test/java/cloud/eppo/ConfigurationRequestorTest.java

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package cloud.eppo;
22

3-
import static org.junit.jupiter.api.Assertions.assertNotNull;
4-
import static org.junit.jupiter.api.Assertions.assertNull;
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.ArgumentMatchers.any;
55
import static org.mockito.Mockito.mock;
66
import static org.mockito.Mockito.when;
77

@@ -12,6 +12,7 @@
1212
import java.util.concurrent.CompletableFuture;
1313
import org.apache.commons.io.FileUtils;
1414
import org.junit.jupiter.api.Test;
15+
import org.mockito.Mockito;
1516

1617
public class ConfigurationRequestorTest {
1718
private final File initialFlagConfigFile =
@@ -21,7 +22,7 @@ public class ConfigurationRequestorTest {
2122

2223
@Test
2324
public void testInitialConfigurationFuture() throws IOException {
24-
IConfigurationStore configStore = new ConfigurationStore();
25+
IConfigurationStore configStore = Mockito.spy(new ConfigurationStore());
2526
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
2627

2728
ConfigurationRequestor requestor =
@@ -32,17 +33,20 @@ public void testInitialConfigurationFuture() throws IOException {
3233

3334
requestor.setInitialConfiguration(futureConfig);
3435

35-
assertNull(configStore.getConfiguration());
36+
// verify config is empty to start
37+
assertTrue(configStore.getConfiguration().isEmpty());
38+
Mockito.verify(configStore, Mockito.times(0)).saveConfiguration(any());
3639

3740
futureConfig.complete(Configuration.builder(flagConfig, false).build());
3841

39-
assertNotNull(configStore.getConfiguration());
42+
assertFalse(configStore.getConfiguration().isEmpty());
43+
Mockito.verify(configStore, Mockito.times(1)).saveConfiguration(any());
4044
assertNotNull(configStore.getConfiguration().getFlag("numeric_flag"));
4145
}
4246

4347
@Test
4448
public void testInitialConfigurationDoesntClobberFetch() throws IOException {
45-
IConfigurationStore configStore = new ConfigurationStore();
49+
IConfigurationStore configStore = Mockito.spy(new ConfigurationStore());
4650
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
4751

4852
ConfigurationRequestor requestor =
@@ -56,10 +60,11 @@ public void testInitialConfigurationDoesntClobberFetch() throws IOException {
5660

5761
when(mockHttpClient.getAsync("/api/flag-config/v1/config")).thenReturn(configFetchFuture);
5862

59-
// Set initial config and verify that the config is still null as the inital config future has
60-
// not completed yet.
63+
// Set initial config and verify that no config has been set yet.
6164
requestor.setInitialConfiguration(initialConfigFuture);
62-
assertNull(configStore.getConfiguration());
65+
66+
assertTrue(configStore.getConfiguration().isEmpty());
67+
Mockito.verify(configStore, Mockito.times(0)).saveConfiguration(any());
6368

6469
// The initial config contains only one flag keyed `numeric_flag`. The fetch response has only
6570
// one flag keyed
@@ -71,7 +76,8 @@ public void testInitialConfigurationDoesntClobberFetch() throws IOException {
7176
configFetchFuture.complete(fetchedFlagConfig.getBytes(StandardCharsets.UTF_8));
7277
initialConfigFuture.complete(new Configuration.Builder(flagConfig, false).build());
7378

74-
assertNotNull(configStore.getConfiguration());
79+
assertFalse(configStore.getConfiguration().isEmpty());
80+
Mockito.verify(configStore, Mockito.times(1)).saveConfiguration(any());
7581

7682
// `numeric_flag` is only in the cache which should have been ignored.
7783
assertNull(configStore.getConfiguration().getFlag("numeric_flag"));
@@ -82,7 +88,7 @@ public void testInitialConfigurationDoesntClobberFetch() throws IOException {
8288

8389
@Test
8490
public void testBrokenFetchDoesntClobberCache() throws IOException {
85-
IConfigurationStore configStore = new ConfigurationStore();
91+
IConfigurationStore configStore = Mockito.spy(new ConfigurationStore());
8692
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
8793

8894
ConfigurationRequestor requestor =
@@ -94,10 +100,11 @@ public void testBrokenFetchDoesntClobberCache() throws IOException {
94100

95101
when(mockHttpClient.getAsync("/api/flag-config/v1/config")).thenReturn(configFetchFuture);
96102

97-
// Set initial config and verify that the config is still null as the inital config future has
98-
// not completed yet.
103+
// Set initial config and verify that no config has been set yet.
99104
requestor.setInitialConfiguration(initialConfigFuture);
100-
assertNull(configStore.getConfiguration());
105+
106+
assertTrue(configStore.getConfiguration().isEmpty());
107+
Mockito.verify(configStore, Mockito.times(0)).saveConfiguration(any());
101108

102109
requestor.fetchAndSaveFromRemoteAsync();
103110

@@ -107,7 +114,8 @@ public void testBrokenFetchDoesntClobberCache() throws IOException {
107114
// Error out the fetch
108115
configFetchFuture.completeExceptionally(new Exception("Intentional exception"));
109116

110-
assertNotNull(configStore.getConfiguration());
117+
assertFalse(configStore.getConfiguration().isEmpty());
118+
Mockito.verify(configStore, Mockito.times(1)).saveConfiguration(any());
111119

112120
// `numeric_flag` is only in the cache which should be available
113121
assertNotNull(configStore.getConfiguration().getFlag("numeric_flag"));
@@ -117,7 +125,7 @@ public void testBrokenFetchDoesntClobberCache() throws IOException {
117125

118126
@Test
119127
public void testCacheWritesAfterBrokenFetch() throws IOException {
120-
IConfigurationStore configStore = new ConfigurationStore();
128+
IConfigurationStore configStore = Mockito.spy(new ConfigurationStore());
121129
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
122130

123131
ConfigurationRequestor requestor =
@@ -129,20 +137,23 @@ public void testCacheWritesAfterBrokenFetch() throws IOException {
129137

130138
when(mockHttpClient.getAsync("/api/flag-config/v1/config")).thenReturn(configFetchFuture);
131139

132-
// Set initial config and verify that the config is still null as the inital config future has
133-
// not completed yet.
140+
// Set initial config and verify that no config has been set yet.
134141
requestor.setInitialConfiguration(initialConfigFuture);
135-
assertNull(configStore.getConfiguration());
142+
Mockito.verify(configStore, Mockito.times(0)).saveConfiguration(any());
136143

137-
requestor.fetchAndSaveFromRemoteAsync();
144+
// default configuration is empty config.
145+
assertTrue(configStore.getConfiguration().isEmpty());
138146

139-
// Error out the fetch
147+
// Fetch from remote with an error
148+
requestor.fetchAndSaveFromRemoteAsync();
140149
configFetchFuture.completeExceptionally(new Exception("Intentional exception"));
141150

142151
// Resolve the initial config after the fetch throws an error.
143152
initialConfigFuture.complete(new Configuration.Builder(flagConfig, false).build());
144153

145-
assertNotNull(configStore.getConfiguration());
154+
// Verify that a configuration was saved by the requestor
155+
Mockito.verify(configStore, Mockito.times(1)).saveConfiguration(any());
156+
assertFalse(configStore.getConfiguration().isEmpty());
146157

147158
// `numeric_flag` is only in the cache which should be available
148159
assertNotNull(configStore.getConfiguration().getFlag("numeric_flag"));

0 commit comments

Comments
 (0)