├── .gitignore ├── Jenkinsfile ├── .travis.yml ├── src ├── main │ ├── resources │ │ └── application.yml │ └── java │ │ └── com │ │ └── codependent │ │ └── niorest │ │ ├── service │ │ ├── DataService.java │ │ └── DataServiceImpl.java │ │ ├── dto │ │ └── Data.java │ │ ├── controller │ │ ├── SyncRestController.java │ │ └── AsyncRestController.java │ │ ├── filter │ │ └── CorsFilter.java │ │ └── SpringNioRestApplication.java └── test │ └── java │ └── com │ └── codependent │ └── niorest │ ├── dto │ └── LoadTestInfo.java │ ├── SpringNioRestApplicationRestTemplateTest.java │ ├── SpringNioRestApplicationLoadTest.java │ └── SpringNioRestApplicationTest.java ├── pom.xml ├── README.md └── jmeter └── spring-nio-rest.jmx /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /target/ 3 | /.settings/ 4 | /.classpath 5 | /.project 6 | /test-output/ 7 | /.springBeans 8 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | @Library('jenkins-pipeline-shared-library-example') _ 3 | buildPipeline(['clean', 'install']) 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | script: 5 | - mvn clean install 6 | # - mvn mylab-core cobertura:cobertura coveralls:report -DrepoToken=LK5cUszS1GJtt9v0eqKXUY90ORmeNtSsa -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | cors: 2 | allowed: 3 | origins: http://localhost:9090 4 | hystrix: 5 | threadpool: 6 | default: 7 | coreSize: 1000 8 | maxQueueSize: 100000 9 | queueSizeRejectionThreshold: 10000 10 | execution: 11 | isolation: 12 | thread: 13 | timeoutInMilliseconds: 20000 14 | 15 | logging.level: 16 | org.springframework.web: ERROR -------------------------------------------------------------------------------- /src/main/java/com/codependent/niorest/service/DataService.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest.service; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.Future; 5 | 6 | import com.codependent.niorest.dto.Data; 7 | 8 | import rx.Observable; 9 | 10 | public interface DataService { 11 | 12 | List loadData(); 13 | Observable> loadDataHystrix(); 14 | Future> loadDataHystrixAsync(); 15 | Observable> loadDataObservable(); 16 | 17 | 18 | } -------------------------------------------------------------------------------- /src/main/java/com/codependent/niorest/dto/Data.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest.dto; 2 | 3 | import java.io.Serializable; 4 | 5 | import io.swagger.annotations.ApiModelProperty; 6 | 7 | public class Data implements Serializable{ 8 | 9 | private static final long serialVersionUID = 1049438747605741485L; 10 | @ApiModelProperty(required=true) 11 | private String key; 12 | @ApiModelProperty(required=true) 13 | private String value; 14 | 15 | public Data(){} 16 | 17 | public Data(String key, String value){ 18 | this.key=key; 19 | this.value=value; 20 | } 21 | 22 | public String getKey() { 23 | return key; 24 | } 25 | public void setKey(String key) { 26 | this.key = key; 27 | } 28 | public String getValue() { 29 | return value; 30 | } 31 | public void setValue(String value) { 32 | this.value = value; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/codependent/niorest/dto/LoadTestInfo.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest.dto; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class LoadTestInfo { 7 | 8 | private long minTime = 0; 9 | private long maxTime = 0; 10 | private int errors = 0; 11 | private List requests = new ArrayList(); 12 | 13 | public void saveRequest(long time){ 14 | requests.add(time); 15 | if(minTime>time){ 16 | minTime = time; 17 | } 18 | if(maxTime} 30 | */ 31 | @GetMapping(value="/sync/data", produces="application/json") 32 | @ApiOperation(value = "Gets data", notes="Gets data synchronously") 33 | @ApiResponses(value={@ApiResponse(code=200, message="OK")}) 34 | public List getData(){ 35 | return dataService.loadData(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/codependent/niorest/SpringNioRestApplicationRestTemplateTest.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; 10 | import org.springframework.web.client.RestTemplate; 11 | import org.testng.Assert; 12 | import org.testng.annotations.Test; 13 | 14 | @SpringBootTest(classes = SpringNioRestApplication.class, webEnvironment=WebEnvironment.DEFINED_PORT, properties="server.port=9090") 15 | public class SpringNioRestApplicationRestTemplateTest extends AbstractTestNGSpringContextTests{ 16 | 17 | private static final String BASE_URL = "http://localhost:9090"; 18 | 19 | RestTemplate rt = new RestTemplate(); 20 | 21 | @Test 22 | public void syncRestTemplateTest() { 23 | doRequest(BASE_URL+"/sync/data"); 24 | } 25 | 26 | @Test 27 | public void asyncRestTemplateTest() { 28 | doRequest(BASE_URL+"/deferred/data"); 29 | } 30 | 31 | @SuppressWarnings("rawtypes") 32 | private void doRequest(String url){ 33 | long t0 = System.currentTimeMillis(); 34 | ResponseEntity response = rt.getForEntity(url, List.class); 35 | if(response.getStatusCode()==HttpStatus.INTERNAL_SERVER_ERROR){ 36 | Assert.fail(); 37 | }else{ 38 | long t1 = System.currentTimeMillis(); 39 | System.out.printf("url %s - %s mseg%n",url, (t1-t0)); 40 | Assert.assertEquals(response.getBody().size(), 20); 41 | } 42 | }} 43 | -------------------------------------------------------------------------------- /src/main/java/com/codependent/niorest/filter/CorsFilter.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest.filter; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.Filter; 6 | import javax.servlet.FilterChain; 7 | import javax.servlet.FilterConfig; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.ServletRequest; 10 | import javax.servlet.ServletResponse; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.web.context.WebApplicationContext; 18 | import org.springframework.web.context.support.WebApplicationContextUtils; 19 | 20 | public class CorsFilter implements Filter{ 21 | 22 | private final Logger logger = LoggerFactory.getLogger(getClass()); 23 | 24 | @Value("${cors.allowed.origins}") 25 | private String corsAllowedOrigins; 26 | 27 | public CorsFilter() {} 28 | 29 | public void init(FilterConfig fConfig) throws ServletException { 30 | WebApplicationContext ac = WebApplicationContextUtils.getRequiredWebApplicationContext(fConfig.getServletContext()); 31 | ac.getAutowireCapableBeanFactory().autowireBean(this); 32 | } 33 | 34 | public void destroy() {} 35 | 36 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 37 | 38 | HttpServletRequest hRequest = (HttpServletRequest)request; 39 | HttpServletResponse hResponse = (HttpServletResponse)response; 40 | 41 | String remoteHost = hRequest.getHeader("Origin"); 42 | logger.debug("### rest request from remote host[{}]",remoteHost); 43 | 44 | if(remoteHost!=null && (corsAllowedOrigins.contains(remoteHost) || corsAllowedOrigins.equals("*"))){ 45 | logger.debug("### adding Access Control Headers for {} ###",remoteHost); 46 | hResponse.setHeader("Access-Control-Allow-Origin", remoteHost); 47 | //hResponse.setHeader("Access-Control-Allow-Credentials", "true"); 48 | hResponse.setHeader("Access-Control-Allow-Headers", "Content-Type,Accept"); 49 | } 50 | chain.doFilter(request, response); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/codependent/niorest/service/DataServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest.service; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.Future; 6 | 7 | import org.springframework.stereotype.Service; 8 | 9 | import com.codependent.niorest.dto.Data; 10 | import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; 11 | import com.netflix.hystrix.contrib.javanica.annotation.ObservableExecutionMode; 12 | import com.netflix.hystrix.contrib.javanica.command.AsyncResult; 13 | 14 | import rx.Observable; 15 | 16 | @Service 17 | public class DataServiceImpl implements DataService{ 18 | 19 | @Override 20 | public List loadData() { 21 | return generateData(false); 22 | } 23 | 24 | @Override 25 | public Observable> loadDataObservable() { 26 | return Observable.fromCallable(() -> generateData(false)); 27 | } 28 | 29 | @HystrixCommand(observableExecutionMode=ObservableExecutionMode.LAZY, fallbackMethod="loadDataHystrixFallback") 30 | @Override 31 | public Observable> loadDataHystrix() { 32 | double random = Math.random(); 33 | return Observable.fromCallable(() -> generateData( random < 0.9 ? false : true )); 34 | } 35 | 36 | @HystrixCommand(observableExecutionMode=ObservableExecutionMode.LAZY, fallbackMethod="loadDataHystrixFallback") 37 | @Override 38 | public Future> loadDataHystrixAsync() { 39 | double random = Math.random(); 40 | return new AsyncResult>() { 41 | @Override 42 | public List invoke() { 43 | return generateData( random < 0.9 ? false : true ); 44 | } 45 | }; 46 | } 47 | 48 | @SuppressWarnings("unused") 49 | private List loadDataHystrixFallback(){ 50 | return new ArrayList<>(); 51 | } 52 | 53 | private List generateData(boolean fail){ 54 | if(fail){ 55 | throw new RuntimeException("Counldn't generate data"); 56 | } 57 | List dataList = new ArrayList(); 58 | try { 59 | Thread.sleep(400); 60 | } catch (InterruptedException e) { 61 | e.printStackTrace(); 62 | } 63 | for (int i = 0; i < 20; i++) { 64 | Data data = new Data("key"+i, "value"+i); 65 | dataList.add(data); 66 | } 67 | return dataList; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/codependent/niorest/SpringNioRestApplicationLoadTest.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; 10 | import org.springframework.web.client.RestTemplate; 11 | import org.testng.annotations.AfterSuite; 12 | import org.testng.annotations.BeforeSuite; 13 | import org.testng.annotations.Test; 14 | 15 | import com.codependent.niorest.dto.LoadTestInfo; 16 | 17 | 18 | @SpringBootTest(classes = SpringNioRestApplication.class, webEnvironment=WebEnvironment.DEFINED_PORT, properties="server.port=9090") 19 | public class SpringNioRestApplicationLoadTest extends AbstractTestNGSpringContextTests{ 20 | 21 | private static final String BASE_URL = "http://localhost:9090"; 22 | 23 | RestTemplate rt = new RestTemplate(); 24 | 25 | LoadTestInfo syncTest = new LoadTestInfo(); 26 | LoadTestInfo asyncTest = new LoadTestInfo(); 27 | 28 | @BeforeSuite 29 | public void beforeSuite(){ 30 | } 31 | 32 | @Test(invocationCount=200, threadPoolSize=10) 33 | public void syncLoadTest() { 34 | doRequest(BASE_URL+"/sync/data", syncTest); 35 | } 36 | 37 | @Test(invocationCount=200, threadPoolSize=10) 38 | public void asyncCallableLoadTest() { 39 | doRequest(BASE_URL+"/callable/data", asyncTest); 40 | } 41 | 42 | @SuppressWarnings("rawtypes") 43 | private void doRequest(String url, LoadTestInfo info){ 44 | final Bool finished = new Bool(false); 45 | long t0 = System.currentTimeMillis(); 46 | new Thread( 47 | ()->{ 48 | ResponseEntity response = rt.getForEntity(url, List.class); 49 | if(response.getStatusCode()==HttpStatus.INTERNAL_SERVER_ERROR){ 50 | info.saveErrorRequest(); 51 | }else{ 52 | long t1 = System.currentTimeMillis(); 53 | info.saveRequest((t1-t0)); 54 | } 55 | finished.setValue(true); 56 | } 57 | ).start(); 58 | while(!finished.isValue()){ 59 | try { 60 | Thread.sleep(10); 61 | } catch (InterruptedException e) { 62 | e.printStackTrace(); 63 | } 64 | } 65 | } 66 | 67 | @AfterSuite 68 | public void afterSuite(){ 69 | if(syncTest.getNumberOfRequests()>0){ 70 | System.out.printf("[SYNC] Result: #ofRequests %s - Average time %s mseg - minTime %s mseg - maxTime %s mseg - errors %s%n", 71 | syncTest.getNumberOfRequests(), syncTest.getAverageTime(), syncTest.getMinTime(), syncTest.getMaxTime(), syncTest.getErrors()); 72 | } 73 | if(asyncTest.getNumberOfRequests()>0){ 74 | System.out.printf("[ASYNC] Result: #ofRequests %s - Average time %s mseg - minTime %s mseg - maxTime %s mseg - errors %s%n", 75 | asyncTest.getNumberOfRequests(), asyncTest.getAverageTime(), asyncTest.getMinTime(), asyncTest.getMaxTime(), asyncTest.getErrors()); 76 | } 77 | } 78 | 79 | private class Bool{ 80 | private boolean value; 81 | public Bool(boolean value){ 82 | this.value=value; 83 | } 84 | public boolean isValue() { 85 | return value; 86 | } 87 | public void setValue(boolean value) { 88 | this.value = value; 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/codependent/niorest/SpringNioRestApplication.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 9 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 10 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 11 | import org.springframework.cloud.netflix.rx.RxJavaAutoConfiguration; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Import; 14 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 15 | 16 | import com.codependent.niorest.filter.CorsFilter; 17 | import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; 18 | 19 | import springfox.documentation.builders.ApiInfoBuilder; 20 | import springfox.documentation.service.ApiInfo; 21 | import springfox.documentation.spi.DocumentationType; 22 | import springfox.documentation.spring.web.plugins.Docket; 23 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 24 | 25 | @EnableCircuitBreaker 26 | @Import(RxJavaAutoConfiguration.class) 27 | @SpringBootApplication 28 | @EnableSwagger2 29 | public class SpringNioRestApplication { 30 | 31 | @Bean 32 | public Docket nioApi() { 33 | return new Docket(DocumentationType.SWAGGER_2) 34 | //.groupName("full-spring-nio-api") 35 | .apiInfo(apiInfo()) 36 | .useDefaultResponseMessages(false) 37 | .select() 38 | .build(); 39 | } 40 | 41 | @Bean 42 | public FilterRegistrationBean registerCorsFilter(){ 43 | CorsFilter cf = new CorsFilter(); 44 | List urlPatterns = new ArrayList(); 45 | urlPatterns.add("/*"); 46 | FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); 47 | registrationBean.setFilter(cf); 48 | registrationBean.setUrlPatterns(urlPatterns); 49 | registrationBean.setOrder(1); 50 | return registrationBean; 51 | } 52 | 53 | @Bean 54 | public ThreadPoolTaskExecutor executor(){ 55 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 56 | executor.setThreadGroupName("MyThreadGroup"); 57 | executor.setThreadNamePrefix("MyThreadNamePrefix"); 58 | executor.setCorePoolSize(5000); 59 | return executor; 60 | } 61 | 62 | @Bean 63 | public ServletRegistrationBean hystrixMetricsStreamServlet(){ 64 | ServletRegistrationBean srb = new ServletRegistrationBean<>(new HystrixMetricsStreamServlet(), "/hystrix.stream"); 65 | return srb; 66 | } 67 | 68 | private ApiInfo apiInfo() { 69 | return new ApiInfoBuilder() 70 | .title("Spring NIO Rest API") 71 | .description("A couple of services to test Java NIO Performance") 72 | .termsOfServiceUrl("http://some.io") 73 | .contact("codependent") 74 | .license("Apache License Version 2.0") 75 | .licenseUrl("https://github.com/codependent/spring-nio-rest/LICENSE") 76 | .version("2.0") 77 | .build(); 78 | } 79 | 80 | public static void main(String[] args) { 81 | SpringApplication.run(SpringNioRestApplication.class, args); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/com/codependent/niorest/SpringNioRestApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest; 2 | 3 | import static org.hamcrest.Matchers.hasSize; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; 5 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 8 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | import org.springframework.test.web.servlet.MvcResult; 17 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 18 | import org.springframework.web.context.WebApplicationContext; 19 | import org.testng.annotations.BeforeClass; 20 | import org.testng.annotations.Test; 21 | 22 | @SpringBootTest(classes = SpringNioRestApplication.class) 23 | public class SpringNioRestApplicationTest extends AbstractTestNGSpringContextTests { 24 | 25 | @Autowired 26 | private WebApplicationContext wac; 27 | 28 | private MockMvc mockMvc; 29 | 30 | @BeforeClass 31 | public void setup() { 32 | this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); 33 | } 34 | 35 | @Test 36 | public void getSyncData() throws Exception{ 37 | mockMvc.perform(get("/sync/data").accept(MediaType.parseMediaType("application/json;charset=UTF-8"))) 38 | .andExpect(status().isOk()) 39 | .andExpect(content().contentType("application/json;charset=UTF-8")) 40 | .andExpect(jsonPath("$",hasSize(20))); 41 | } 42 | 43 | @Test 44 | public void getAsyncCallableData() throws Exception{ 45 | performAsync("/callable/data"); 46 | } 47 | 48 | @Test 49 | public void getAsyncDeferredData() throws Exception{ 50 | performAsync("/deferred/data"); 51 | } 52 | 53 | @Test 54 | public void getAsyncObservableData() throws Exception{ 55 | performAsync("/observable/data"); 56 | } 57 | 58 | @Test 59 | public void getAsyncObservableDeferredData() throws Exception{ 60 | performAsync("/observable-deferred/data"); 61 | } 62 | 63 | @Test 64 | public void getAsyncHystrixData() throws Exception{ 65 | performAsync("/hystrix/data"); 66 | } 67 | 68 | @Test 69 | public void getAsyncHystrixCallableData() throws Exception{ 70 | performAsync("/hystrix-callable/data"); 71 | } 72 | 73 | private void performAsync(String url) throws Exception{ 74 | MvcResult mvcResult = mockMvc.perform(get(url).accept(MediaType.parseMediaType("application/json;charset=UTF-8"))) 75 | .andExpect(request().asyncStarted()) 76 | .andReturn(); 77 | 78 | mvcResult.getAsyncResult(); 79 | 80 | mockMvc.perform(asyncDispatch(mvcResult)) 81 | .andExpect(status().isOk()) 82 | .andExpect(content().contentType("application/json;charset=UTF-8")) 83 | .andExpect(jsonPath("$",hasSize(20))); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.codependent.niorest 7 | spring-nio-rest 8 | 1.0.0-SNAPSHOT 9 | jar 10 | 11 | spring-nio-rest2 12 | Demo project for Spring Boot NIO Rest 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.M4 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 2.7.0 26 | 27 | 28 | 29 | 30 | 31 | org.springframework.cloud 32 | spring-cloud-dependencies 33 | Finchley.M2 34 | pom 35 | import 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-web 45 | 46 | 47 | 48 | org.springframework.cloud 49 | spring-cloud-starter-hystrix 50 | 51 | 52 | 53 | com.netflix.hystrix 54 | hystrix-metrics-event-stream 55 | 56 | 57 | 58 | io.springfox 59 | springfox-swagger2 60 | ${springfox-version} 61 | 62 | 63 | 64 | io.reactivex 65 | rxjava 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-starter-test 71 | test 72 | 73 | 74 | 75 | org.seleniumhq.selenium 76 | selenium-java 77 | test 78 | 79 | 80 | 81 | com.jayway.jsonpath 82 | json-path 83 | test 84 | 85 | 86 | 87 | org.testng 88 | testng 89 | 6.9.10 90 | test 91 | 92 | 93 | 94 | 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-maven-plugin 99 | 100 | 101 | 102 | 103 | 104 | 105 | spring-snapshots 106 | Spring Snapshots 107 | https://repo.spring.io/snapshot 108 | 109 | true 110 | 111 | 112 | 113 | spring-milestones 114 | Spring Milestones 115 | https://repo.spring.io/milestone 116 | 117 | false 118 | 119 | 120 | 121 | 122 | 123 | 124 | spring-snapshots 125 | Spring Snapshots 126 | https://repo.spring.io/snapshot 127 | 128 | true 129 | 130 | 131 | 132 | spring-milestones 133 | Spring Milestones 134 | https://repo.spring.io/milestone 135 | 136 | false 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/main/java/com/codependent/niorest/controller/AsyncRestController.java: -------------------------------------------------------------------------------- 1 | package com.codependent.niorest.controller; 2 | 3 | import java.util.List; 4 | import java.util.concurrent.Callable; 5 | import java.util.concurrent.ExecutionException; 6 | import java.util.concurrent.Future; 7 | 8 | import javax.annotation.PostConstruct; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.core.task.TaskExecutor; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.context.request.async.DeferredResult; 15 | import org.springframework.web.context.request.async.WebAsyncManager; 16 | 17 | import com.codependent.niorest.dto.Data; 18 | import com.codependent.niorest.service.DataService; 19 | 20 | import io.swagger.annotations.Api; 21 | import io.swagger.annotations.ApiOperation; 22 | import io.swagger.annotations.ApiResponse; 23 | import io.swagger.annotations.ApiResponses; 24 | import rx.Observable; 25 | import rx.Scheduler; 26 | import rx.Single; 27 | import rx.schedulers.Schedulers; 28 | 29 | /** 30 | * Asynchronous data controller 31 | * @author JINGA4X 32 | */ 33 | @RestController 34 | @Api(value="", description="Synchronous data controller") 35 | public class AsyncRestController { 36 | 37 | @Autowired 38 | private DataService dataService; 39 | 40 | private Scheduler scheduler; 41 | 42 | @Autowired 43 | private TaskExecutor executor; 44 | 45 | @PostConstruct 46 | protected void initializeScheduler(){ 47 | scheduler = Schedulers.from(executor); 48 | } 49 | 50 | /** 51 | * Callable usa el task executor de {@link WebAsyncManager} 52 | * @return 53 | */ 54 | @GetMapping(value="/callable/data", produces="application/json") 55 | @ApiOperation(value = "Gets data", notes="Gets data asynchronously") 56 | @ApiResponses(value={@ApiResponse(code=200, message="OK")}) 57 | public Callable> getDataCallable(){ 58 | return ( () -> {return dataService.loadData();} ); 59 | } 60 | 61 | /** 62 | * Con DeferredResult tienes que proporcionar tu propio executor, se asume que la tarea 63 | * es asíncrona 64 | * @return 65 | */ 66 | @GetMapping(value="/deferred/data", produces="application/json") 67 | @ApiOperation(value = "Gets data", notes="Gets data asynchronously") 68 | @ApiResponses(value={@ApiResponse(code=200, message="OK")}) 69 | public DeferredResult> getDataDeferredResult(){ 70 | DeferredResult> dr = new DeferredResult>(); 71 | Thread th = new Thread(() -> { 72 | List data = dataService.loadData(); 73 | dr.setResult(data); 74 | },"MyThread"); 75 | th.start(); 76 | return dr; 77 | } 78 | 79 | @GetMapping(value="/observable-deferred/data", produces="application/json") 80 | @ApiOperation(value = "Gets data through Observable", notes="Gets data asynchronously through Observable") 81 | @ApiResponses(value={@ApiResponse(code=200, message="OK")}) 82 | public DeferredResult> getDataObservable(){ 83 | DeferredResult> dr = new DeferredResult>(); 84 | Observable> dataObservable = dataService.loadDataObservable(); 85 | //XXX subscribeOn es necesario, si no se haría en el hilo http 86 | dataObservable.subscribeOn(scheduler).subscribe( 87 | dr::setResult, 88 | dr::setErrorResult); 89 | return dr; 90 | } 91 | 92 | @GetMapping(value="/observable/data", produces="application/json") 93 | @ApiOperation(value = "Gets data through Observable returning Observable", notes="Gets data asynchronously through Observable returning Observable") 94 | @ApiResponses(value={@ApiResponse(code=200, message="OK")}) 95 | public Single> getDataObservable2(){ 96 | Observable> dataObservable = dataService.loadDataObservable(); 97 | //XXX subscribeOn es necesario, si no se haría en el hilo http 98 | return dataObservable.toSingle().subscribeOn(scheduler); 99 | } 100 | 101 | @GetMapping(value="/hystrix/data", produces="application/json") 102 | @ApiOperation(value = "Gets data hystrix", notes="Gets data asynchronously with hystrix") 103 | @ApiResponses(value={@ApiResponse(code=200, message="OK")}) 104 | public Single> getDataHystrix(){ 105 | Observable> observable = dataService.loadDataHystrix(); 106 | //XXX subscribeOn es necesario, si no se haría en el hilo http 107 | return observable.toSingle().subscribeOn(scheduler); 108 | } 109 | 110 | @GetMapping(value="/hystrix-callable/data", produces="application/json") 111 | @ApiOperation(value = "Gets data hystrix", notes="Gets data asynchronously with hystrix") 112 | @ApiResponses(value={@ApiResponse(code=200, message="OK")}) 113 | public Callable> getDataHystrixAsync() throws InterruptedException, ExecutionException{ 114 | Future> future = dataService.loadDataHystrixAsync(); 115 | Callable> callable = () -> { 116 | return future.get(); 117 | }; 118 | return callable; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-nio-rest 2 | 3 | [![Build Status](https://travis-ci.org/codependent/spring-nio-rest.svg?branch=master)](https://travis-ci.org/codependent/spring-nio-rest) 4 | 5 | Spring Boot Project showing how efficient nio REST services (Callable & RxJava's Observable) can be, instead of blocking REST services. 6 | 7 | Swagger documentation available at path `/v2/api-docs`. 8 | 9 | ## Considerations 10 | * Load tests have been performed using [loadtest](https://github.com/alexfernandez/loadtest). 11 | * The business operation has been set up so that it takes 500msec to process. 12 | 13 | ## Results 14 | 15 | ### Synchronous controller 16 | 17 | The response time degrades as the test runs. 99% of the requests end up taking up to 25 seconds after one minute, with increasing latency and server errors (1 out of 4 requests). 18 | 19 | >>loadtest -c 15 -t 60 --rps 700 http://localhost:8080/sync/data 20 | ... 21 | Requests: 7683, requests per second: 400, mean latency: 7420 ms 22 | Requests: 9683, requests per second: 400, mean latency: 9570 ms 23 | Requests: 11680, requests per second: 399, mean latency: 11720 ms 24 | Requests: 13699, requests per second: 404, mean latency: 13760 ms 25 | ... 26 | Percentage of the requests served within a certain time 27 | 50% 8868 ms 28 | 90% 22434 ms 29 | 95% 24103 ms 30 | 99% 25351 ms 31 | 100% 26055 ms (longest request) 32 | 33 | 100% 26055 ms (longest request) 34 | 35 | -1: 7559 errors 36 | Requests: 31193, requests per second: 689, mean latency: 14350 ms 37 | Errors: 1534, accumulated errors: 7559, 24.2% of total requests 38 | 39 | ### Asynchronous controller with Callable 40 | 41 | It is able to process up to the rps setup limit (700), with no errors and having the response time only limited by the business service's processing time: 42 | 43 | >>loadtest -c 15 -t 60 --rps 700 http://localhost:8080/callable/data 44 | ... 45 | Requests: 0, requests per second: 0, mean latency: 0 ms 46 | Requests: 2839, requests per second: 568, mean latency: 500 ms 47 | Requests: 6337, requests per second: 700, mean latency: 500 ms 48 | Requests: 9836, requests per second: 700, mean latency: 500 ms 49 | Requests: 13334, requests per second: 700, mean latency: 500 ms 50 | Requests: 16838, requests per second: 701, mean latency: 500 ms 51 | Requests: 20336, requests per second: 700, mean latency: 500 ms 52 | Requests: 23834, requests per second: 700, mean latency: 500 ms 53 | Requests: 27338, requests per second: 701, mean latency: 500 ms 54 | Requests: 30836, requests per second: 700, mean latency: 500 ms 55 | Requests: 34334, requests per second: 700, mean latency: 500 ms 56 | Requests: 37834, requests per second: 700, mean latency: 500 ms 57 | Requests: 41336, requests per second: 700, mean latency: 500 ms 58 | 59 | Target URL: http://localhost:8080/async/data 60 | Max time (s): 60 61 | Concurrency level: 15 62 | Agent: none 63 | Requests per second: 700 64 | 65 | Completed requests: 41337 66 | Total errors: 0 67 | Total time: 60.002348360999996 s 68 | Requests per second: 689 69 | Total time: 60.002348360999996 s 70 | 71 | Percentage of the requests served within a certain time 72 | 50% 503 ms 73 | 90% 506 ms 74 | 95% 507 ms 75 | 99% 512 ms 76 | 100% 527 ms (longest request) 77 | 78 | Pushing it to its limits it manages to cope with up to an impressive 1700 rps (99996 requests in a minute) without errors: 79 | 80 | >>loadtest -c 15 --rps 1700 -t 60 http://localhost:8080/async/data 81 | Requests: 0, requests per second: 0, mean latency: 0 ms 82 | Requests: 6673, requests per second: 1329, mean latency: 590 ms 83 | Requests: 15197, requests per second: 1706, mean latency: 650 ms 84 | Requests: 23692, requests per second: 1702, mean latency: 610 ms 85 | Requests: 31950, requests per second: 1653, mean latency: 640 ms 86 | Requests: 40727, requests per second: 1757, mean latency: 620 ms 87 | Requests: 49139, requests per second: 1684, mean latency: 600 ms 88 | Requests: 57655, requests per second: 1701, mean latency: 660 ms 89 | Requests: 66197, requests per second: 1710, mean latency: 610 ms 90 | Requests: 74748, requests per second: 1707, mean latency: 610 ms 91 | Requests: 83111, requests per second: 1677, mean latency: 630 ms 92 | Requests: 91410, requests per second: 1658, mean latency: 690 ms 93 | 94 | Target URL: http://localhost:8080/async/data 95 | Max time (s): 60 96 | Concurrency level: 15 97 | Agent: none 98 | Requests per second: 1700 99 | 100 | Completed requests: 99996 101 | Total errors: 0 102 | Total time: 60.000301544 s 103 | Requests per second: 1667 104 | Total time: 60.000301544 s 105 | 106 | Percentage of the requests served within a certain time 107 | 50% 625 ms 108 | 90% 728 ms 109 | 95% 761 ms 110 | 99% 821 ms 111 | 100% 1286 ms (longest request) 112 | Requests: 99996, requests per second: 1713, mean latency: 750 ms 113 | 114 | ### Asynchronous controller with RxJava's Observable 115 | 116 | It's performance is slightly better than the Callable version 117 | 118 | >>loadtest -c 15 -t 60 --rps 1700 http://localhost:8080/observable/data 119 | Requests: 0, requests per second: 0, mean latency: 0 ms 120 | Requests: 6707, requests per second: 1341, mean latency: 560 ms 121 | Requests: 15070, requests per second: 1675, mean latency: 560 ms 122 | Requests: 23658, requests per second: 1716, mean latency: 630 ms 123 | Requests: 31620, requests per second: 1594, mean latency: 570 ms 124 | Requests: 40698, requests per second: 1816, mean latency: 730 ms 125 | Requests: 49173, requests per second: 1695, mean latency: 560 ms 126 | ... 127 | Completed requests: 100088 128 | Total errors: 0 129 | Total time: 60.000421678 s 130 | Requests per second: 1668 131 | Total time: 60.000421678 s 132 | 133 | -------------------------------------------------------------------------------- /jmeter/spring-nio-rest.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | continue 16 | 17 | false 18 | 100 19 | 20 | 2000 21 | 0 22 | 1445939878000 23 | 1445939878000 24 | false 25 | 26 | 27 | 28 | 29 | 30 | true 31 | 32 | 33 | 34 | false 35 | 36 | = 37 | 38 | 39 | 40 | localhost 41 | 8080 42 | 43 | 44 | 45 | 46 | /sync/data 47 | GET 48 | true 49 | false 50 | true 51 | false 52 | false 53 | 54 | 55 | 56 | 57 | 58 | 59 | Content-Type 60 | application/json 61 | 62 | 63 | 64 | 65 | 66 | 67 | true 68 | 69 | 70 | 71 | false 72 | 73 | = 74 | 75 | 76 | 77 | localhost 78 | 8080 79 | 80 | 81 | 82 | 83 | /async/data 84 | GET 85 | true 86 | false 87 | true 88 | false 89 | false 90 | 91 | 92 | 93 | 94 | 95 | 96 | Content-Type 97 | application/json 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | localhost 108 | 8080 109 | 110 | 111 | 112 | 113 | /observable/data 114 | GET 115 | true 116 | false 117 | true 118 | false 119 | false 120 | 121 | 122 | 123 | 124 | 125 | 126 | Content-Type 127 | application/json 128 | 129 | 130 | 131 | 132 | 133 | 134 | true 135 | 136 | 137 | 138 | false 139 | 140 | = 141 | 142 | 143 | 144 | localhost 145 | 8080 146 | 147 | 148 | 149 | 150 | /observable2/data 151 | GET 152 | true 153 | false 154 | true 155 | false 156 | false 157 | 158 | 159 | 160 | 161 | 162 | 163 | Content-Type 164 | application/json 165 | 166 | 167 | 168 | 169 | 170 | 171 | false 172 | 173 | saveConfig 174 | 175 | 176 | true 177 | true 178 | true 179 | 180 | true 181 | true 182 | true 183 | true 184 | false 185 | true 186 | true 187 | false 188 | false 189 | false 190 | false 191 | false 192 | false 193 | false 194 | false 195 | 0 196 | true 197 | true 198 | 199 | 200 | 201 | 202 | 203 | 204 | false 205 | 206 | saveConfig 207 | 208 | 209 | true 210 | true 211 | true 212 | 213 | true 214 | true 215 | true 216 | true 217 | false 218 | true 219 | true 220 | false 221 | false 222 | false 223 | false 224 | false 225 | false 226 | false 227 | false 228 | 0 229 | true 230 | true 231 | 232 | 233 | 234 | 235 | 236 | 237 | false 238 | 239 | saveConfig 240 | 241 | 242 | true 243 | true 244 | true 245 | 246 | true 247 | true 248 | true 249 | true 250 | false 251 | true 252 | true 253 | false 254 | false 255 | false 256 | false 257 | false 258 | false 259 | false 260 | false 261 | 0 262 | true 263 | true 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | --------------------------------------------------------------------------------