Tech Blog

Calling APIs in parallel with Java code

The Alma REST APIs return JSON natively. This is the natural choice for developers working in dynamic languages, such as Node.js and Ruby. These languages interpret the JSON into native types, such as Array, Hash, String, and make it easy to manipulate data returned by the APIs.

Below we can see an example of updating users information using Alma’s APIs:

We’ll GET user information into a JSON object, update the object and writing it back with a PUT request. (error handling removed for brevity).

public static void updateUser(String userId) {
        String url = BASE_URL + userId + API_KEY;

        HttpResponse outGetReq = sendHttpReqAndCheckThresholds(url, "GET", null);

        JSONObject jsonObject = new JSONObject(outGetReq.getBody());

        // PUT to change middle_name
        jsonObject.put("middle_name", "new middle name");
        HttpResponse outPutReq = sendHttpReqAndCheckThresholds(url, "PUT", jsonObject.toString());
    }

The method sendHttpReqAndCheckThresholds() handles:

  • Retry in case the HTTP request returned 500 (Internal Server Error) or 429 (Too Many Requests per sec). In both cases we’ll retry RETRY_TIMES number of times.
  • We wait before running the request again 1-5 seconds randomly. Randomly, because we don’t want all threads to retry at the same time at the 2nd attempt, and possibly receiving 429 again and again.
  • After the request succeeds it runs checkThresholds() which is explained below.
private static HttpResponse sendHttpReqAndCheckThresholds(String url, String method, String body) {
        HttpResponse httpResponse = null;
        for (int i = 0; i < RETRY_TIMES; i++) {
            httpResponse = sendHttpReq(url, method, body);
            if (httpResponse.getResponseCode() != HttpsURLConnection.HTTP_INTERNAL_ERROR
                    && httpResponse.getResponseCode() != HTTP_TOO_MANY_REQ) {
                // request was successful - no need to retry
                break;
            }
            logger.info("HTTP Code is : " + httpResponse.getResponseCode() + " Retrying...");
            try {
                // wait 1-5 sec. and try again
                Thread.sleep((new Random().nextInt(5) + 1) * 1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        checkThresholds(httpResponse.getHeaders());
        return httpResponse;
    }
  • The number of requests left for the day is included in the response header X-Exl-Api-Remaining. We warn when reaching 80% and exit (all threads) when reaching 95%.
private static void checkThresholds(Map<String, List<String>> headersMap) {
        long apiRemaining = 0;
        try {
            apiRemaining = Long.parseLong(headersMap.get("X-Exl-Api-Remaining").get(0));
        } catch (Exception e) {
            return;
        }
        double remainingPercentage = 100 - (apiRemaining * 100D / MAX_API_REQ_PER_DAY);
        if (remainingPercentage >= 95D) {
            logger.error("Stopping - API Remaining - Not enough Api's for today ");
            Runtime.getRuntime().halt(0);
        }
        if (remainingPercentage >= 80D) {
            logger.warn("Warning - API Remaining - close to threshold (" + remainingPercentage + "%)");
        }
    }

To update a small amount of users you can write their usernames into the Java code itself. To update a larger amount of user, the list can be read from an input file. To speed up the processing we can run with up to 20 threads in parallel. It is not recommended to run with more than 20 threads since the API Gateway allows up to 25 requests per sec.

public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(MAX_NUM_OF_THREADS);
        // String userIds = "bdikazzz1 bdikaxxx1";
        // for (String userId : userIds.split(" ")) {
        for (String userId : readIdsFromFile()) {
            pool.execute(new Task(userId));
            // updateUser(userId); // sequential processing
        }
        logger.info("Done with main()");
    }

If you choose to read the list from an input file create a text file with primary-ids:

The full code is available in this Github repository.

One Reply to “Calling APIs in parallel with Java code”

  1. To replace primary-id with one of the additional-identifiers the below method can be used:


    public static void updateUser(String userId) {
    String primaryId;
    JSONObject otherId;
    logger.info("Thread " + Thread.currentThread().getName() + " starting to handle user ID: " + userId + DOT);

    logger.info("User ID: " + userId + " - calling GET");
    String url = BASE_URL + userId + API_KEY;

    HttpResponse outGetReq = sendHttpReqAndCheckThresholds(url, "GET", null);
    if (outGetReq.getResponseCode() != HttpsURLConnection.HTTP_OK) {
    logger.error("User GET failed: " + userId + " - skip...\n");
    return;
    }

    JSONObject jsonObject = new JSONObject(outGetReq.getBody());

    try {
    // remove OTHER_ID_3 field and save the value
    otherId = removeAlternativeIdField(jsonObject);
    if (otherId == null) {
    logger.error("Could not found " + OTHER_ID_3);
    return;
    }
    HttpResponse outPutReq = sendHttpReqAndCheckThresholds(url, "PUT", jsonObject.toString());
    logger.debug("Output of first PUT request(removing " + OTHER_ID_3 + "): " + outPutReq.getBody());
    if (outPutReq.getResponseCode() != HttpsURLConnection.HTTP_OK) {
    logger.error("User not fetched correctly in 1st PUT request: " + userId + " - skip...\n");
    return;
    }
    // Put the OTHER_ID_3 value in primary id and save the primaryID
    primaryId = replaceIdFields(jsonObject, otherId);
    if (primaryId == null) {
    return;
    }
    outPutReq = sendHttpReqAndCheckThresholds(url, "PUT", jsonObject.toString());
    logger.debug("Output of second PUT request(replace primary): " + outPutReq.getBody());
    if (outPutReq.getResponseCode() != HttpsURLConnection.HTTP_OK) {
    logger.error("User not fetched correctly in 2nd PUT request: " + userId + " - skip...\n");
    return;
    }
    // puts the primary as OTHER_ID_3 field
    addPrimaryAsOther(jsonObject, primaryId, new JSONObject(otherId, JSONObject.getNames(otherId)));
    outPutReq = sendHttpReqAndCheckThresholds(url.replaceFirst(primaryId, (String) otherId.get(VALUE)), "PUT",
    jsonObject.toString());
    logger.debug("Output of third PUT request(replace primary): " + outPutReq.getBody());
    if (outPutReq.getResponseCode() != HttpsURLConnection.HTTP_OK) {
    logger.error("User not fetched correctly in 3rd PUT request: " + userId + " - skip...\n");
    return;
    }
    logger.info("Done. See: " + BASE_URL + userId + "\n");
    } catch (Exception e) {
    e.printStackTrace();
    logger.error(e.getLocalizedMessage());
    return;
    }
    }

    private static void addPrimaryAsOther(JSONObject jsonObject, String primaryId, JSONObject otherId)
    throws Exception {
    otherId.put(VALUE, primaryId);
    if (jsonObject.get(USER_IDENTIFIER) == null) {
    throw new Exception("no " + USER_IDENTIFIER + " field");
    }
    JSONArray userIdentifiers = (JSONArray) jsonObject.get(USER_IDENTIFIER);
    userIdentifiers.put(otherId);
    }

    private static JSONObject removeAlternativeIdField(JSONObject jsonObject) throws Exception {
    if (jsonObject.get(USER_IDENTIFIER) == null) {
    throw new Exception("no " + USER_IDENTIFIER + " field");
    }
    JSONArray userIdentifiers = (JSONArray) jsonObject.get(USER_IDENTIFIER);
    for (int i = 0; i < userIdentifiers.length(); i++) { JSONObject idDataJSON = userIdentifiers.getJSONObject(i); if (idDataJSON.get(ID_TYPE) == null) { logger.error("Could not read " + ID_TYPE); return null; } JSONObject idType = (JSONObject) idDataJSON.get(ID_TYPE); if (idType.get(VALUE) != null && idType.get(VALUE).toString().equals(OTHER_ID_3)) { // returns other_id_3 JSON object (copy) JSONObject otherId = new JSONObject(idDataJSON, JSONObject.getNames(idDataJSON)); userIdentifiers.remove(i); return otherId; } } return null; } private static String replaceIdFields(JSONObject jsonObject, JSONObject otherId) throws Exception { if (jsonObject.get(PRIMARY_ID) == null) { throw new Exception("no " + PRIMARY_ID + " field"); } String primaryId = (String) jsonObject.get(PRIMARY_ID); // If primary id is already an email address, do nothing if (primaryId.contains(AT) && primaryId.contains(DOT)) { logger.info(primaryId + "Is already an email address"); return null; } if (otherId.get(VALUE) == null) { throw new Exception("no " + USER_IDENTIFIER + " field"); } jsonObject.put(PRIMARY_ID, otherId.get(VALUE)); return primaryId; }

Leave a Reply