Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

public final class DVCLocalClient {

private static LocalBucketing localBucketing = new LocalBucketing();
private LocalBucketing localBucketing = new LocalBucketing();

private EnvironmentConfigManager configManager;

Expand Down Expand Up @@ -101,10 +101,6 @@ public <T> Variable<T> variable(User user, String key, T defaultValue) {
throw new IllegalArgumentException("Missing parameter: defaultValue");
}

if (!configManager.isConfigInitialized()) {
System.out.println("Variable called before DVCClient has initialized, returning default value");
}

TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
Variable<T> defaultVariable = (Variable<T>) Variable.builder()
.key(key)
Expand All @@ -114,40 +110,43 @@ public <T> Variable<T> variable(User user, String key, T defaultValue) {
.isDefaulted(true)
.build();

if (!isInitialized) {
if (!configManager.isConfigInitialized() || !isInitialized) {
System.out.println("Variable called before DVCClient has initialized, returning default value");
try {
eventQueueManager.queueAggregateEvent(Event.builder().type("aggVariableDefaulted").target(key).build(), null);
} catch (Exception e) {
System.out.printf("Unable to parse aggVariableDefaulted event for Variable %s due to error: %s", key, e.toString());
}
return defaultVariable;
}

String variableJSON = null;
try {
BucketedUserConfig bucketedUserConfig = localBucketing.generateBucketedConfig(sdkKey, user);
if (bucketedUserConfig.variables.containsKey(key)) {
BaseVariable baseVariable = bucketedUserConfig.variables.get(key);
variableJSON = localBucketing.getVariable(sdkKey, user, key, variableType, true);
if (variableJSON == null || variableJSON.isEmpty()) {
return defaultVariable;
} else {
ObjectMapper mapper = new ObjectMapper();
Variable baseVariable = mapper.readValue(variableJSON, Variable.class);

Variable<T> variable = (Variable<T>) Variable.builder()
.key(key)
.type(baseVariable.getType())
.value(baseVariable.getValue())
.defaultValue(defaultValue)
.isDefaulted(false)
.build();
.key(key)
.type(baseVariable.getType())
.value(baseVariable.getValue())
.defaultValue(defaultValue)
.isDefaulted(false)
.build();
if (variable.getType() != variableType) {
throw new IllegalArgumentException("Variable type mismatch, returning default value");
System.out.printf("Variable type mismatch, returning default value");
return defaultVariable;
}
variable.setDefaultValue(defaultValue);
variable.setIsDefaulted(false);
eventQueueManager.queueAggregateEvent(Event.builder().type("aggVariableEvaluated").target(key).build(), bucketedUserConfig);
return variable;
} else {
eventQueueManager.queueAggregateEvent(Event.builder().type("aggVariableDefaulted").target(key).build(), bucketedUserConfig);
return defaultVariable;
}
} catch(JsonProcessingException jpe){
System.out.printf("Unable to parse Variable %s due to JSON error: err=%s, data=", key, jpe.getMessage(), variableJSON);
} catch (Exception e) {
System.out.printf("Unable to parse JSON for Variable %s due to error: %s", key, e.toString());
}

try {
eventQueueManager.queueAggregateEvent(Event.builder().type("aggVariableDefaulted").target(key).build(), null);
} catch (Exception e) {
System.out.printf("Unable to parse aggVariableDefaulted event for Variable %s due to error: %s", key, e.toString());
System.out.printf("Unable to evaluate Variable %s due to error: %s", key, e);
}
return defaultVariable;
}
Expand Down Expand Up @@ -220,8 +219,12 @@ public void close() {
if (!isInitialized) {
return;
}
configManager.cleanup();
eventQueueManager.cleanup();
if (configManager != null) {
configManager.cleanup();
}
if (eventQueueManager != null) {
eventQueueManager.cleanup();
}
}

private void validateUser(User user) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.concurrent.atomic.AtomicReference;

import com.devcycle.sdk.server.common.model.User;
import com.devcycle.sdk.server.common.model.Variable;
import com.devcycle.sdk.server.local.model.BucketedUserConfig;
import com.devcycle.sdk.server.local.model.FlushPayload;
import com.fasterxml.jackson.annotation.JsonInclude;
Expand All @@ -31,6 +32,8 @@ public class LocalBucketing {
private Set<Integer> pinnedAddresses;
private HashMap<String, Integer> sdkKeyAddresses;

private HashMap<Variable.TypeEnum, Integer> variableTypeMap = new HashMap<Variable.TypeEnum, Integer>();

public LocalBucketing() {
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);

Expand All @@ -53,6 +56,12 @@ public LocalBucketing() {

Memory mem = linker.get(store, "", "memory").get().memory();
memRef.set(mem);

// WASM time seems problematic for getting global values so we'll just hardcode them
variableTypeMap.put(Variable.TypeEnum.BOOLEAN, 0);
variableTypeMap.put(Variable.TypeEnum.NUMBER, 1);
variableTypeMap.put(Variable.TypeEnum.STRING, 2);
variableTypeMap.put(Variable.TypeEnum.JSON, 3);
}

private Collection<Extern> setImportsOnLinker() {
Expand Down Expand Up @@ -176,6 +185,29 @@ public BucketedUserConfig generateBucketedConfig(String sdkKey, User user) throw
return config;
}

public String getVariable(String sdkKey, User user, String key, Variable.TypeEnum variableTypeEnum, boolean shouldTrackEvent) throws JsonProcessingException {
// need some kind of mutex?
String userString = OBJECT_MAPPER.writeValueAsString(user);

int wasmVariableType = this.variableTypeMap.get(variableTypeEnum);

unpinAll();
int sdkKeyAddress = getSDKKeyAddress(sdkKey);
int userAddress = newWasmString(userString);
int keyAddress = newWasmString(key);

Func getVariablePtr = linker.get(store, "", "variableForUser").get().func();
WasmFunctions.Function5<Integer, Integer, Integer, Integer, Integer, Integer> variableForUser = WasmFunctions.func(
store, getVariablePtr, I32, I32, I32, I32, I32, I32);

int resultAddress = variableForUser.call(sdkKeyAddress, userAddress, keyAddress, wasmVariableType, shouldTrackEvent ? 1 : 0);
if (resultAddress == 0){
return null;
}
String variableString = readWasmString(resultAddress);
return variableString;
}

public void initEventQueue(String sdkKey, String options) {
unpinAll();
int sdkKeyAddress = getSDKKeyAddress(sdkKey);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.devcycle.sdk.server.helpers;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;

import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;

public class LocalConfigServer {
private HttpServer server;
private String configData = "";
public LocalConfigServer(String configData) throws IOException{
this.configData = configData;
InetSocketAddress address = new InetSocketAddress(8000);
server = HttpServer.create(address, 0);
server.createContext("/", this::handleConfigRequest);
server.setExecutor(null); // use the default executor
System.out.println("Starting config server on " + address);
}

public void handleConfigRequest(HttpExchange exchange) throws IOException {
byte[] responseData = configData.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, responseData.length);
OutputStream outputStream = exchange.getResponseBody();
outputStream.write(responseData);
outputStream.flush();
outputStream.close();
}

public void setConfigData(String configData) {
this.configData = configData;
}

public void start() {
this.server.start();
}

public void stop() {
this.server.stop(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.devcycle.sdk.server.helpers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class TestDataFixtures {
/**
* Loads the config data from the file in the resources folder
* @param fileName name of the file to load
* @return the String contents of that resource file
*/
private static String loadConfigData(String fileName) {
String configData = "";
ClassLoader classLoader = TestDataFixtures.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(fileName);
InputStreamReader streamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(streamReader)) {
String line;
while ((line = reader.readLine()) != null) {
configData += line;
}
} catch (IOException e) {
System.out.println("Failed to load config data ["+fileName+"]: " + e.getMessage());
e.printStackTrace();
}
return configData;
}


public static String SmallConfig() {
return loadConfigData("fixture_small_config.json");
}

public static String LargeConfig() {
return loadConfigData("fixture_large_config.json");
}

public static String SmallConfigWithSpecialCharacters() {
return loadConfigData("fixture_small_config_special_characters.json");
}
}
Loading