[JAVA] Implementation of multi-tenant asynchronous processing in Tomcat

Implementation of multi-tenant asynchronous processing in Tomcat

This article is part of "Implementing Asynchronous Processing in Tomcat".

-"Implementation of asynchronous processing in Tomcat" -"Implementation of asynchronous processing for single tenant in Tomcat" --"Implementation of multi-tenant asynchronous processing in Tomcat" ← This article

Introduction

In "Implementation of asynchronous processing in Tomcat", [@Async](https://docs.spring.io/spring-framework/docs/current/ I introduced that you can control the flow of asynchronous tasks by combining javadoc-api / org / springframework / scheduling / annotation / Async.html) and thread pool. However, there is a one-to-one correspondence between @Async and the thread pool. Therefore, it is not possible to support multi-tenant.

Multi-tenancy is a mechanism that provides services to multiple customers with an instance of one application. For example, when the tenant ID is specified as follows, tenants "t1" and "t2" are assigned to different thread pools, and different resources / assets are used for each tenant, such as accessing different schemas of the data store. It is an image that does.

- 「GET http://localhost/async?tenantId=t1」
- 「GET http://localhost/async?tenantId=t2」

** Figure 4.0 Multi-tenant HTTP request **

So, let's think about how to implement asynchronous processing for multi-tenancy. It also introduces that there are some parts that are out of the management of Spring Framework, and you need to implement thread pool lifecycle management yourself.

Added thread pool management class

Create a ThreadPoolTaskExecutors class that manages a multi-tenant thread pool.

In this class, Map type with tenant ID and thread pool as key-value is held in the field, and getExecutor () returns the thread pool corresponding to the tenant ID. However, registering this class as a bean does not mean that the Spring Framework will recognize that it contains a thread pool.

Therefore, in order to shut down the thread pool by yourself, destroy with @PreDestroy added. () Is added. Since the method of @PreDestroy is called back when Tomcat normally ends, the thread is threaded at that timing. It shuts down the pool. However, [Create @Configuration class](https://qiita.com/sipadan2003/items/3277024b7da63c844663#configuration%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE% Note that this shutdown also destroys the tasks that are in the queue, as mentioned in E4% BD% 9C% E6% 88% 90).

public class ThreadPoolTaskExecutors {
	private Map<String, ThreadPoolTaskExecutor> map = new HashMap<>();

	public ThreadPoolTaskExecutors(ThreadPoolSettings settings) {
		//Create a thread pool for each tenant
		map.put("Tenant1", newExecutor("Tenant1", settings));
		map.put("Tenant2", newExecutor("Tenant2", settings));
		map.put("Tenant3", newExecutor("Tenant3", settings));
	}

	//Create thread pool
	private static ThreadPoolTaskExecutor newExecutor(String tenantId, ThreadPoolSettings settings) {
		final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setCorePoolSize(settings.getCorePoolSize());
		executor.setQueueCapacity(settings.getQueueCapacity());
		executor.setMaxPoolSize(settings.getMaxPoolSize());
		executor.setKeepAliveSeconds(settings.getKeepAliveSeconds());
		executor.setThreadNamePrefix("Executor2-"+tenantId+"-");
		executor.setDaemon(false);
		executor.initialize();
		return executor;
	}

	//Returns the thread pool corresponding to the tenant ID
	public ThreadPoolTaskExecutor getExecutor(String tenantId) {
		return map.get(tenantId);
	}

	//Shut down the thread pool
	@PreDestroy
	private void destroy() {
		this.map.values().forEach(ThreadPoolTaskExecutor::shutdown);
	}
}

** Figure 4.1 Contents of ThreadPoolTaskExecutors class **

@Configuration Class modification

[SampleConfig] mentioned above (https://qiita.com/sipadan2003/items/3277024b7da63c844663#configuration%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E4%BD% 9C%E6%88%90) Add taskExecutors2 () to the class as shown below. This method uses @Bean to [multi-tenant For Thread Pool Management Object](#Add Thread Pool Management Class) is registered as a bean.

public class SampleConfig {
		:
	@Bean
	public ThreadPoolTaskExecutors taskExecutors2() {
		return new ThreadPoolTaskExecutors(threadPoolSettings);
	}

** Figure 4.2 Contents of SampleConfig # taskExecutors2 () **

@Service Class modification

[Sample Service] mentioned above (https://qiita.com/sipadan2003/items/3277024b7da63c844663#service%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E4%BD% 9C%E6%88%90) Add cmd2 () to the class as shown below. Unlike cmd (), @Async is not added and it returns. The value is a String object, not a Future.

public class SampleService {
		:
	public String cmd2(final String[] command) {
		final int exitValue = run(command);
		return "exitValue="+exitValue;
	}
		:
}

** Figure 4.3 Contents of SampleService # cmd2 () **

@Controller Class modification

[SampleController] mentioned above (https://qiita.com/sipadan2003/items/3277024b7da63c844663#controller%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E4%BD% 9C% E6% 88% 90) Refurbish the class. First, add the taskExecutors2 field. Beans registered in SampleConfig class because there is @Autowired Is automatically injected.

public class SampleController {
		:
	@Autowired
	private ThreadPoolTaskExecutors taskExecutors2;
		:
}

** Figure 4.4.1 Contents of SampleService # taskExecutors2 **

Next, add a method that calls cmd2 () of SampleService. The processing flow of this method is the same as the cmd () method, except that it gets the thread pool and explicitly makes asynchronous calls.

public class SampleController {
		:
	@RequestMapping(value="/cmd2", method=RequestMethod.GET)
	@ResponseBody
	public String cmd2(@RequestParam(value="id", required=true)final String id) {
		final String tenantId = String.format("%s#%03d", id, counter.getAndIncrement());

		//Get ThreadPool
		final ThreadPoolTaskExecutor executor = taskExecutors2.getExecutor(id);
		if(executor == null) {
			return "Invalid id: id="+id+"\n";
		}

		try{
			final Future<String> future = executor.submit(()->{
				return sampleService.cmd2(COMMAND); //Call Service
			});

			synchronized(this.futures) {
				this.futures.put(tenantId, future);
			}
			return "Accepted: id="+tenantId+"\n";
		}catch(RejectedExecutionException e) {
			final CompletableFuture<String> future = new CompletableFuture<>();
			future.complete("Rejected");
			synchronized(this.futures) {
				this.futures.put(tenantId, future);
			}
			return "Rejected: id="+tenantId+"\n";
		}
	}

** Figure 4.4.2 Contents of SampleService # cmd2 () **

Operation check

You can start the Tomcat container locally and execute the command as follows to check the operation.

$ curl -X GET http://localhost:8080/cmd2?id=Tenant1
Accepted: id=Tenant1#001

$ curl -X GET http://localhost:8080/cmd2?id=Tenant1
Accepted: id=Tenant1#001

$ curl -X GET http://localhost:8080/cmd2?id=Tenant1
Accepted: id=Tenant1#001
$ curl -X GET http://localhost:8080/cmd2?id=Tenant1

$ curl -X GET http://localhost:8080/cmd2?id=Tenant1
Rejected: id=Tenant1#001

$ curl -X GET http://localhost:8080/cmd2?id=Tenant1
Rejected: id=Tenant1#001

#The thread pool is "maxPoolSize"=2」、「queueCapacity=When "2"
#After waiting a little over 10 seconds/When you get the status
#2 tasks that have finished executing, 2 tasks that are running, 2 tasks that have been rejected
#Is output
$ curl -X GET http://localhost:8080/status
Tenant1#001: exitValue=0
Tenant1#002: Running
Tenant1#003: Running
Tenant1#004: exitValue=0
Tenant1#005: Rejected
Tenant1#006: Rejected

#Normal termination of tomcat container
$ curl -X POST http://localhost:8080/shutdown

** Figure 4.5 Multi-tenant support Asynchronous task operation check **

Recommended Posts

Implementation of multi-tenant asynchronous processing in Tomcat
Implementation of asynchronous processing in Tomcat
Implementation of asynchronous processing for single tenant in Tomcat
Implementation of gzip in java
Implementation of tri-tree in Java
Implementation of HashMap in kotlin
[Rails] Asynchronous implementation of like function
Try implementing asynchronous processing in Azure
Implementation of ls command in Ruby
Order of processing in the program
Implementation of like function in Java
Implementation of DBlayer in Java (RDB, MySQL)
About UI thread processing in Android asynchronous
How to implement asynchronous processing in Outsystems
Asynchronous processing with regular execution in Spring Boot
Implementation of digit grouping in flea market apps
SKStoreReviewController implementation memo in Swift UI of iOS14
[Rails] Implementation of retweet function in SNS application
Implementation of GKAccessPoint
[Rails] Implementation of "notify notification in some way"
[Java / Spring Boot] Spring security ⑤ --Implementation of logout processing
[Rails] Implementation of batch processing using whenever (gem)
Format of the log output by Tomcat itself in Tomcat 8
Prepare the execution environment of Tomcat in IntelliJ Community
Access the war file in the root directory of Tomcat
About the problem of deadlock in parallel processing in gem'sprockets' 4.0
Implementation example of simple LISP processing system (Java version)
Asynchronous processing and Web API integration in Android Studio
Addition of variables in iterative processing in a while statement
Implementation example of simple LISP processing system (Ruby version)
Interpreter implementation in Java
Implementation of flash messages
[Swift] Asynchronous processing "GCD"
Implementation of search function
[Java] I want to write asynchronous processing using Promise in Java-Trial of Promise-like grammar of JavaScript-
Boyer-Moore implementation in Java
Applied implementation of chat-space
Heapsort implementation (in java)
Use MouseListener in Processing
Implementation of pagination function
Output in multiples of 3
About merge processing implementation including sorting function of Stream API
Rails sorting function implementation (displayed in order of number of like)