12

my applications should have 2 core endpoints: push, pull for sending and fetching data.

Pull operation should works asynchronously and result DeferredResult. When user call pull service over rest, new DefferedResult is created and stored into Map<Long, DefferedResult> results = new ConcurrentHashMap<>() where is waiting for new data or until timeout is expired.

Push operation call user over rest as well, and this operation checks map of results for recipient of data pushed by this operation. When map contains result of recipient, these data are set to his result, DefferedResult is returned.

Here is base code:

@Service
public class FooServiceImpl {
    Map<Long, DefferedResult> results = new ConcurrentHashMap<>();

    @Transactional
    @Override
    public DeferredResult<String> pull(Long userId) {
        // here is database call, String data = fooRepository.getNewData(); where I check if there are some new data in database, and if there are, just return it, if not add deferred result into collection to wait for it
        DeferredResult<String> newResult = new DeferredResult<>(5000L);
        results.putIfAbsent(userId, newResult);
        newResult.onCompletion(() -> results.remove(userId));

        // if (data != null)
        //      newResult.setResult(data);

        return newResult;
    }

    @Transactional
    @Override
    public void push(String data, Long recipientId) {
        // fooRepository.save(data, recipientId);
        if (results.containsKey(recipientId)) {
            results.get(recipientId).setResult(data);
        }
    }
}

Code is working as I expected problem is that should also works for multiple users. I guess the max active users which will call pull operation will max 1000. So every call of pull take max 5 seconds as I set in DefferedResult but it isn't.

As you can see in image, if I immediately call rest of pull operation from my javascript client multiple times you can see that tasks will executed sequentially instead of simultaneously. Tasks which I fired as last take about 25 seconds, but I need that when 1000 users execute at same time pull operation, that operation should take max 5 seconds + latency.

enter image description here

How to configure my app to execute these tasks simultaneously and ensure each each task will about 5 seconds or less (when another user send something to waiting user)? I tried add this configuration into property file:

server.tomcat.max-threads=1000

and also this configuration:

@Configuration
public class AsyncConfig extends AsyncSupportConfigurer {

    @Override
    protected AsyncTaskExecutor getTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(1000);
        taskExecutor.initialize();
        return taskExecutor;
    }
}

But it didn't help, still same result. Can you help me configure it please?

EDIT:

This is how I calling this service from angular:

this.http.get<any>(this.url, {params})
  .subscribe((data) => {
    console.log('s', data);
  }, (error) => {
    console.log('e', error);
  });

When I tried call it multiple times with pure JS code like this:

function httpGet()
{
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.open( "GET", 'http://localhost:8080/api/pull?id=1', true );
    xmlHttp.send( null );
    return xmlHttp.responseText;
}
setInterval(httpGet, 500);

it will execute every request call much faster (about 7 seconds). I expected that increasing is caused database calling in service, but it still better than 25 sec. Do I have something wrong with calling this service in angular?

EDIT 2:

I tried another form of testing and instead of browser I used jMeter. I execute 100 requests in 100 threads and here is result:

enter image description here

As you can see requests will be proceed by 10, and after reach 50 requests application throw exception:

java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
    at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:667) ~[HikariCP-2.7.8.jar:na]
    at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:183) ~[HikariCP-2.7.8.jar:na]
    at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:148) ~[HikariCP-2.7.8.jar:na]
    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-2.7.8.jar:na]
    at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final]
    at org.hibernate.internal.NonContextualJdbcConnectionAccess.obtainConnection(NonContextualJdbcConnectionAccess.java:35) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final]
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:106) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final]
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:136) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final]
    at org.hibernate.internal.SessionImpl.connection(SessionImpl.java:523) ~[hibernate-core-5.2.16.Final.jar:5.2.16.Final]
    at sun.reflect.GeneratedMethodAccessor61.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_171]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_171]
    at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:223) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:207) ~[spring-core-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle.doGetConnection(HibernateJpaDialect.java:391) ~[spring-orm-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.beginTransaction(HibernateJpaDialect.java:154) ~[spring-orm-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:400) ~[spring-orm-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:378) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:474) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:289) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) ~[spring-tx-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689) ~[spring-aop-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at sk.moe.zoya.service.impl.FooServiceImpl$$EnhancerBySpringCGLIB$$ebab570a.pull(<generated>) ~[classes/:na]
    at sk.moe.zoya.web.FooController.pull(FooController.java:25) ~[classes/:na]
    at sun.reflect.GeneratedMethodAccessor60.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_171]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_171]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-embed-websocket-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_171]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_171]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.29.jar:8.5.29]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_171]

2018-06-02 13:21:47.163  WARN 26978 --- [io-8080-exec-48] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: null
2018-06-02 13:21:47.163  WARN 26978 --- [io-8080-exec-40] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: null
2018-06-02 13:21:47.163 ERROR 26978 --- [io-8080-exec-48] o.h.engine.jdbc.spi.SqlExceptionHelper   : HikariPool-1 - Connection is not available, request timed out after 30000ms.
2018-06-02 13:21:47.163 ERROR 26978 --- [io-8080-exec-40] o.h.engine.jdbc.spi.SqlExceptionHelper   : HikariPool-1 - Connection is not available, request timed out after 30000ms.
2018-06-02 13:21:47.164 ERROR 26978 --- [io-8080-exec-69] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection] with root cause

I also comment code where I use Repositories to ensure there is nothing with database, and same result. ALso I set uniqe userId for each request with AtomicLong class.

EDIT 3:

I find out when I comment also @Transactional everything works fine! So can you tell me how to set spring's transactions for large amount of operations without increasing delay?

I added spring.datasource.maximumPoolSize=1000 to increase pool size which I guess shoulds, so the only problem is how to speed up methods with @Transactional.

Every call to pull method is annotated with @Transactional because I need at first load data from database and check if there are new data, because yes, I do not have to do creating waiting deferred result. push methods have to be annotation with @Transaction as well, because there I need at first store received data in database and next set that value to waiting results. For my data I am using Postgres.

ouflak
  • 2,458
  • 10
  • 44
  • 49
Denis Stephanov
  • 4,563
  • 24
  • 78
  • 174
  • See https://stackoverflow.com/questions/13206792/spring-async-limit-number-of-threads. Looks like maybe you need @EnableAsync, and possibly getAsyncExecutor. – user1676075 May 29 '18 at 16:21
  • I tried it and same result :/ – Denis Stephanov May 29 '18 at 16:22
  • 1
    may it is not the java backend which is the issue. a browser can open a limited number of connection to a server. 2-6. number varies based on browser. so even if you made 1000 requests, may be they are queued up at the browser itself. Just for testing try increasing this value. in firefox config param is network.http.max-persistent-connections-per-server. You can also try using multiple browser on multiple machines. or you can get out of the browser and using some npm client. – gagan singh May 29 '18 at 23:56
  • 2
    You cannot test the latency through same browser. That is a wrong way of testing. A browser will at most 6 connections to a given domain on HTTP1.1, so if you open 100, all will be queued and executed when a connection becomes available. So your test approach is wrong. Open multiple incognito windows and then try to test your approach – Tarun Lalwani May 30 '18 at 06:05
  • @TarunLalwani Ok, I tried multiple incognito windows and in every window I open my app which call pull method, but in last window this operation take about 25 seconds as well, so I guess problem is something else :/ – Denis Stephanov May 30 '18 at 06:38
  • Provide a minimal git repo and I will dig – Tarun Lalwani May 30 '18 at 06:41
  • @TarunLalwani thanks for your effort, I tried create minimal running code but I find out it works when I call this service in interval multiple times. So I think problem is with my clients app maybe and I little change question and show how is my service called. – Denis Stephanov May 30 '18 at 08:00
  • you may create a simple junit test and execute it in several thread so you can verify your server side code is OK while you need to investigate on the client side – Angelo Immediata May 30 '18 at 11:46
  • We're missing some frontend code here. How do you call the service/method that makes the http call? – maxime1992 May 30 '18 at 13:18
  • @maxime1992 Hi, I've added how I call this REST in angular, check please EDIT section on end of post. – Denis Stephanov May 30 '18 at 13:22
  • I've seen that section. I'm wondering from where you call that. There's not loop here – maxime1992 May 30 '18 at 13:24
  • @maxime1992 I just add call for that function to button, so I just clicking button a lot of times – Denis Stephanov May 30 '18 at 13:27
  • 1
    As I and Tarun mentioned, your way of testing is not correct. Just write some non browser junit test case with multiple threads (1000 if you pefer). Everything is fine, except your way of testing. – gagan singh May 30 '18 at 16:33
  • @gagansingh can you please give me hint how to test execution time of multiple calling some service? – Denis Stephanov May 31 '18 at 20:28
  • 1
    Just to be sure this is not a testing mistake, I'm assuming you're well aware the an invocation to your pull endpoint will block until the corresponding result has not been provided by your push endpoint or the tomcat thread timesout. So, it is perfectly normal to see that duration depending on how soon you're providing results in the shared map. This is not matter of how fast the application is. It is a matter of how fast you can provide results. – Edwin Dalorzo Jun 01 '18 at 19:57
  • @EdwinDalorzo I also tried mozila with additional settings which give me here in comment, and I got same result. This what you say make sense, but I have no idea how to do it without map. I need store pending requests (deferred results) in map, and when new data are received by server I have check in map if there are some request waiting for that data, and set it which cause deferred result is sent to client and I can remove it from map. – Denis Stephanov Jun 01 '18 at 20:02
  • @DenisStephanov Maybe so, but that would be a different question. Your current question is about the testing performance, your comments now are about your solution design. – Edwin Dalorzo Jun 01 '18 at 20:12
  • @EdwinDalorzo no my question is about configuration which fix my increasing time of requests, I also put it into bounty hint. – Denis Stephanov Jun 01 '18 at 20:14
  • 1
    @DenisStephanov I already provided an answer about that and what I believe is the cause of those times you see. But my understanding, as per my reading of your code, is that that is by your own design and how you're testing. – Edwin Dalorzo Jun 01 '18 at 20:16
  • 1
    @DenisStephanov There is also a but in your service, when it process requests for the same id. I comment that on my answer and that could be the cause of the long waits you see. – Edwin Dalorzo Jun 01 '18 at 20:20
  • *"I also put it into bounty hint."* - That won't help ... if you already have the real answer, and you don't like / believe / want to accept it. Which is the case here, I think. – Stephen C Jun 02 '18 at 01:54
  • I think you slow processing is caused by @Transactional – Martin Ondo-Eštok Jun 02 '18 at 12:18
  • @MartinOndo-Eštok yes, you are right, but how can I fix it?? :/ – Denis Stephanov Jun 02 '18 at 12:34
  • 1
    It is not obvious in your question where and how you use transactions. it seems as if your Hikari connection pool is running out of connections. Perhaps the number of concurrent requests you’re making is big enough to exhaust the database connections in your pool to a point where new requests are put in a queue and eventually timeout and cause this exception. – Edwin Dalorzo Jun 02 '18 at 14:38

3 Answers3

7

It seems the problem here is that you're running out of connections in the database pool.

You have your method tagged with @Transaction but your controller is also expecting the result of the method i.e. the DeferredResult to be delivered as soon as possible such that the thread is set free.

Now, this is what happens when you run a request:

  • The @Transaction functionality is implemented in a Spring proxy which must open a connection, call your subject method and then commit or rollback the transaction.
  • Therefore, when your controller invokes the fooService.pull method, it is in fact calling a proxy.
  • The proxy must first request a connection from the pool, then it invokes your service method, which within that transaction does some database operation. Finally, it must commit or rollback the transaction and finally return the connection to the pool.
  • After all this your method returns a DeferredResult, which is then passed to the controller for it to return.

Now, the problem is that DeferredResult is designed in such a way that it should be used asynchronously. In other words, the promise is expected to be resolved later in some other thread, and we are supposed to free the request thread as soon as possible.

In fact, Spring documentation on DeferredResult says:

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // Save the deferredResult somewhere..
    return deferredResult;
}

// From some other thread...
deferredResult.setResult(data);

The problem in your code is precisely that the DeferredResult is being solved in the same request thread.

So, the thing is that when the Spring proxy requests a connection to the database pool, when you do your heavy load tests, many requests will find the pool is full and does not have connections available. So the request is put on hold, but at that point your DeferredResult has not been created yet, so its timeout functionality does not exist.

Your request is basically waiting for some connection from the database pool to become available. So, let's say 5 seconds pass, then the request gets a connection, and now you get DeferredResult which the controller uses to handle the response. Eventually, 5 seconds later it timeout. So you have to add your time waiting for a connection from the pool and your time waiting for the DeferredResult to get resolved.

That's why you probably see that, when you test with JMeter, the request time gradually increases as connections get depleted from the database pool.

You can enable some logging for the thread pool by adding the following your application.properties file:

logging.level.com.zaxxer.hikari=DEBUG

You could also configure the size of your database pool and even add some JMX support such that you can monitor it from Java Mission Control:

spring.datasource.hikari.maximumPoolSize=10
spring.datasource.hikari.registerMbeans=true

Using JMX support you will be able to see how the database pool gets depleted.

The trick here consists in moving the logic that resolves the promise to another thread:

@Override
public DeferredResult pull(Long previousId, String username) {


    DeferredResult result = createPollingResult(previousId, username);

    CompletableFuture.runAsync(() -> {
        //this is where you encapsulate your db transaction
        List<MessageDTO> messages = messageService.findRecents(previousId, username); // should be final or effective final
        if (messages.isEmpty()) {
           pollingResults.putIfAbsent(username, result);
        } else {
           result.setResult(messages);
        }
    });

    return result;
}

By doing this, your DeferredResult is returned immediately and Spring can do its magic of asynchronous request handling while it sets free that precious Tomcat thread.

Edwin Dalorzo
  • 76,803
  • 25
  • 144
  • 205
  • This userId which is key of map is id of logged user, and it should be always only one in application – Denis Stephanov Jun 02 '18 at 09:18
  • Check ma updated question please. I use another form of testing and also ensure manually to be unique Id of userId. My requests are proceed by 10 so there is some limitation in application which could be set I guess – Denis Stephanov Jun 02 '18 at 12:08
  • One guy in commed has a good point, it is caused by @Transactional, can you tell me how to fix it and keep these annotations below methods? – Denis Stephanov Jun 02 '18 at 12:38
  • 1
    You have edited your question *3 times* already which means you have us all here trying to answer the wrong question from the beginning and you yet have not even shown any appreciation for the efforts to help you so far. So, I think you should probably sit down and investigate your problem more carefully yourself and come back when you have the actual question you want to ask. From my point of view this question is no good anymore since it is all over the place and seems impossible to know what’s going on with your code based on what you share. – Edwin Dalorzo Jun 02 '18 at 14:09
  • I'm sorry about that but I also trying to solve it, and when I find something what could more specify problem I edited question. I can't delete question after new finding because of existing answers and running bounty. So the only thing what can I do is just update question. You can trust me I spend a lot of time solve it by myself before I asking here. – Denis Stephanov Jun 02 '18 at 14:23
  • 1
    I understand. But you have to also understand that writing my answer took me a while. I did take the time to study your question and wrote your code and ran it. You haven’t even upvoted any answer, and constantly change your knowledge of the problem making any effort we have done obsolete and we, the responders, get nothing out of it. You have a bounty, we’re all here for it. By the way, your transactional problem seems to be that your Hikari connection pool is too small for the amount of requests you want to make. It runs out of connections. Make it bigger! – Edwin Dalorzo Jun 02 '18 at 14:32
  • I know, I was also too busy to solving my problem so I forgot it until now. But be sure I would do it later. – Denis Stephanov Jun 02 '18 at 14:35
  • Yes, it make sense but also when I increase this value it will works until I got commented @Transactional annotations, after that I uncomment it it will be again slow – Denis Stephanov Jun 02 '18 at 14:37
  • I’m sorry, but I don’t understand your last comment. What happens when you increase the value? – Edwin Dalorzo Jun 02 '18 at 14:41
  • Sorry for my english, I know that it is not the best. I extended my last edit and I added more information about transactions in code. Last commend mean I can fix that exception adding property to increase pool size which I also did. But it just fix exception not main problem of slowing processing methods annotated by @Transactional. – Denis Stephanov Jun 02 '18 at 14:46
  • We can chat about it if you want. Maybe together we can find the answer. Feel free to move this to a chat and I’ll be there. – Edwin Dalorzo Jun 02 '18 at 14:47
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/172307/discussion-between-denis-stephanov-and-edwin-dalorzo). – Denis Stephanov Jun 02 '18 at 14:48
2

I think you need producer and consumer structure model. i write code for you. i hope it will help you.

This is sample code :

DeferredResultStrore

@Component
public class DeferredResultStrore {

    private Queue<DeferredResult<String>> responseBodyQueue;
    private HashMap<String, List<DeferredResult<InterfaceModel>>> groupMap;
    private final long resultTimeOut;

    public DeferredResultStrore() {
        responseBodyQueue = new LinkedBlockingQueue<DeferredResult<String>>();
        groupMap = new HashMap<String, List<DeferredResult<InterfaceModel>>>();
        // write time.
        resultTimeOut = 1000 * 60 * 60;
    }

    public Queue<DeferredResult<String>> getResponseBodyQueue() {
        return responseBodyQueue;
    }

    public HashMap<String, List<DeferredResult<InterfaceModel>>> getGroupMap() {
        return groupMap;
    }

    public long getResultTimeOut() {
        return resultTimeOut;
    }

}

DeferredResultService

public interface DeferredResultService {

    public DeferredResult<?> biteResponse(HttpServletResponse resp, HttpServletRequest req);

    public DeferredResult<?> biteGroupResponse(String key, HttpServletResponse resp);

}

DeferredResultServiceImpl

@Service
public class DeferredResultServiceImpl implements DeferredResultService {

    @Autowired
    private DeferredResultStrore deferredResultStore;

    @Override
    public DeferredResult<?> biteResponse(final HttpServletResponse resp, HttpServletRequest req) {

        final DeferredResult<String> defResult = new DeferredResult<String>(deferredResultStore.getResultTimeOut());

        removeObserver(resp, defResult, null);

        deferredResultStore.getResponseBodyQueue().add(defResult);

        return defResult;
    }

    @Override
    public DeferredResult<?> biteGroupResponse(String key, final HttpServletResponse resp) {

        final DeferredResult<InterfaceModel> defResult = new DeferredResult<InterfaceModel>(
                deferredResultStore.getResultTimeOut());

        List<DeferredResult<InterfaceModel>> defResultList = null;

        removeObserver(resp, defResult, key);

        if (deferredResultStore.getGroupMap().containsKey(key)) {

            defResultList = deferredResultStore.getGroupMap().get(key);
            defResultList.add(defResult);

        } else {

            defResultList = new ArrayList<DeferredResult<InterfaceModel>>();
            defResultList.add(defResult);
            deferredResultStore.getGroupMap().put(key, defResultList);

        }

        return defResult;
    }

    private void removeObserver(final HttpServletResponse resp, final DeferredResult<?> defResult, final String key) {

        defResult.onCompletion(new Runnable() {
            public void run() {
                if (key != null) {
                    List<DeferredResult<InterfaceModel>> defResultList = deferredResultStore.getGroupMap().get(key);

                    if (defResultList != null) {
                        for (DeferredResult<InterfaceModel> deferredResult : defResultList) {
                            if (deferredResult.hashCode() == defResult.hashCode()) {
                                defResultList.remove(deferredResult);
                            }
                        }
                    }

                } else {
                    if (!deferredResultStore.getResponseBodyQueue().isEmpty()) {
                        deferredResultStore.getResponseBodyQueue().remove(defResult);
                    }
                }
            }
        });

        defResult.onTimeout(new Runnable() {
            public void run() {
                // 206
                resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

                if (key != null) {
                    List<DeferredResult<InterfaceModel>> defResultList = deferredResultStore.getGroupMap().get(key);

                    if (defResultList != null) {
                        for (DeferredResult<InterfaceModel> deferredResult : defResultList) {
                            if (deferredResult.hashCode() == defResult.hashCode()) {

                                InterfaceModel model = new InterfaceModel();
                                model.setId(key);
                                model.setMessage("onTimeout");

                                deferredResult.setErrorResult(model);
                                defResultList.remove(deferredResult);
                            }
                        }
                    }

                } else {
                    defResult.setErrorResult("onTimeout");
                    deferredResultStore.getResponseBodyQueue().remove(defResult);
                }
            }
        });
    }

}

PushService

public interface PushService {

    public boolean pushMessage(String message);

    public boolean pushGroupMessage(String key, String topic, HttpServletResponse resp);

}

PushServiceImpl

@Service
public class PushServiceImpl implements PushService {

    @Autowired
    private DeferredResultStrore deferredResultStore;

    @Override
    public boolean pushMessage(String message) {

        if (!deferredResultStore.getResponseBodyQueue().isEmpty()) {

            for (DeferredResult<String> deferredResult : deferredResultStore.getResponseBodyQueue()) {

                deferredResult.setResult(message);
            }

            deferredResultStore.getResponseBodyQueue().remove();
        }

        return true;
    }

    @Override
    public boolean pushGroupMessage(String key, String topic, HttpServletResponse resp) {
        List<DeferredResult<InterfaceModel>> defResultList = null;

        // select data in DB. that is sample group push service. need to connect db.
        InterfaceModel model = new InterfaceModel();
        model.setMessage("write group message.");
        model.setId(key);

        if (deferredResultStore.getGroupMap().containsKey(key)) {
            defResultList = deferredResultStore.getGroupMap().get(key);

            for (DeferredResult<InterfaceModel> deferredResult : defResultList) {
                deferredResult.setResult(model);
            }

            deferredResultStore.getGroupMap().remove(key);
        }

        return true;
    }

}

InterfaceModel

public class InterfaceModel {

    private String message;

    private int idx;
    private String id;

    // DB Column

    public InterfaceModel() {
        // TODO Auto-generated constructor stub
    }

    public InterfaceModel(String message, int idx, String id) {
        this.message = message;
        this.idx = idx;
        this.id = id;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public int getIdx() {
        return idx;
    }

    public void setIdx(int idx) {
        this.idx = idx;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

}

web.xml

async-supported very important in settings.

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

Java base

@Bean
public ServletRegistrationBean dispatcherServlet() {
    ServletRegistrationBean registration = new ServletRegistrationBean(
            new DispatcherServlet(), "/");
    registration.setAsyncSupported(true);
    return registration;
}

In fact :

A DeferredResult is associated with an open request. When the request completes, the DeferredResult is removed from the map, and then, the client issues a new long polling request, which adds a new DeferredResult instance


Spring Boot will automatically register any Servlet beans in your application context with the servlet container. By default async supported is set to true so there's nothing for you to do beyond creating a bean for your Servlet.

@Aligtor, for you => public @interface EnableAsync Enables Spring's asynchronous method execution capability, similar to functionality found in Spring's XML namespace.

0gam
  • 1,343
  • 1
  • 9
  • 21
  • Hi thanks for answer, I will try it today and give you response. Can you please provide java-based configuration of last block of code? – Denis Stephanov Jun 01 '18 at 06:51
  • 1
    There is a lot of code which I guess doesn't OP help. Why are you registering servlet bean like that? I think it does boot instead of you, and also for async behaviour you can use @EnableAsync – Aligator Jun 01 '18 at 15:22
  • @Byeon0gam I tried your code but I got Could not open ServletContext resource [/WEB-INF/dispatcherServlet-servlet.xml ... so there is something wrong with that configuration, it require some xml but I don't use any in my app – Denis Stephanov Jun 01 '18 at 19:51
  • @Aligtor, ok, i know... i just give you(Denis Stephanov) java base configuration. – 0gam Jun 02 '18 at 01:19
  • @DenisStephanov, your app is boot. By default async supported is set to true. please try it again – 0gam Jun 02 '18 at 01:21
2

As many guys mentioned it's not correct way to test the performance. You asked to do automated requests at certain period of time as you were doing in XMLHttpRequest. You can use interval of Observable as:

import {Observable} from "rxjs/Observable";

import {Subscription} from "rxjs/Subscription";

private _intervalSubscription: Subscription;

ngOnInit() {
    this._intervalSubscription = Observable.interval(500).subscribe(x => {
        this.getDataFromServer();
    });
}

ngOnDestroy(): void {
    this._intervalSubscription.unsubscribe();
}

getDataFromServer() {
    // do your network call
    this.http.get<any>(this.url, {params})
                .subscribe((data) => {
                    console.log('s', data);
                }, (error) => {
                    console.log('e', error);
                }); 
}

This is the best possible way of doing polling from client side.

EDIT 1

private prevRequestTime: number;

ngAfterViewInit(): void {
    this.getDataFromServer();
}

getDataFromServer() {
    this.prevRequestTime = Date.now();
    // do your network call
    this.http.get<any>(this.url, {params})
            .subscribe((data) => {
                console.log('s', data);
                this.scheduleRequestAgain();
            }, (error) => {
                console.log('e', error);
                this.scheduleRequestAgain();
            }); 
}

scheduleRequestAgain() {
    let diff = Date.now() - this.prevRequestTime;
    setTimeout(this.getDataFromServer(), diff);
}
Community
  • 1
  • 1
Anshuman Jaiswal
  • 5,352
  • 1
  • 29
  • 46
  • My polling is depends on server data, I am not sure, but I think when client manage time when is called service, it is short-polling, and when client just send request to server and it is waiting there until new data (it is my case) it is called long-polling. But maybe I am wrong. – Denis Stephanov Jun 01 '18 at 19:34
  • do you want to set it on interval depending on first request time from server? suppose you hit first request and it takes around 20s so you want to make a polling on 20s interval (i.e. further requests should hit server at 20s inetrval)? – Anshuman Jaiswal Jun 01 '18 at 19:38
  • Also if you want to notify the client whenever data has been updated on server, you can do it in 2 ways: `1)` websockets `2)` Polling. You can optimize polling by sending some kind of version from client like if it's already have latest data there is no need to send the data again and if it's not send the latest data with version. – Anshuman Jaiswal Jun 01 '18 at 19:45
  • yes, I have some timeout constant (for example 5 seconds, in real app it will be about 30s), and request is waiting on server until there are not something to send. If server receive new data suitable for waiting request, set this data as response, if no new data and timeout is expired send just empty object or null, and after that client send another request, and process is repeating. So time of request could be from 0s (new data on server when request sent) to 30 s (there are no data - timeout). In my example just manually call send polling request to test, if it will work for many users – Denis Stephanov Jun 01 '18 at 19:45
  • optimize will be next step, but for now I need working polling in current version without increasing of execution time when more request are sent – Denis Stephanov Jun 01 '18 at 19:48
  • @DenisStephanov I have updated answer with `EDIT 1`. Now first request would be in `AfterViewInit` and then it will check the time for completion of request and on basis of that will decide when to make next request call. It will also handle network timeout to make next request in case of timeout from server. – Anshuman Jaiswal Jun 01 '18 at 19:59
  • Thank you, but I think there is no problem with my client in current case, because I need solve why my requests aren't executed simultaneously – Denis Stephanov Jun 01 '18 at 20:04