├── .gitignore ├── LICENSE ├── README.md ├── out └── production │ ├── spring-mvc-asynch-teststub │ └── application.properties │ └── spring-mvc-asynch │ ├── application.properties │ └── logback.xml ├── spring-mvc-asynch-teststub ├── build.gradle ├── docs │ ├── blocking │ │ ├── Gatling Stats - 1. Global Information.pdf │ │ ├── Gatling Stats - 2. Details.pdf │ │ └── JConsole 2014-04-01 at 10.04.27.png │ └── non-blocking │ │ ├── Gatling Stats - 1. Global Information.pdf │ │ ├── Gatling Stats - 2. Detailst.pdf │ │ └── JConsole 2014-03-31 at 14.58.25.png ├── gatling │ └── spring-mvc-asynch-teststub-simulation.scala ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ ├── main │ ├── java │ │ └── se │ │ │ └── callista │ │ │ └── springmvc │ │ │ └── asynch │ │ │ └── teststub │ │ │ ├── Application.java │ │ │ ├── MyEmbeddedServletContainerCustomizer.java │ │ │ ├── ProcessingController.java │ │ │ ├── ProcessingStatus.java │ │ │ └── ProcessingTask.java │ └── resources │ │ ├── application.properties │ │ └── logback.xml │ └── test │ └── java │ └── se │ └── callista │ └── springmvc │ └── asynch │ └── teststub │ └── ProcessingControllerTest.java └── spring-mvc-asynch ├── .gitignore ├── build.gradle ├── gatling └── spring-mvc-asynch-simulation.scala ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── src ├── main │ ├── java │ │ └── se │ │ │ └── callista │ │ │ └── springmvc │ │ │ └── asynch │ │ │ ├── Application.java │ │ │ ├── common │ │ │ ├── deferredresult │ │ │ │ ├── DeferredResultStateMachineCallback.java │ │ │ │ └── DeferredResultWithBlockingWait.java │ │ │ ├── lambdasupport │ │ │ │ ├── AsyncHttpClientLambdaAware.java │ │ │ │ ├── Completed.java │ │ │ │ └── Error.java │ │ │ ├── log │ │ │ │ ├── LogHelper.java │ │ │ │ └── LogHelperFactory.java │ │ │ └── statemachine │ │ │ │ ├── Processor.java │ │ │ │ ├── State.java │ │ │ │ ├── StateMachine.java │ │ │ │ ├── StateMachineCallback.java │ │ │ │ └── internal │ │ │ │ └── StateProcessingStepIterator.java │ │ │ ├── config │ │ │ └── MyEmbeddedServletContainerCustomizer.java │ │ │ └── pattern │ │ │ ├── aggregator │ │ │ ├── blocking │ │ │ │ ├── AggregatorBlockingController.java │ │ │ │ └── DbLookup.java │ │ │ └── nonblocking │ │ │ │ ├── callback │ │ │ │ ├── AggregatorCallback.java │ │ │ │ ├── AggregatorEventHandler.java │ │ │ │ ├── AggregatorNonBlockingCallbackController.java │ │ │ │ └── DbLookupRunnable.java │ │ │ │ └── lambda │ │ │ │ ├── AggregatorNonBlockingLambdaController.java │ │ │ │ ├── DbLookup.java │ │ │ │ └── Executor.java │ │ │ ├── router │ │ │ ├── blocking │ │ │ │ └── RouterBlockingController.java │ │ │ └── nonblocking │ │ │ │ ├── anonymous │ │ │ │ └── RouterNonBlockingAnonymousController.java │ │ │ │ ├── callback │ │ │ │ ├── RouterCallback.java │ │ │ │ └── RouterNonBlockingCallbackController.java │ │ │ │ ├── lambda │ │ │ │ └── RouterNonBlockingLambdaController.java │ │ │ │ └── spring │ │ │ │ ├── RouterCallback_Spring_AsyncRestTemplate.java │ │ │ │ └── RouterNonBlockingSpringController.java │ │ │ └── routingslip │ │ │ ├── blocking │ │ │ ├── plain │ │ │ │ └── RoutingSlipBlockingPlainController.java │ │ │ └── statemachine │ │ │ │ ├── RoutingSlipBlockingConfiguration.java │ │ │ │ ├── RoutingSlipBlockingStateMachineController.java │ │ │ │ └── SynchProcessor.java │ │ │ └── nonblocking │ │ │ ├── lambda │ │ │ └── RoutingSlipNonBlockingLambdaController.java │ │ │ └── statemachine │ │ │ ├── AsynchProcessor.java │ │ │ ├── AsynchProcessorCallback.java │ │ │ ├── RoutingSlipNonBlockingConfiguration.java │ │ │ └── RoutingSlipNonBlockingStateMachineController.java │ └── resources │ │ ├── application.properties │ │ └── logback.xml └── test │ └── java │ └── se │ └── callista │ └── springmvc │ └── asynch │ ├── common │ ├── AsynchTestBase.java │ ├── ProcessingStatus.java │ └── embeddedhttpserver │ │ ├── EmbeddedHttpServer.java │ │ ├── EmbeddedHttpServerTestBase.java │ │ └── RequestHandler.java │ └── pattern │ ├── aggregator │ └── AggregatorControllerTest.java │ ├── router │ └── RouterControllerTest.java │ └── routingslip │ └── RoutingSlipControllerTest.java └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Java Files # 2 | *.class 3 | 4 | # Package Files # 5 | # *.jar, we need to checkin gradle/wrapper/gradle-wrapper.jar 6 | *.war 7 | *.ear 8 | 9 | # Eclipse files # 10 | .settings 11 | .classpath 12 | .project 13 | 14 | # IntelliJ files # 15 | .idea 16 | local.properties 17 | *.iml 18 | out 19 | 20 | # Gradle files # 21 | .gradle 22 | build 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | blog-non-blocking-rest-service-with-spring-mvc 2 | ============================================== 3 | 4 | Source code for the blog: http://callistaenterprise.se/blogg/teknik/2014/04/22/c10k-developing-non-blocking-rest-services-with-spring-mvc/ 5 | -------------------------------------------------------------------------------- /out/production/spring-mvc-asynch-teststub/application.properties: -------------------------------------------------------------------------------- 1 | server.port = 9090 2 | 3 | #THE DEFAULT VALUE FOT THREAD POOL SIZE IN TOMCAT 4 | servlet.container.maxThreads=200 5 | #servlet.container.maxThreads=50 FOR NON-BLOCKING TESTS 6 | #servlet.container.maxThreads=500 FOR BLOCKING TESTS 7 | 8 | statistics.requestsPerLog=5000 9 | #statistics.requestsPerLog=1000 FOR BLOCKING TESTS 10 | -------------------------------------------------------------------------------- /out/production/spring-mvc-asynch/application.properties: -------------------------------------------------------------------------------- 1 | server.port=9080 2 | servlet.container.maxThreads=50 3 | 4 | threadPool.db.init_size=5 5 | threadPool.db.max_size=50 6 | threadPool.db.queue_size=1000000 7 | 8 | #sp.blocking.url=http://localhost:9090/process-blocking 9 | #sp.non_blocking.url=http://localhost:${server.port}/process-non-blocking 10 | sp.server.port=9090 11 | sp.non_blocking.url=http://localhost:${sp.server.port}/process-non-blocking 12 | 13 | aggregator.timeoutMs=3000 14 | 15 | statistics.requestsPerLog=5000 16 | -------------------------------------------------------------------------------- /out/production/spring-mvc-asynch/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | System.out 13 | 14 | %d{yyyy-MM-dd HH:mm:ss:SSS} %-5p %t %c{1}:%L - %m%n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | mavenLocal() 5 | } 6 | dependencies { 7 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.1.RELEASE") 8 | } 9 | } 10 | 11 | apply plugin: 'java' 12 | apply plugin: 'eclipse' 13 | apply plugin: 'idea' 14 | apply plugin: 'spring-boot' 15 | apply plugin: 'war' 16 | 17 | war { 18 | baseName = 'spring-mvc-asynch-teststub' 19 | version = '1.0.0-SNAPSHOT' 20 | } 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | dependencies { 27 | compile("org.springframework.boot:spring-boot-starter-web") 28 | // compile("org.springframework.boot:spring-boot-starter-jetty") 29 | compile("org.springframework.boot:spring-boot-starter-actuator") 30 | compile("javax.xml.bind:jaxb-api:2.1") 31 | compile("com.sun.xml.bind:jaxb-impl:2.1.9") 32 | testCompile("org.springframework.boot:spring-boot-starter-test") 33 | } 34 | 35 | task wrapper(type: Wrapper) { 36 | gradleVersion = '1.11' 37 | } 38 | -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/docs/blocking/Gatling Stats - 1. Global Information.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callistaenterprise/blog-non-blocking-rest-service-with-spring-mvc/28491c3c4f50f08939d0ac3bc36e5668d441d563/spring-mvc-asynch-teststub/docs/blocking/Gatling Stats - 1. Global Information.pdf -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/docs/blocking/Gatling Stats - 2. Details.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callistaenterprise/blog-non-blocking-rest-service-with-spring-mvc/28491c3c4f50f08939d0ac3bc36e5668d441d563/spring-mvc-asynch-teststub/docs/blocking/Gatling Stats - 2. Details.pdf -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/docs/blocking/JConsole 2014-04-01 at 10.04.27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callistaenterprise/blog-non-blocking-rest-service-with-spring-mvc/28491c3c4f50f08939d0ac3bc36e5668d441d563/spring-mvc-asynch-teststub/docs/blocking/JConsole 2014-04-01 at 10.04.27.png -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/docs/non-blocking/Gatling Stats - 1. Global Information.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callistaenterprise/blog-non-blocking-rest-service-with-spring-mvc/28491c3c4f50f08939d0ac3bc36e5668d441d563/spring-mvc-asynch-teststub/docs/non-blocking/Gatling Stats - 1. Global Information.pdf -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/docs/non-blocking/Gatling Stats - 2. Detailst.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callistaenterprise/blog-non-blocking-rest-service-with-spring-mvc/28491c3c4f50f08939d0ac3bc36e5668d441d563/spring-mvc-asynch-teststub/docs/non-blocking/Gatling Stats - 2. Detailst.pdf -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/docs/non-blocking/JConsole 2014-03-31 at 14.58.25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callistaenterprise/blog-non-blocking-rest-service-with-spring-mvc/28491c3c4f50f08939d0ac3bc36e5668d441d563/spring-mvc-asynch-teststub/docs/non-blocking/JConsole 2014-03-31 at 14.58.25.png -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/gatling/spring-mvc-asynch-teststub-simulation.scala: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import com.excilys.ebi.gatling.core.Predef._ 4 | import com.excilys.ebi.gatling.http.Predef._ 5 | import com.excilys.ebi.gatling.jdbc.Predef._ 6 | import com.excilys.ebi.gatling.http.Headers.Names._ 7 | import akka.util.duration._ 8 | import bootstrap._ 9 | 10 | /** 11 | * Started with a command like: 12 | * $ cd $GATLING_HOME/bin 13 | * $ ./gatling.sh -s "basic.SpringMvcAsynchTeststubSimulation" 14 | */ 15 | class SpringMvcAsynchTeststubSimulation extends Simulation { 16 | 17 | val rampUpTimeSecs = 60 18 | val testTimeSecs = 360 19 | val noOfUsers = 5000 20 | val minWaitMs = 1000 milliseconds 21 | val maxWaitMs = 3000 milliseconds 22 | 23 | val baseURL = "http://localhost:9090" 24 | val baseName = "spring-mvc-asynch-teststub" 25 | val requestName = baseName + "-request" 26 | val scenarioName = baseName + "-scenario" 27 | val URI = "/process-non-blocking?minMs=1000&maxMs=2000" 28 | // val URI = "/process-blocking?minMs=1000&maxMs=2000" 29 | 30 | val httpConf = httpConfig.baseURL(baseURL) 31 | 32 | val http_headers = Map( 33 | "Accept-Encoding" -> "gzip,deflate", 34 | "Content-Type" -> "text/json;charset=UTF-8", 35 | "Keep-Alive" -> "115") 36 | 37 | val scn = scenario(scenarioName) 38 | .during(testTimeSecs) { 39 | exec( 40 | http(requestName) 41 | .get(URI) 42 | .headers(http_headers) 43 | .check(status.is(200)) 44 | ) 45 | .pause(minWaitMs, maxWaitMs) 46 | } 47 | setUp(scn.users(noOfUsers).ramp(rampUpTimeSecs).protocolConfig(httpConf)) 48 | } -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callistaenterprise/blog-non-blocking-rest-service-with-spring-mvc/28491c3c4f50f08939d0ac3bc36e5668d441d563/spring-mvc-asynch-teststub/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 18 06:19:10 CEST 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip 7 | -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/src/main/java/se/callista/springmvc/asynch/teststub/Application.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.teststub; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 6 | import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.ComponentScan; 10 | 11 | @ComponentScan 12 | @EnableAutoConfiguration 13 | public class Application { 14 | 15 | @SuppressWarnings("unused") 16 | private static final Logger LOG = LoggerFactory.getLogger(Application.class); 17 | 18 | @Bean 19 | public EmbeddedServletContainerCustomizer embeddedServletCustomizer(){ 20 | return new MyEmbeddedServletContainerCustomizer(); 21 | } 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(Application.class, args); 25 | } 26 | } -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/src/main/java/se/callista/springmvc/asynch/teststub/MyEmbeddedServletContainerCustomizer.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.teststub; 2 | 3 | import org.apache.catalina.connector.Connector; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; 8 | import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; 9 | import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer; 10 | import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; 11 | 12 | public class MyEmbeddedServletContainerCustomizer implements EmbeddedServletContainerCustomizer { 13 | 14 | private static final Logger LOG = LoggerFactory.getLogger(MyEmbeddedServletContainerCustomizer.class); 15 | 16 | @Value("${servlet.container.maxThreads}") 17 | private int MAX_THREADS; 18 | 19 | @Override 20 | public void customize(ConfigurableEmbeddedServletContainer factory) { 21 | if(factory instanceof TomcatEmbeddedServletContainerFactory) { 22 | customizeTomcat((TomcatEmbeddedServletContainerFactory) factory); 23 | } 24 | } 25 | 26 | public void customizeTomcat(TomcatEmbeddedServletContainerFactory factory) { 27 | factory.addConnectorCustomizers(new TomcatConnectorCustomizer() { 28 | @Override 29 | public void customize(Connector connector) { 30 | Object defaultMaxThreads = connector.getAttribute("maxThreads"); 31 | connector.setAttribute("maxThreads", MAX_THREADS); 32 | LOG.info("Changed Tomcat connector maxThreads from " + defaultMaxThreads + " to " + MAX_THREADS); 33 | } 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/src/main/java/se/callista/springmvc/asynch/teststub/ProcessingController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.teststub; 2 | 3 | import java.lang.management.ManagementFactory; 4 | import java.lang.management.OperatingSystemMXBean; 5 | import java.util.Timer; 6 | import java.util.concurrent.atomic.AtomicLong; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.context.request.async.DeferredResult; 15 | 16 | import com.sun.management.UnixOperatingSystemMXBean; 17 | 18 | @RestController 19 | public class ProcessingController { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(ProcessingController.class); 22 | 23 | private static OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); 24 | private static final AtomicLong lastRequestId = new AtomicLong(0); 25 | private static final AtomicLong concurrentRequests = new AtomicLong(0); 26 | private static long maxConcurrentRequests = 0; 27 | 28 | private Timer timer = new Timer(); 29 | 30 | @Value("${statistics.requestsPerLog}") 31 | private int STAT_REQS_PER_LOG; 32 | private int defaultMinMs = 0; 33 | private int defaultMaxMs = 0; 34 | 35 | @RequestMapping("/set-default-processing-time") 36 | public void setDefaultProcessingTime( 37 | @RequestParam(value = "minMs", required = true) int minMs, 38 | @RequestParam(value = "maxMs", required = true) int maxMs) { 39 | this.defaultMinMs = minMs; 40 | this.defaultMaxMs = maxMs; 41 | LOG.info("Set default response time to {} - {} ms.", minMs, maxMs); 42 | } 43 | 44 | /** 45 | * Sample usage: curl "http://localhost:9090/process-blocking?minMs=1000&maxMs=2000" 46 | * 47 | * @param minMs 48 | * @param maxMs 49 | * @return 50 | */ 51 | @RequestMapping("/process-blocking") 52 | public ProcessingStatus blockingProcessing( 53 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 54 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) { 55 | 56 | long reqId = lastRequestId.getAndIncrement(); 57 | long concReqs = concurrentRequests.getAndIncrement(); 58 | 59 | updateStatistics(reqId, concReqs); 60 | 61 | int processingTimeMs = calculateProcessingTime(minMs, maxMs); 62 | 63 | LOG.debug("{}: Start blocking request #{}, processing time: {} ms.", concReqs, reqId, processingTimeMs); 64 | 65 | try { 66 | Thread.sleep(processingTimeMs); 67 | } 68 | catch (InterruptedException e) {} 69 | 70 | finally { 71 | concurrentRequests.decrementAndGet(); 72 | LOG.debug("{}: Processing of blocking request #{} is done", concReqs, reqId); 73 | } 74 | 75 | return new ProcessingStatus("Ok", processingTimeMs); 76 | } 77 | 78 | /** 79 | * Sample usage: curl "http://localhost:9090/process-non-blocking?minMs=1000&maxMs=2000" 80 | * 81 | * @param minMs 82 | * @param maxMs 83 | * @return 84 | */ 85 | @RequestMapping("/process-non-blocking") 86 | public DeferredResult nonBlockingProcessing( 87 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 88 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) { 89 | 90 | long reqId = lastRequestId.getAndIncrement(); 91 | long concReqs = concurrentRequests.getAndIncrement(); 92 | 93 | updateStatistics(reqId, concReqs); 94 | 95 | int processingTimeMs = calculateProcessingTime(minMs, maxMs); 96 | 97 | LOG.debug("{}: Start non-blocking request #{}, processing time: {} ms.", concReqs, reqId, processingTimeMs); 98 | 99 | // Create the deferredResult and initiate a callback object, task, with it 100 | DeferredResult deferredResult = new DeferredResult<>(); 101 | ProcessingTask task = new ProcessingTask(reqId, concurrentRequests, processingTimeMs, deferredResult); 102 | 103 | // Schedule the task for asynch completion in the future 104 | timer.schedule(task, processingTimeMs); 105 | 106 | LOG.debug("{}: Processing of non-blocking request #{} leave the request thread", concReqs, reqId); 107 | 108 | // Return to let go of the precious thread we are holding on to... 109 | return deferredResult; 110 | } 111 | 112 | private void updateStatistics(long reqId, long concReqs) { 113 | if (concReqs > maxConcurrentRequests) { 114 | maxConcurrentRequests = concReqs; 115 | } 116 | 117 | if (reqId % STAT_REQS_PER_LOG == 0 && reqId > 0) { 118 | Object openFiles = "UNKNOWN"; 119 | if (os instanceof UnixOperatingSystemMXBean) { 120 | openFiles = ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount(); 121 | } 122 | LOG.info("Statistics: noOfReqs: {}, maxConcReqs: {}, openFiles: {}", reqId, maxConcurrentRequests, openFiles); 123 | } 124 | } 125 | 126 | private int calculateProcessingTime(int minMs, int maxMs) { 127 | if (minMs == 0 && maxMs == 0) { 128 | minMs = defaultMinMs; 129 | maxMs = defaultMaxMs; 130 | } 131 | 132 | if (maxMs < minMs) maxMs = minMs; 133 | int processingTimeMs = minMs + (int) (Math.random() * (maxMs - minMs)); 134 | return processingTimeMs; 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/src/main/java/se/callista/springmvc/asynch/teststub/ProcessingStatus.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.teststub; 2 | 3 | import javax.xml.bind.annotation.XmlElement; 4 | import javax.xml.bind.annotation.XmlRootElement; 5 | 6 | @XmlRootElement 7 | public class ProcessingStatus { 8 | 9 | @XmlElement 10 | private final String status; 11 | 12 | @XmlElement 13 | private final int processingTimeMs; 14 | 15 | public ProcessingStatus() { 16 | status = "UNKNOWN"; 17 | processingTimeMs = -1; 18 | } 19 | 20 | public ProcessingStatus(String status, int processingTimeMs) { 21 | this.status = status; 22 | this.processingTimeMs = processingTimeMs; 23 | } 24 | 25 | public String getStatus() { 26 | return status; 27 | } 28 | 29 | public int getProcessingTimeMs() { 30 | return processingTimeMs; 31 | } 32 | } -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/src/main/java/se/callista/springmvc/asynch/teststub/ProcessingTask.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.teststub; 2 | 3 | import java.util.TimerTask; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.web.context.request.async.DeferredResult; 9 | 10 | public class ProcessingTask extends TimerTask { 11 | 12 | private static final Logger LOG = LoggerFactory.getLogger(ProcessingTask.class); 13 | 14 | private long reqId; 15 | private AtomicLong concurrentRequests; 16 | private DeferredResult deferredResult; 17 | private int processingTimeMs; 18 | 19 | public ProcessingTask(long reqId, AtomicLong concurrentRequests, int processingTimeMs, DeferredResult deferredResult) { 20 | this.reqId = reqId; 21 | this.concurrentRequests = concurrentRequests; 22 | this.processingTimeMs = processingTimeMs; 23 | this.deferredResult = deferredResult; 24 | } 25 | 26 | @Override 27 | public void run() { 28 | 29 | long concReqs = concurrentRequests.getAndDecrement(); 30 | 31 | if (deferredResult.isSetOrExpired()) { 32 | LOG.warn("{}: Processing of non-blocking request #{} already expired", concReqs, reqId); 33 | } else { 34 | boolean deferredStatus = deferredResult.setResult(new ProcessingStatus("Ok", processingTimeMs)); 35 | LOG.debug("{}: Processing of non-blocking request #{} done, deferredStatus = {}", concReqs, reqId, deferredStatus); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port = 9090 2 | 3 | #THE DEFAULT VALUE FOT THREAD POOL SIZE IN TOMCAT 4 | servlet.container.maxThreads=200 5 | #servlet.container.maxThreads=50 FOR NON-BLOCKING TESTS 6 | #servlet.container.maxThreads=500 FOR BLOCKING TESTS 7 | 8 | statistics.requestsPerLog=5000 9 | #statistics.requestsPerLog=1000 FOR BLOCKING TESTS 10 | -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | System.out 13 | 14 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5p %t %c{1}:%L - %m%n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /spring-mvc-asynch-teststub/src/test/java/se/callista/springmvc/asynch/teststub/ProcessingControllerTest.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.teststub; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.MockitoAnnotations; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.SpringApplicationConfiguration; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 13 | import org.springframework.test.context.web.WebAppConfiguration; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | import org.springframework.test.web.servlet.MvcResult; 16 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 17 | import org.springframework.web.context.WebApplicationContext; 18 | 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 24 | 25 | /** 26 | * Created by magnus on 07/06/14. 27 | */ 28 | @RunWith(SpringJUnit4ClassRunner.class) 29 | @SpringApplicationConfiguration(classes = Application.class) 30 | @WebAppConfiguration 31 | public class ProcessingControllerTest { 32 | 33 | private MockMvc mockMvc; 34 | 35 | @Autowired 36 | WebApplicationContext wac; 37 | 38 | private final String expectedResult = "{\"status\":\"Ok\",\"processingTimeMs\":2000}"; 39 | 40 | @Before 41 | public void setup(){ 42 | 43 | // Process mock annotations 44 | MockitoAnnotations.initMocks(this); 45 | 46 | // Setup Spring test in webapp-mode (same config as spring-boot) 47 | this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); 48 | } 49 | 50 | @Test 51 | public void testProcessBlocking() throws Exception{ 52 | this.mockMvc.perform(get("/process-blocking?minMs=2000&maxMs=2000")) 53 | .andExpect(status().isOk()) 54 | .andExpect(content().contentType("application/json;charset=UTF-8")) 55 | .andExpect(content().string(expectedResult)); 56 | } 57 | 58 | @Test 59 | public void testProcessNonBlocking() throws Exception { 60 | 61 | MvcResult mvcResult = this.mockMvc.perform(get("/process-non-blocking?minMs=2000&maxMs=2000")) 62 | .andExpect(request().asyncStarted()) 63 | .andReturn(); 64 | 65 | mvcResult.getAsyncResult(); 66 | 67 | this.mockMvc.perform(asyncDispatch(mvcResult)) 68 | .andExpect(status().isOk()) 69 | .andExpect(content().contentType("application/json;charset=UTF-8")) 70 | .andExpect(content().string(expectedResult)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /spring-mvc-asynch/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /bin 3 | -------------------------------------------------------------------------------- /spring-mvc-asynch/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | mavenLocal() 5 | } 6 | dependencies { 7 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.1.RELEASE") 8 | } 9 | } 10 | 11 | apply plugin: 'java' 12 | apply plugin: 'eclipse' 13 | apply plugin: 'idea' 14 | apply plugin: 'spring-boot' 15 | apply plugin: 'war' 16 | 17 | sourceCompatibility = 1.8 18 | 19 | war { 20 | baseName = 'spring-mvc-asynch' 21 | version = '1.0.0-SNAPSHOT' 22 | } 23 | 24 | repositories { 25 | mavenCentral() 26 | } 27 | 28 | dependencies { 29 | compile("org.springframework.boot:spring-boot-starter-web") 30 | compile("org.springframework.boot:spring-boot-starter-jetty") 31 | compile("org.springframework.boot:spring-boot-starter-actuator") 32 | compile("com.ning:async-http-client:1.8.4") 33 | compile("javax.xml.bind:jaxb-api:2.1") 34 | compile("com.sun.xml.bind:jaxb-impl:2.1.9") 35 | testCompile("org.springframework.boot:spring-boot-starter-test") 36 | // testCompile("com.github.tomakehurst:wiremock:1.46") 37 | testCompile("org.apache.commons:commons-io:1.3.2") 38 | testCompile("org.apache.httpcomponents:httpclient:4.3.4") 39 | } 40 | 41 | task wrapper(type: Wrapper) { 42 | gradleVersion = '1.11' 43 | } 44 | -------------------------------------------------------------------------------- /spring-mvc-asynch/gatling/spring-mvc-asynch-simulation.scala: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import com.excilys.ebi.gatling.core.Predef._ 4 | import com.excilys.ebi.gatling.http.Predef._ 5 | import com.excilys.ebi.gatling.jdbc.Predef._ 6 | import com.excilys.ebi.gatling.http.Headers.Names._ 7 | import akka.util.duration._ 8 | import bootstrap._ 9 | 10 | class SpringMvcAsynchSimulation extends Simulation { 11 | 12 | val rampUpTimeSecs = 60 13 | val testTimeSecs = 360 14 | val noOfUsers = 5000 15 | val minWaitMs = 1000 milliseconds 16 | val maxWaitMs = 3000 milliseconds 17 | 18 | val baseURL = "http://localhost:9080" 19 | val baseName = "spring-mvc-asynch" 20 | val requestName = baseName + "-request" 21 | val scenarioName = baseName + "-scenario" 22 | val URI = "/route-non-blocking?minMs=1000&maxMs=2000" 23 | 24 | val httpConf = httpConfig.baseURL(baseURL) 25 | 26 | val http_headers = Map( 27 | "Accept-Encoding" -> "gzip,deflate", 28 | "Content-Type" -> "text/json;charset=UTF-8", 29 | "Keep-Alive" -> "115") 30 | 31 | val scn = scenario(scenarioName) 32 | .during(testTimeSecs) { 33 | exec( 34 | http(requestName) 35 | .get(URI) 36 | .headers(http_headers) 37 | .check(status.is(200)) 38 | ) 39 | .pause(minWaitMs, maxWaitMs) 40 | } 41 | setUp(scn.users(noOfUsers).ramp(rampUpTimeSecs).protocolConfig(httpConf)) 42 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callistaenterprise/blog-non-blocking-rest-service-with-spring-mvc/28491c3c4f50f08939d0ac3bc36e5668d441d563/spring-mvc-asynch/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /spring-mvc-asynch/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 18 06:04:39 CEST 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip 7 | -------------------------------------------------------------------------------- /spring-mvc-asynch/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /spring-mvc-asynch/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/Application.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.ComponentScan; 11 | import org.springframework.core.task.TaskExecutor; 12 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 13 | import se.callista.springmvc.asynch.common.lambdasupport.AsyncHttpClientLambdaAware; 14 | import se.callista.springmvc.asynch.config.MyEmbeddedServletContainerCustomizer; 15 | 16 | @ComponentScan() 17 | @EnableAutoConfiguration 18 | public class Application { 19 | 20 | @SuppressWarnings("unused") 21 | private static final Logger LOG = LoggerFactory.getLogger(Application.class); 22 | 23 | @Bean 24 | public EmbeddedServletContainerCustomizer embeddedServletCustomizer(){ 25 | return new MyEmbeddedServletContainerCustomizer(); 26 | } 27 | 28 | @Value("${threadPool.db.init_size}") 29 | private int THREAD_POOL_DB_INIT_SIZE; 30 | 31 | @Value("${threadPool.db.max_size}") 32 | private int THREAD_POOL_DB_MAX_SIZE; 33 | 34 | @Value("${threadPool.db.queue_size}") 35 | private int THREAD_POOL_DB_QUEUE_SIZE; 36 | 37 | @Bean(name="dbThreadPoolExecutor") 38 | public TaskExecutor getTaskExecutor() { 39 | ThreadPoolTaskExecutor tpte = new ThreadPoolTaskExecutor(); 40 | tpte.setCorePoolSize(THREAD_POOL_DB_INIT_SIZE); 41 | tpte.setMaxPoolSize(THREAD_POOL_DB_MAX_SIZE); 42 | tpte.setQueueCapacity(THREAD_POOL_DB_QUEUE_SIZE); 43 | tpte.initialize(); 44 | return tpte; 45 | } 46 | 47 | @Bean 48 | public AsyncHttpClientLambdaAware getAsyncHttpClient() { 49 | LOG.info("### Creates a new AsyncHttpClientLambdaAware-object"); 50 | return new AsyncHttpClientLambdaAware(); 51 | } 52 | 53 | public static void main(String[] args) { 54 | SpringApplication.run(Application.class, args); 55 | } 56 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/deferredresult/DeferredResultStateMachineCallback.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.deferredresult; 2 | 3 | import org.springframework.web.context.request.async.DeferredResult; 4 | import se.callista.springmvc.asynch.common.statemachine.State; 5 | import se.callista.springmvc.asynch.common.statemachine.StateMachineCallback; 6 | 7 | /** 8 | * Created by magnus on 08/06/14. 9 | */ 10 | public class DeferredResultStateMachineCallback implements StateMachineCallback { 11 | 12 | private DeferredResult deferredResult; 13 | 14 | public DeferredResultStateMachineCallback(DeferredResult deferredResult) { 15 | this.deferredResult = deferredResult; 16 | } 17 | 18 | @Override 19 | public void onCompleted(State state) { 20 | if (deferredResult.isSetOrExpired()) { 21 | state.getLog().logAlreadyExpiredNonBlocking(); 22 | 23 | } else { 24 | boolean deferredStatus = deferredResult.setResult(state.getResult()); 25 | state.getLog().logEndNonBlocking(200, deferredStatus); 26 | } 27 | } 28 | 29 | @Override 30 | public void onFailure(State state, Throwable t) { 31 | deferredResult.setErrorResult(t); 32 | state.getLog().logExceptionNonBlocking(t); 33 | } 34 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/deferredresult/DeferredResultWithBlockingWait.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.deferredresult; 2 | 3 | import org.springframework.web.context.request.async.DeferredResult; 4 | 5 | import java.util.concurrent.CountDownLatch; 6 | 7 | /** 8 | * Created by magnus on 16/05/14. 9 | */ 10 | public class DeferredResultWithBlockingWait extends DeferredResult { 11 | final CountDownLatch latch = new CountDownLatch(1); 12 | 13 | @Override 14 | public boolean setResult(T result) { 15 | latch.countDown(); 16 | return super.setResult(result); 17 | } 18 | 19 | public void await() { 20 | try { 21 | latch.await(); 22 | } catch (InterruptedException e) { 23 | throw new RuntimeException(e); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/lambdasupport/AsyncHttpClientLambdaAware.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.lambdasupport; 2 | 3 | import com.ning.http.client.AsyncCompletionHandler; 4 | import com.ning.http.client.AsyncHttpClient; 5 | import com.ning.http.client.ListenableFuture; 6 | import com.ning.http.client.Response; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * Created by magnus on 18/07/14. 12 | */ 13 | public class AsyncHttpClientLambdaAware { 14 | 15 | private static final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(); 16 | 17 | public ListenableFuture execute(String url, final Completed c, final Error e) throws IOException { 18 | return asyncHttpClient.prepareGet(url).execute(new AsyncCompletionHandler() { 19 | 20 | @Override 21 | public Response onCompleted(Response response) throws Exception { 22 | return c.onCompleted(response); 23 | } 24 | 25 | @Override 26 | public void onThrowable(Throwable t) { 27 | e.onThrowable(t); 28 | } 29 | 30 | }); 31 | }; 32 | 33 | public ListenableFuture execute(String url, final Completed c) throws IOException { 34 | return asyncHttpClient.prepareGet(url).execute(new AsyncCompletionHandler() { 35 | 36 | @Override 37 | public Response onCompleted(Response response) throws Exception { 38 | return c.onCompleted(response); 39 | } 40 | }); 41 | }; 42 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/lambdasupport/Completed.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.lambdasupport; 2 | 3 | import com.ning.http.client.Response; 4 | 5 | /** 6 | * Created by magnus on 18/07/14. 7 | */ 8 | public interface Completed { 9 | public Response onCompleted(Response response) throws Exception; 10 | } 11 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/lambdasupport/Error.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.lambdasupport; 2 | 3 | /** 4 | * Created by magnus on 18/07/14. 5 | */ 6 | public interface Error { 7 | public void onThrowable(Throwable t); 8 | } 9 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/log/LogHelper.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.log; 2 | 3 | import com.sun.management.UnixOperatingSystemMXBean; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.lang.management.ManagementFactory; 8 | import java.lang.management.OperatingSystemMXBean; 9 | import java.util.concurrent.atomic.AtomicLong; 10 | 11 | /** 12 | * Created by magnus on 18/05/14. 13 | */ 14 | public class LogHelper { 15 | 16 | private static OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean(); 17 | private static final AtomicLong lastRequestId = new AtomicLong(0); 18 | private static final AtomicLong concurrentRequests = new AtomicLong(0); 19 | private static long maxConcurrentRequests = 0; 20 | 21 | private final String name; 22 | private final Logger log; 23 | private final int requestsPerLog; 24 | private final long reqId; 25 | 26 | public LogHelper(Class clz, String name, int requestsPerLog) { 27 | this.log = LoggerFactory.getLogger(clz); 28 | this.name = name; 29 | this.requestsPerLog = requestsPerLog; 30 | this.reqId = lastRequestId.getAndIncrement(); 31 | } 32 | 33 | public void logStartBlocking() { 34 | logStart("blocking"); 35 | } 36 | 37 | public void logStartNonBlocking() { 38 | logStart("non-blocking"); 39 | } 40 | 41 | public void logStartProcessingStepBlocking(int processingStepNo) { 42 | logStartProcessingStep("blocking", processingStepNo); 43 | } 44 | public void logStartProcessingStepNonBlocking(int processingStepNo) { 45 | logStartProcessingStep("non-blocking", processingStepNo); 46 | } 47 | 48 | public void logAsynchProcessingStepComplete() { 49 | 50 | long concReqs = concurrentRequests.get(); 51 | 52 | updateStatistics(reqId, concReqs); 53 | 54 | log.debug("{}: Asynch call complete för request #{}, hand over to the state machine for the next action", concReqs, reqId); 55 | } 56 | 57 | public void logMessage(String message) { 58 | long concReqs = concurrentRequests.get(); 59 | 60 | log.debug("{}: Request #{} - {}", concReqs, reqId, message); 61 | } 62 | 63 | public void logEndProcessingStepBlocking(int processingStepNo, int httpStatus) { 64 | logEndProcessingStep("blocking", processingStepNo, httpStatus); 65 | } 66 | 67 | public void logEndProcessingStepNonBlocking(int processingStepNo, int httpStatus) { 68 | logEndProcessingStep("non-blocking", processingStepNo, httpStatus); 69 | } 70 | 71 | public void logEndBlocking(int httpStatus) { 72 | logEnd("blocking", httpStatus, null); 73 | } 74 | 75 | public void logEndNonBlocking(int httpStatus, boolean deferredStatus) { 76 | logEnd("non-blocking", httpStatus, deferredStatus); 77 | } 78 | 79 | public void logExceptionBlocking(Throwable t) { 80 | logException("blocking", t); 81 | } 82 | 83 | public void logExceptionNonBlocking(Throwable t) { 84 | logException("non-blocking", t); 85 | } 86 | 87 | public void logLeaveThreadNonBlocking() { 88 | 89 | long concReqs = concurrentRequests.get(); 90 | 91 | log.debug("{}: Processing of non-blocking {} request #{}, leave the request thread", concReqs, name, reqId); 92 | } 93 | 94 | public void logAlreadyExpiredNonBlocking() { 95 | 96 | long concReqs = concurrentRequests.getAndDecrement(); 97 | 98 | log.warn("{}: Processing of non-blocking {} request #{} already expired", concReqs, name, reqId); 99 | } 100 | 101 | /* 102 | * PRIVATE PARTS :-) 103 | */ 104 | 105 | protected void logStart(String type) { 106 | 107 | long concReqs = concurrentRequests.getAndIncrement(); 108 | 109 | updateStatistics(reqId, concReqs); 110 | 111 | log.debug("{}: Start of {} {} request #{}.", concReqs, type, name, reqId); 112 | } 113 | 114 | protected void logEnd(String type, int httpStatus, Boolean deferredStatus) { 115 | 116 | long concReqs = concurrentRequests.getAndDecrement(); 117 | 118 | if (deferredStatus == null) { 119 | log.debug("{}: End of {} {} request #{}, http-status: {}", concReqs, type, name, reqId, httpStatus); 120 | } else { 121 | log.debug("{}: End of {} {} request #{}, http-status: {}, deferred-status: {}", concReqs, type, name, reqId, httpStatus, deferredStatus); 122 | } 123 | } 124 | 125 | protected void logStartProcessingStep(String type, int processingStepNo) { 126 | 127 | long concReqs = concurrentRequests.getAndIncrement(); 128 | 129 | log.debug("{}: Start processing of {} call #{} in request #{}", concReqs, type, processingStepNo, reqId); 130 | } 131 | 132 | protected void logEndProcessingStep(String type, int processingStepNo, int httpStatus) { 133 | 134 | long concReqs = concurrentRequests.getAndDecrement(); 135 | 136 | log.debug("{}: End of processing of {} call #{} in request #{}, http-status: {}", concReqs, type, processingStepNo, reqId, httpStatus); 137 | } 138 | 139 | protected void logException(String type, Throwable t) { 140 | 141 | long concReqs = concurrentRequests.getAndDecrement(); 142 | 143 | log.warn("{}: Processing of {} {} request #{} caused an exception: {}", concReqs, type, name, reqId, t); 144 | } 145 | protected void updateStatistics(long reqId, long concReqs) { 146 | if (concReqs > maxConcurrentRequests) { 147 | maxConcurrentRequests = concReqs; 148 | } 149 | 150 | if (reqId % requestsPerLog == 0 && reqId > 0) { 151 | Object openFiles = "UNKNOWN"; 152 | if (os instanceof UnixOperatingSystemMXBean) { 153 | openFiles = ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount(); 154 | } 155 | log.info("Statistics: noOfReqs: {}, maxConcReqs: {}, openFiles: {}", reqId, maxConcurrentRequests, openFiles); 156 | } 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/log/LogHelperFactory.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.log; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.beans.factory.config.AutowireCapableBeanFactory; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * Created by magnus on 30/05/14. 10 | */ 11 | @Component 12 | public class LogHelperFactory { 13 | private @Autowired AutowireCapableBeanFactory beanFactory; 14 | 15 | @Value("${statistics.requestsPerLog}") 16 | private int STAT_REQS_PER_LOG; 17 | 18 | public LogHelper getLog(Class clz, String name) { 19 | return new LogHelper(clz, name, STAT_REQS_PER_LOG); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/statemachine/Processor.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.statemachine; 2 | 3 | /** 4 | * Created by magnus on 15/05/14. 5 | */ 6 | public interface Processor { 7 | public void process(State state); 8 | } 9 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/statemachine/State.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.statemachine; 2 | 3 | import se.callista.springmvc.asynch.common.log.LogHelper; 4 | import se.callista.springmvc.asynch.common.statemachine.internal.StateProcessingStepIterator; 5 | 6 | import java.util.Iterator; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | /** 10 | * Created by magnus on 15/05/14. 11 | */ 12 | public class State { 13 | 14 | private static final AtomicLong lastProcessId = new AtomicLong(0); 15 | 16 | private StateProcessingStepIterator processingSteps; 17 | private long processId; 18 | private LogHelper log; 19 | private StateMachineCallback completionCallback; 20 | private String result = ""; 21 | 22 | public State(Iterator processingSteps, LogHelper log, StateMachineCallback completionCallback) { 23 | this.processingSteps = new StateProcessingStepIterator(processingSteps); 24 | this.log = log; 25 | this.completionCallback = completionCallback; 26 | this.processId = lastProcessId.incrementAndGet(); 27 | } 28 | 29 | public LogHelper getLog() { 30 | return log; 31 | } 32 | public long getProcessId() { 33 | return processId; 34 | } 35 | 36 | public Iterator getProcessingSteps() { 37 | return processingSteps; 38 | } 39 | public int getProcessingStepNo() { return processingSteps.getProcessingStepNo(); } 40 | 41 | public StateMachineCallback getCompletionCallback() { 42 | return completionCallback; 43 | } 44 | 45 | public String appendResult(String newResult) { 46 | return result += newResult; 47 | } 48 | public String getResult() { 49 | return result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/statemachine/StateMachine.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.statemachine; 2 | 3 | import org.springframework.stereotype.Component; 4 | import se.callista.springmvc.asynch.common.log.LogHelper; 5 | 6 | import java.util.Iterator; 7 | 8 | /** 9 | * A stateless minimal state machine 10 | */ 11 | @Component 12 | public class StateMachine { 13 | 14 | public void initProcessing(Iterator processingSteps, LogHelper log, StateMachineCallback completionCallback) { 15 | executeNextStep(new State(processingSteps, log, completionCallback)); 16 | } 17 | 18 | public void executeNextStep(State state) { 19 | 20 | if (state.getProcessingSteps().hasNext()) { 21 | // Initiate next processing step... 22 | state.getProcessingSteps().next().process(state); 23 | 24 | } else { 25 | // We are done... 26 | state.getCompletionCallback().onCompleted(state); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/statemachine/StateMachineCallback.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.statemachine; 2 | 3 | /** 4 | * Created by magnus on 29/05/14. 5 | */ 6 | public interface StateMachineCallback { 7 | void onCompleted(State state); 8 | void onFailure(State state, Throwable t); 9 | } 10 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/common/statemachine/internal/StateProcessingStepIterator.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.statemachine.internal; 2 | 3 | import se.callista.springmvc.asynch.common.statemachine.Processor; 4 | 5 | import java.util.Iterator; 6 | 7 | /** 8 | * Created by magnus on 08/06/14. 9 | */ 10 | public class StateProcessingStepIterator implements Iterator { 11 | 12 | private final Iterator processingSteps; 13 | private int processingStepNo = 0; 14 | 15 | public StateProcessingStepIterator(Iterator processingSteps) { 16 | this.processingSteps = processingSteps; 17 | } 18 | 19 | @Override 20 | public boolean hasNext() { 21 | return processingSteps.hasNext(); 22 | } 23 | 24 | @Override 25 | public Processor next() { 26 | if (processingSteps.hasNext()) processingStepNo++; 27 | return processingSteps.next(); 28 | } 29 | 30 | public int getProcessingStepNo() { 31 | return processingStepNo; 32 | } 33 | 34 | @Override 35 | public void remove() { 36 | processingSteps.remove(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/config/MyEmbeddedServletContainerCustomizer.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.config; 2 | 3 | import org.apache.catalina.connector.Connector; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; 8 | import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; 9 | import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer; 10 | import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; 11 | 12 | public class MyEmbeddedServletContainerCustomizer implements EmbeddedServletContainerCustomizer { 13 | 14 | private static final Logger LOG = LoggerFactory.getLogger(MyEmbeddedServletContainerCustomizer.class); 15 | 16 | @Value("${servlet.container.maxThreads}") 17 | private int MAX_THREADS; 18 | 19 | @Override 20 | public void customize(ConfigurableEmbeddedServletContainer factory) { 21 | if(factory instanceof TomcatEmbeddedServletContainerFactory) { 22 | customizeTomcat((TomcatEmbeddedServletContainerFactory) factory); 23 | } 24 | } 25 | 26 | public void customizeTomcat(TomcatEmbeddedServletContainerFactory factory) { 27 | factory.addConnectorCustomizers(new TomcatConnectorCustomizer() { 28 | @Override 29 | public void customize(Connector connector) { 30 | Object defaultMaxThreads = connector.getAttribute("maxThreads"); 31 | connector.setAttribute("maxThreads", MAX_THREADS); 32 | LOG.info("Changed Tomcat connector maxThreads from " + defaultMaxThreads + " to " + MAX_THREADS); 33 | } 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/blocking/AggregatorBlockingController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.blocking; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.client.RestTemplate; 11 | import se.callista.springmvc.asynch.common.log.LogHelper; 12 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 13 | 14 | import javax.annotation.PostConstruct; 15 | 16 | @RestController 17 | public class AggregatorBlockingController { 18 | 19 | private static LogHelper LOG; 20 | 21 | private RestTemplate restTemplate = new RestTemplate(); 22 | 23 | @Autowired 24 | private LogHelperFactory logFactory; 25 | 26 | @Value("${sp.non_blocking.url}") 27 | private String SP_NON_BLOCKING_URL; 28 | 29 | @PostConstruct 30 | public void initAfterInject() { 31 | LOG = logFactory.getLog(AggregatorBlockingController.class, "aggregator"); 32 | } 33 | 34 | /** 35 | * Sample usage: curl "http://localhost:9080/aggregate-blocking?minMs=1000&maxMs=2000" 36 | * 37 | * @param dbLookupMs 38 | * @param dbHits 39 | * @param minMs 40 | * @param maxMs 41 | * @return 42 | */ 43 | @RequestMapping("/aggregate-blocking") 44 | public String blockingAggregator( 45 | @RequestParam(value = "dbLookupMs", required = false, defaultValue = "200") int dbLookupMs, 46 | @RequestParam(value = "dbHits", required = false, defaultValue = "3") int dbHits, 47 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 48 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) { 49 | 50 | DbLookup dbLookup = new DbLookup(LOG, dbLookupMs, dbHits); 51 | 52 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 53 | 54 | LOG.logStartBlocking(); 55 | 56 | String aggregatedResult = ""; 57 | try { 58 | int noOfCalls = dbLookup.executeDbLookup(); 59 | 60 | for (int i = 0; i < noOfCalls; i++) { 61 | LOG.logStartProcessingStepBlocking(i); 62 | ResponseEntity result = restTemplate.getForEntity( 63 | SP_NON_BLOCKING_URL + "?minMs={minMs}&maxMs={maxMs}", String.class, minMs, maxMs); 64 | 65 | // TODO: Handle status codes other than 200... 66 | status = result.getStatusCode(); 67 | LOG.logEndProcessingStepBlocking(i, status.value()); 68 | 69 | aggregatedResult += result.getBody() + '\n'; 70 | } 71 | 72 | return aggregatedResult; 73 | 74 | } finally { 75 | LOG.logEndBlocking(status.value()); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/blocking/DbLookup.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.blocking; 2 | 3 | import se.callista.springmvc.asynch.common.log.LogHelper; 4 | 5 | /** 6 | * Created by magnus on 20/07/14. 7 | */ 8 | public class DbLookup { 9 | private final LogHelper LOG; 10 | private final int dbLookupMs; 11 | private final int dbHits; 12 | 13 | public DbLookup(LogHelper log, int dbLookupMs, int dbHits) { 14 | LOG = log; 15 | this.dbLookupMs = dbLookupMs; 16 | this.dbHits = dbHits; 17 | } 18 | 19 | public int executeDbLookup() { 20 | 21 | LOG.logMessage("Start of blocking dbLookup"); 22 | int hits = simulateDbLookup(); 23 | LOG.logMessage("Processing of blocking dbLookup done"); 24 | 25 | return hits; 26 | } 27 | 28 | // Simulate a blocking db-lookup by putting the current thread to sleep for a while... 29 | protected int simulateDbLookup(){ 30 | 31 | try { 32 | Thread.sleep(dbLookupMs); 33 | } catch (InterruptedException e) {} 34 | 35 | return dbHits; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/nonblocking/callback/AggregatorCallback.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.nonblocking.callback; 2 | 3 | import com.ning.http.client.AsyncCompletionHandler; 4 | import com.ning.http.client.Response; 5 | 6 | public class AggregatorCallback extends AsyncCompletionHandler { 7 | 8 | private final int id; 9 | private final AggregatorEventHandler eventHandler; 10 | 11 | public AggregatorCallback(int id, AggregatorEventHandler eventHandler) { 12 | this.id = id; 13 | this.eventHandler = eventHandler; 14 | } 15 | 16 | @Override 17 | public Response onCompleted(Response response) throws Exception{ 18 | eventHandler.onResult(id, response); 19 | return response; 20 | } 21 | 22 | @Override 23 | public void onThrowable(Throwable t){ 24 | eventHandler.onError(id, t); 25 | } 26 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/nonblocking/callback/AggregatorEventHandler.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.nonblocking.callback; 2 | 3 | import com.ning.http.client.AsyncHttpClient; 4 | import com.ning.http.client.ListenableFuture; 5 | import com.ning.http.client.Response; 6 | import org.springframework.web.context.request.async.DeferredResult; 7 | import se.callista.springmvc.asynch.common.log.LogHelper; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Timer; 13 | import java.util.TimerTask; 14 | import java.util.concurrent.CancellationException; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | /** 18 | * Created by magnus on 22/04/14. 19 | */ 20 | public class AggregatorEventHandler { 21 | 22 | private Timer timer = new Timer(); 23 | 24 | private final LogHelper log; 25 | 26 | // @Value("${sp.non_blocking.url}") 27 | private String SP_NON_BLOCKING_URL; 28 | 29 | private final int noOfCalls; 30 | private final int maxMs; 31 | private final int minMs; 32 | private final int timeoutMs; 33 | private final DeferredResult deferredResult; 34 | 35 | private final static AsyncHttpClient asyncHttpClient = new AsyncHttpClient(); 36 | 37 | private final AtomicInteger noOfResults = new AtomicInteger(0); 38 | private String result = ""; 39 | 40 | private List> executors = new ArrayList<>(); 41 | 42 | public AggregatorEventHandler(LogHelper log, int noOfCalls, String url, int minMs, int maxMs, int timeoutMs, DeferredResult deferredResult) { 43 | this.log = log; 44 | this.noOfCalls = noOfCalls; 45 | this.SP_NON_BLOCKING_URL = url; 46 | this.minMs = minMs; 47 | this.maxMs = maxMs; 48 | this.timeoutMs = timeoutMs; 49 | this.deferredResult = deferredResult; 50 | } 51 | 52 | public void onStart() { 53 | try { 54 | for (int i = 0; i < noOfCalls; i++) { 55 | log.logStartProcessingStepNonBlocking(i); 56 | String url = SP_NON_BLOCKING_URL + "?minMs=" + minMs + "&maxMs=" + maxMs; 57 | executors.add(asyncHttpClient.prepareGet(url).execute(new AggregatorCallback(i, this))); 58 | } 59 | } catch (IOException e) { 60 | throw new RuntimeException(e); 61 | } 62 | 63 | // Setup a timer for the max wait-period 64 | log.logMessage("Start timer for: " + timeoutMs + " ms."); 65 | TimerTask timeoutTask = new TimerTask() { 66 | 67 | @Override 68 | public void run() { 69 | onTimeout(); 70 | } 71 | }; 72 | 73 | // Schedule the timeout task 74 | timer.schedule(timeoutTask, timeoutMs); 75 | } 76 | 77 | public void onResult(int id, Response response) { 78 | 79 | try { 80 | // TODO: Handle status codes other than 200... 81 | int httpStatus = response.getStatusCode(); 82 | log.logEndProcessingStepNonBlocking(id, httpStatus); 83 | 84 | // If many requests completes at the same time the following code must be executed in sequence for one thread at a time 85 | // Since we don't have any Actor-like mechanism to rely on (for the time being...) we simply ensure that the code block is executed by one thread at a time by an old school synchronized block 86 | // Since the processing in the block is very limited it will not cause a bottleneck. 87 | synchronized (result) { 88 | // Count down, aggregate answer and return if all answers (also cancel timer)... 89 | int noOfRes = noOfResults.incrementAndGet(); 90 | 91 | // Perform the aggregation... 92 | log.logMessage("Safely adding response #" + id); 93 | result += response.getResponseBody() + '\n'; 94 | 95 | if (noOfRes >= noOfCalls) { 96 | onAllCompleted(); 97 | } 98 | } 99 | 100 | 101 | 102 | } catch (IOException e) { 103 | throw new RuntimeException(e); 104 | } 105 | } 106 | 107 | public void onError(int id, Throwable t) { 108 | 109 | // Skip logging an error if we just canceled the request due to an timeout 110 | if (t instanceof CancellationException) return; 111 | 112 | log.logExceptionNonBlocking(t); 113 | 114 | // Count down, aggregate answer and return if all answers (also cancel timer)... 115 | int noOfRes = noOfResults.incrementAndGet(); 116 | result += t.toString() + '\n'; 117 | 118 | if (noOfRes >= noOfCalls) { 119 | onAllCompleted(); 120 | } 121 | 122 | // TODO: Handle asynchronous processing errors... 123 | 124 | } 125 | 126 | public void onTimeout() { 127 | 128 | // complete missing answers and return ... 129 | log.logMessage("Timeout in aggregating service, only received answers from " + noOfResults.get() + " out of total " + noOfCalls + " expected responses."); 130 | int i = 0; 131 | for (ListenableFuture executor : executors) { 132 | if (!executor.isDone()) { 133 | log.logMessage("Cancel asych request #" + i); 134 | executor.cancel(true); 135 | } 136 | i++; 137 | } 138 | onAllCompleted(); 139 | } 140 | 141 | public void onAllCompleted() { 142 | log.logMessage("All done, cancel timer"); 143 | timer.cancel(); 144 | 145 | if (deferredResult.isSetOrExpired()) { 146 | log.logAlreadyExpiredNonBlocking(); 147 | } else { 148 | boolean deferredStatus = deferredResult.setResult(result); 149 | log.logEndNonBlocking(200, deferredStatus); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/nonblocking/callback/AggregatorNonBlockingCallbackController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.nonblocking.callback; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.core.task.TaskExecutor; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.context.request.async.DeferredResult; 11 | import se.callista.springmvc.asynch.common.log.LogHelper; 12 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 13 | 14 | import javax.annotation.PostConstruct; 15 | import java.io.IOException; 16 | 17 | @RestController 18 | public class AggregatorNonBlockingCallbackController { 19 | 20 | private static LogHelper LOG; 21 | 22 | @Autowired 23 | private LogHelperFactory logFactory; 24 | 25 | @Value("${sp.non_blocking.url}") 26 | private String SP_NON_BLOCKING_URL; 27 | 28 | @Value("${aggregator.timeoutMs}") 29 | private int TIMEOUT_MS; 30 | 31 | @PostConstruct 32 | public void initAfterInject() { 33 | LOG = logFactory.getLog(AggregatorNonBlockingCallbackController.class, "aggregator"); 34 | } 35 | 36 | @Autowired 37 | @Qualifier("dbThreadPoolExecutor") 38 | private TaskExecutor dbThreadPoolExecutor; 39 | 40 | /** 41 | * Sample usage: curl "http://localhost:9080/aggregate-non-blocking-callback?minMs=1000&maxMs=2000" 42 | * 43 | * @param dbLookupMs 44 | * @param dbHits 45 | * @param minMs 46 | * @param maxMs 47 | * @return 48 | * @throws IOException 49 | */ 50 | @RequestMapping("/aggregate-non-blocking-callback") 51 | public DeferredResult nonBlockingAggregator( 52 | @RequestParam(value = "dbLookupMs", required = false, defaultValue = "0") int dbLookupMs, 53 | @RequestParam(value = "dbHits", required = false, defaultValue = "3") int dbHits, 54 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 55 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) throws IOException { 56 | 57 | LOG.logStartNonBlocking(); 58 | 59 | DeferredResult deferredResult = new DeferredResult(); 60 | 61 | dbThreadPoolExecutor.execute(new DbLookupRunnable(LOG, dbLookupMs, dbHits, SP_NON_BLOCKING_URL, minMs, maxMs, TIMEOUT_MS, deferredResult)); 62 | 63 | LOG.logLeaveThreadNonBlocking(); 64 | 65 | // Return to let go of the precious thread we are holding on to... 66 | return deferredResult; 67 | } 68 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/nonblocking/callback/DbLookupRunnable.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.nonblocking.callback; 2 | 3 | import org.springframework.web.context.request.async.DeferredResult; 4 | import se.callista.springmvc.asynch.common.log.LogHelper; 5 | 6 | public class DbLookupRunnable implements Runnable { 7 | 8 | private final LogHelper log; 9 | 10 | private int dbLookupMs; 11 | private int dbHits; 12 | private final int maxMs; 13 | private final int minMs; 14 | private final int timeoutMs; 15 | private DeferredResult deferredResult; 16 | private String url; 17 | 18 | public DbLookupRunnable(LogHelper log, int dbLookupMs, int dbHits, String url, int minMs, int maxMs, int timeoutMs, DeferredResult deferredResult) { 19 | this.log = log; 20 | this.dbLookupMs = dbLookupMs; 21 | this.dbHits = dbHits; 22 | this.url = url; 23 | this.minMs = minMs; 24 | this.maxMs = maxMs; 25 | this.timeoutMs = timeoutMs; 26 | this.deferredResult = deferredResult; 27 | } 28 | 29 | @Override 30 | public void run() { 31 | //seconds later in another thread... 32 | int noOfCalls = execute(); 33 | AggregatorEventHandler aeh = new AggregatorEventHandler(log, noOfCalls, url, minMs, maxMs, timeoutMs, deferredResult); 34 | aeh.onStart(); 35 | } 36 | 37 | public int execute() { 38 | 39 | log.logMessage("Start of blocking dbLookup"); 40 | int hits = simulateDbLookup(); 41 | log.logMessage("Processing of blocking dbLookup done"); 42 | 43 | return hits; 44 | } 45 | 46 | protected int simulateDbLookup(){ 47 | 48 | try { 49 | Thread.sleep(dbLookupMs); 50 | } catch (InterruptedException e) {} 51 | 52 | return dbHits; 53 | } 54 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/nonblocking/lambda/AggregatorNonBlockingLambdaController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.nonblocking.lambda; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.core.task.TaskExecutor; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.context.request.async.DeferredResult; 11 | import se.callista.springmvc.asynch.common.log.LogHelper; 12 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 13 | 14 | import javax.annotation.PostConstruct; 15 | import java.io.IOException; 16 | 17 | @RestController 18 | public class AggregatorNonBlockingLambdaController { 19 | 20 | private static LogHelper LOG; 21 | 22 | @Autowired 23 | private LogHelperFactory logFactory; 24 | 25 | @Value("${sp.non_blocking.url}") 26 | private String SP_NON_BLOCKING_URL; 27 | 28 | @Value("${aggregator.timeoutMs}") 29 | private int TIMEOUT_MS; 30 | 31 | @PostConstruct 32 | public void initAfterInject() { 33 | LOG = logFactory.getLog(AggregatorNonBlockingLambdaController.class, "aggregator"); 34 | } 35 | 36 | @Autowired 37 | @Qualifier("dbThreadPoolExecutor") 38 | private TaskExecutor dbThreadPoolExecutor; 39 | 40 | /** 41 | * Sample usage: curl "http://localhost:9080/aggregate-non-blocking-lambda?minMs=1000&maxMs=2000" 42 | * 43 | * @param dbLookupMs 44 | * @param dbHits 45 | * @param minMs 46 | * @param maxMs 47 | * @return 48 | * @throws java.io.IOException 49 | */ 50 | @RequestMapping("/aggregate-non-blocking-lambda") 51 | public DeferredResult nonBlockingAggregator( 52 | @RequestParam(value = "dbLookupMs", required = false, defaultValue = "0") int dbLookupMs, 53 | @RequestParam(value = "dbHits", required = false, defaultValue = "3") int dbHits, 54 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 55 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) throws IOException { 56 | 57 | // Delegate the whole processing to a executor-instance to avoid concurrency problems with other concurrent requests 58 | Executor exec = new Executor(LOG, SP_NON_BLOCKING_URL, TIMEOUT_MS, dbThreadPoolExecutor, dbLookupMs, dbHits, minMs, maxMs); 59 | 60 | DeferredResult deferredResult = exec.startNonBlockingExecution(); 61 | 62 | // Return to let go of the precious thread we are holding on to... 63 | return deferredResult; 64 | } 65 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/nonblocking/lambda/DbLookup.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.nonblocking.lambda; 2 | 3 | import se.callista.springmvc.asynch.common.log.LogHelper; 4 | 5 | /** 6 | * Created by magnus on 20/07/14. 7 | */ 8 | public class DbLookup { 9 | private final LogHelper LOG; 10 | private final int dbLookupMs; 11 | private final int dbHits; 12 | 13 | public DbLookup(LogHelper log, int dbLookupMs, int dbHits) { 14 | LOG = log; 15 | this.dbLookupMs = dbLookupMs; 16 | this.dbHits = dbHits; 17 | } 18 | 19 | public int executeDbLookup() { 20 | 21 | LOG.logMessage("Start of blocking dbLookup"); 22 | int hits = simulateDbLookup(); 23 | LOG.logMessage("Processing of blocking dbLookup done"); 24 | 25 | return hits; 26 | } 27 | 28 | // Simulate a blocking db-lookup by putting the current thread to sleep for a while... 29 | protected int simulateDbLookup(){ 30 | 31 | try { 32 | Thread.sleep(dbLookupMs); 33 | } catch (InterruptedException e) {} 34 | 35 | return dbHits; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/aggregator/nonblocking/lambda/Executor.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator.nonblocking.lambda; 2 | 3 | import com.ning.http.client.ListenableFuture; 4 | import com.ning.http.client.Response; 5 | import org.springframework.core.task.TaskExecutor; 6 | import org.springframework.web.context.request.async.DeferredResult; 7 | import se.callista.springmvc.asynch.common.lambdasupport.AsyncHttpClientLambdaAware; 8 | import se.callista.springmvc.asynch.common.log.LogHelper; 9 | 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Timer; 14 | import java.util.TimerTask; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | /** 18 | * Created by magnus on 20/07/14. 19 | */ 20 | public class Executor { 21 | 22 | final private LogHelper log; 23 | final private String baseUrl; 24 | final private int timeoutMs; 25 | final private TaskExecutor dbThreadPoolExecutor; 26 | final private int dbLookupMs; 27 | final private int dbHits; 28 | final private int minMs; 29 | final private int maxMs; 30 | 31 | private static final AsyncHttpClientLambdaAware asyncHttpClient = new AsyncHttpClientLambdaAware(); 32 | private static Timer timer = new Timer(); 33 | private TimerTask timeoutTask = null; 34 | private List> concurrentExecutors = new ArrayList<>(); 35 | final AtomicInteger noOfResults = new AtomicInteger(0); 36 | private int noOfCalls = 0; 37 | private DeferredResult deferredResult = null; 38 | private List resultArr = new ArrayList<>(); 39 | 40 | public Executor(LogHelper log, String baseUrl, int timeoutMs, TaskExecutor dbThreadPoolExecutor, int dbLookupMs, int dbHits, int minMs, int maxMs) { 41 | this.log = log; 42 | this.baseUrl = baseUrl; 43 | this.timeoutMs = timeoutMs; 44 | this.dbThreadPoolExecutor = dbThreadPoolExecutor; 45 | this.dbLookupMs = dbLookupMs; 46 | this.dbHits = dbHits; 47 | this.minMs = minMs; 48 | this.maxMs = maxMs; 49 | } 50 | 51 | public DeferredResult startNonBlockingExecution() { 52 | log.logStartNonBlocking(); 53 | 54 | DbLookup dbLookup = new DbLookup(log, dbLookupMs, dbHits); 55 | deferredResult = new DeferredResult(); 56 | 57 | dbThreadPoolExecutor.execute(() -> { 58 | noOfCalls = dbLookup.executeDbLookup(); 59 | try { 60 | for (int i = 0; i < noOfCalls; i++) { 61 | final int id = i+1; 62 | 63 | log.logStartProcessingStepNonBlocking(id); 64 | String url = baseUrl + "?minMs=" + minMs + "&maxMs=" + maxMs; 65 | concurrentExecutors.add(asyncHttpClient.execute(url, (response) -> { 66 | 67 | // TODO: Handle status codes other than 200... 68 | int httpStatus = response.getStatusCode(); 69 | log.logEndProcessingStepNonBlocking(id, httpStatus); 70 | 71 | // If many requests completes at the same time the following code must be executed in sequence for one thread at a time 72 | // Since we don't have any Actor-like mechanism to rely on (for the time being...) we simply ensure that the code block is executed by one thread at a time by an old school synchronized block 73 | // Since the processing in the block is very limited it will not cause a bottleneck. 74 | synchronized (resultArr) { 75 | // Count down, aggregate answer and return if all answers (also cancel timer)... 76 | int noOfRes = noOfResults.incrementAndGet(); 77 | 78 | // Perform the aggregation... 79 | log.logMessage("Safely adding response #" + id); 80 | resultArr.add(response.getResponseBody()); 81 | 82 | if (noOfRes >= noOfCalls) { 83 | onAllCompleted(); 84 | } 85 | } 86 | return response; 87 | })); 88 | } 89 | } catch (IOException e) { 90 | throw new RuntimeException(e); 91 | } 92 | 93 | // Setup a timer for the max wait-period 94 | log.logMessage("Start timer for: " + timeoutMs + " ms."); 95 | timeoutTask = new TimerTask() { 96 | 97 | @Override 98 | public void run() { 99 | onTimeout(); 100 | } 101 | }; 102 | 103 | // Schedule the timeout task 104 | timer.schedule(timeoutTask, timeoutMs); 105 | 106 | }); 107 | 108 | // Ok, everything is now setup for asynchronous processing, let the play begin... 109 | log.logLeaveThreadNonBlocking(); 110 | 111 | return deferredResult; 112 | } 113 | 114 | private void onTimeout() { 115 | 116 | // complete missing answers and return ... 117 | log.logMessage("Timeout in aggregating service, only received answers from " + noOfResults.get() + " out of total " + noOfCalls + " expected responses."); 118 | int i = 0; 119 | for (ListenableFuture executor : concurrentExecutors) { 120 | if (!executor.isDone()) { 121 | log.logMessage("Cancel asych request #" + i); 122 | executor.cancel(true); 123 | } 124 | i++; 125 | } 126 | onAllCompleted(); 127 | } 128 | 129 | private void onAllCompleted() { 130 | log.logMessage("All done, cancel timer"); 131 | timeoutTask.cancel(); 132 | 133 | if (deferredResult.isSetOrExpired()) { 134 | log.logAlreadyExpiredNonBlocking(); 135 | } else { 136 | boolean deferredStatus = deferredResult.setResult(getTotalResult()); 137 | log.logEndNonBlocking(200, deferredStatus); 138 | } 139 | } 140 | 141 | private String getTotalResult() { 142 | String totalResult = ""; 143 | for (String r : resultArr) 144 | totalResult += r + '\n'; 145 | return totalResult; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/router/blocking/RouterBlockingController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.router.blocking; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.client.RestTemplate; 11 | import se.callista.springmvc.asynch.common.log.LogHelper; 12 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 13 | 14 | import javax.annotation.PostConstruct; 15 | 16 | @RestController 17 | public class RouterBlockingController { 18 | 19 | private LogHelper LOG; 20 | 21 | private RestTemplate restTemplate = new RestTemplate(); 22 | 23 | @Autowired 24 | private LogHelperFactory logFactory; 25 | 26 | @Value("${sp.non_blocking.url}") 27 | private String SP_NON_BLOCKING_URL; 28 | 29 | @PostConstruct 30 | public void initAfterInject() { 31 | LOG = logFactory.getLog(RouterBlockingController.class, "router"); 32 | } 33 | 34 | /** 35 | * Sample usage: curl "http://localhost:9080/router-blocking?minMs=1000&maxMs=2000" 36 | * 37 | * @param minMs 38 | * @param maxMs 39 | * @return 40 | */ 41 | @RequestMapping("/router-blocking") 42 | public String blockingRouter( 43 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 44 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) { 45 | 46 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 47 | 48 | LOG.logStartBlocking(); 49 | 50 | try { 51 | String url = SP_NON_BLOCKING_URL + "?minMs={minMs}&maxMs={maxMs}"; 52 | ResponseEntity result = restTemplate.getForEntity(url, String.class, minMs, maxMs); 53 | 54 | // TODO: Handle status codes other than 200... 55 | status = result.getStatusCode(); 56 | 57 | return result.getBody(); 58 | 59 | } finally { 60 | LOG.logEndBlocking(status.value()); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/router/nonblocking/anonymous/RouterNonBlockingAnonymousController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.router.nonblocking.anonymous; 2 | 3 | import com.ning.http.client.AsyncCompletionHandler; 4 | import com.ning.http.client.AsyncHttpClient; 5 | import com.ning.http.client.Response; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.RestController; 11 | import org.springframework.web.context.request.async.DeferredResult; 12 | import se.callista.springmvc.asynch.common.log.LogHelper; 13 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 14 | 15 | import javax.annotation.PostConstruct; 16 | import java.io.IOException; 17 | 18 | @RestController 19 | public class RouterNonBlockingAnonymousController { 20 | 21 | private LogHelper LOG; 22 | 23 | private static final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(); 24 | 25 | @Autowired 26 | private LogHelperFactory logFactory; 27 | 28 | @Value("${sp.non_blocking.url}") 29 | private String SP_NON_BLOCKING_URL; 30 | 31 | @PostConstruct 32 | public void initAfterInject() { 33 | LOG = logFactory.getLog(RouterNonBlockingAnonymousController.class, "router"); 34 | } 35 | 36 | /** 37 | * Sample usage: curl "http://localhost:9080/router-non-blocking-anonymous?minMs=1000&maxMs=2000" 38 | * 39 | * @param minMs 40 | * @param maxMs 41 | * @return 42 | * @throws java.io.IOException 43 | */ 44 | @RequestMapping("/router-non-blocking-anonymous") 45 | public DeferredResult nonBlockingRouter( 46 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 47 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) throws IOException { 48 | 49 | LOG.logStartNonBlocking(); 50 | 51 | final DeferredResult deferredResult = new DeferredResult<>(); 52 | 53 | String url = SP_NON_BLOCKING_URL + "?minMs=" + minMs + "&maxMs=" + maxMs; 54 | 55 | asyncHttpClient.prepareGet(url).execute( 56 | new AsyncCompletionHandler() { 57 | @Override 58 | public Response onCompleted(Response response) throws Exception { 59 | // TODO: Handle status codes other than 200... 60 | int httpStatus = response.getStatusCode(); 61 | 62 | if (deferredResult.isSetOrExpired()) { 63 | LOG.logAlreadyExpiredNonBlocking(); 64 | 65 | } else { 66 | boolean deferredStatus = deferredResult.setResult(response.getResponseBody()); 67 | LOG.logEndNonBlocking(httpStatus, deferredStatus); 68 | } 69 | return response; 70 | } 71 | 72 | @Override 73 | public void onThrowable(Throwable t){ 74 | 75 | // TODO: Handle asynchronous processing errors... 76 | 77 | if (deferredResult.isSetOrExpired()) { 78 | LOG.logExceptionNonBlocking(t); 79 | } 80 | } 81 | }); 82 | 83 | LOG.logLeaveThreadNonBlocking(); 84 | 85 | // Return to let go of the precious thread we are holding on to... 86 | return deferredResult; 87 | } 88 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/router/nonblocking/callback/RouterCallback.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.router.nonblocking.callback; 2 | 3 | import com.ning.http.client.AsyncCompletionHandler; 4 | import com.ning.http.client.Response; 5 | import org.springframework.web.context.request.async.DeferredResult; 6 | import se.callista.springmvc.asynch.common.log.LogHelper; 7 | 8 | public class RouterCallback extends AsyncCompletionHandler { 9 | 10 | private final LogHelper log; 11 | 12 | private DeferredResult deferredResult; 13 | 14 | 15 | public RouterCallback(LogHelper log, DeferredResult deferredResult) { 16 | this.log = log; 17 | this.deferredResult = deferredResult; 18 | } 19 | 20 | @Override 21 | public Response onCompleted(Response response) throws Exception{ 22 | 23 | // TODO: Handle status codes other than 200... 24 | int httpStatus = response.getStatusCode(); 25 | 26 | if (deferredResult.isSetOrExpired()) { 27 | log.logAlreadyExpiredNonBlocking(); 28 | 29 | } else { 30 | boolean deferredStatus = deferredResult.setResult(response.getResponseBody()); 31 | log.logEndNonBlocking(httpStatus, deferredStatus); 32 | } 33 | return response; 34 | } 35 | 36 | @Override 37 | public void onThrowable(Throwable t){ 38 | 39 | // TODO: Handle asynchronous processing errors... 40 | 41 | if (deferredResult.isSetOrExpired()) { 42 | log.logExceptionNonBlocking(t); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/router/nonblocking/callback/RouterNonBlockingCallbackController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.router.nonblocking.callback; 2 | 3 | import com.ning.http.client.AsyncHttpClient; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import org.springframework.web.context.request.async.DeferredResult; 10 | import se.callista.springmvc.asynch.common.log.LogHelper; 11 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 12 | 13 | import javax.annotation.PostConstruct; 14 | import java.io.IOException; 15 | 16 | @RestController 17 | public class RouterNonBlockingCallbackController { 18 | 19 | private LogHelper LOG; 20 | 21 | private static final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(); 22 | 23 | @Autowired 24 | private LogHelperFactory logFactory; 25 | 26 | @Value("${sp.non_blocking.url}") 27 | private String SP_NON_BLOCKING_URL; 28 | 29 | @PostConstruct 30 | public void initAfterInject() { 31 | LOG = logFactory.getLog(RouterNonBlockingCallbackController.class, "router"); 32 | } 33 | 34 | /** 35 | * Sample usage: curl "http://localhost:9080/router-non-blocking-callback?minMs=1000&maxMs=2000" 36 | * 37 | * @param minMs 38 | * @param maxMs 39 | * @return 40 | * @throws IOException 41 | */ 42 | @RequestMapping("/router-non-blocking-callback") 43 | public DeferredResult nonBlockingRouter( 44 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 45 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) throws IOException { 46 | 47 | LOG.logStartNonBlocking(); 48 | 49 | DeferredResult deferredResult = new DeferredResult(); 50 | 51 | String url = SP_NON_BLOCKING_URL + "?minMs=" + minMs + "&maxMs=" + maxMs; 52 | asyncHttpClient.prepareGet(url).execute(new RouterCallback(LOG, deferredResult)); 53 | 54 | LOG.logLeaveThreadNonBlocking(); 55 | 56 | // Return to let go of the precious thread we are holding on to... 57 | return deferredResult; 58 | } 59 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/router/nonblocking/lambda/RouterNonBlockingLambdaController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.router.nonblocking.lambda; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RequestParam; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import org.springframework.web.context.request.async.DeferredResult; 9 | import se.callista.springmvc.asynch.common.lambdasupport.AsyncHttpClientLambdaAware; 10 | import se.callista.springmvc.asynch.common.log.LogHelper; 11 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 12 | 13 | import javax.annotation.PostConstruct; 14 | import java.io.IOException; 15 | 16 | @RestController 17 | public class RouterNonBlockingLambdaController { 18 | 19 | private LogHelper LOG; 20 | 21 | private static final AsyncHttpClientLambdaAware asyncHttpClient = new AsyncHttpClientLambdaAware(); 22 | 23 | @Autowired 24 | private LogHelperFactory logFactory; 25 | 26 | @Value("${sp.non_blocking.url}") 27 | private String SP_NON_BLOCKING_URL; 28 | 29 | @PostConstruct 30 | public void initAfterInject() { 31 | LOG = logFactory.getLog(RouterNonBlockingLambdaController.class, "router"); 32 | } 33 | 34 | /** 35 | * Sample usage: curl "http://localhost:9080/router-non-blocking-lambda?minMs=1000&maxMs=2000" 36 | * 37 | * @param minMs 38 | * @param maxMs 39 | * @return 40 | * @throws java.io.IOException 41 | */ 42 | @RequestMapping("/router-non-blocking-lambda") 43 | public DeferredResult nonBlockingRouter( 44 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 45 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) throws IOException { 46 | 47 | LOG.logStartNonBlocking(); 48 | 49 | final DeferredResult deferredResult = new DeferredResult<>(); 50 | 51 | String url = SP_NON_BLOCKING_URL + "?minMs=" + minMs + "&maxMs=" + maxMs; 52 | 53 | asyncHttpClient.execute(url, 54 | (response) -> { 55 | // TODO: Handle status codes other than 200... 56 | int httpStatus = response.getStatusCode(); 57 | 58 | if (deferredResult.isSetOrExpired()) { 59 | LOG.logAlreadyExpiredNonBlocking(); 60 | 61 | } else { 62 | boolean deferredStatus = deferredResult.setResult(response.getResponseBody()); 63 | LOG.logEndNonBlocking(httpStatus, deferredStatus); 64 | } 65 | return response; 66 | }, 67 | (throwable) -> { 68 | // TODO: Handle asynchronous processing errors... 69 | 70 | if (deferredResult.isSetOrExpired()) { 71 | LOG.logExceptionNonBlocking(throwable); 72 | } 73 | } 74 | ); 75 | 76 | LOG.logLeaveThreadNonBlocking(); 77 | 78 | // Return to let go of the precious thread we are holding on to... 79 | return deferredResult; 80 | } 81 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/router/nonblocking/spring/RouterCallback_Spring_AsyncRestTemplate.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.router.nonblocking.spring; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.util.concurrent.ListenableFutureCallback; 5 | import org.springframework.web.context.request.async.DeferredResult; 6 | import se.callista.springmvc.asynch.common.log.LogHelper; 7 | 8 | public class RouterCallback_Spring_AsyncRestTemplate implements ListenableFutureCallback> { 9 | 10 | private LogHelper log; 11 | private long reqId; 12 | private DeferredResult deferredResult; 13 | 14 | public RouterCallback_Spring_AsyncRestTemplate(LogHelper log, DeferredResult deferredResult) { 15 | this.log = log; 16 | this.reqId = reqId; 17 | this.deferredResult = deferredResult; 18 | } 19 | 20 | @Override 21 | public void onSuccess(ResponseEntity result) { 22 | 23 | // TODO: Handle status codes other than 200... 24 | 25 | if (deferredResult.isSetOrExpired()) { 26 | log.logAlreadyExpiredNonBlocking(); 27 | } else { 28 | boolean deferredStatus = deferredResult.setResult(result.getBody()); 29 | log.logEndNonBlocking(result.getStatusCode().value(), deferredStatus); 30 | } 31 | 32 | } 33 | 34 | @Override 35 | public void onFailure(Throwable t) { 36 | 37 | // TODO: Handle asynchronous processing errors... 38 | 39 | if (deferredResult.isSetOrExpired()) { 40 | log.logExceptionNonBlocking(t); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/router/nonblocking/spring/RouterNonBlockingSpringController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.router.nonblocking.spring; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.util.concurrent.ListenableFuture; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.client.AsyncRestTemplate; 11 | import org.springframework.web.context.request.async.DeferredResult; 12 | import se.callista.springmvc.asynch.common.log.LogHelper; 13 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 14 | 15 | import javax.annotation.PostConstruct; 16 | import java.io.IOException; 17 | 18 | @RestController 19 | public class RouterNonBlockingSpringController { 20 | 21 | private LogHelper LOG; 22 | 23 | private AsyncRestTemplate asyncRestTemplate = new AsyncRestTemplate(); 24 | 25 | @Autowired 26 | private LogHelperFactory logFactory; 27 | 28 | @Value("${sp.non_blocking.url}") 29 | private String SP_NON_BLOCKING_URL; 30 | 31 | @PostConstruct 32 | public void initAfterInject() { 33 | LOG = logFactory.getLog(se.callista.springmvc.asynch.pattern.router.nonblocking.callback.RouterNonBlockingCallbackController.class, "router"); 34 | } 35 | 36 | /** 37 | * Sample usage: curl "http://localhost:9080/router-non-blocking-spring?minMs=1000&maxMs=2000" 38 | * 39 | * The spring version of asynch http client has two major drawbacks 40 | * 1. It doesn't work with the code below, no call is made to the SP (probably my fault :-) 41 | * 2. The call is not executed non-blocking but instead in a separate thread, i.e. it doesn't scale very good... 42 | * 43 | * Due to the scalability issue it is not used but left as documentation on how it can be used given it is change under the hood to being non-blocking 44 | * 45 | * @param minMs y3 46 | * @param maxMs y3 47 | * @return y3 48 | * @throws java.io.IOException 49 | */ 50 | @RequestMapping("/router-non-blocking-spring") 51 | public DeferredResult nonBlockingRouter_Spring( 52 | @RequestParam(value = "minMs", required = false, defaultValue = "0") int minMs, 53 | @RequestParam(value = "maxMs", required = false, defaultValue = "0") int maxMs) throws IOException { 54 | 55 | LOG.logStartNonBlocking(); 56 | 57 | DeferredResult deferredResult = new DeferredResult(); 58 | 59 | String url = SP_NON_BLOCKING_URL + "?minMs={minMs}&maxMs={maxMs}"; 60 | ListenableFuture> futureEntity = asyncRestTemplate.getForEntity(url, String.class, minMs, maxMs); 61 | 62 | // Register a callback for the completion of the asynchronous rest call 63 | futureEntity.addCallback(new RouterCallback_Spring_AsyncRestTemplate(LOG, deferredResult)); 64 | 65 | LOG.logLeaveThreadNonBlocking(); 66 | 67 | // Return to let go of the precious thread we are holding on to... 68 | return deferredResult; 69 | } 70 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/blocking/plain/RoutingSlipBlockingPlainController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.blocking.plain; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import org.springframework.web.client.RestTemplate; 10 | import se.callista.springmvc.asynch.common.log.LogHelper; 11 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 12 | 13 | import javax.annotation.PostConstruct; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | @RestController 18 | public class RoutingSlipBlockingPlainController { 19 | 20 | private LogHelper LOG; 21 | 22 | private RestTemplate restTemplate = new RestTemplate(); 23 | 24 | @Autowired 25 | private LogHelperFactory logFactory; 26 | 27 | @Value("${sp.non_blocking.url}") 28 | private String SP_NON_BLOCKING_URL; 29 | 30 | List resultList = new ArrayList<>(); 31 | 32 | @PostConstruct 33 | public void initAfterInject() { 34 | LOG = logFactory.getLog(RoutingSlipBlockingPlainController.class, "routing-slip"); 35 | } 36 | 37 | /** 38 | * Sample usage: curl "http://localhost:9080/routing-slip-blocking-plain" 39 | * 40 | * @return 41 | */ 42 | @RequestMapping("/routing-slip-blocking-plain") 43 | public String blockingRouter() { 44 | 45 | HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; 46 | 47 | LOG.logStartBlocking(); 48 | 49 | // Send request #1 and process its result 50 | String result = executeProcessingStep(1); 51 | processResult(result); 52 | 53 | // Send request #2 and process its result 54 | result = executeProcessingStep(2); 55 | processResult(result); 56 | 57 | // Send request #3 and process its result 58 | result = executeProcessingStep(3); 59 | processResult(result); 60 | 61 | // Send request #4 and process its result 62 | result = executeProcessingStep(4); 63 | processResult(result); 64 | 65 | // Send request #5 and process its result 66 | result = executeProcessingStep(5); 67 | processResult(result); 68 | 69 | LOG.logEndBlocking(HttpStatus.OK.value()); 70 | 71 | return getTotalResult(); 72 | } 73 | 74 | private String executeProcessingStep(int processingStepNo) { 75 | LOG.logStartProcessingStepNonBlocking(processingStepNo); 76 | ResponseEntity result = restTemplate.getForEntity(getUrl(processingStepNo), String.class); 77 | LOG.logEndProcessingStepNonBlocking(processingStepNo, result.getStatusCode().value()); 78 | return result.getBody(); 79 | } 80 | 81 | private String getUrl(int processingStepNo) { 82 | int sleeptimeMs = 100 * processingStepNo; 83 | return SP_NON_BLOCKING_URL + "?minMs=" + sleeptimeMs + "&maxMs=" + sleeptimeMs; 84 | } 85 | 86 | private void processResult(String result) { 87 | resultList.add(result); 88 | } 89 | 90 | private String getTotalResult() { 91 | String totalResult = ""; 92 | for (String r : resultList) 93 | totalResult += r + '\n'; 94 | return totalResult; 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/blocking/statemachine/RoutingSlipBlockingConfiguration.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.blocking.statemachine; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Component; 5 | import se.callista.springmvc.asynch.common.statemachine.Processor; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Iterator; 9 | import java.util.List; 10 | 11 | /** 12 | * Created by magnus on 08/06/14. 13 | */ 14 | @Component 15 | public class RoutingSlipBlockingConfiguration { 16 | 17 | @Autowired 18 | private SynchProcessor synchProcessor; 19 | 20 | /** 21 | * Simulates setting up a number of processing steps from some kind of configuration... 22 | * 23 | * @return 24 | */ 25 | public Iterator getProcessingSteps() { 26 | 27 | List processingSteps = new ArrayList(); 28 | for (int i = 0; i < 5; i++) { 29 | processingSteps.add(synchProcessor); 30 | } 31 | return processingSteps.iterator(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/blocking/statemachine/RoutingSlipBlockingStateMachineController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.blocking.statemachine; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | import se.callista.springmvc.asynch.common.deferredresult.DeferredResultStateMachineCallback; 8 | import se.callista.springmvc.asynch.common.deferredresult.DeferredResultWithBlockingWait; 9 | import se.callista.springmvc.asynch.common.log.LogHelper; 10 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 11 | import se.callista.springmvc.asynch.common.statemachine.StateMachine; 12 | 13 | import javax.annotation.PostConstruct; 14 | 15 | @RestController 16 | public class RoutingSlipBlockingStateMachineController { 17 | 18 | private LogHelper LOG; 19 | 20 | @Autowired 21 | private RoutingSlipBlockingConfiguration configuration; 22 | 23 | @Autowired 24 | private StateMachine stateMachine; 25 | 26 | @Autowired 27 | private LogHelperFactory logFactory; 28 | 29 | @PostConstruct 30 | public void initAfterInject() { 31 | LOG = logFactory.getLog(RoutingSlipBlockingStateMachineController.class, "routing-slip"); 32 | } 33 | 34 | /** 35 | * Sample usage: curl "http://localhost:9080/routing-slip-blocking-state-machine" 36 | * 37 | * @return 38 | */ 39 | @RequestMapping("/routing-slip-blocking-state-machine") 40 | public String blockingRoutingSlip() { 41 | 42 | HttpStatus status = HttpStatus.OK; 43 | LOG.logStartBlocking(); 44 | 45 | try { 46 | // Create a deferred result that we can wait for in blocking mode 47 | final DeferredResultWithBlockingWait deferredResult = new DeferredResultWithBlockingWait<>(); 48 | 49 | // Kick off the processing 50 | stateMachine.initProcessing(configuration.getProcessingSteps(), LOG, new DeferredResultStateMachineCallback(deferredResult)); 51 | 52 | // Wait (blocking) for its completion 53 | deferredResult.await(); 54 | 55 | // Return the result 56 | return deferredResult.getResult().toString(); 57 | 58 | } finally { 59 | LOG.logEndBlocking(status.value()); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/blocking/statemachine/SynchProcessor.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.blocking.statemachine; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.client.RestTemplate; 11 | import se.callista.springmvc.asynch.common.statemachine.Processor; 12 | import se.callista.springmvc.asynch.common.statemachine.State; 13 | import se.callista.springmvc.asynch.common.statemachine.StateMachine; 14 | 15 | /** 16 | * Created by magnus on 15/05/14. 17 | */ 18 | @Component 19 | public class SynchProcessor implements Processor { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(SynchProcessor.class); 22 | 23 | @Autowired 24 | private StateMachine stateMachine; 25 | 26 | private RestTemplate restTemplate = new RestTemplate(); 27 | 28 | @Value("${sp.non_blocking.url}") 29 | private String SP_NON_BLOCKING_URL; 30 | 31 | @Override 32 | public void process(State state) { 33 | 34 | int sleeptimeMs = 100 * (state.getProcessingStepNo()); 35 | String url = SP_NON_BLOCKING_URL + "?minMs=" + sleeptimeMs + "&maxMs=" + sleeptimeMs; 36 | 37 | LOG.debug("Launch synch call"); 38 | 39 | ResponseEntity result = restTemplate.getForEntity( 40 | SP_NON_BLOCKING_URL + "?minMs={minMs}&maxMs={maxMs}", String.class, sleeptimeMs, sleeptimeMs); 41 | 42 | // TODO: Handle status codes other than 200... 43 | HttpStatus status = result.getStatusCode(); 44 | 45 | LOG.debug("Synch call complete, hand over to the state machine for the next action"); 46 | state.appendResult(result.getBody() + '\n'); 47 | stateMachine.executeNextStep(state); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/nonblocking/lambda/RoutingSlipNonBlockingLambdaController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.nonblocking.lambda; 2 | 3 | import com.ning.http.client.ListenableFuture; 4 | import com.ning.http.client.Response; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import org.springframework.web.context.request.async.DeferredResult; 10 | import se.callista.springmvc.asynch.common.lambdasupport.AsyncHttpClientLambdaAware; 11 | import se.callista.springmvc.asynch.common.log.LogHelper; 12 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 13 | 14 | import javax.annotation.PostConstruct; 15 | import java.io.IOException; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | @RestController 20 | public class RoutingSlipNonBlockingLambdaController { 21 | 22 | private class Result { 23 | List resultList = new ArrayList<>(); 24 | 25 | public void processResult(String result) { 26 | resultList.add(result); 27 | } 28 | 29 | public String getTotalResult() { 30 | String totalResult = ""; 31 | for (String r : resultList) 32 | totalResult += r + '\n'; 33 | return totalResult; 34 | } 35 | } 36 | 37 | private LogHelper LOG; 38 | 39 | private static final AsyncHttpClientLambdaAware asyncHttpClient = new AsyncHttpClientLambdaAware(); 40 | 41 | @Autowired 42 | private LogHelperFactory logFactory; 43 | 44 | @Value("${sp.non_blocking.url}") 45 | private String SP_NON_BLOCKING_URL; 46 | 47 | @PostConstruct 48 | public void initAfterInject() { 49 | LOG = logFactory.getLog(se.callista.springmvc.asynch.pattern.routingslip.nonblocking.lambda.RoutingSlipNonBlockingLambdaController.class, "routing-slip"); 50 | } 51 | 52 | /** 53 | * Sample usage: curl "http://localhost:9181/routing-slip-non-blocking-lambds" 54 | * 55 | * @return 56 | */ 57 | @RequestMapping("/routing-slip-non-blocking-lambda") 58 | public DeferredResult nonBlockingRoutingSlip() throws IOException { 59 | 60 | LOG.logStartNonBlocking(); 61 | 62 | DeferredResult deferredResult = new DeferredResult<>(); 63 | Result r = new Result(); 64 | 65 | // Kick off the asynch processing of five sequentially executed asynch processing steps 66 | 67 | // Send request #1 68 | LOG.logStartProcessingStepNonBlocking(1); 69 | ListenableFuture execute = asyncHttpClient.execute(getUrl(1), 70 | (Response r1) -> { 71 | LOG.logEndProcessingStepNonBlocking(1, r1.getStatusCode()); 72 | 73 | // Process response #1 74 | r.processResult(r1.getResponseBody()); 75 | 76 | // Send request #2 77 | LOG.logStartProcessingStepNonBlocking(2); 78 | asyncHttpClient.execute(getUrl(2), 79 | (Response r2) -> { 80 | LOG.logEndProcessingStepNonBlocking(2, r2.getStatusCode()); 81 | 82 | // Process response #2 83 | r.processResult(r2.getResponseBody()); 84 | 85 | // Send request #3 86 | LOG.logStartProcessingStepNonBlocking(3); 87 | asyncHttpClient.execute(getUrl(3), 88 | (Response r3) -> { 89 | LOG.logEndProcessingStepNonBlocking(3, r3.getStatusCode()); 90 | 91 | // Process response #3 92 | r.processResult(r3.getResponseBody()); 93 | 94 | // Send request #4 95 | LOG.logStartProcessingStepNonBlocking(4); 96 | asyncHttpClient.execute(getUrl(4), 97 | (Response r4) -> { 98 | LOG.logEndProcessingStepNonBlocking(4, r4.getStatusCode()); 99 | 100 | // Process response #4 101 | r.processResult(r4.getResponseBody()); 102 | 103 | // Send request #5 104 | LOG.logStartProcessingStepNonBlocking(5); 105 | asyncHttpClient.execute(getUrl(5), 106 | (Response r5) -> { 107 | LOG.logEndProcessingStepNonBlocking(5, r5.getStatusCode()); 108 | 109 | // Process response #5 110 | r.processResult(r5.getResponseBody()); 111 | 112 | // Get the total result and set it on the deferred result 113 | boolean deferredStatus = deferredResult.setResult(r.getTotalResult()); 114 | LOG.logEndNonBlocking(r5.getStatusCode(), deferredStatus); 115 | 116 | return r5; 117 | }); 118 | return r4; 119 | }); 120 | return r3; 121 | }); 122 | return r2; 123 | }); 124 | return r1; 125 | }); 126 | 127 | LOG.logLeaveThreadNonBlocking(); 128 | 129 | // Return to let go of the precious thread we are holding on to... 130 | return deferredResult; 131 | } 132 | 133 | private String getUrl(int processingStepNo) { 134 | int sleeptimeMs = 100 * processingStepNo; 135 | return SP_NON_BLOCKING_URL + "?minMs=" + sleeptimeMs + "&maxMs=" + sleeptimeMs; 136 | } 137 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/nonblocking/statemachine/AsynchProcessor.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.nonblocking.statemachine; 2 | 3 | import com.ning.http.client.AsyncHttpClient; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Component; 9 | import se.callista.springmvc.asynch.common.statemachine.Processor; 10 | import se.callista.springmvc.asynch.common.statemachine.State; 11 | import se.callista.springmvc.asynch.common.statemachine.StateMachine; 12 | 13 | import java.io.IOException; 14 | 15 | /** 16 | * Created by magnus on 15/05/14. 17 | */ 18 | @Component 19 | public class AsynchProcessor implements Processor { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(AsynchProcessor.class); 22 | 23 | @Autowired 24 | private StateMachine stateMachine; 25 | 26 | private static final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(); 27 | 28 | @Value("${sp.non_blocking.url}") 29 | private String SP_NON_BLOCKING_URL; 30 | 31 | @Override 32 | public void process(State state) { 33 | 34 | try { 35 | int sleeptimeMs = 100 * (state.getProcessingStepNo()); 36 | String url = SP_NON_BLOCKING_URL + "?minMs=" + sleeptimeMs + "&maxMs=" + sleeptimeMs; 37 | 38 | LOG.debug("Launch asynch call"); 39 | asyncHttpClient.prepareGet(url).execute(new AsynchProcessorCallback(stateMachine, state)); 40 | 41 | } catch (IOException e) { 42 | throw new RuntimeException(e); 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/nonblocking/statemachine/AsynchProcessorCallback.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.nonblocking.statemachine; 2 | 3 | import com.ning.http.client.AsyncCompletionHandler; 4 | import com.ning.http.client.Response; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import se.callista.springmvc.asynch.common.statemachine.State; 8 | import se.callista.springmvc.asynch.common.statemachine.StateMachine; 9 | 10 | public class AsynchProcessorCallback extends AsyncCompletionHandler { 11 | 12 | private static final Logger LOG = LoggerFactory.getLogger(AsynchProcessorCallback.class); 13 | 14 | private final StateMachine stateMachine; 15 | private final State state; 16 | 17 | // private long reqId; 18 | // private AtomicLong concurrentRequests; 19 | // private DeferredResult deferredResult; 20 | 21 | public AsynchProcessorCallback(StateMachine stateMachine, State state) { 22 | this.stateMachine = stateMachine; 23 | this.state = state; 24 | } 25 | 26 | @Override 27 | public Response onCompleted(Response response) throws Exception{ 28 | 29 | // TODO: Handle status codes other than 200... 30 | // int httpStatus = response.getStatusCode(); 31 | 32 | state.getLog().logAsynchProcessingStepComplete(); 33 | state.appendResult(response.getResponseBody() + '\n'); 34 | stateMachine.executeNextStep(state); 35 | 36 | return response; 37 | } 38 | 39 | @Override 40 | public void onThrowable(Throwable t){ 41 | 42 | // TODO: Handle asynchronous processing errors... 43 | state.getLog().logExceptionNonBlocking(t); 44 | } 45 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/nonblocking/statemachine/RoutingSlipNonBlockingConfiguration.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.nonblocking.statemachine; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Component; 5 | import se.callista.springmvc.asynch.common.statemachine.Processor; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Iterator; 9 | import java.util.List; 10 | 11 | /** 12 | * Created by magnus on 08/06/14. 13 | */ 14 | @Component 15 | public class RoutingSlipNonBlockingConfiguration { 16 | 17 | @Autowired 18 | private AsynchProcessor asynchProcessor; 19 | 20 | /** 21 | * Simulates setting up a number of processing steps from some kind of configuration... 22 | * 23 | * @return 24 | */ 25 | public Iterator getProcessingSteps() { 26 | 27 | List processingSteps = new ArrayList(); 28 | for (int i = 0; i < 5; i++) { 29 | processingSteps.add(asynchProcessor); 30 | } 31 | return processingSteps.iterator(); 32 | } 33 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/java/se/callista/springmvc/asynch/pattern/routingslip/nonblocking/statemachine/RoutingSlipNonBlockingStateMachineController.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip.nonblocking.statemachine; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | import org.springframework.web.context.request.async.DeferredResult; 7 | import se.callista.springmvc.asynch.common.deferredresult.DeferredResultStateMachineCallback; 8 | import se.callista.springmvc.asynch.common.log.LogHelper; 9 | import se.callista.springmvc.asynch.common.log.LogHelperFactory; 10 | import se.callista.springmvc.asynch.common.statemachine.Processor; 11 | import se.callista.springmvc.asynch.common.statemachine.StateMachine; 12 | 13 | import javax.annotation.PostConstruct; 14 | import java.io.IOException; 15 | import java.util.Iterator; 16 | 17 | @RestController 18 | public class RoutingSlipNonBlockingStateMachineController { 19 | 20 | private LogHelper LOG; 21 | 22 | @Autowired 23 | private RoutingSlipNonBlockingConfiguration configuration; 24 | 25 | @Autowired 26 | private StateMachine stateMachine; 27 | 28 | @Autowired 29 | private LogHelperFactory logFactory; 30 | 31 | @PostConstruct 32 | public void initAfterInject() { 33 | LOG = logFactory.getLog(RoutingSlipNonBlockingStateMachineController.class, "routing-slip"); 34 | } 35 | 36 | /** 37 | * Sample usage: curl "http://localhost:9080/routing-slip-non-blocking-state-machine" 38 | * 39 | * @return 40 | */ 41 | @RequestMapping("/routing-slip-non-blocking-state-machine") 42 | public DeferredResult nonBlockingRoutingSlip() throws IOException { 43 | 44 | LOG.logStartNonBlocking(); 45 | 46 | // Create a deferred result 47 | final DeferredResult deferredResult = new DeferredResult<>(); 48 | 49 | // Kick off the asynch processing of a number of sequentially executed asynch processing steps 50 | Iterator processingSteps = configuration.getProcessingSteps(); 51 | 52 | stateMachine.initProcessing(processingSteps, LOG, new DeferredResultStateMachineCallback(deferredResult)); 53 | 54 | LOG.logLeaveThreadNonBlocking(); 55 | 56 | // Return to let go of the precious thread we are holding on to... 57 | return deferredResult; 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=9080 2 | 3 | 4 | #THE DEFAULT VALUE FOT THREAD POOL SIZE IN TOMCAT 5 | servlet.container.maxThreads=200 6 | #servlet.container.maxThreads=50 FOR NON-BLOCKING TESTS 7 | #servlet.container.maxThreads=500 FOR BLOCKING TESTS 8 | 9 | threadPool.db.init_size=5 10 | threadPool.db.max_size=50 11 | threadPool.db.queue_size=1000000 12 | 13 | #sp.blocking.url=http://localhost:9090/process-blocking 14 | #sp.non_blocking.url=http://localhost:${server.port}/process-non-blocking 15 | sp.server.port=9090 16 | sp.non_blocking.url=http://localhost:${sp.server.port}/process-non-blocking 17 | 18 | aggregator.timeoutMs=3000 19 | 20 | statistics.requestsPerLog=5000 21 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | System.out 13 | 14 | %d{yyyy-MM-dd HH:mm:ss:SSS} %-5p %t %c{1}:%L - %m%n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/test/java/se/callista/springmvc/asynch/common/AsynchTestBase.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import se.callista.springmvc.asynch.common.embeddedhttpserver.EmbeddedHttpServerTestBase; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | import java.net.URISyntaxException; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | import static javax.servlet.http.HttpServletResponse.SC_OK; 15 | import static org.apache.commons.io.IOUtils.write; 16 | 17 | /** 18 | * Created by magnus on 22/07/14. 19 | */ 20 | abstract public class AsynchTestBase extends EmbeddedHttpServerTestBase { 21 | 22 | private static final Logger LOG = LoggerFactory.getLogger(AsynchTestBase.class); 23 | 24 | protected void createResponse(HttpServletRequest request, String requestBody, HttpServletResponse response) throws IOException, URISyntaxException { 25 | 26 | Map parMap = new HashMap<>(); 27 | parMap.put("minMs", "0"); 28 | parMap.put("maxMs", "0"); 29 | getQueryParameters(request, parMap); 30 | 31 | int minMs = Integer.parseInt(parMap.get("minMs")); 32 | int maxMs = Integer.parseInt(parMap.get("maxMs")); 33 | int processingTimeMs = calculateProcessingTime(minMs, maxMs); 34 | 35 | LOG.debug("Start blocking request, processing time: {} ms (" + minMs + " - " + maxMs + ").", processingTimeMs); 36 | try { 37 | Thread.sleep(processingTimeMs); 38 | } 39 | catch (InterruptedException e) {} 40 | 41 | String responseBody = "{\"status\":\"Ok\",\"processingTimeMs\":" + processingTimeMs + "}"; 42 | response.setStatus(SC_OK); 43 | response.setContentType("text/plain;charset=ISO-8859-1"); 44 | write(responseBody, response.getOutputStream()); 45 | } 46 | 47 | private int calculateProcessingTime(int minMs, int maxMs) { 48 | if (maxMs < minMs) maxMs = minMs; 49 | int processingTimeMs = minMs + (int) (Math.random() * (maxMs - minMs)); 50 | return processingTimeMs; 51 | } 52 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/test/java/se/callista/springmvc/asynch/common/ProcessingStatus.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common; 2 | 3 | import javax.xml.bind.annotation.XmlElement; 4 | import javax.xml.bind.annotation.XmlRootElement; 5 | 6 | @XmlRootElement 7 | public class ProcessingStatus { 8 | 9 | @XmlElement 10 | private final String status; 11 | 12 | @XmlElement 13 | private final int processingTimeMs; 14 | 15 | public ProcessingStatus() { 16 | status = "UNKNOWN"; 17 | processingTimeMs = -1; 18 | } 19 | 20 | public ProcessingStatus(String status, int processingTimeMs) { 21 | this.status = status; 22 | this.processingTimeMs = processingTimeMs; 23 | } 24 | 25 | public String getStatus() { 26 | return status; 27 | } 28 | 29 | public int getProcessingTimeMs() { 30 | return processingTimeMs; 31 | } 32 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/test/java/se/callista/springmvc/asynch/common/embeddedhttpserver/EmbeddedHttpServer.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.embeddedhttpserver; 2 | 3 | import org.eclipse.jetty.server.Request; 4 | import org.eclipse.jetty.server.Server; 5 | import org.eclipse.jetty.server.handler.AbstractHandler; 6 | 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Created by magnus on 22/07/14. 14 | */ 15 | public class EmbeddedHttpServer { 16 | 17 | private int port; 18 | private Server server; 19 | private RequestHandler requestHandler; 20 | 21 | public EmbeddedHttpServer(int port, RequestHandler requestHandler) { 22 | this.port = port; 23 | this.requestHandler = requestHandler; 24 | } 25 | 26 | public void start() throws Exception { 27 | configureServer(); 28 | server.start(); 29 | } 30 | 31 | public void stop() throws Exception { 32 | server.stop(); 33 | } 34 | 35 | protected void configureServer() { 36 | server = new Server(port); 37 | server.setHandler(new AbstractHandler() { 38 | @Override 39 | public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { 40 | try { 41 | requestHandler.handle(target, baseRequest, request, response); 42 | } catch (ServletException e) { 43 | throw e; 44 | } catch (IOException e) { 45 | throw e; 46 | } catch (Exception e) { 47 | throw new RuntimeException(e); 48 | } 49 | } 50 | }); 51 | /* 52 | server.setHandler(new AbstractHandler() { 53 | public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException, ServletException { 54 | Request baseRequest = request instanceof Request ? (Request) request : HttpConnection.getCurrentConnection().getRequest(); 55 | setResponseBody(getMockResponseData()); 56 | setRequestBody(IOUtils.toString(baseRequest.getInputStream())); 57 | response.setStatus(SC_OK); 58 | response.setContentType("text/xml;charset=utf-8"); 59 | write(getResponseBody(), response.getOutputStream()); 60 | baseRequest.setHandled(true); 61 | } 62 | }); 63 | */ 64 | } 65 | 66 | protected Server getServer() { 67 | return server; 68 | } 69 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/test/java/se/callista/springmvc/asynch/common/embeddedhttpserver/EmbeddedHttpServerTestBase.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.embeddedhttpserver; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.apache.http.NameValuePair; 5 | import org.apache.http.client.utils.URLEncodedUtils; 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Value; 11 | 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | import java.net.URI; 16 | import java.net.URISyntaxException; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | /** 21 | * Created by magnus on 22/07/14. 22 | */ 23 | abstract public class EmbeddedHttpServerTestBase { 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(EmbeddedHttpServerTestBase.class); 26 | 27 | private EmbeddedHttpServer server; 28 | 29 | @Value("${sp.server.port}") 30 | private int port; 31 | 32 | @Before 33 | public void startTestServer() throws Exception { 34 | LOG.debug("Start embedded Jetty-server on port: " + port); 35 | server = new EmbeddedHttpServer(port, (target, baseRequest, request, response) -> { 36 | String requestBody = IOUtils.toString(baseRequest.getInputStream()); 37 | createResponse(request, requestBody, response); 38 | baseRequest.setHandled(true); 39 | }); 40 | server.start(); 41 | } 42 | 43 | @After 44 | public void stopTestServer() throws Exception { 45 | server.stop(); 46 | } 47 | 48 | abstract protected void createResponse(HttpServletRequest request, String requestBody, HttpServletResponse response) throws IOException, URISyntaxException; 49 | 50 | protected void getQueryParameters(HttpServletRequest request, Map parMap) throws URISyntaxException { 51 | List params = URLEncodedUtils.parse(new URI(request.getRequestURL().toString() + "?" + request.getQueryString()), "UTF-8"); 52 | 53 | for (NameValuePair param : params) { 54 | parMap.put(param.getName(), param.getValue()); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/test/java/se/callista/springmvc/asynch/common/embeddedhttpserver/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.common.embeddedhttpserver; 2 | 3 | import org.eclipse.jetty.server.Request; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | 10 | /** 11 | * Created by magnus on 22/07/14. 12 | */ 13 | public interface RequestHandler { 14 | 15 | /** Handle a request. 16 | * @param target The target of the request - either a URI or a name. 17 | * @param baseRequest The original unwrapped request object. 18 | * @param request The request either as the {@link Request} 19 | * object or a wrapper of that request. The {@link org.eclipse.jetty.server.AbstractHttpConnection#getCurrentConnection()} 20 | * method can be used access the Request object if required. 21 | * @param response The response as the {@link org.eclipse.jetty.server.Response} 22 | * object or a wrapper of that request. The {@link org.eclipse.jetty.server.AbstractHttpConnection#getCurrentConnection()} 23 | * method can be used access the Response object if required. 24 | * @throws IOException 25 | * @throws ServletException 26 | */ 27 | public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws Exception; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /spring-mvc-asynch/src/test/java/se/callista/springmvc/asynch/pattern/aggregator/AggregatorControllerTest.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.aggregator; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.MockitoAnnotations; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.test.SpringApplicationConfiguration; 11 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 12 | import org.springframework.test.context.web.WebAppConfiguration; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | import org.springframework.test.web.servlet.MvcResult; 15 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 16 | import org.springframework.web.context.WebApplicationContext; 17 | import se.callista.springmvc.asynch.Application; 18 | import se.callista.springmvc.asynch.common.AsynchTestBase; 19 | import se.callista.springmvc.asynch.common.ProcessingStatus; 20 | 21 | import static org.junit.Assert.assertTrue; 22 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; 23 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 25 | 26 | /** 27 | * Created by magnus on 29/05/14. 28 | */ 29 | @RunWith(SpringJUnit4ClassRunner.class) 30 | @SpringApplicationConfiguration(classes = Application.class) 31 | @WebAppConfiguration 32 | public class AggregatorControllerTest extends AsynchTestBase { 33 | 34 | private MockMvc mockMvc; 35 | 36 | @Autowired 37 | WebApplicationContext wac; 38 | 39 | @Value("${aggregator.timeoutMs}") 40 | private int TIMEOUT_MS; 41 | 42 | private final String expectedResultSingle = "{\"status\":\"Ok\",\"processingTimeMs\":2000}"; 43 | private final String expectedResult = 44 | expectedResultSingle + '\n' + 45 | expectedResultSingle + '\n' + 46 | expectedResultSingle + '\n'; 47 | 48 | @Before 49 | public void setup(){ 50 | 51 | // Process mock annotations 52 | MockitoAnnotations.initMocks(this); 53 | 54 | // Setup Spring test in webapp-mode (same config as spring-boot) 55 | this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); 56 | 57 | } 58 | 59 | @Test 60 | public void testAggregatorBlocking() throws Exception{ 61 | this.mockMvc.perform(get("/aggregate-blocking?minMs=2000&maxMs=2000")) 62 | .andExpect(status().isOk()) 63 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 64 | .andExpect(content().string(expectedResult)); 65 | } 66 | 67 | @Test 68 | public void testAggregatorNonBlockingCallback() throws Exception { 69 | 70 | MvcResult mvcResult = this.mockMvc.perform(get("/aggregate-non-blocking-callback?minMs=2000&maxMs=2000")) 71 | .andExpect(request().asyncStarted()) 72 | .andReturn(); 73 | 74 | mvcResult.getAsyncResult(); 75 | 76 | this.mockMvc.perform(asyncDispatch(mvcResult)) 77 | .andExpect(status().isOk()) 78 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 79 | .andExpect(content().string(expectedResult)); 80 | } 81 | 82 | @Test 83 | public void testAggregatorNonBlockingCallbackTimeout() throws Exception { 84 | 85 | int minMs = (TIMEOUT_MS < 1000) ? 0 : TIMEOUT_MS - 1000; 86 | int maxMs = TIMEOUT_MS + 1000; 87 | int dbHits = 10; 88 | 89 | MvcResult mvcResult = this.mockMvc.perform(get("/aggregate-non-blocking-callback?dbHits=" + dbHits + "&minMs=" + minMs + "&maxMs=" + maxMs)) 90 | .andExpect(request().asyncStarted()) 91 | .andReturn(); 92 | 93 | mvcResult.getAsyncResult(); 94 | 95 | this.mockMvc.perform(asyncDispatch(mvcResult)) 96 | .andExpect(status().isOk()) 97 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")); 98 | 99 | String result = mvcResult.getAsyncResult().toString(); 100 | 101 | System.err.println("JSON: " + result); 102 | String[] psArr = result.split("\n"); 103 | 104 | // Verify that we got some timeouts 105 | assertTrue("Expected at least one timeout to occur", psArr.length < dbHits); 106 | 107 | System.err.println("assert that no response time was over the timeout: " + TIMEOUT_MS); 108 | ObjectMapper mapper = new ObjectMapper(); 109 | for (int i = 0; i < psArr.length; i++) { 110 | ProcessingStatus ps = mapper.readValue(psArr[i], ProcessingStatus.class); 111 | System.err.println("psArr: " + ps.getStatus() + " - " + ps.getProcessingTimeMs()); 112 | assertTrue(ps.getProcessingTimeMs() < TIMEOUT_MS); 113 | } 114 | } 115 | 116 | @Test 117 | public void testAggregatorNonBlockingLambda() throws Exception { 118 | 119 | MvcResult mvcResult = this.mockMvc.perform(get("/aggregate-non-blocking-lambda?minMs=2000&maxMs=2000")) 120 | .andExpect(request().asyncStarted()) 121 | .andReturn(); 122 | 123 | mvcResult.getAsyncResult(); 124 | 125 | this.mockMvc.perform(asyncDispatch(mvcResult)) 126 | .andExpect(status().isOk()) 127 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 128 | .andExpect(content().string(expectedResult)); 129 | } 130 | 131 | @Test 132 | public void testAggregatorNonBlockingLambdaTimeout() throws Exception { 133 | 134 | int minMs = (TIMEOUT_MS < 1000) ? 0 : TIMEOUT_MS - 1000; 135 | int maxMs = TIMEOUT_MS + 1000; 136 | int dbHits = 10; 137 | 138 | MvcResult mvcResult = this.mockMvc.perform(get("/aggregate-non-blocking-lambda?dbHits=" + dbHits + "&minMs=" + minMs + "&maxMs=" + maxMs)) 139 | .andExpect(request().asyncStarted()) 140 | .andReturn(); 141 | 142 | mvcResult.getAsyncResult(); 143 | 144 | this.mockMvc.perform(asyncDispatch(mvcResult)) 145 | .andExpect(status().isOk()) 146 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")); 147 | 148 | String result = mvcResult.getAsyncResult().toString(); 149 | 150 | System.err.println("JSON: " + result); 151 | String[] psArr = result.split("\n"); 152 | 153 | // Verify that we got some timeouts 154 | assertTrue("Expected at least one timeout to occur", psArr.length < dbHits); 155 | 156 | System.err.println("assert that no response time was over the timeout: " + TIMEOUT_MS); 157 | ObjectMapper mapper = new ObjectMapper(); 158 | for (int i = 0; i < psArr.length; i++) { 159 | ProcessingStatus ps = mapper.readValue(psArr[i], ProcessingStatus.class); 160 | System.err.println("psArr: " + ps.getStatus() + " - " + ps.getProcessingTimeMs()); 161 | assertTrue(ps.getProcessingTimeMs() < TIMEOUT_MS); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/test/java/se/callista/springmvc/asynch/pattern/router/RouterControllerTest.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.router; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.MockitoAnnotations; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.SpringApplicationConfiguration; 9 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 10 | import org.springframework.test.context.web.WebAppConfiguration; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | import org.springframework.test.web.servlet.MvcResult; 13 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 14 | import org.springframework.web.context.WebApplicationContext; 15 | import se.callista.springmvc.asynch.Application; 16 | import se.callista.springmvc.asynch.common.AsynchTestBase; 17 | 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 21 | 22 | 23 | /** 24 | * Created by magnus on 29/05/14. 25 | */ 26 | @RunWith(SpringJUnit4ClassRunner.class) 27 | @SpringApplicationConfiguration(classes = Application.class) 28 | @WebAppConfiguration 29 | public class RouterControllerTest extends AsynchTestBase { 30 | 31 | private MockMvc mockMvc; 32 | 33 | @Autowired 34 | WebApplicationContext wac; 35 | 36 | private final String expectedResult = "{\"status\":\"Ok\",\"processingTimeMs\":2000}"; 37 | 38 | @Before 39 | public void setup(){ 40 | 41 | // Process mock annotations 42 | MockitoAnnotations.initMocks(this); 43 | 44 | // Setup Spring test in webapp-mode (same config as spring-boot) 45 | this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); 46 | } 47 | 48 | @Test 49 | public void testRouterBlocking() throws Exception{ 50 | String url = "/router-blocking?minMs=2000&maxMs=2000"; 51 | this.mockMvc.perform(get(url)) 52 | .andExpect(status().isOk()) 53 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 54 | .andExpect(content().string(expectedResult)); 55 | } 56 | 57 | @Test 58 | public void testRouterNonBlockingCallback() throws Exception { 59 | 60 | String url = "/router-non-blocking-callback?minMs=2000&maxMs=2000"; 61 | testNonBlocking(url); 62 | } 63 | 64 | @Test 65 | public void testRouterNonBlockingAnonymous() throws Exception { 66 | 67 | String url = "/router-non-blocking-anonymous?minMs=2000&maxMs=2000"; 68 | testNonBlocking(url); 69 | } 70 | 71 | @Test 72 | public void testRouterNonBlockingLambda() throws Exception { 73 | 74 | String url = "/router-non-blocking-lambda?minMs=2000&maxMs=2000"; 75 | testNonBlocking(url); 76 | } 77 | 78 | @Test 79 | public void testRouterNonBlockingSpring() throws Exception { 80 | 81 | // NOTE: Today's implementation in Spring of the AsyncRestTemplate is thread-blocking and therefore doesn't scale 82 | String url = "/router-non-blocking-anonymous?minMs=2000&maxMs=2000"; 83 | testNonBlocking(url); 84 | } 85 | 86 | private void testNonBlocking(String url) throws Exception { 87 | MvcResult mvcResult = this.mockMvc.perform(get(url)) 88 | .andExpect(request().asyncStarted()) 89 | .andReturn(); 90 | 91 | mvcResult.getAsyncResult(); 92 | 93 | this.mockMvc.perform(asyncDispatch(mvcResult)) 94 | .andExpect(status().isOk()) 95 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 96 | .andExpect(content().string(expectedResult)); 97 | } 98 | 99 | 100 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/src/test/java/se/callista/springmvc/asynch/pattern/routingslip/RoutingSlipControllerTest.java: -------------------------------------------------------------------------------- 1 | package se.callista.springmvc.asynch.pattern.routingslip; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.MockitoAnnotations; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.SpringApplicationConfiguration; 9 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 10 | import org.springframework.test.context.web.WebAppConfiguration; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | import org.springframework.test.web.servlet.MvcResult; 13 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 14 | import org.springframework.web.context.WebApplicationContext; 15 | import se.callista.springmvc.asynch.Application; 16 | import se.callista.springmvc.asynch.common.AsynchTestBase; 17 | 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 21 | 22 | 23 | /** 24 | * Created by magnus on 29/05/14. 25 | */ 26 | @RunWith(SpringJUnit4ClassRunner.class) 27 | @SpringApplicationConfiguration(classes = Application.class) 28 | @WebAppConfiguration 29 | public class RoutingSlipControllerTest extends AsynchTestBase { 30 | 31 | private MockMvc mockMvc; 32 | 33 | @Autowired 34 | WebApplicationContext wac; 35 | 36 | private final String expectedResultSingle = "{\"status\":\"Ok\",\"processingTimeMs\":2000}"; 37 | private final String expectedResult = 38 | "{\"status\":\"Ok\",\"processingTimeMs\":100}\n" + 39 | "{\"status\":\"Ok\",\"processingTimeMs\":200}\n" + 40 | "{\"status\":\"Ok\",\"processingTimeMs\":300}\n" + 41 | "{\"status\":\"Ok\",\"processingTimeMs\":400}\n" + 42 | "{\"status\":\"Ok\",\"processingTimeMs\":500}\n"; 43 | 44 | @Before 45 | public void setup(){ 46 | 47 | // Process mock annotations 48 | MockitoAnnotations.initMocks(this); 49 | 50 | // Setup Spring test in webapp-mode (same config as spring-boot) 51 | this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); 52 | } 53 | 54 | @Test 55 | public void testRoutingSlipBlockingStateMachine() throws Exception{ 56 | this.mockMvc.perform(get("/routing-slip-blocking-state-machine")) 57 | .andExpect(status().isOk()) 58 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 59 | .andExpect(content().string(expectedResult)); 60 | } 61 | 62 | @Test 63 | public void testRoutingSlipBlockingPlain() throws Exception{ 64 | this.mockMvc.perform(get("/routing-slip-blocking-plain")) 65 | .andExpect(status().isOk()) 66 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 67 | .andExpect(content().string(expectedResult)); 68 | } 69 | 70 | @Test 71 | public void testRoutingSlipNonBlockingStateMachine() throws Exception { 72 | 73 | MvcResult mvcResult = this.mockMvc.perform(get("/routing-slip-non-blocking-state-machine")) 74 | .andExpect(request().asyncStarted()) 75 | // .andExpect(request().asyncResult(expectedResult)) 76 | .andReturn(); 77 | 78 | mvcResult.getAsyncResult(); 79 | 80 | this.mockMvc.perform(asyncDispatch(mvcResult)) 81 | .andExpect(status().isOk()) 82 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 83 | .andExpect(content().string(expectedResult)); 84 | } 85 | 86 | @Test 87 | public void testRoutingSlipNonBlockingLambda() throws Exception { 88 | 89 | MvcResult mvcResult = this.mockMvc.perform(get("/routing-slip-non-blocking-lambda")) 90 | .andExpect(request().asyncStarted()) 91 | // .andExpect(request().asyncResult(expectedResult)) 92 | .andReturn(); 93 | 94 | mvcResult.getAsyncResult(); 95 | 96 | this.mockMvc.perform(asyncDispatch(mvcResult)) 97 | .andExpect(status().isOk()) 98 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 99 | .andExpect(content().string(expectedResult)); 100 | } 101 | 102 | /** 103 | * Same test as testRoutingSlipNonBlockingLambda that ensures that no state is kept between calls to the routing-slip-non-blocking-lambda service 104 | * 105 | * @throws Exception 106 | */ 107 | @Test 108 | public void testRoutingSlipNonBlockingLambda2() throws Exception { 109 | 110 | MvcResult mvcResult = this.mockMvc.perform(get("/routing-slip-non-blocking-lambda")) 111 | .andExpect(request().asyncStarted()) 112 | // .andExpect(request().asyncResult(expectedResult)) 113 | .andReturn(); 114 | 115 | mvcResult.getAsyncResult(); 116 | 117 | this.mockMvc.perform(asyncDispatch(mvcResult)) 118 | .andExpect(status().isOk()) 119 | .andExpect(content().contentType("text/plain;charset=ISO-8859-1")) 120 | .andExpect(content().string(expectedResult)); 121 | } 122 | } -------------------------------------------------------------------------------- /spring-mvc-asynch/todo.txt: -------------------------------------------------------------------------------- 1 | -> routing slip, två impl till: En med cascading annonyma interface impl (+ ev Javav 8 Labmdas) + en med en massa callbacks + en för sekventiellt... 2 | 3 | - testa av java 8 stöd i IntelliJ, skal man köra gradle idea tasket eller räcker det at importera projektetn igen? 4 | 5 | - autostart av teststubbe --------------------------------------------------------------------------------