I am trying to implement lock by which I want to avoid reads from happening whenever I am doing a write.
My requirements are:
- Reads block until all two maps have been set for the first time.
- Now second time, If I am updating the maps, I can still return all the two old maps value(before the updates are done on all two maps) or it should block and return me all the new two maps value whenever the updates are done on all the two maps.
As I have two Maps - primaryMapping
, secondaryMapping
so it should return either all the new values of two updated maps or it should return all the old values of the map. Basically, while updating I don't want to return primaryMapping
having old values, secondaryMapping
having having new values. It should be consistent, either it should return old values or it should return new values after updating the maps. In my case, updating of maps will happen once in 7 or 8 months (very rarely). I am using Countdown Latch for this and it is working fine so far without any issues.
Now I have two flows as shown below: For each flow, I have a URL from where we get the data for above two maps. In general, I will have two maps for both the flow so we will have different values for those two maps for PROCESS flow as compared to DEVICE flow.
public enum FlowType {
PROCESS, DEVICE;
}
I have a background thread running every 5 minutes which gets the data from each FLOW url and populate those two maps whenever there is an update. When the application is started for the first time, then it will update the mapping and after that, it will update the mapping 7-8 months afterwards. And it doesn't mean that both the flow mapping will change at the same time. It might be possible PROCESS mapping has change but not DEVICE mapping.
public class DataScheduler {
private RestTemplate restTemplate = new RestTemplate();
private static final String PROCESS_URL = "http://process_flow_url/";
private static final String DEVICE_URL = "http://device_flow_url/";
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public void startScheduleTask() {
scheduler.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
callService();
} catch (Exception ex) {
// logging exception here using logger
}
}
}, 0, 5, TimeUnit.MINUTES);
}
public void callService() throws Exception {
String url = null;
Map<FlowType, String> holder = new HashMap<FlowType, String>();
for (FlowType flow : FlowType.values()) {
try {
url = getURL(flow);
String response = restTemplate.getForObject(url, String.class);
holder.put(flow, response);
} catch (RestClientException ex) {
// logging exception here using logger
}
}
parseResponse(holder);
}
private void parseResponse(Map<FlowType, String> responses) throws Exception {
Map<FlowType, Mapping> partitionMapper = new HashMap<FlowType, Mapping>();
boolean update = false;
for (Map.Entry<FlowType, String> responseEntry : responses.entrySet()) {
FlowType flow = responseEntry.getKey();
String response = responseEntry.getValue();
Map<String, Map<Integer, Integer>> primaryMapping = new HashMap<>();
Map<String, Map<Integer, Integer>> secondaryMapping = new HashMap<>();
if (!DataUtils.isEmpty(response)) {
try (Scanner scanner = new Scanner(response)) {
boolean hasProcess = Boolean.parseBoolean(scanner.nextLine().trim().substring(HAS_PROCESS_LEN));
if (hasProcess) {
update = true;
// some code
partitionMapper.put(flow, PartitionHolder.createMapping(
primaryMapping, secondaryMapping));
}
}
}
}
// if there is any update, then only update the mappings, otherwise not.
if (update) {
PartitionHolder.setMappings(partitionMapper);
}
}
}
As you can see above, it updates the mapping by calling setMappings
method if any of the flow mapping has been updated.
And below is my PartitionHolder class:
public class PartitionHolder {
public static class Mapping {
public final Map<String, Map<Integer, Integer>> primaryMapping;
public final Map<String, Map<Integer, Integer>> secondaryMapping;
public Mapping(Map<String, Map<Integer, Integer>> primaryMapping,
Map<String, Map<Integer, Integer>> secondaryMapping) {
this.primaryMapping = primaryMapping;
this.secondaryMapping = secondaryMapping;
}
// getters here
}
private static final AtomicReference<Map<FlowType, Mapping>> mappingsHolder = new AtomicReference<Map<FlowType, Mapping>>();
private static final CountDownLatch hasInitialized = new CountDownLatch(1);
public static Mapping getFlowMapping(FlowType flowType) {
try {
hasInitialized.await();
return mappingsHolder.get().get(flowType);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException(e);
}
}
public static void setMappings(Map<FlowType, Mapping> newMapData) {
mappingsHolder.set(newMapData);
hasInitialized.countDown();
}
public static Mapping createMapping(
Map<String, Map<Integer, Integer>> primaryMapping,
Map<String, Map<Integer, Integer>> secondaryMapping) {
return new Mapping(primaryMapping, secondaryMapping);
}
}
Now this is the way I am using PartitionHolder
class from the main thread. In a single call, we will get the mapping for one flow only. In below code dataKey.getFlowType()
can be PROCESS OR DEVICE.
@Override
public DataResponse call() throws Exception {
Mapping mappings = PartitionHolder.getFlowMapping(dataKey.getFlowType());
// use mappings object here
}
Now in my above code as you can see, I am updating the mapping by calling setMappings
method if any of the flow mapping has been updated. There are two scenarios which can happen:
- When the application is started for the first time, then it works fine without any issues, since it will update the mapping for both the flow.
- Now second time, let's say for PROCESS flow, mapping has been updated but for DEVICE flow mapping has not been updated, then in that case it will update the overall mapping by calling
setMappings
method which will overwritemappingsHolder
. Problem with this approach is, for DEVICE flow I will start getting empty mapping when I will callgetFlowMapping
since it overwrote DEVICE mapping to empty maps. Right?
How can I avoid my second problem? Is there any better way to solve this problem instead of using Map and key as the FlowType? Do I need to use Factory pattern here to solve this problem or any other better way?