Reputation: 1110
we have a service that has one endpoint which needs to be restricted to process 2 requests at a time. These 2 requests can take a while to be completed.
Currently we use the tomcat properties to do so. The problem we face is now is that - when these 2 threads are used up for that endpoint - our healthcheck does not work anymore.
So we would like to restrict the number of requests for that particular endpoint. We pondered a while about it and one idea was to do so via filter, but that seems very hacky to me...
So I was hoping someone has another idea?
Upvotes: 2
Views: 639
Reputation: 13009
Here's an example of how to implement an asynchronous REST controller that will handle no more than 2 simultaneous requests at the same time. This implementation will not block any of your Tomcat servlet threads while the requests are being processed.
If another one arrives while the two are in progress then the caller will get an HTTP 429 (Too Many Requests).
This example immediately rejects requests that cannot be handled with a 429. If instead you'd like to queue pending requests until one of the 2 processing threads are available then replace SynchronousQueue
with another implementation of BlockingQueue
.
You might want to tidy up this sample, I've intentionally embedded all classes used to fit it in here:
@Configuration
@RestController
public class TestRestController {
static class MyRunnable implements Runnable {
DeferredResult<ResponseEntity<String>> deferredResult;
MyRunnable(DeferredResult<ResponseEntity<String>> dr) {
this.deferredResult = dr;
}
@Override
public void run() {
// do your work here and adjust the following
// line to set your own result for the caller...
this.deferredResult.setResult(ResponseEntity.ok("it worked"));
}
}
@SuppressWarnings("serial")
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
static class TooManyRequests extends RuntimeException {
}
private final ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0L, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
(runnable, executor) -> {
((MyRunnable) runnable).deferredResult.setErrorResult(new TooManyRequests());
});
@GetMapping(value = "/blah", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public DeferredResult<ResponseEntity<String>> yourRestService() {
final DeferredResult<ResponseEntity<String>> deferredResult = new DeferredResult<>();
this.executorService.execute(new MyRunnable(deferredResult));
return deferredResult;
}
}
Upvotes: 3
Reputation: 90507
By default RequestMappingHandlerAdapter
handles @Controller
's @RequestMapping
methods . So the most easiest way is to create your own RequestMappingHandlerAdapter
and override its handleInternal
to add your control logic.
Below is the pseudocode:
public static class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
//counter to keep track number of concurrent request for each HandlerMethod
//HandlerMethod represent a @RequestMapping method
private Map<HandlerMethod, Integer> requestCounterMap = new ConcurrentHashMap<>();
@Override
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response,
HandlerMethod handlerMethod) throws Exception {
//Increase the counter for this handlerMethod by 1.
//Throw exception if the counter is more than 2 request
ModelAndView mv = super.handleInternal(request, response, handlerMethod);
//Method finish , decrease the counter by 1
return mv;
}
}
Assume you are using the spring boot MVC auto-configuration , you can replace RequestMappingHandlerAdapter
with your customized one by creating a WebMvcRegistrations
bean and override its getRequestMappingHandlerAdapter()
methods:
@Bean
public WebMvcRegistrations webMvcRegistrations() {
return new WebMvcRegistrations() {
@Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return new MyRequestMappingHandlerAdapter();
}
};
}
Upvotes: 1