I have a system in which user id is "sharded" across all the machines which means each machine is responsible for certain user id data. I am working on a library which will take DataKey
builder object which contains user id, timeout, and some other stuff. I will be using this user id to figure out which machines have the data for this user id.
Customer will pass DataKey
builder object to my executeAsync
method and then I will use DataKey
builder object to figure out the machines which has the data for this user id and then I will use those machines to make the call and get the data for them. For example, DataKey object has user id 12345 so below are steps which I will be executing.
- Get all the
hostnames
which will have the data for this user id 12345. It should be linked list. - Now, pick the first machine from the
hostnames
list and generate the URL for that and execute the URL. If response is successful then make a DataResponse object and return the response and we will not log anything. But if the response is not successful like, server returned back any HttpClientErrorException or HttpServerErrorException, then I need to log it and make DataResponse object accordingly and return it. - But let's say if the first machine in that list was down for some reason then I will log it that this machine is down and then I will try calling second machine in the
hostnames
list and if it is also down, then I will try third or fourth and keep doing the same thing. And suppose if all the machines are down in thathostnames
list, then I will make DataResponse object saying all servers are down and log it as well.
We have three datacenters with 5 machines in each datacenters. And each user data is in two machines (primary and secondary) in each datacenters. Let's say if somebody is calling my library from DC1, then I will first call my primary machine server in DC1 only and depending on what flags they have enabled in DataKey, I will call other machines if they are down.
Below is the code I have so far which works fine but it is looking more messy as I am putting all the logic in executeAsync
method.
public interface Client {
// for asynchronous
public ListenableFuture<DataResponse> executeAsync(DataKey key);
}
public class DataClient implements Client {
// using spring 4 AsyncRestTemplate
private final AsyncRestTemplate restTemplate = new AsyncRestTemplate();
@Override
public ListenableFuture<DataResponse> executeAsync(final DataKey keys) {
final SettableFuture<DataResponse> responseFuture = SettableFuture.create();
int shardId = getShardId(keys);
String localDataCenterPath = FindDataCenterEnum.getLocalDataCenterString(keys.typeOfFlow());
String remoteDataCenterPath = FindDataCenterEnum.getRemoteDataCenterString(keys.typeOfFlow());
boolean remoteFlag = keys.isRemoteFlag(); // remote datacenter if primary and secondary locals are down
boolean secondaryFlag = keys.isSecondaryFlag();
List<String> hostnames = new LinkedList<String>();
// this will give me the mappings to find out which machine is responsible for the "shardId"
ShardMappings mappings = MachineToShardMapping.getMappings(keys.typeOfFlow());
String localPrimaryHostname = null;
String remotePrimaryHostname = null;
if (!DataUtils.isEmpty(mappings.primaryHostIdToPartitionMapping)) {
String localPrimaryHostId = mappings.primaryHostIdToPartitionMapping.get(localDataCenterPath).get(shardId);
localPrimaryHostname = mappings.hostIdToMachineMapping.get(localDataCenterPath).get(
Integer.parseInt(localPrimaryHostId));
String remotePrimaryHostId = mappings.primaryHostIdToPartitionMapping.get(remoteDataCenterPath).get(shardId);
remotePrimaryHostname = mappings.hostIdToMachineMapping.get(remoteDataCenterPath).get(
Integer.parseInt(remotePrimaryHostId));
}
String localSecondaryHostname = null;
String remoteSecondaryHostname = null;
if (!DataUtils.isEmpty(mappings.secondaryHostIdToPartitionMapping)) {
String localSecondaryHostId = mappings.secondaryHostIdToPartitionMapping.get(localDataCenterPath).get(shardId);
localSecondaryHostname = mappings.hostIdToMachineMapping.get(localDataCenterPath).get(
Integer.parseInt(localSecondaryHostId));
String remoteSecondaryHostId = mappings.secondaryHostIdToPartitionMapping.get(remoteDataCenterPath).get(shardId);
remoteSecondaryHostname = mappings.hostIdToMachineMapping.get(remoteDataCenterPath).get(
Integer.parseInt(remoteSecondaryHostId));
}
// now gather all the hostnames which has the data for user id 12345
// depending on what flag is enabled
if (remoteFlag && secondaryFlag) {
hostnames.add(localPrimaryHostname);
hostnames.add(localSecondaryHostname);
hostnames.add(remotePrimaryHostname);
hostnames.add(remoteSecondaryHostname);
} else if (remoteFlag && !secondaryFlag) {
hostnames.add(localPrimaryHostname);
hostnames.add(remotePrimaryHostname);
} else if (!remoteFlag && !secondaryFlag) {
hostnames.add(localPrimaryHostname);
} else if (!remoteFlag && secondaryFlag) {
hostnames.add(localPrimaryHostname);
hostnames.add(localSecondaryHostname);
}
// now I have machines in the hostnames list so exeucute it
executeForServers(responseFuture, keys, hostnames.get(0), hostnames.subList(1, hostnames.size()));
return responseFuture;
}
/**
* A simple method to execute each machine by making a URL for them. If one machine is down,
* then it will execute other machine from the hostnames list.
*/
private void executeForServers(final SettableFuture<DataResponse> responseFuture, final DataKey keys,
final String hostName, final List<String> restHostNames) {
final org.springframework.util.concurrent.ListenableFuture orig =
restTemplate.exchange(createURL(hostName, keys), HttpMethod.GET, keys.getEntity(), String.class);
orig.addCallback(
new ListenableFutureCallback<ResponseEntity<String>>() {
@Override
public void onSuccess(ResponseEntity<String> result) {
responseFuture.set(new DataResponse(result.getBody(), DataErrorEnum.OK,
DataStatusEnum.SUCCESS));
}
// can we simplify this?
@Override
public void onFailure(Throwable ex) {
// this is for calling other machine if one machine is down
if (ex instanceof SocketException) {
if (restHostNames.isEmpty()) {
// all the machines are down
DataLogging.logErrors(ex, DataErrorEnum.SERVERS_DOWN, keys);
responseFuture.set(new DataResponse(null, DataErrorEnum.SERVERS_DOWN,
DataStatusEnum.ERROR));
} else {
// this means one particular machine didn't responded so try another from the list
DataLogging.logErrors(ex, DataErrorEnum.SOCKET_EXCEPTION, keys);
executeForServers(responseFuture, key, restHostNames.get(0),
restHostNames.subList(1, restHostNames.size()));
}
} else {
// this is for logging and making DataResponse object if we get status code like 4xx or 5xx
HttpStatusCodeException httpException = (HttpStatusCodeException) ex;
DataErrorEnum error = DataErrorEnum.getErrorEnumByException(httpException);
DataLogging.logErrors(ex, error, keys);
responseFuture.set(new DataResponse(httpException.getResponseBodyAsString(), error,
DataStatusEnum.ERROR));
}
}
});
// propagate cancellation back to the original request
responseFuture.addListener(new Runnable() {
@Override public void run() {
if (responseFuture.isCancelled()) {
orig.cancel(false); // I am keeping this false for now
}
}
}, MoreExecutors.directExecutor());
}
// this will generate the URL
private String createURL(final String hostName, final DataKey keys) {
StringBuilder url = new StringBuilder();
// make a url here
return url.toString();
}
// this will give me the shard id for user id "12345"
private int getShardId(final DataKey keys) {
// return the shard id for userId present in keys
}
}
And below is my DataErrorEnum class which is being used mainly for reporting error :
public enum DataErrorEnum {
OK(200, "NONE"),
BAD_REQUEST(400, "Server Bad Request"),
UNAUTHORIZED(401, " Server Unauthorized"),
SERVER_TIMEOUT(408, "Server Timeout"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
SERVERS_DOWN(6000, "Servers Down"),
ERROR_SERVICE(6002, "Random Error Occurred on Server"),
SOCKET_EXCEPTION(6004, "Specific Server is Down");
// constructor and getters
public static DataErrorEnum getErrorEnumByException(HttpStatusCodeException httpException) {
HttpStatus statusCode = httpException.getStatusCode();
switch (statusCode) {
case BAD_REQUEST:
return DataErrorEnum.BAD_REQUEST;
case INTERNAL_SERVER_ERROR:
return DataErrorEnum.INTERNAL_SERVER_ERROR;
case NO_CONTENT:
return DataErrorEnum.NO_CONTENT;
case OK:
return DataErrorEnum.OK;
case REQUEST_TIMEOUT:
return DataErrorEnum.SERVER_TIMEOUT;
case SERVICE_UNAVAILABLE:
return DataErrorEnum.SERVERS_DOWN;
case UNAUTHORIZED:
return DataErrorEnum.UNAUTHORIZED;
default:
return DataErrorEnum.ERROR_SERVICE;
}
}
}
Is there any way to simplify my DataClient
code a bit? I was thinking to separate the logic of getting the hostnames given the user id but not sure how should I separate them out and where do I put it? As of now my DataClient class is looking pretty big.
I am using AsyncRestTemplate as my async http client which will be async non blocking architecture.