Last week I wrote about how to interface with the Egnyte RESTful API using a custom Java class, so that you can use methods like uploading, downloading, or listing files from anywhere in your code (https://adeelscorner.wordpress.com/2016/07/24/creating-java-helper-class-to-interact-with-egnyte-restful-api/).
It turns out that Egnyte throttles requests to their API. According to their documentation, it is throttled at two transactions per second, so one per 500ms. And if your network connection is fast enough, you will most certainly hit that limit and get denial of service from the API, if you make several calls to API back to back. So, you need to throttle the requests. I’ll talk on how to go about doing that in this post.
Recall the EgnyteHelper class that I created in the aforementioned blog post. A few modifications to this class and methods, with the use of a Timer, can easily overcome this issue. And I’ll show you how you can retry your call recursively if it still gets blocked.
First let’s define some class level constants, variables, a constructor, and two methods to assist. And then I’ll show you how you can use that in one of the Egnyte API helper methods (such as listFileOrFolder()) to prevent against throttling, and assist in retrying.
private static final int EGNYTE_API_WAIT_MS_BETWEEN_TRANSACTIONS = 501; private static final int RETRY_API_CALL_TIMES = 3; private Timer timer; private Object lock; private HashMap < String, Integer > recurseHelperHashMap; public EgnyteHelper() { lock = new Object(); recurseHelperHashMap = new HashMap < String, Integer > (); } private void setTimer() { timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { timer.cancel(); timer = null; synchronized(lock) { lock.notify(); } } }, EGNYTE_API_WAIT_MS_BETWEEN_TRANSACTIONS); } private void waitForTimerOrSetTimer() { if (timer == null) setTimer(); else { synchronized(lock) { try { lock.wait(); } catch (InterruptedException e) {} setTimer(); } } }
Starting from the top, we define a static variable called EGNYTE_API_WAIT_MS_BETWEEN_TRANSACTIONS. You’ll notice that I’ve set it to 501ms instead of the 500ms limit, so there’s no chance a boundary case is hit and you get blocked. The RETRY_API_CALL_TIMES static variable define show many times you want your method to retry before giving up. The timer variable is used to do the actual waiting, lock variable is used when a method needs to wait for the previous call’s 500ms to elapse, and recurseHelperHashMap is used to assist in recursion (more on that later).
In the constructor we simply just initialize the lock and recurseHelperHashMap. The next two methods are where the magic happens. setTimer() initializes a Timer to wait 501ms, and then calls notify on the lock object, which we catch later. So if a subsequent method is waiting for the lock, it can move on, and do the Egnyte API RESTful call. waitForTimerOrSetTimer() is the method that is actually used in our helper methods. It checks if the timer has already been set, and if not, it simply creates it and moves on. But if the timer is set, it waits on the lock object, until the timer from the previous call releases it, and then it moves on, thus preventing your helper method from moving too fast and hitting the throttle limit.
Next let’s see how we can put this all together and use it in a helper method such as listFileOrFolder(). Here is a modified version of listFileOrFolder() that we explored last time:
public JSONObject listFileOrFolder(String fullFileOrFolderPathWithSpaces) throws Exception { String thisMethodName = Thread.currentThread().getStackTrace()[1].getMethodName(); waitForTimerOrSetTimer(); JSONObject listing; URL url = UriBuilder.fromPath(Constants.EGNYTEAPI_BASE_URL + "fs/" + fullFileOrFolderPathWithSpaces).build().toURL(); URLConnection urlConnection = url.openConnection(); HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection; httpUrlConnection.setRequestMethod("GET"); httpUrlConnection.setRequestProperty("Authorization", "Bearer " + Constants.EGNYTEAPI_AUTH_TOKEN); httpUrlConnection.connect(); if (httpUrlConnection.getResponseCode() != HttpURLConnection.HTTP_OK && httpUrlConnection.getResponseCode() != HttpURLConnection.HTTP_CREATED) { Map < String, List < String >> hdrs = httpUrlConnection.getHeaderFields(); if (hdrs.containsKey("Retry-After")) { if (!recurseHelperHashMap.containsKey(thisMethodName)) recurseHelperHashMap.put(thisMethodName, 1); else recurseHelperHashMap.put(thisMethodName, recurseHelperHashMap.get(thisMethodName) + 1); if (recurseHelperHashMap.get(thisMethodName).intValue() <= RETRY_API_CALL_TIMES) { Thread.sleep(Integer.parseInt(hdrs.get("Retry-After").get(0)) * 1000); return listFileOrFolder(fullFileOrFolderPathWithSpaces); } } listing = new JSONObject(); } else { Scanner scanner = new Scanner(httpUrlConnection.getInputStream()); scanner.useDelimiter("\\A"); String returnJsonString = (scanner.hasNext() ? scanner.next() : "{}"); scanner.close(); listing = new JSONObject(returnJsonString); } httpUrlConnection.disconnect(); if (recurseHelperHashMap.containsKey(thisMethodName)) recurseHelperHashMap.remove(thisMethodName); return listing; }
Starting from the top, we enter the method, get the method name (I used reflection instead of hardcoding the method name in a string, since I can copy/paste that code in any other helper method), and call waitForOrSetTimer(). This makes the current method call either wait first if a previous call was made within the last 500ms, or it sets the timer if no previous call’s throttle limit was waiting. Next you see that we make the RESTful API call to Egnyte, and then we detect if there was a non-normal HTTP code returned. Fortunately Egnyte sends back an HTTP header in the response if your request does get throttled, called “Retry-After”, which defines the number of seconds we need to wait before retrying. If the header is detected, the code starts with the recursive magic. First it sleeps for the amount of time Egnyte API wants us to sleep (you may want to put EgnyteHelper’s helper method calls on a background thread so your main thread doesn’t get blocked). Then it recursively calls the helper method, up to “RETRY_API_CALL_TIMES” times, before giving up. It does so with the help of the recurseHelperHashMap class level variable, which maps the method name to the number of times the recursion has already happened. Finally when exiting the method, we clean up the entry for this method from recurseHelperHashMap.
And that’s all!
Note that you should have one instance of EgnyteHelper shared per your whole Java application. So if different parts of the application need to access the Egnyte API at the same time, EgnyteHelper can throttle all those requests appropriately using the same timer, application-wide.