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

#30 , refactoring

This commit is contained in:
svelovla 2014-10-28 14:18:06 +01:00 committed by Ivar Conradi Østhus
parent ed27d891b5
commit 06b450da7b
26 changed files with 158 additions and 410 deletions

View File

@ -36,12 +36,6 @@
<version>${version.log4j2}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.1</version>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
@ -57,6 +51,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${version.log4j2}</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@ -1,35 +1,32 @@
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.Collections;
package no.finn.unleash;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
class FeatureToggleBackupFileHandler {
import java.io.*;
import java.util.Collections;
class BackupFileHandler {
private static final Logger LOG = LogManager.getLogger();
private static final String BACKUP_FILE =
System.getProperty("java.io.tmpdir") + File.separatorChar + "unleash-repo.json";
ToggleCollection readBackupFile() {
ToggleCollection read() {
LOG.info("Unleash will try to load feature toggle states from temporary backup");
try(FileReader reader = new FileReader(BACKUP_FILE)) {
try (FileReader reader = new FileReader(BACKUP_FILE)) {
BufferedReader br = new BufferedReader(reader);
return JsonToggleParser.collectionFormJson(br);
return JsonToggleParser.fromJson(br);
} catch (FileNotFoundException e) {
LOG.warn("Unable to locate backup file:'{}'", BACKUP_FILE, e);
} catch (IOException e) {
//TODO: error if file corrupt, warning if file not found.
LOG.warn("Unleash was unable to feature toggle states from temporary backup", e);
return new ToggleCollection(Collections.emptyList());
LOG.error("Failed to read backup file:'{}'", BACKUP_FILE, e);
}
return new ToggleCollection(Collections.emptyList());
}
void write(ToggleCollection toggleCollection) {
try(FileWriter writer = new FileWriter(BACKUP_FILE)) {
try (FileWriter writer = new FileWriter(BACKUP_FILE)) {
writer.write(JsonToggleParser.toJsonString(toggleCollection));
} catch (IOException e) {
LOG.warn("Unleash was unable to backup feature toggles to file: {}", BACKUP_FILE, e);

View File

@ -1,8 +1,8 @@
package no.finn.unleash.strategy;
package no.finn.unleash;
import java.util.Map;
public final class DefaultStrategy implements Strategy {
final class DefaultStrategy implements Strategy {
public static final String NAME = "default";
@Override

View File

@ -1,18 +1,11 @@
package no.finn.unleash.repository;
package no.finn.unleash;
import java.net.URI;
import java.util.Collection;
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.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.URI;
import java.util.concurrent.*;
public final class FeatureToggleRepository implements ToggleRepository {
private static final Logger LOG = LogManager.getLogger();
@ -32,16 +25,16 @@ public final class FeatureToggleRepository implements ToggleRepository {
TIMER.setRemoveOnCancelPolicy(true);
}
private final FeatureToggleBackupFileHandler featureToggleBackupFileHandler;
private final BackupFileHandler featureToggleBackupFileHandler;
private final ToggleFetcher toggleFetcher;
private ToggleCollection toggleCollection;
public FeatureToggleRepository(URI featuresUri, long pollIntervalSeconds) {
featureToggleBackupFileHandler = new FeatureToggleBackupFileHandler();
featureToggleBackupFileHandler = new BackupFileHandler();
toggleFetcher = new HttpToggleFetcher(featuresUri);
toggleCollection = featureToggleBackupFileHandler.readBackupFile();
toggleCollection = featureToggleBackupFileHandler.read();
startBackgroundPolling(pollIntervalSeconds);
}
@ -50,26 +43,25 @@ public final class FeatureToggleRepository implements ToggleRepository {
return TIMER.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
ToggleResponse response = toggleFetcher.fetchToggles();
if(response.getStatus() == ToggleResponse.Status.CHANGED) {
featureToggleBackupFileHandler.write(toggleCollection);
toggleCollection = response.getToggleCollection();
try {
Response response = toggleFetcher.fetchToggles();
if (response.getStatus() == Response.Status.CHANGED) {
featureToggleBackupFileHandler.write(toggleCollection);
toggleCollection = response.getToggleCollection();
}
} catch (UnleashException e) {
LOG.warn("Could not refresh feature toggles", e);
}
}
}, pollIntervalSeconds, pollIntervalSeconds, TimeUnit.SECONDS);
} catch (RejectedExecutionException ex) {
LOG.error("Unleash background task crashed");
LOG.error("Unleash background task crashed", ex);
return null;
}
}
@Override
public Toggle getToggle(String name) throws ToggleException {
public Toggle getToggle(String name) {
return toggleCollection.getToggle(name);
}
@Override
public Collection<Toggle> getToggles() throws ToggleException {
return null;
}
}

View File

@ -1,4 +1,4 @@
package no.finn.unleash.repository;
package no.finn.unleash;
import java.io.BufferedReader;
import java.io.IOException;
@ -10,7 +10,7 @@ import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public final class HttpToggleFetcher implements ToggleFetcher {
final class HttpToggleFetcher implements ToggleFetcher {
public static final int CONNECT_TIMEOUT = 10000;
private String etag = null;
@ -20,12 +20,12 @@ public final class HttpToggleFetcher implements ToggleFetcher {
try {
toggleUrl = repo.toURL();
} catch (MalformedURLException ex) {
throw new IllegalArgumentException("Invalid repo uri", ex);
throw new UnleashException("Invalid repo uri", ex);
}
}
@Override
public ToggleResponse fetchToggles() throws ToggleException {
public Response fetchToggles() throws UnleashException {
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) toggleUrl.openConnection();
@ -38,10 +38,10 @@ public final class HttpToggleFetcher implements ToggleFetcher {
if(responseCode < 300) {
return getToggleResponse(connection);
} else {
return new ToggleResponse(ToggleResponse.Status.NOT_CHANGED);
return new Response(Response.Status.NOT_CHANGED);
}
} catch (IOException e) {
throw new ToggleException("Could not fetch toggles", e);
throw new UnleashException("Could not fetch toggles", e);
} finally {
if(connection != null) {
connection.disconnect();
@ -49,14 +49,14 @@ public final class HttpToggleFetcher implements ToggleFetcher {
}
}
private ToggleResponse getToggleResponse(HttpURLConnection request) throws IOException {
private Response getToggleResponse(HttpURLConnection request) throws IOException {
etag = request.getHeaderField("ETag");
try(BufferedReader reader = new BufferedReader(
new InputStreamReader((InputStream) request.getContent(), StandardCharsets.UTF_8))) {
ToggleCollection toggles = JsonToggleParser.collectionFormJson(reader);
return new ToggleResponse(ToggleResponse.Status.CHANGED, toggles);
ToggleCollection toggles = JsonToggleParser.fromJson(reader);
return new Response(Response.Status.CHANGED, toggles);
}
}
}

View File

@ -0,0 +1,29 @@
package no.finn.unleash;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.Reader;
import java.util.Collection;
final class JsonToggleParser {
private JsonToggleParser() {
}
public static String toJsonString(ToggleCollection toggleCollection) {
Gson gson = new GsonBuilder().create();
return gson.toJson(toggleCollection);
}
public static Collection<Toggle> fromJson(String jsonString) {
Gson gson = new GsonBuilder().create();
return gson.fromJson(jsonString, ToggleCollection.class).getFeatures();
}
public static ToggleCollection fromJson(Reader reader) {
Gson gson = new GsonBuilder().create();
ToggleCollection gsonCollection = gson.fromJson(reader, ToggleCollection.class);
return new ToggleCollection(gsonCollection.getFeatures());
}
}

View File

@ -1,19 +1,19 @@
package no.finn.unleash.repository;
package no.finn.unleash;
import java.util.Collections;
public final class ToggleResponse {
final class Response {
enum Status {NOT_CHANGED, CHANGED}
private final Status status;
private final ToggleCollection toggleCollection;
public ToggleResponse(Status status, ToggleCollection toggleCollection) {
public Response(Status status, ToggleCollection toggleCollection) {
this.status = status;
this.toggleCollection = toggleCollection;
}
public ToggleResponse(Status status) {
public Response(Status status) {
this.status = status;
this.toggleCollection = new ToggleCollection(Collections.emptyList());
}

View File

@ -1,4 +1,4 @@
package no.finn.unleash.strategy;
package no.finn.unleash;
import java.util.Map;

View File

@ -1,12 +1,10 @@
package no.finn.unleash.repository;
package no.finn.unleash;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import no.finn.unleash.Toggle;
final class ToggleCollection {
private Collection<Toggle> features = Collections.emptyList();
private Map<String, Toggle> cache;

View File

@ -0,0 +1,5 @@
package no.finn.unleash;
public interface ToggleFetcher {
Response fetchToggles() throws UnleashException;
}

View File

@ -0,0 +1,5 @@
package no.finn.unleash;
public interface ToggleRepository {
Toggle getToggle(String name);
}

View File

@ -1,8 +1,8 @@
package no.finn.unleash.strategy;
package no.finn.unleash;
import java.util.Map;
public final class UnknownStrategy implements Strategy {
final class UnknownStrategy implements Strategy {
public static final String NAME = "unknown";
@Override

View File

@ -3,18 +3,12 @@ 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 static final DefaultStrategy DEFAULT_STRATEGY = new DefaultStrategy();
private static final UnknownStrategy UNKNOWN_STRATEGY = new UnknownStrategy();
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;
@ -26,28 +20,23 @@ public final class Unleash {
}
public boolean isEnabled(final String toggleName, final boolean defaultSetting) {
try {
Toggle toggle = toggleRepository.getToggle(toggleName);
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) {
if (toggle == null) {
return defaultSetting;
}
Strategy strategy = getStrategy(toggle.getStrategy());
return toggle.isEnabled() && strategy.isEnabled(toggle.getParameters());
}
private Map<String, Strategy> buildStrategyMap(Strategy[] strategies) {
Map<String, Strategy> map = new HashMap<>();
map.put(defaultStrategy.getName(), defaultStrategy);
map.put(DEFAULT_STRATEGY.getName(), DEFAULT_STRATEGY);
if(strategies != null) {
for(Strategy strategy : strategies) {
if (strategies != null) {
for (Strategy strategy : strategies) {
map.put(strategy.getName(), strategy);
}
}
@ -56,12 +45,11 @@ public final class Unleash {
}
private Strategy getStrategy(String strategy) {
if(strategyMap.containsKey(strategy)) {
if (strategyMap.containsKey(strategy)) {
return strategyMap.get(strategy);
} else {
return unknownStrategy;
return UNKNOWN_STRATEGY;
}
}
}

View File

@ -0,0 +1,8 @@
package no.finn.unleash;
public class UnleashException extends RuntimeException {
public UnleashException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,58 +0,0 @@
package no.finn.unleash.repository;
import java.io.IOException;
import java.util.Collection;
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;
//TODO: take advantage of Etag and 304 responses.
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 final 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 final 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 Collection<Toggle> fetchToggles() throws IOException {
HttpResponse httpResponse = httpClient.execute(new HttpGet(serverEndpoint));
final String jsonString = EntityUtils.toString(httpResponse.getEntity());
return JsonToggleParser.fromJson(jsonString);
}
}

View File

@ -1,51 +0,0 @@
package no.finn.unleash.repository;
import java.io.Reader;
import java.util.Collection;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import no.finn.unleash.Toggle;
public final class JsonToggleParser {
private JsonToggleParser() {
}
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(new ToggleCollection(toggles));
}
public static String toJsonString(ToggleCollection toggleCollection) {
Gson gson = new GsonBuilder().create();
return gson.toJson(toggleCollection);
}
public static Collection<Toggle> fromJson(String jsonString) {
Gson gson = new GsonBuilder().create();
return gson.fromJson(jsonString,ToggleCollection.class).getFeatures();
}
public static ToggleCollection collectionFormJson(String jsonString) {
Gson gson = new GsonBuilder().create();
return gson.fromJson(jsonString, ToggleCollection.class);
}
public static Collection<Toggle> fromJson(Reader reader) {
Gson gson = new GsonBuilder().create();
return gson.fromJson(reader,ToggleCollection.class).getFeatures();
}
public static ToggleCollection collectionFormJson(Reader reader) {
Gson gson = new GsonBuilder().create();
ToggleCollection gsonCollection = gson.fromJson(reader, ToggleCollection.class);
return new ToggleCollection(gsonCollection.getFeatures());
}
}

View File

@ -1,137 +0,0 @@
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.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 final Toggle getToggle(final String name) {
return togglesCache.get(name);
}
@Override
public final 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(JsonToggleParser.toJsonString(toggles));
return toggles;
} catch (ToggleException ex) {
if (togglesCache.isEmpty()) {
return loadFromTempFile();
}
throw ex;
}
}
private Collection<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 JsonToggleParser.fromJson(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

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

View File

@ -1,5 +0,0 @@
package no.finn.unleash.repository;
public interface ToggleFetcher {
ToggleResponse fetchToggles() throws ToggleException;
}

View File

@ -1,11 +0,0 @@
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

@ -1,4 +1,6 @@
package no.finn.unleash.repository;
package no.finn.unleash;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.IOException;
@ -7,9 +9,6 @@ import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import no.finn.unleash.Toggle;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

View File

@ -3,9 +3,6 @@ package no.finn.unleash;
import java.net.URI;
import java.util.Random;
import no.finn.unleash.repository.FeatureToggleRepository;
import no.finn.unleash.repository.ToggleRepository;
public class ManualTesting {
public static void main(String[] args) throws Exception {
ToggleRepository repository = new FeatureToggleRepository(URI.create("http://localhost:4242/features"), 1);

View File

@ -1,18 +1,11 @@
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.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.anyMap;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
public class UnleashTest {
@ -39,13 +32,6 @@ public class UnleashTest {
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);

View File

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

View File

@ -0,0 +1,22 @@
package no.finn.unleash.example;
import no.finn.unleash.FeatureToggleRepository;
import no.finn.unleash.ToggleRepository;
import no.finn.unleash.Unleash;
import org.junit.Test;
import java.net.URI;
import static org.junit.Assert.assertFalse;
public class UnleashUsageTest {
@Test
public void wire() {
ToggleRepository repository = new FeatureToggleRepository(URI.create("http://localhost:4242/features"), 1);
Unleash unleash = new Unleash(repository, new CustomStrategy());
assertFalse(unleash.isEnabled("myFeature"));
}
}

View File

@ -1,22 +0,0 @@
package no.finn.unleash.repository;
import java.net.URI;
import org.junit.Ignore;
import org.junit.Test;
public class HttpToggleFetcherTest {
@Test
@Ignore
public void explore() {
HttpToggleFetcher httpToggleFetcher = new HttpToggleFetcher(URI.create("http://localhost:4242/features"));
ToggleResponse toggleResponse = httpToggleFetcher.fetchToggles();
toggleResponse = httpToggleFetcher.fetchToggles();
System.out.println("toggleResponse = " + toggleResponse);
}
}