Throttler Class in Java – How to implement limiting requests in Java.

Have you ever come across a situation that you need to do rate limiting to an api.  In this article we look at one of the ways rate limiting can be done.

We would limit access to an api and make only 1 call to api within 2 seconds.

This class would use threads and give an option to the caller whether to wait for the api call to finish or just let the throller call the api. 

 

import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * API has limits on number of concurrent connections and the number of calls
 * that can send within a minutes. This controller acts as a throttler to the
 * api.
 * 
 * @author T Tak
 *
 */
@Component
public class ApiCallsThrottler {

	private static final Logger logger = LogManager.getLogger();
	
	@Autowired
	private Api api;

	// 3 threads
	private ExecutorService tasksExecutor;

	private volatile long lastTimeExecution = System.currentTimeMillis();

	private ScheduledExecutorService infoScheduler;

	@PostConstruct
	public void init() {
		logger.info("Init ApiCallsThrottler");
		BasicThreadFactory factoryNormal = new BasicThreadFactory.Builder().namingPattern("normalPriothread-%d")
				.build();
		tasksExecutor = Executors.newFixedThreadPool(3, factoryNormal);
		
		BasicThreadFactory infoSchedulerFactory = new BasicThreadFactory.Builder().namingPattern("apiinfoscheduler-%d")
				.build();
		infoScheduler = Executors.newSingleThreadScheduledExecutor(infoSchedulerFactory);
		// initial delay = 1 minute, and then each 1 minutes
		infoScheduler.scheduleWithFixedDelay(ApiCallsThrottler.this::printQueueDetails, 1, 1, TimeUnit.MINUTES);

	}

	protected void printQueueDetails() {
		try {
			printThreadPoolDetails((ThreadPoolExecutor) tasksExecutor);
		} catch (Exception e) {
			logger.error("couldn't print request executor details : {}", e, e);
		}
	}

	private void printThreadPoolDetails(ThreadPoolExecutor threadPoolExecutor) {
		int activeCount = threadPoolExecutor.getActiveCount();
		long taskCount = threadPoolExecutor.getTaskCount();
		long completedTaskCount = threadPoolExecutor.getCompletedTaskCount();
		long tasksToDo = taskCount - completedTaskCount - activeCount;
		logger.info("tasks in queue : {}, sending now : {} , apx total requests send till now : {}", tasksToDo,
				activeCount, completedTaskCount);
	}

	
	public Future<ApiRequest> sendRequest(ApiRequest sendrequestTask) {
		sendrequestTask.setQueueSubmitTime(Instant.now());
		return tasksExecutor.submit(() -> sendrequestNoReciept(sendrequestTask));
	}

	private ApiRequest sendrequestNoReciept(ApiRequest task) throws InterruptedException {
		task.setQueueProcessingStartTime(Instant.now());

		if (System.currentTimeMillis() - lastTimeExecution < 2000) {
			logger.info("NP :Waiting 2000 milis to send request due to quantity limits! {} ", task.getTaskId());
			Thread.sleep(2000);
			logger.info("NP :Now sending request! {} ", task.getTaskId());
		}
		lastTimeExecution = System.currentTimeMillis();
		
		api.sendRequest(task.getTaskId());
		
		logger.info("NP :request sent! {} ", task.getTaskId());
		task.setQueueProcessingFinishTime(Instant.now());
		return task;
	}

	@PreDestroy
	public void destroy() {
		System.out.println("destroy thread"); // NOSONAR
		shutdownAnyExecutor(Arrays.asList(tasksExecutor, infoScheduler));
		tasksExecutor = null;
	}

	private void shutdownAnyExecutor(List<ExecutorService> executorServices) {
		// shutdown all
		for (ExecutorService each : executorServices) {
			if (each != null) {
				// wait 3 second for closing all threads
				System.out.println("Destroy : shutdown scheduler"); // NOSONAR
				each.shutdown();
			}
		}
		// wait for termination
		for (ExecutorService each : executorServices) {
			if (each != null) {
				try {
					// wait 60 second for closing all threads
					// requester should be able to do all the task before shutdown
					System.out.println("Destroy : Await Termination for max 60 seconds"); // NOSONAR
					each.awaitTermination(180, TimeUnit.SECONDS);
					// try again and let go
					if (!each.isTerminated()) {
						System.out.println(// NOSONAR
								"Destroy : even after 60 seconds wait the scheduler has not terminated, try shutdownNow() and exit"); // NOSONAR
						each.shutdownNow();
					} else {
						System.out.println("Destroy : Scheduler terminated successfully"); // NOSONAR
					}
				} catch (InterruptedException e) {
					Thread.currentThread().interrupt();
				}
			}
		}
	}

	public static class ApiRequest {

		private int taskId;

		private int sendRetry = 0;
		private boolean processing;

		/** indicator, that request was sent */
		private boolean requestSent = false;

		// queue tracking
		private Instant queueSubmitTime;
		private Instant queueProcessingStartTime;
		private Instant queueProcessingFinishTime;
		private String token;
		private String errorText;

		public int getSendRetry() {
			return sendRetry;
		}

		public void setSendRetry(int sendRetry) {
			this.sendRetry = sendRetry;
		}

		public boolean isProcessing() {
			return processing;
		}

		public void setProcessing(boolean processing) {
			this.processing = processing;
		}

		public boolean isRequestSent() {
			return requestSent;
		}

		public void setRequestSent(boolean requestSent) {
			this.requestSent = requestSent;
		}

		public Instant getQueueSubmitTime() {
			return queueSubmitTime;
		}

		public void setQueueSubmitTime(Instant queueSubmitTime) {
			this.queueSubmitTime = queueSubmitTime;
		}

		public Instant getQueueProcessingStartTime() {
			return queueProcessingStartTime;
		}

		public void setQueueProcessingStartTime(Instant queueProcessingStartTime) {
			this.queueProcessingStartTime = queueProcessingStartTime;
		}

		public Instant getQueueProcessingFinishTime() {
			return queueProcessingFinishTime;
		}

		public void setQueueProcessingFinishTime(Instant queueProcessingFinishTime) {
			this.queueProcessingFinishTime = queueProcessingFinishTime;
		}

		public String getToken() {
			return token;
		}

		public void setToken(String token) {
			this.token = token;
		}

		public String getErrorText() {
			return errorText;
		}

		public void setErrorText(String errorText) {
			this.errorText = errorText;
		}

		public int getTaskId() {
			return taskId;
		}

		public void setTaskId(int taskId) {
			this.taskId = taskId;
		}
	}

  Dependencies needed :

org.springframework spring-core 5.3.10 org.apache.commons commons-lang3 3.12.0 org.apache.logging.log4j log4j-core 2.14.1 javax.annotation javax.annotation-api 1.3.2 org.springframework spring-context 5.3.10

Like this post? Don’t forget to share it!

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.