1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

Extended java-client to support the following:

* Unleash - the actual unleash client exposed to the clients.
* Toggle - represents a concrete feature toggle.
* ToggleRepository - the logic to communicate with the unleash-server.
* Strategy - implements the logic associated with strategies. I have also included to common strategies: "Default" and "Unknown".

related to issue #17.
This commit is contained in:
Ivar Conradi Østhus 2014-10-21 15:45:49 +02:00
parent 2ca8291eac
commit 0ed61a1f0f
12 changed files with 453 additions and 3 deletions

View File

@ -19,6 +19,17 @@
</properties>
<dependencies>
<!--
TODO: we should write our own manual json serialize/deserializer
to avvoid this dependency
-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
@ -31,12 +42,22 @@
<version>4.3.1</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,33 @@
package no.finn.unleash;
import java.util.Map;
public final class Toggle {
private final String name;
private final boolean enabled;
private final String strategy;
private final Map<String, String> parameters;
public Toggle(String name, boolean enabled, String strategy, Map<String, String> parameters) {
this.name = name;
this.enabled = enabled;
this.strategy = strategy;
this.parameters = parameters;
}
public String getName() {
return name;
}
public boolean isEnabled() {
return enabled;
}
public String getStrategy() {
return strategy;
}
public Map<String, String> getParameters() {
return parameters;
}
}

View File

@ -1,4 +1,66 @@
package no.finn.unleash;
import java.util.HashMap;
import java.util.Map;
import no.finn.unleash.repository.ToggleException;
import no.finn.unleash.repository.ToggleRepository;
import no.finn.unleash.strategy.DefaultStrategy;
import no.finn.unleash.strategy.Strategy;
import no.finn.unleash.strategy.UnknownStrategy;
public final class Unleash {
private final ToggleRepository toggleRepository;
private final Map<String, Strategy> strategyMap;
private final UnknownStrategy unknownStrategy = new UnknownStrategy();
private final DefaultStrategy defaultStrategy = new DefaultStrategy();
public Unleash(ToggleRepository toggleRepository, Strategy... strategies) {
this.toggleRepository = toggleRepository;
this.strategyMap = buildStrategyMap(strategies);
}
public boolean isEnabled(final String toggleName) {
return isEnabled(toggleName, false);
}
public boolean isEnabled(final String toggleName, final boolean defaultSetting) {
try {
Toggle toggle = toggleRepository.getToggle(toggleName);
if(toggle == null) {
return defaultSetting;
}
Strategy strategy = getStrategy(toggle.getStrategy());
return toggle.isEnabled() && strategy.isEnabled(toggle.getParameters());
} catch (ToggleException rx) {
return defaultSetting;
}
}
private Map<String, Strategy> buildStrategyMap(Strategy[] strategies) {
Map<String, Strategy> map = new HashMap<>();
map.put(defaultStrategy.getName(), defaultStrategy);
if(strategies != null) {
for(Strategy strategy : strategies) {
map.put(strategy.getName(), strategy);
}
}
return map;
}
private Strategy getStrategy(String strategy) {
if(strategyMap.containsKey(strategy)) {
return strategyMap.get(strategy);
} else {
return unknownStrategy;
}
}
}

View File

@ -0,0 +1,57 @@
package no.finn.unleash.repository;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import no.finn.unleash.Toggle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
public class HTTPToggleRepository implements ToggleRepository {
private static final Log LOG = LogFactory.getLog(HTTPToggleRepository.class);
private final HttpClient httpClient;
private final String serverEndpoint;
public HTTPToggleRepository(final String serverEndpoint) {
this.serverEndpoint = serverEndpoint;
this.httpClient = HttpClients.createDefault();
}
@Override
public Toggle getToggle(final String name) throws ToggleException {
try {
for (Toggle toggle : fetchToggles()) {
if (name.equals(toggle.getName())) {
return toggle;
}
}
} catch (IOException e) {
LOG.warn("Could not fetch toggles via HTTP", e);
throw new ToggleException("Could not fetch toggles via HTTP");
}
throw new ToggleException("unknown toggle: " + name);
}
@Override
public Collection<Toggle> getToggles() {
try {
return fetchToggles();
} catch (IOException e) {
LOG.warn("Could not fetch toggles via HTTP");
throw new ToggleException("Could not fetch toggles via HTTP");
}
}
private List<Toggle> fetchToggles() throws IOException {
HttpResponse httpResponse = httpClient.execute(new HttpGet(serverEndpoint));
final String jsonString = EntityUtils.toString(httpResponse.getEntity());
return JsonParser.toListOfToggles(jsonString);
}
}

View File

@ -0,0 +1,27 @@
package no.finn.unleash.repository;
import java.util.Collection;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import no.finn.unleash.Toggle;
public final class JsonParser {
public static Toggle toToggle(String jsonString) {
Gson gson = new GsonBuilder().create();
return gson.fromJson(jsonString, Toggle.class);
}
public static String toJsonString(Collection<Toggle> toggles) {
Gson gson = new GsonBuilder().create();
return gson.toJson(toggles);
}
public static List<Toggle> toListOfToggles(String jsonString) {
Gson gson = new GsonBuilder().create();
return gson.fromJson(jsonString, new TypeToken<List<Toggle>>(){}.getType());
}
}

View File

@ -0,0 +1,136 @@
package no.finn.unleash.repository;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import no.finn.unleash.Toggle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class PollingToggleRepository implements ToggleRepository {
private static final Log LOG = LogFactory.getLog(PollingToggleRepository.class);
private static final ScheduledThreadPoolExecutor TIMER = new ScheduledThreadPoolExecutor(
1,
new ThreadFactory() {
@Override
public Thread newThread(final Runnable r) {
Thread thread = Executors.defaultThreadFactory().newThread(r);
thread.setName("unleash-toggle-repository");
thread.setDaemon(true);
return thread;
}
});
static {
TIMER.setRemoveOnCancelPolicy(true);
}
private final ToggleRepository toggleRepository;
private final int pollIntervalSeconds;
private Map<String, Toggle> togglesCache;
public PollingToggleRepository(final ToggleRepository toggleRepository, final int pollIntervalSeconds){
this.toggleRepository = toggleRepository;
this.pollIntervalSeconds = pollIntervalSeconds;
this.togglesCache = new HashMap<>();
updateTogglesCache();
startBackgroundPolling();
}
@Override
public Toggle getToggle(final String name){
return togglesCache.get(name);
}
@Override
public Collection<Toggle> getToggles() {
return Collections.unmodifiableCollection(togglesCache.values());
}
private void updateTogglesCache() {
try {
Map<String, Toggle> freshToggleMap = new HashMap<>();
for(Toggle toggle : fetchToggles()) {
freshToggleMap.put(toggle.getName(), toggle);
}
this.togglesCache = Collections.unmodifiableMap(freshToggleMap);
} catch (ToggleException e) {
//Do nothing
}
}
private ScheduledFuture startBackgroundPolling() {
try {
return TIMER.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
updateTogglesCache();
}
}, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS);
} catch (RejectedExecutionException ex) {
LOG.error("Unleash background task crashed");
return null;
}
}
private Collection<Toggle> fetchToggles() throws ToggleException {
try {
Collection<Toggle> toggles = toggleRepository.getToggles();
storeRepoAsTempFile(JsonParser.toJsonString(toggles));
return toggles;
} catch (ToggleException ex) {
if(togglesCache.isEmpty()) {
return loadFromTempFile();
}
throw ex;
}
}
private List<Toggle> loadFromTempFile() throws ToggleException {
LOG.info("Unleash will try to load feature toggle states from temporary backup");
try(FileReader reader = new FileReader(pathToTmpBackupFile())) {
BufferedReader br = new BufferedReader(reader);
StringBuilder builder = new StringBuilder();
String line;
while((line = br.readLine()) != null) {
builder.append(line);
}
return JsonParser.toListOfToggles(builder.toString());
} catch (IOException e) {
LOG.error("Unleash was unable to feature toggle repo from temporary backup: " + pathToTmpBackupFile());
throw new ToggleException("Unleash was unable to feature toggle states from temporary backup");
}
}
private void storeRepoAsTempFile(final String serverResponse) {
try(FileWriter writer = new FileWriter(pathToTmpBackupFile())) {
writer.write(serverResponse);
} catch (IOException e) {
e.printStackTrace();
}
}
private static String pathToTmpBackupFile() {
return System.getProperty("java.io.tmpdir") + File.separatorChar + "unleash-repo.json";
}
}

View File

@ -0,0 +1,7 @@
package no.finn.unleash.repository;
public class ToggleException extends RuntimeException {
public ToggleException(String message) {
super(message);
}
}

View File

@ -0,0 +1,10 @@
package no.finn.unleash.repository;
import java.util.Collection;
import no.finn.unleash.Toggle;
public interface ToggleRepository {
Toggle getToggle(String name) throws ToggleException;
Collection<Toggle> getToggles() throws ToggleException;
}

View File

@ -0,0 +1,17 @@
package no.finn.unleash.strategy;
import java.util.Map;
public final class DefaultStrategy implements Strategy {
public static final String NAME = "default";
@Override
public String getName() {
return NAME;
}
@Override
public boolean isEnabled(Map<String, String> parameters) {
return true;
}
}

View File

@ -0,0 +1,9 @@
package no.finn.unleash.strategy;
import java.util.Map;
public interface Strategy {
String getName();
boolean isEnabled(Map<String, String> parameters);
}

View File

@ -0,0 +1,17 @@
package no.finn.unleash.strategy;
import java.util.Map;
public final class UnknownStrategy implements Strategy {
public static final String NAME = "unknown";
@Override
public String getName() {
return NAME;
}
@Override
public boolean isEnabled(Map<String, String> parameters) {
return false;
}
}

View File

@ -1,13 +1,67 @@
package no.finn.unleash;
import no.finn.unleash.repository.ToggleException;
import no.finn.unleash.repository.ToggleRepository;
import no.finn.unleash.strategy.Strategy;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;
public class UnleashTest {
private ToggleRepository toggleRepository;
private Unleash unleash;
@Before
public void setup() {
toggleRepository = mock(ToggleRepository.class);
unleash = new Unleash(toggleRepository);
}
@Test
public void helloWorld() {
assertTrue(true);
public void known_toogle_and_strategy_should_be_active() {
when(toggleRepository.getToggle("test")).thenReturn(new Toggle("test", true, "default", null));
assertThat(unleash.isEnabled("test"), is(true));
}
@Test
public void unknown_strategy_should_be_considered_inactive() {
when(toggleRepository.getToggle("test")).thenReturn(new Toggle("test", true, "whoot_strat", null));
assertThat(unleash.isEnabled("test"), is(false));
}
@Test
public void failing_repository_should_obey_default() {
when(toggleRepository.getToggle("test")).thenThrow(new ToggleException("service down"));
assertThat(unleash.isEnabled("test", true), is(true));
}
@Test
public void unknown_feature_should_be_considered_inactive() {
when(toggleRepository.getToggle("test")).thenReturn(null);
assertThat(unleash.isEnabled("test"), is(false));
}
@Test
public void should_register_custom_strategies() {
//custom strategy
Strategy customStrategy = mock(Strategy.class);
when(customStrategy.getName()).thenReturn("custom");
//register custom strategy
unleash = new Unleash(toggleRepository, customStrategy);
when(toggleRepository.getToggle("test")).thenReturn(new Toggle("test", true, "custom", null));
unleash.isEnabled("test");
verify(customStrategy, times(1)).isEnabled(anyMap());
}
}