5
\$\begingroup\$

I am working on a project in which I construct a URL with a valid hostname (but not a blocked hostname) and then execute that URL using RestTemplate from my main thread. I also have a single background thread in my application which parses the data from the URL and extracts the block list of hostnames from it.

If any block list of hostnames is present, then I won't make a call to that hostname from the main thread and I will try making a call to another hostname. By block list, I mean whenever any server is down, its hostname is on the block list.

Here is my background thread code. It will get the data from my service URL and keep on running every 10 minutes once my application has started up. It will then parse the data coming from the URL and store it in a ClientData class variable.

public class TempScheduler {

    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        public void startScheduler() {
            final ScheduledFuture<?> taskHandle = scheduler.scheduleAtFixedRate(new Runnable() {
                public void run() {
                    try {
                        callServiceURL();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }, 0, 10, TimeUnit.MINUTES);
        }
    }

    // call the service and get the data and then parse 
    // the response.
    private void callServiceURL() {
        String url = "url";
        RestTemplate restTemplate = new RestTemplate();
        String response = restTemplate.getForObject(url, String.class);
        parseResponse(response);

    }

    // parse the response and store it in a variable
    private void parseResponse(String response) {
        //...       
        Map<String, Map<Integer, String>> primaryTables = null;
        Map<String, Map<Integer, String>> secondaryTables = null;
        Map<String, Map<Integer, String>> tertiaryTables = null;

        //...

        // store the data in ClientData class variables if anything has changed  
        // which can be used by other threads
        if(changed) {
            ClientData.setMappings(primaryTables, secondaryTables, tertiaryTables);
        }

        // get the block list of hostnames
        Map<String, List<String>> coloExceptionList = gson.fromJson(response.split("blocklist=")[1], Map.class);
        List<String> blockList = new ArrayList<String>();
        for(Map.Entry<String, List<String>> entry : coloExceptionList.entrySet()) {
            for(String hosts : entry.getValue()) {
                blockList.add(hosts);
            }
        }

        // store the block list of hostnames which I am not supposed to make a call
        // from my main application
        ClientData.setBlockListOfHostname(blockList);
    }
}

Below is my ClientData class in which I am using CountDownLatch -

public class ClientData {

    public static class Mappings {
        public final Map<String, Map<Integer, String>> primary;
        public final Map<String, Map<Integer, String>> secondary;
        public final Map<String, Map<Integer, String>> tertiary;

        public Mappings(
            Map<String, Map<Integer, String>> primary,
            Map<String, Map<Integer, String>> secondary,
            Map<String, Map<Integer, String>> tertiary
        ) {
            this.primary = primary;
            this.secondary = secondary;
            this.tertiary = tertiary;
        }
    }

    private static final AtomicReference<Mappings> mappings = new AtomicReference<>();
    private static final CountDownLatch hasBeenInitialized = new CountDownLatch(1);

    // do I need this extra AtomicReference?
    private static final AtomicReference<List<String>> blockListOfHosts = new AtomicReference<List<String>>();
    // do I need this extra latch here?
    private static final CountDownLatch hasBeenInitializedBlockHostnames = new CountDownLatch(1);

    public static Mappings getMappings() {
        try {
            hasBeenInitialized.await();
            return mappings.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }
    }

    public static void setMappings(
        Map<String, Map<Integer, String>> primary,
        Map<String, Map<Integer, String>> secondary,
        Map<String, Map<Integer, String>> tertiary
    ) {
        setMappings(new Mappings(primary, secondary, tertiary));
    }

    public static void setMappings(Mappings newMappings) {
        mappings.set(newMappings);
        hasBeenInitialized.countDown();
    }

    public static void setBlockListOfHostname(List<String> listsOfHostnames) {
        blockListOfHosts.set(listsOfHostnames);
        hasBeenInitializedBlockHostnames.countDown();
    }

    public static boolean isExceptionHost(String hostName) {
        List<String> blockHostList = blockListOfHosts.get();

        if (blockHostList != null) {
            return blockHostList.contains(hostName);
        } else {
            return false;
        }
    }
}

Here is my main application thread code in which I find all the hostnames on which I can make a call and then iterate the hostnames list to make a call.

If that hostname is null or in the block list, then I won't make a call to that particular hostname and will try the next hostname in the list.

@Override
public DataResponse call() throws Exception {

    List<String> hostnames = new LinkedList<String>();

    Mappings mappings = ClientData.getMappings(); 
    // use mappings.primary 
    // use mappings.secondary 
    // use mappings.tertiary

    // .. some code here

    for (String hostname : hostnames) {     

        // If host name is null or host name is in block list category, skip sending request to this host
        if (hostname == null || ClientData.isExceptionHost(hostname)) {
            continue;
        }

        try {
            String url = generateURL(hostname);

            response = restTemplate.getForObject(url, String.class);

            break;
        } catch (RestClientException ex) {
            // log exception
            // how to add this hostname in the block list as well in `ClientData` class?
        }
    }
}

I don't need to make a call to the hostname whenever it is down from the main thread. And my background thread gets these detail from one of my services, whenever any server is down. It will have the list of hostnames and whenever they are up, that list will get updated.

  • Do I need extra CountDownLatch for block list of hostname in ClientData class or not?
  • Do I need extra AtomicReference for block list of hostname as well or not?

This code will be called at a rate of 1000 requests per second so it has to be fast. For the first time, whenever my blockListOfHosts is being updated from the background thread, I can return false instead of blocking the call using CountDownLatch but it has to be atomic, all the threads should see correct value of the block list of hostnames.

And also, whenever any RestClientException is being thrown, I will add that hostname in the blockListOfHosts as well since my background thread is running every 10 minutes so that list won't have this hostname until 10 minutes is done. And whenever this server came back up, my background will update this list automatically.

\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$

For adding and removing single host names, I would use a simple ConcurrentHashMap without the AtomicReference. Initialize it with an empty map and drop the additional latch.

Update: I really doubt you need to replace the list of hosts all at once, but here's the combined form anyway:

private static final AtomicReference<ConcurrentHashMap<String, String>> blockedHosts = 
        new AtomicReference<ConcurrentHashMap<String, String>>(new ConcurrentHashMap<String, String>());

public static boolean isHostBlocked(String hostName) {
    return blockedHosts.get().containsKey(hostName);
}

public static void blockHost(String hostName) {
    blockedHosts.get().put(hostName, hostName);
}

public static void unblockHost(String hostName) {
    blockedHosts.get().remove(hostName);
}

public static void replaceBlockedHosts(List<String> hostNames) {
    ConcurrentHashMap<String, String> newBlockedHosts = new ConcurrentHashMap<>();
    for (String hostName : hostNames) {
        newBlockedHosts.put(hostName, hostName);
    }
    blockedHosts.set(newBlockedHosts);
}
\$\endgroup\$
22
  • \$\begingroup\$ Thanks for suggestion. In my case, hostNames list will have only three or four entries at a time maximum and minimum it will be zero. Let me edit my question with the code basis on your suggestion. I would probably go with the last option of AtomicReference but in that case, do we really need ConcurrentHashMap? \$\endgroup\$
    – arsenal
    Commented May 18, 2014 at 4:30
  • \$\begingroup\$ Go with the first part of my answer, then. The concurrent hash map will be better than replacing the map each time. \$\endgroup\$ Commented May 18, 2014 at 4:32
  • \$\begingroup\$ I see. But in that case, it won't be atomic operation right? And some threads might see inconsistent data? And also in the concurrent hash map key and value they both are same thing in your suggestion? \$\endgroup\$
    – arsenal
    Commented May 18, 2014 at 4:37
  • \$\begingroup\$ That's correct, but will that matter? If so, you must do the replace-at-once with the atomic reference. Always ask the question "What precise level of atomicity is required?" The reason for using a map with key/value same is to simulate a set which doesn't exist in java.util.concurrent. This is what HashSet and friends do under the hood. \$\endgroup\$ Commented May 18, 2014 at 6:44
  • \$\begingroup\$ Thanks a lot David. Makes sense now. It's good to know some of the internals of the data structures. I have updated the question with your suggestion of AtomicReference. Can you take a look whether I got everything right or not? There might be some silly mistake I guess. \$\endgroup\$
    – arsenal
    Commented May 19, 2014 at 3:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.