├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.adoc ├── benchmarks.adoc ├── flux.adoc ├── flux ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── DemoApplication.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── com │ └── example │ ├── DemoApplicationTests.java │ ├── FluxFeaturesTests.java │ ├── MonoFeaturesTests.java │ ├── ScatterGatherTests.java │ └── StreamFeaturesTests.java ├── gather ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ ├── DemoApplication.java │ │ │ ├── NettyClientController.java │ │ │ └── RestTemplateStoreGatherer.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── com │ └── example │ └── DemoApplicationTests.java ├── intro.adoc ├── io.adoc ├── mvnw ├── mvnw.cmd ├── pom.xml └── reactive ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── example │ │ └── ReactiveApplication.java └── resources │ └── application.properties └── test └── java └── com └── example └── DemoApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | .#* 2 | *# 3 | *~ 4 | target/ 5 | build/ 6 | .springBeans 7 | .settings 8 | .classpath 9 | .project 10 | .gradle 11 | *.iml 12 | *.ipr 13 | *.iws 14 | .idea 15 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/reactive-notes/5b574e7a378c048ac906a31ddfa7edd848b843fd/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Notes on Reactive Programming 2 | 3 | include::intro.adoc[leveloffset=+1] 4 | 5 | == Basics of Reactive Programming in Java 6 | 7 | include::flux.adoc[leveloffset=+1] 8 | 9 | == Concurrent Programming with Spring Reactive 10 | 11 | include::io.adoc[leveloffset=+1] 12 | 13 | == Appendix: Server Platform Comparison 14 | 15 | include::benchmarks.adoc[leveloffset=+1] 16 | 17 | TODO: more chat to tidy up 18 | 19 | ``` 20 | such a solution will only get you so far 21 | 22 | not to mention that it is still byte[]-based, and does not expose ByteBuffers in any way 23 | 24 | to be honest, there is no way to answer your question yet, as we haven’t done any serious performance testing. But we ​*expect*​ Netty to be a better fit, since its threading model is more close aligned to a reactive world. Servlet containers still use a one-thread-per-request model, even if you are using the async support 25 | 26 | so threading is reason one, exposing byte buffers is reason two for preferring netty 27 | 28 | David Syer [11:03 AM] 29 | But buffers are just more efficient? 30 | 31 | Because of heap memory? 32 | 33 | Arjen Poutsma [11:03 AM] 34 | yes 35 | 36 | and non-blockingness of the java.nio API 37 | ``` 38 | 39 | == The Role of Back Pressure 40 | 41 | * `subscribeOn()` allows a `Flux` to relay a slow publisher 42 | 43 | * `publishOn()` allows a `Flux` to have slow subscribers 44 | 45 | TODO: tidy this up... 46 | 47 | since callback is called for each subscriber you can use operators that share a context 48 | 49 | 50 | ``` 51 | Mono.defer( () -> { 52 | AtomicInteger d = new AtomicNumber(); 53 | return Mono.just(1) 54 | .map(d::incrementAndGet) 55 | .doOnError(d::decrementAndGet); 56 | }) 57 | ``` 58 | 59 | `d` here is scoped for a given `Subscriber` that can be useful 60 | 61 | == Don't Be the Bottleneck 62 | 63 | Microbenchmarks always show that, as long as no-one blocks a thread, data flows faster if we match the number of threads to the number of cores (possibly with an extra core dispatching thread). That's why the thread pools created by Reactor by default limit themselves to the number of available processors, and it's why Reactor is frugal with threads, never jumping a thread boundary unless explicitly instructed. 64 | 65 | Do you need to know all this stuff? Possibly not. You can be the bottleneck, if you are confident that you are no worse than your downstream dependencies. You might as well stick with plain old imperative programming and synchronous request-response. Use a few background threads to grease your way a bit. The result is easier to understand and easier to operationalize. 66 | 67 | == Links 68 | 69 | http://blog.paralleluniverse.co/2014/02/20/reactive/[Parallel universe blog on Reactive] 70 | 71 | http://www.javaworld.com/article/2071370/core-java/of-fibers-and-continuations.html[Of Fibers and Continuations | JavaWorld] 72 | 73 | Readable (but unfortunately database-biased) article: 74 | http://news.dice.com/2014/01/13/how-is-reactive-different-from-procedural-programming/[How is Reactive Programming Different?] 75 | 76 | Propaganda: 77 | http://www.reactivemanifesto.org/[The Reactive Manifesto] 78 | 79 | http://www.reactive-streams.org/[Reactive Streams] 80 | 81 | Blog from RxJava lead http://akarnokd.blogspot.co.uk/[David Karnok] including the one on http://akarnokd.blogspot.co.uk/2016/03/operator-fusion-part-1.html[Generations of Reactive] 82 | 83 | https://github.com/reactor/reactive-streams-commons/issues/21[Reactive Gems] github issue with documentation of lots of use cases 84 | 85 | https://en.wikipedia.org/wiki/Continuation(which you can think of as state)[Continuation - Wikipedia] 86 | 87 | A multilingual framework (including Java): 88 | https://github.com/kentuckyfriedtakahe/sodium[Sodium - GitHub] 89 | 90 | http://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html[Fork-join docs at Oracle] 91 | 92 | Sebastien Deleuze's https://spring.io/blog/2016/04/19/understanding-reactive-types[Reactive Types] Blog 93 | 94 | https://lmax-exchange.github.io/disruptor/[LMAX Disruptor] 95 | 96 | https://robharrop.github.io/[Rob Harrop's Blog] -------------------------------------------------------------------------------- /benchmarks.adoc: -------------------------------------------------------------------------------- 1 | = Basic Benchmarks 2 | 3 | Here are some not very scientific, basic benchmarks done over a local 4 | network with a commodity WiFi router. The server is running on a 4 5 | core i7 with Ubuntu 14.04 and OpenJDK 1.8 (it's mostly I/O bound 6 | though, so hopefully the bulk of the latency is actually caused by 7 | backend calls to teh internet). The load test client is on the same 8 | type of hardware. 9 | 10 | == Blocking Client 11 | 12 | With Spring MVC 13 | 14 | ```java 15 | @RequestMapping("/parallel") 16 | public CompletableFuture parallel() { 17 | return Flux.range(1, 10) // <1> 18 | .log() // 19 | .flatMap( // <2> 20 | value -> Mono.fromCallable(() -> block(value)) // <3> 21 | .subscribeOn(this.scheduler), // <4> 22 | 4) // <5> 23 | .collect(Result::new, Result::add) // <6> 24 | .doOnSuccess(Result::stop ) // <7> 25 | .toFuture(); 26 | } 27 | ``` 28 | <1> make 10 calls 29 | <2> drop down to a new publisher to process in parallel 30 | <3> blocking code here inside a Callable to defer execution 31 | <4> subscribe to the slow publisher on a background thread 32 | <5> concurrency hint in flatMap 33 | <6> collect results and aggregate into a single object 34 | <7> at the end stop the clock 35 | 36 | ``` 37 | $ ab -c 4 -n 600 http://server:8080/parallel 38 | ``` 39 | 40 | Tomcat 8.0.33 and Spring MVC 41 | 42 | ``` 43 | Requests per second: 3.67 [#/sec] (mean) 44 | Time per request: 1090.976 [ms] (mean) 45 | Time per request: 272.744 [ms] (mean, across all concurrent requests) 46 | Transfer rate: 0.68 [Kbytes/sec] received 47 | ... 48 | Percentage of the requests served within a certain time (ms) 49 | 50% 777 50 | 66% 797 51 | 75% 816 52 | 80% 832 53 | 90% 1013 54 | 95% 1262 55 | 98% 3109 56 | 99% 15798 57 | 100% 30223 (longest request) 58 | ``` 59 | 60 | == Blocking Client with No Parallel Processing 61 | 62 | ```java 63 | @RequestMapping("/serial") 64 | public CompletableFuture serial() { 65 | return Flux.range(1, 10) // <1> 66 | .log() // 67 | .map( // <2> 68 | value -> block(value)) // <3> 69 | .collect(Result::new, Result::add) // <4> 70 | .subscribeOn(this.scheduler) // <6> 71 | .doOnSuccess(Result::stop) // <5> 72 | .toFuture(); 73 | } 74 | ``` 75 | <1> make 10 calls 76 | <2> stay in the same publisher chain 77 | <3> blocking call not deferred (no point in this case) 78 | <4> collect results and aggregate into a single object 79 | <5> at the end stop the clock 80 | <6> subscribe on a background thread 81 | 82 | Result: 83 | 84 | ``` 85 | Requests per second: 1.61 [#/sec] (mean) 86 | Time per request: 2487.547 [ms] (mean) 87 | Time per request: 621.887 [ms] (mean, across all concurrent requests) 88 | Transfer rate: 0.29 [Kbytes/sec] received 89 | ... 90 | Percentage of the requests served within a certain time (ms) 91 | 50% 2365 92 | 66% 2429 93 | 75% 2521 94 | 80% 2564 95 | 90% 2708 96 | 95% 3243 97 | 98% 3692 98 | 99% 3920 99 | 100% 4490 (longest request) 100 | ``` 101 | 102 | == Spring Reactive Blocking Client 103 | 104 | ```java 105 | @RequestMapping("/parallel") 106 | public Mono parallel() { 107 | return Flux.range(1, 10) // <1> 108 | .log() // 109 | .flatMap( // <2> 110 | value -> Mono.fromCallable(() -> block(value)) // <3> 111 | .subscribeOn(scheduler), // <4> 112 | 4) // <5> 113 | .collect(Result::new, Result::add) // <6> 114 | .doOnSuccess(Result::stop); // <7> 115 | 116 | } 117 | ``` 118 | <1> make 10 calls 119 | <2> drop down to a new publisher to process in parallel 120 | <3> blocking code here inside a Callable to defer execution 121 | <4> subscribe to the slow publisher on a background thread 122 | <5> concurrency hint in flatMap 123 | <6> collect results and aggregate into a single object 124 | <7> at the end stop the clock 125 | 126 | 127 | Tomcat 8.0.33: 128 | 129 | ``` 130 | Percentage of the requests served within a certain time (ms) 131 | 50% 424 132 | 66% 491 133 | 75% 508 134 | 80% 520 135 | 90% 552 136 | 95% 611 137 | 98% 651 138 | 99% 701 139 | 100% 1135 (longest request) 140 | ``` 141 | 142 | Tomcat 8.5.2: 143 | 144 | ``` 145 | Percentage of the requests served within a certain time (ms) 146 | 50% 415 147 | 66% 493 148 | 75% 509 149 | 80% 519 150 | 90% 583 151 | 95% 682 152 | 98% 843 153 | 99% 973 154 | 100% 1058 (longest request) 155 | ``` 156 | 157 | Reactor IO (Netty) server: 158 | 159 | ``` 160 | Requests per second: 7.68 [#/sec] (mean) 161 | Time per request: 520.663 [ms] (mean) 162 | Time per request: 130.166 [ms] (mean, across all concurrent requests) 163 | Transfer rate: 1.04 [Kbytes/sec] received 164 | ... 165 | Percentage of the requests served within a certain time (ms) 166 | 50% 475 167 | 66% 525 168 | 75% 573 169 | 80% 601 170 | 90% 687 171 | 95% 858 172 | 98% 1236 173 | 99% 1296 174 | 100% 1439 (longest request) 175 | ``` 176 | 177 | == Spring Reactive Non-Blocking Client 178 | 179 | ```java 180 | @RequestMapping("/netty") 181 | public Mono netty() { 182 | return Flux.range(1, 10) // <1> 183 | .log() // 184 | .flatMap(this::fetch) // <2> 185 | .collect(Result::new, Result::add) 186 | .doOnSuccess(Result::stop); // <3> 187 | } 188 | ``` 189 | <1> make 10 calls 190 | <2> drop down to a new publisher to process in parallel 191 | <3> at the end stop the clock 192 | 193 | Reactor IO (Netty) server: 194 | 195 | ``` 196 | Percentage of the requests served within a certain time (ms) 197 | 50% 552 198 | 66% 576 199 | 75% 593 200 | 80% 601 201 | 90% 625 202 | 95% 658 203 | 98% 700 204 | 99% 728 205 | 100% 764 (longest request) 206 | ``` 207 | 208 | Tomcat 8.0.33 209 | 210 | ``` 211 | Requests per second: 9.68 [#/sec] (mean) 212 | Time per request: 413.040 [ms] (mean) 213 | Time per request: 103.260 [ms] (mean, across all concurrent requests) 214 | Transfer rate: 1.92 [Kbytes/sec] received 215 | ... 216 | Percentage of the requests served within a certain time (ms) 217 | 50% 287 218 | 66% 302 219 | 75% 317 220 | 80% 334 221 | 90% 839 222 | 95% 1313 223 | 98% 1620 224 | 99% 1855 225 | 100% 2017 (longest request) 226 | ``` 227 | 228 | == Spring MVC Non-Blocking Client 229 | 230 | ```java 231 | @RequestMapping("/netty") 232 | public CompletableFuture gather() { 233 | return Flux.range(1, 10) // <1> 234 | .log() // 235 | .flatMap(value -> fetch(value)) // <2> 236 | .collect(Result::new, Result::add) // 237 | .doOnSuccess(Result::stop) // 238 | .toFuture(); 239 | } 240 | ``` 241 | <1> Make 10 calls 242 | <2> Drop down to a new publisher, but this time non-blocking 243 | 244 | Result: 245 | 246 | ``` 247 | Requests per second: 4.75 [#/sec] (mean) 248 | Time per request: 842.030 [ms] (mean) 249 | Time per request: 210.508 [ms] (mean, across all concurrent requests) 250 | Transfer rate: 1.07 [Kbytes/sec] received 251 | ... 252 | Percentage of the requests served within a certain time (ms) 253 | 50% 335 254 | 66% 469 255 | 75% 669 256 | 80% 1339 257 | 90% 1632 258 | 95% 1837 259 | 98% 3494 260 | 99% 7468 261 | 100% 30098 (longest request) 262 | ``` -------------------------------------------------------------------------------- /flux.adoc: -------------------------------------------------------------------------------- 1 | :github: https://github.com/dsyer/reactive-notes 2 | :master: {github}/blob/master 3 | :parti: https://spring.io/blog/2016/06/07/notes-on-reactive-programming-part-i-the-reactive-landscape 4 | :partiii: https://spring.io/blog/2016/07/20/notes-on-reactive-programming-part-iii-a-simple-http-server-application 5 | 6 | In this article we continue the series on {parti}[Reactive Programming], and we concentrate on explaining some concepts through actual code samples. The end result should be that you understand a bit better what makes Reactive different, and what makes it functional. The examples here are quite abstract, but they give you a way to think about the APIs and the programming style, and start to get a feel for how it is different. We will see the elements of Reactive, and learn how to control the flow of data, and process in background threads if necessary. 7 | 8 | == Setting Up a Project 9 | 10 | We will use the Reactor libraries to illustrate the points we need to make. The code could just as easily be written with other tools. If you want to play with the code and see it working without having to copy-paste anything, there are working samples with tests in {github}[Github]. 11 | 12 | To get started grab a blank project from https://start.spring.io and add the Reactor Core dependency. With Maven 13 | 14 | ```xml 15 | 16 | io.projectreactor 17 | reactor-core 18 | 3.0.2.RELEASE 19 | 20 | ``` 21 | 22 | With Gradle it's very similar: 23 | 24 | ```java 25 | compile 'io.projectreactor:reactor-core:3.0.2.RELEASE' 26 | ``` 27 | 28 | Now let's write some code. 29 | 30 | == What Makes it Functional? 31 | 32 | The basic building block of Reactive is a sequence of events, and two protagonists, a publisher and a subscriber to those events. It's also OK to call a sequence a "stream" because that's what it is. If we need to, we will use the word "stream" with a small "s", but Java 8 has a `java.util.Stream` which is different, so try not to get confused. We will try to concentrate the narrative on the publisher and subscriber anyway (that's what Reactive Streams does). 33 | 34 | Reactor is the library we are going to use in samples, so we'll stick to the notation there, and call the publisher a `Flux` (it implements the interface `Publisher` from Reactive Streams). The RxJava library is very similar and has a lot of parallel features, so in that case we would be talking about an `Observable` instead, but the code would be very similar. (Reactor 2.0 called it a `Stream` which is confusing if we need to talk about Java 8 `Streams` as well, so we'll only use the new code in Reactor 2.5.) 35 | 36 | === Generators 37 | 38 | A `Flux` is a publisher of a sequence of events of a specific POJO type, so it is generic, i.e. `Flux` is a publisher of `T`. `Flux` has some static convenience methods to create instances of itself from a variety of sources. For example, to create a `Flux` from an array: 39 | 40 | ```java 41 | Flux flux = Flux.just("red", "white", "blue"); 42 | ``` 43 | 44 | We just generated a `Flux`, and now we can do stuff with it. There are actually only two things you can do with it: operate on it (transform it, or combine it with other sequences), subscribe to it (it's a publisher). 45 | 46 | === Single Valued Sequences 47 | 48 | Often you encounter a sequence that you know has only one or zero elements, for example a repository method that finds an entity by its id. Reactor has a `Mono` type representing a single valued or empty `Flux`. `Mono` has a very similar API to `Flux` but more focused because not all operators make sense for single-valued sequences. RxJava also has a bolt on (in version 1.x) called `Single`, and also `Completable` for an empty sequence. The empty sequence in Reactor is `Mono`. 49 | 50 | === Operators 51 | 52 | There are a _lot_ of methods on a `Flux` and nearly all of them are operators. We aren't going to look at them all here because there are better places to look for that (like the Javadocs). We only need to get a flavour for what an operator is, and what it can do for you. 53 | 54 | For instance, to ask for the internal events inside a `Flux` to be logged to standard out, you can call the `.log()` method. Or you can transform it using a `map()`: 55 | 56 | ```java 57 | Flux flux = Flux.just("red", "white", "blue"); 58 | 59 | Flux upper = flux 60 | .log() 61 | .map(String::toUpperCase); 62 | ``` 63 | 64 | In this code we transformed the strings in the input by converting them to upper case. So far, so trivial. 65 | 66 | What's interesting about this little sample -- mind blowing, even, if you're not used to it -- is that no data have been processed yet. Nothing has even been logged because literally, nothing happened (try it and you will see). Calling operators on a `Flux` amounts to building a plan of execution for later. It is completely declarative, and it's why people call it "functional". The logic implemented in the operators is only executed when data starts to flow, and that doesn't happen until someone subscribes to the `Flux` (or equivalently to the `Publisher`). 67 | 68 | The same declarative, functional approach to processing a sequence of data exists in all Reactive libraries, and also in Java 8 `Streams`. Consider this, similar looking code, using a `Stream` with the same contents as the `Flux`: 69 | 70 | ```java 71 | Stream stream = Stream.of("red", "white", "blue"); 72 | Stream upper = stream.map(value -> { 73 | System.out.println(value); 74 | return value.toUpperCase(); 75 | }); 76 | ``` 77 | 78 | The observation we made about `Flux` applies here: no data is processed, it's just a plan of execution. There are, however, some important differences between `Flux` and `Stream`, which make `Stream` an inappropriate API for Reactive use cases. `Flux` has a lot more operators, much of which is just convenience, but the real difference comes when you want to consume the data, so that's what we need to look at next. 79 | 80 | TIP: There is a useful blog by Sebastien Deleuze on https://spring.io/blog/2016/04/19/understanding-reactive-types[Reactive Types], where he describes the differences between the various streaming and reactive APIs by looking at the types they define, and how you would use them. The differences between `Flux` and `Stream` are highlighted there in more detail. 81 | 82 | === Subscribers 83 | 84 | To make the data flow you have to subscribe to the `Flux` using one of the `subscribe()` methods. Only those methods make the data flow. They reach back through the chain of operators you declared on your sequence (if any) and request the publisher to start creating data. In the sample samples we have been working with, this means the underlying collection of strings is iterated. In more complicated use case it might trigger a file to be read from the filesystem, or a pull from a database or a call to an HTTP service. 85 | 86 | Here's a call to `subscribe()` in action: 87 | 88 | ```java 89 | Flux.just("red", "white", "blue") 90 | .log() 91 | .map(String::toUpperCase) 92 | .subscribe(); 93 | ``` 94 | 95 | The output is: 96 | 97 | ``` 98 | 09:17:59.665 [main] INFO reactor.core.publisher.FluxLog - onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@3ffc5af1) 99 | 09:17:59.666 [main] INFO reactor.core.publisher.FluxLog - request(unbounded) 100 | 09:17:59.666 [main] INFO reactor.core.publisher.FluxLog - onNext(red) 101 | 09:17:59.667 [main] INFO reactor.core.publisher.FluxLog - onNext(white) 102 | 09:17:59.667 [main] INFO reactor.core.publisher.FluxLog - onNext(blue) 103 | 09:17:59.667 [main] INFO reactor.core.publisher.FluxLog - onComplete() 104 | ``` 105 | 106 | So we can see from this that the effect of `subscribe()` without an argument, is to request the publisher to send _all_ data -- there's only one `request()` logged and it's "unbounded". We can also see callbacks for each item that is published (`onNext()`), for the end of the sequence (`onComplete()`), and for the original subscription (`onSubscribe()`). If you needed to you could listen for those events yourself using the `doOn*()` methods in `Flux`, which are themselves operators, not subscribers, so they don't cause any data to flow on their own. 107 | 108 | The `subscribe()` method is overloaded, and the other variants give you different options to control what happens. One important and convenient form is `subscribe()` with callbacks as arguments. The first argument is a `Consumer`, which gives you a callback with each of the items, and you can also optionally add a `Consumer` for an error if there is one, and a vanilla `Runnable` to execute when the sequence is complete. For example, just with the per-item callback: 109 | 110 | ```java 111 | Flux.just("red", "white", "blue") 112 | .log() 113 | .map(String::toUpperCase) 114 | .subscribe(System.out::println); 115 | ``` 116 | 117 | Here's the output: 118 | 119 | ``` 120 | 09:56:12.680 [main] INFO reactor.core.publisher.FluxLog - onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@59f99ea) 121 | 09:56:12.682 [main] INFO reactor.core.publisher.FluxLog - request(unbounded) 122 | 09:56:12.682 [main] INFO reactor.core.publisher.FluxLog - onNext(red) 123 | RED 124 | 09:56:12.682 [main] INFO reactor.core.publisher.FluxLog - onNext(white) 125 | WHITE 126 | 09:56:12.682 [main] INFO reactor.core.publisher.FluxLog - onNext(blue) 127 | BLUE 128 | 09:56:12.682 [main] INFO reactor.core.publisher.FluxLog - onComplete() 129 | ``` 130 | 131 | We could control the flow of data, and make it "bounded", in a variety of ways. The raw API for control is the `Subscription` you get from a `Subscriber`. The equivalent long form of the short call to `subscribe()` above is: 132 | 133 | ```java 134 | .subscribe(new Subscriber() { 135 | 136 | @Override 137 | public void onSubscribe(Subscription s) { 138 | s.request(Long.MAX_VALUE); 139 | } 140 | @Override 141 | public void onNext(String t) { 142 | System.out.println(t); 143 | } 144 | @Override 145 | public void onError(Throwable t) { 146 | } 147 | @Override 148 | public void onComplete() { 149 | } 150 | 151 | }); 152 | ``` 153 | 154 | To control the flow, e.g. to consume at most 2 items at a time, you could use the `Subscription` more intelligently: 155 | 156 | ```java 157 | .subscribe(new Subscriber() { 158 | 159 | private long count = 0; 160 | private Subscription subscription; 161 | 162 | @Override 163 | public void onSubscribe(Subscription subscription) { 164 | this.subscription = subscription; 165 | subscription.request(2); 166 | } 167 | 168 | @Override 169 | public void onNext(String t) { 170 | count++; 171 | if (count>=2) { 172 | count = 0; 173 | subscription.request(2); 174 | } 175 | } 176 | ... 177 | ``` 178 | 179 | This `Subscriber` is "batching" items 2 at a time. It's a common use case so you might think about extracting the implementation to a convenience class, and that would make the code more readable too. The output looks like this: 180 | 181 | ``` 182 | 09:47:13.562 [main] INFO reactor.core.publisher.FluxLog - onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@61832929) 183 | 09:47:13.564 [main] INFO reactor.core.publisher.FluxLog - request(2) 184 | 09:47:13.564 [main] INFO reactor.core.publisher.FluxLog - onNext(red) 185 | 09:47:13.565 [main] INFO reactor.core.publisher.FluxLog - onNext(white) 186 | 09:47:13.565 [main] INFO reactor.core.publisher.FluxLog - request(2) 187 | 09:47:13.565 [main] INFO reactor.core.publisher.FluxLog - onNext(blue) 188 | 09:47:13.565 [main] INFO reactor.core.publisher.FluxLog - onComplete() 189 | ``` 190 | 191 | In fact the batching subscriber is such a common use case that there are convenience methods already available in `Flux`. The batching example above can be implemented like this: 192 | 193 | ```java 194 | Flux.just("red", "white", "blue") 195 | .log() 196 | .map(String::toUpperCase) 197 | .subscribe(2); 198 | ``` 199 | 200 | (note the call to `subscribe()` with a request limit). Here's the output: 201 | 202 | ``` 203 | 10:25:43.739 [main] INFO reactor.core.publisher.FluxLog - onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@4667ae56) 204 | 10:25:43.740 [main] INFO reactor.core.publisher.FluxLog - request(2) 205 | 10:25:43.740 [main] INFO reactor.core.publisher.FluxLog - onNext(red) 206 | 10:25:43.741 [main] INFO reactor.core.publisher.FluxLog - onNext(white) 207 | 10:25:43.741 [main] INFO reactor.core.publisher.FluxLog - request(2) 208 | 10:25:43.741 [main] INFO reactor.core.publisher.FluxLog - onNext(blue) 209 | 10:25:43.741 [main] INFO reactor.core.publisher.FluxLog - onComplete() 210 | ``` 211 | 212 | TIP: A library that will process sequences for you, like Spring Reactive Web, can handle the subscriptions. It's good to be able to push these concerns down the stack because it saves you from cluttering your code with non-business logic, making it more readable and easier to test and maintain. So as a rule, it is a good thing if you can **avoid subscribing** to a sequence, or at least push that code into a processing layer, and out of the business logic. 213 | 214 | === Threads, Schedulers and Background Processing 215 | 216 | An interesting feature of all the logs above is that they are all on the "main" thread, which is the thread of the caller to `subscribe()`. This highlights an important point: Reactor is extremely frugal with threads, because that gives you the greatest chance of the best possible performance. That might be a surprising statement if you've been wrangling threads and thread pools and asynchronous executions for the last 5 years, trying to squeeze more juice out of your services. But it's true: in the absence of any imperative to switch threads, even if the JVM is optimized to handle threads very efficiently, it is always faster to do computation on a single thread. Reactor has handed you the keys to control all the asynchronous processing, and it assumes you know what you are doing. 217 | 218 | `Flux` provides a few configurer methods that control the thread boundaries. For example, you can configure the subscriptions to be handled in a background thread using `Flux.subscribeOn()`: 219 | 220 | ```java 221 | Flux.just("red", "white", "blue") 222 | .log() 223 | .map(String::toUpperCase) 224 | .subscribeOn(Schedulers.parallel()) 225 | .subscribe(2); 226 | ``` 227 | 228 | the result can be seen in the output: 229 | 230 | ``` 231 | 13:43:41.279 [parallel-1-1] INFO reactor.core.publisher.FluxLog - onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@58663fc3) 232 | 13:43:41.280 [parallel-1-1] INFO reactor.core.publisher.FluxLog - request(2) 233 | 13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog - onNext(red) 234 | 13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog - onNext(white) 235 | 13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog - request(2) 236 | 13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog - onNext(blue) 237 | 13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog - onComplete() 238 | ``` 239 | 240 | TIP: if you write this code yourself, or copy-paste it, remember to wait for the processing to stop before the JVM exits. 241 | 242 | Note that the subscription, and all the processing, takes place on a single background thread "parallel-1-1" -- this is because we asked for the subscriber to our main `Flux` to be in the background. This is fine if the item processing is CPU intensive (but pointless being in a background thread, in point of fact, since you pay for the context switch but don't get the results any faster). You might also want to be able to perform item processing that is I/O intensive and possibly blocking. In this case, you would want to get it done as quickly as possible without blocking the caller. A thread pool is still your friend, and that's what you get from `Schedulers.parallel()`. To switch the processing of the individual items to separate threads (up to the limit of the pool) we need to break them out into separate publishers, and for each of those publishers ask for the result in a background thread. One way to do this is with an operator called `flatMap()`, which maps the items to a `Publisher` (potentially of a different type), and then back to a sequence of the new type: 243 | 244 | ```java 245 | Flux.just("red", "white", "blue") 246 | .log() 247 | .flatMap(value -> 248 | Mono.just(value.toUpperCase()) 249 | .subscribeOn(Schedulers.parallel()), 250 | 2) 251 | .subscribe(value -> { 252 | log.info("Consumed: " + value); 253 | }) 254 | ``` 255 | 256 | Note here the use of `flatMap()` to push the items down into a "child" publisher, where we can control the subscription per item instead of for the whole sequence. Reactor has built in default behaviour to hang onto a single thread as long as possible, so we need to be explicit if we want it to process specific items or groups of items in a background thread. Actually, this is one of a handful of recognized tricks for forcing parallel processing (see the https://github.com/reactor/reactive-streams-commons/issues/21[Reactive Gems] issue for more detail). 257 | 258 | The output looks like this: 259 | 260 | ``` 261 | 15:24:36.596 [main] INFO reactor.core.publisher.FluxLog - onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@6f1fba17) 262 | 15:24:36.610 [main] INFO reactor.core.publisher.FluxLog - request(2) 263 | 15:24:36.610 [main] INFO reactor.core.publisher.FluxLog - onNext(red) 264 | 15:24:36.613 [main] INFO reactor.core.publisher.FluxLog - onNext(white) 265 | 15:24:36.613 [parallel-1-1] INFO com.example.FluxFeaturesTests - Consumed: RED 266 | 15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog - request(1) 267 | 15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog - onNext(blue) 268 | 15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog - onComplete() 269 | 15:24:36.614 [parallel-3-1] INFO com.example.FluxFeaturesTests - Consumed: BLUE 270 | 15:24:36.617 [parallel-2-1] INFO com.example.FluxFeaturesTests - Consumed: WHITE 271 | ``` 272 | 273 | Notice that there are now multiple threads consuming the items, and the concurrency hint in the `flatMap()` ensures that there are 2 items being processed at any given time, as long as they are available. We see `request(1)` a lot because the system is trying to keep 2 items in the pipeline, and generally they don't finish processing at the same time. Reactor tries to be very smart here in fact, and it pre-fetches items from the upstream `Publisher` to try to eliminate waiting time for the subscriber (we aren't seeing that here because the numbers are low -- we are only processing 3 items). 274 | 275 | TIP: Three items ("red", "white", "blue") might be too few to convincingly see more than one background thread, so it might be better to generate more data. You could do that with a random number generator, for instance. 276 | 277 | `Flux` also has a `publishOn()` method which is the same, but for the listeners (i.e. `onNext()` or consumer callbacks) instead of for the subscriber itself: 278 | 279 | ```java 280 | Flux.just("red", "white", "blue") 281 | .log() 282 | .map(String::toUpperCase) 283 | .subscribeOn(Schedulers.newParallel("sub")) 284 | .publishOn(Schedulers.newParallel("pub"), 2) 285 | .subscribe(value -> { 286 | log.info("Consumed: " + value); 287 | }); 288 | ``` 289 | 290 | The output looks like this: 291 | 292 | ``` 293 | 15:12:09.750 [sub-1-1] INFO reactor.core.publisher.FluxLog - onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@172ed57) 294 | 15:12:09.758 [sub-1-1] INFO reactor.core.publisher.FluxLog - request(2) 295 | 15:12:09.759 [sub-1-1] INFO reactor.core.publisher.FluxLog - onNext(red) 296 | 15:12:09.759 [sub-1-1] INFO reactor.core.publisher.FluxLog - onNext(white) 297 | 15:12:09.770 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: RED 298 | 15:12:09.771 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: WHITE 299 | 15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog - request(2) 300 | 15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog - onNext(blue) 301 | 15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog - onComplete() 302 | 15:12:09.783 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: BLUE 303 | ``` 304 | 305 | Notice that the consumer callbacks (logging "Consumed: ...") are on the publisher thread `pub-1-1`. If you take out the `subscribeOn()` call, you might see all of the 2nd chunk of data processed on the `pub-1-1` thread as well. This, again, is Reactor being frugal with threads -- if there's no explicit request to switch threads it stays on the same one for the next call, whatever that is. 306 | 307 | NOTE: We changed the code in this sample from `subscribe(2)` to adding a `prefetch=2` to the `publishOn()`. In this case the fetch size hint in `subscribe()` would have been ignored. 308 | 309 | === Extractors: The Subscribers from the Dark Side 310 | 311 | There is another way to subscribe to a sequence, which is to call `Mono.block()` or `Mono.toFuture()` or `Flux.toStream()` (these are the "extractor" methods -- they get you out of the Reactive types into a less flexible, blocking abstraction). `Flux` also has converters `collectList()` and `collectMap()` that convert from `Flux` to `Mono`. They don't actually subscribe to the sequence, but they do throw away any control you might have had over the suscription at the level of the individual items. 312 | 313 | WARNING: A good rule of thumb is "**never call an extractor**". There are some exceptions (otherwise the methods would not exist). One notable exception is in tests because it's useful to be able to block to allow results to accumulate. 314 | 315 | These methods are there as an escape hatch to bridge from Reactive to blocking; if you need to adapt to a legacy API, for instance Spring MVC. When you call `Mono.block()` you throw away all the benefits of the Reactive Streams. This is the key difference between Reactive Streams and Java 8 `Streams` -- the native Java `Stream` only has the "all or nothing" subscription model, the equivalent of `Mono.block()`. Of course `subscribe()` can block the calling thread as well, so it's just as dangerous as the converter methods, but you have more control -- you can prevent it from blocking by using `subscribeOn()` and you can drip the items through by applying back pressure and periodically deciding whether to continue. 316 | 317 | == Conclusion 318 | 319 | In this article we have covered the basics of the Reactive Streams and Reactor APIs. If you need to know more there are plenty of places to look, but there's no substitute for hands on coding, so use the code in {github}[GitHub] (for this article in tests in the project called "flux"), or head over to the https://github.com/reactor/lite-rx-api-hands-on[Lite RX Hands On] workshop. So far, really this is just overhead, and we haven't learned much that we couldn't have done in a more obvious way using non-Reactive tools. The {partiii}[next article] in the series will dig a little deeper into the blocking, dispatching and asynchronous sides of the Reactive model, and show you what opportunities there are to reap the real benefits. 320 | 321 | -------------------------------------------------------------------------------- /flux/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.3.5.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url "https://repo.spring.io/snapshot" } 8 | maven { url "https://repo.spring.io/milestone" } 9 | } 10 | dependencies { 11 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 12 | } 13 | } 14 | 15 | apply plugin: 'java' 16 | apply plugin: 'eclipse' 17 | apply plugin: 'spring-boot' 18 | 19 | jar { 20 | baseName = 'demo' 21 | version = '0.0.1-SNAPSHOT' 22 | } 23 | sourceCompatibility = 1.8 24 | targetCompatibility = 1.8 25 | 26 | repositories { 27 | mavenCentral() 28 | maven { url "https://repo.spring.io/snapshot" } 29 | maven { url "https://repo.spring.io/milestone" } 30 | } 31 | 32 | 33 | dependencies { 34 | compile('org.springframework.boot:spring-boot-starter') 35 | compile('io.projectreactor:reactor-core:2.5.0.M4') 36 | testCompile('org.springframework.boot:spring-boot-starter-test') 37 | } 38 | 39 | 40 | eclipse { 41 | classpath { 42 | containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') 43 | containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /flux/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/reactive-notes/5b574e7a378c048ac906a31ddfa7edd848b843fd/flux/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /flux/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip 6 | -------------------------------------------------------------------------------- /flux/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 | -------------------------------------------------------------------------------- /flux/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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows 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 | -------------------------------------------------------------------------------- /flux/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | flux 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | flux 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.4.1.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 3.0.2.RELEASE 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter 31 | 32 | 33 | io.projectreactor 34 | reactor-core 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | spring-snapshots 57 | Spring Snapshots 58 | https://repo.spring.io/snapshot 59 | 60 | true 61 | 62 | 63 | 64 | spring-milestones 65 | Spring Milestones 66 | https://repo.spring.io/milestone 67 | 68 | false 69 | 70 | 71 | 72 | 73 | 74 | spring-snapshots 75 | Spring Snapshots 76 | https://repo.spring.io/snapshot 77 | 78 | true 79 | 80 | 81 | 82 | spring-milestones 83 | Spring Milestones 84 | https://repo.spring.io/milestone 85 | 86 | false 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /flux/src/main/java/com/example/DemoApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | @SpringBootApplication 22 | public class DemoApplication { 23 | 24 | public static void main(String[] args) { 25 | SpringApplication.run(DemoApplication.class, args); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /flux/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/reactive-notes/5b574e7a378c048ac906a31ddfa7edd848b843fd/flux/src/main/resources/application.properties -------------------------------------------------------------------------------- /flux/src/test/java/com/example/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.springframework.boot.test.SpringApplicationConfiguration; 21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 22 | 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | @SpringApplicationConfiguration(classes = DemoApplication.class) 25 | public class DemoApplicationTests { 26 | 27 | @Test 28 | public void contextLoads() { 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /flux/src/test/java/com/example/FluxFeaturesTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | import org.reactivestreams.Subscriber; 24 | import org.reactivestreams.Subscription; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import reactor.core.publisher.Flux; 29 | import reactor.core.publisher.Mono; 30 | import reactor.core.scheduler.Scheduler; 31 | import reactor.core.scheduler.Schedulers; 32 | 33 | public class FluxFeaturesTests { 34 | 35 | private static Logger log = LoggerFactory.getLogger(FluxFeaturesTests.class); 36 | 37 | private static List COLORS = Arrays.asList("red", "white", "blue"); 38 | 39 | private Flux flux; 40 | 41 | @Before 42 | public void generate() throws Exception { 43 | // this.flux = Flux.range(1, 10).map(i -> COLORS.get(random.nextInt(3))); 44 | this.flux = Flux.fromIterable(COLORS); 45 | } 46 | 47 | @Test 48 | public void operate() throws Exception { 49 | this.flux.log().map(String::toUpperCase); 50 | // Nothing happened. No logs, nothing. 51 | } 52 | 53 | @Test 54 | public void subscribe() throws Exception { 55 | this.flux.log().map(String::toUpperCase).subscribe(); 56 | // Logs the subscription, an unbounded request, all elements and finally 57 | // completion. 58 | } 59 | 60 | @Test 61 | public void consume() throws Exception { 62 | this.flux.log().map(String::toUpperCase).subscribe(System.out::println); 63 | // Same as above but items are printed as they emerge from the end of 64 | // the operator 65 | // chain 66 | } 67 | 68 | @Test 69 | public void subscription() throws Exception { 70 | this.flux.log().map(String::toUpperCase).subscribe(new Subscriber() { 71 | 72 | private long count = 0; 73 | private Subscription subscription; 74 | 75 | @Override 76 | public void onSubscribe(Subscription subscription) { 77 | this.subscription = subscription; 78 | subscription.request(2); 79 | } 80 | 81 | @Override 82 | public void onNext(String t) { 83 | this.count++; 84 | if (this.count >= 2) { 85 | this.count = 0; 86 | this.subscription.request(2); 87 | } 88 | } 89 | 90 | @Override 91 | public void onError(Throwable t) { 92 | } 93 | 94 | @Override 95 | public void onComplete() { 96 | } 97 | }); 98 | // Logs the subscription, requests 2 at a time, all elements and 99 | // completion. 100 | } 101 | 102 | @Test 103 | public void batching() throws Exception { 104 | this.flux.log().map(String::toUpperCase).subscribe(2); 105 | // Logs the subscription, requests 2 at a time, all elements and finally 106 | // completion. 107 | } 108 | 109 | @Test 110 | public void parallel() throws Exception { 111 | this.flux.log().map(String::toUpperCase).subscribeOn(Schedulers.parallel()).subscribe(2); 112 | // Logs the subscription, requests 2 at a time, all elements and finally 113 | // completion. 114 | Thread.sleep(500L); 115 | } 116 | 117 | @Test 118 | public void concurrent() throws Exception { 119 | Scheduler scheduler = Schedulers.parallel(); 120 | this.flux.log().flatMap(value -> Mono.just(value.toUpperCase()).subscribeOn(scheduler), 2).subscribe(value -> { 121 | log.info("Consumed: " + value); 122 | }); 123 | // Logs the subscription, requests 2 at a time, all elements and finally 124 | // completion. 125 | Thread.sleep(500L); 126 | } 127 | 128 | @Test 129 | public void publish() throws Exception { 130 | this.flux.log().map(String::toUpperCase).subscribeOn(Schedulers.newParallel("sub")) 131 | .publishOn(Schedulers.newParallel("pub"), 2).subscribe(value -> { 132 | log.info("Consumed: " + value); 133 | }); 134 | // Logs the consumed messages in a separate thread. 135 | Thread.sleep(500L); 136 | } 137 | 138 | } -------------------------------------------------------------------------------- /flux/src/test/java/com/example/MonoFeaturesTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.concurrent.Callable; 19 | 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import reactor.core.Exceptions; 26 | import reactor.core.publisher.Mono; 27 | 28 | public class MonoFeaturesTests { 29 | private Mono mono; 30 | 31 | private static Logger log = LoggerFactory.getLogger(MonoFeaturesTests.class); 32 | 33 | @Before 34 | public void generate() throws Exception { 35 | this.mono = Mono.just("red"); 36 | } 37 | 38 | @Test 39 | public void operate() throws Exception { 40 | this.mono.log().map(String::toUpperCase); 41 | // Nothing happened. No logs, nothing. 42 | } 43 | 44 | @Test 45 | public void subscribe() throws Exception { 46 | this.mono.log().map(String::toUpperCase).subscribe(); 47 | // Logs the subscription, an unbounded request, all elements and finally 48 | // completion. 49 | } 50 | 51 | @Test 52 | public void gem22() throws Exception { 53 | Callable callable = () -> "foo"; 54 | Mono.just("irrelevant").log().map(unused -> { 55 | try { 56 | return callable.call(); 57 | } catch (Exception ex) { 58 | throw Exceptions.bubble(ex); // or Exceptions.propagate(ex) 59 | } 60 | }).subscribe(log::info, Throwable::printStackTrace); 61 | } 62 | 63 | @Test 64 | public void notGem22() throws Exception { 65 | Callable callable = () -> "foo"; 66 | Mono.fromCallable(callable).log().subscribe(log::info, Throwable::printStackTrace); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /flux/src/test/java/com/example/ScatterGatherTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Random; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | import java.util.concurrent.ConcurrentMap; 24 | import java.util.concurrent.atomic.AtomicLong; 25 | import java.util.function.Function; 26 | 27 | import org.junit.Test; 28 | import org.reactivestreams.Publisher; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | import reactor.core.publisher.Flux; 33 | import reactor.core.publisher.Mono; 34 | import reactor.core.scheduler.Scheduler; 35 | import reactor.core.scheduler.Schedulers; 36 | 37 | public class ScatterGatherTests { 38 | 39 | private static Logger log = LoggerFactory.getLogger(ScatterGatherTests.class); 40 | 41 | private static List COLORS = Arrays.asList("red", "white", "blue"); 42 | 43 | private Random random = new Random(); 44 | 45 | @Test 46 | public void subscribe() throws Exception { 47 | Scheduler scheduler = Schedulers.parallel(); 48 | System.err.println( // 49 | Flux.range(1, 10) // 50 | .map(i -> COLORS.get(this.random.nextInt(3))) // 51 | .log() // 52 | .flatMap(value -> Mono.fromCallable(() -> { 53 | Thread.sleep(1000L); 54 | return value; 55 | }).subscribeOn(scheduler), 4) // 56 | .collect(Result::new, Result::add) // 57 | .doOnNext(Result::stop) // 58 | .block() // 59 | ); 60 | } 61 | 62 | @Test 63 | public void subscribeWithBackgroundPublisherExtractedToMethod() throws Exception { 64 | System.err.println( // 65 | Flux.range(1, 10)// 66 | .map(i -> COLORS.get(this.random.nextInt(3))) // 67 | .log() // 68 | .flatMap(background(Schedulers.parallel()), 4) // 69 | .collect(Result::new, Result::add) // 70 | .doOnNext(Result::stop) // 71 | .block() // 72 | ); 73 | } 74 | 75 | @Test 76 | public void publish() throws Exception { 77 | System.err.println(Flux.range(1, 10) // 78 | .map(i -> COLORS.get(this.random.nextInt(3))) // 79 | .log().doOnNext(value -> { 80 | log.info("Next: " + value); 81 | sleep(1000L); 82 | }) // 83 | .subscribeOn(Schedulers.newParallel("sub")) // 84 | .publishOn(Schedulers.newParallel("pub"), 4) // 85 | .collect(Result::new, Result::add) // 86 | .doOnNext(Result::stop) // 87 | .block() // 88 | ); 89 | } 90 | 91 | @Test 92 | public void just() throws Exception { 93 | Scheduler scheduler = Schedulers.parallel(); 94 | System.err.println(Flux.range(1, 10) // 95 | .map(i -> COLORS.get(this.random.nextInt(3))) // 96 | .log() // 97 | .flatMap(value -> Mono.just(value.toUpperCase()).subscribeOn(scheduler), 2) // 98 | .collect(Result::new, Result::add) // 99 | .doOnNext(Result::stop) // 100 | .block() // 101 | ); 102 | } 103 | 104 | private Function> background(Scheduler scheduler) { 105 | return value -> Mono.fromCallable(() -> { 106 | Thread.sleep(1000L); 107 | return value; 108 | }).subscribeOn(scheduler); 109 | } 110 | 111 | private void sleep(long duration) { 112 | try { 113 | Thread.sleep(duration); 114 | } catch (InterruptedException e) { 115 | Thread.currentThread().interrupt(); 116 | throw new IllegalStateException("Interrupted"); 117 | } 118 | } 119 | 120 | } 121 | 122 | class Result { 123 | 124 | private ConcurrentMap counts = new ConcurrentHashMap<>(); 125 | 126 | private long timestamp = System.currentTimeMillis(); 127 | 128 | private long duration; 129 | 130 | public long add(String colour) { 131 | AtomicLong value = this.counts.getOrDefault(colour, new AtomicLong()); 132 | this.counts.putIfAbsent(colour, value); 133 | return value.incrementAndGet(); 134 | } 135 | 136 | public void stop() { 137 | this.duration = System.currentTimeMillis() - this.timestamp; 138 | } 139 | 140 | public long getDuration() { 141 | return this.duration; 142 | } 143 | 144 | public Map getCounts() { 145 | return this.counts; 146 | } 147 | 148 | @Override 149 | public String toString() { 150 | return "Result [duration=" + this.duration + ", counts=" + this.counts + "]"; 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /flux/src/test/java/com/example/StreamFeaturesTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.stream.Stream; 19 | 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | 23 | public class StreamFeaturesTests { 24 | private Stream stream; 25 | 26 | @Before 27 | public void generate() throws Exception { 28 | stream = Stream.of("red", "white", "blue"); 29 | } 30 | 31 | @Test 32 | public void operate() throws Exception { 33 | stream.map(value -> { 34 | System.out.println(value); 35 | return value.toUpperCase(); 36 | }); 37 | // Nothing happened. No logs, nothing. 38 | } 39 | 40 | @Test 41 | public void subscribe() throws Exception { 42 | stream.map(value -> { 43 | System.out.println(value); 44 | return value.toUpperCase(); 45 | }).count(); 46 | // Prints the values (count() is just a quick way to subscribe to the stream). 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /gather/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.3.5.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url "https://repo.spring.io/snapshot" } 8 | maven { url "https://repo.spring.io/milestone" } 9 | } 10 | dependencies { 11 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 12 | } 13 | } 14 | 15 | apply plugin: 'java' 16 | apply plugin: 'eclipse' 17 | apply plugin: 'spring-boot' 18 | 19 | jar { 20 | baseName = 'demo' 21 | version = '0.0.1-SNAPSHOT' 22 | } 23 | sourceCompatibility = 1.8 24 | targetCompatibility = 1.8 25 | 26 | repositories { 27 | mavenCentral() 28 | maven { url "https://repo.spring.io/snapshot" } 29 | maven { url "https://repo.spring.io/milestone" } 30 | } 31 | 32 | 33 | dependencies { 34 | compile('org.springframework.boot:spring-boot-starter-web') 35 | compile('io.projectreactor:reactor-core:2.5.0.BUILD-SNAPSHOT') 36 | testCompile('org.springframework.boot:spring-boot-starter-test') 37 | } 38 | 39 | 40 | eclipse { 41 | classpath { 42 | containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') 43 | containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gather/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/reactive-notes/5b574e7a378c048ac906a31ddfa7edd848b843fd/gather/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gather/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip 6 | -------------------------------------------------------------------------------- /gather/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 | -------------------------------------------------------------------------------- /gather/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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows 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 | -------------------------------------------------------------------------------- /gather/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | gather 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | gather 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.4.1.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 3.0.2.RELEASE 25 | 0.5.2.RELEASE 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | io.projectreactor 35 | reactor-core 36 | 37 | 38 | io.projectreactor.ipc 39 | reactor-netty 40 | ${reactor-io.version} 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-test 45 | test 46 | 47 | 48 | 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-maven-plugin 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | spring-snapshots 62 | Spring Snapshots 63 | https://repo.spring.io/snapshot 64 | 65 | true 66 | 67 | 68 | 69 | spring-milestones 70 | Spring Milestones 71 | https://repo.spring.io/milestone 72 | 73 | false 74 | 75 | 76 | 77 | 78 | 79 | spring-snapshots 80 | Spring Snapshots 81 | https://repo.spring.io/snapshot 82 | 83 | true 84 | 85 | 86 | 87 | spring-milestones 88 | Spring Milestones 89 | https://repo.spring.io/milestone 90 | 91 | false 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /gather/src/main/java/com/example/DemoApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.Map; 19 | import java.util.concurrent.CompletableFuture; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import java.util.concurrent.ConcurrentMap; 22 | import java.util.concurrent.atomic.AtomicLong; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.beans.factory.annotation.Value; 27 | import org.springframework.boot.SpringApplication; 28 | import org.springframework.boot.autoconfigure.SpringBootApplication; 29 | import org.springframework.http.HttpStatus; 30 | import org.springframework.web.bind.annotation.RequestMapping; 31 | import org.springframework.web.bind.annotation.RestController; 32 | import org.springframework.web.client.RestTemplate; 33 | 34 | import reactor.core.publisher.Flux; 35 | import reactor.core.publisher.Mono; 36 | import reactor.core.scheduler.Scheduler; 37 | import reactor.core.scheduler.Schedulers; 38 | 39 | @SpringBootApplication 40 | @RestController 41 | public class DemoApplication { 42 | 43 | @Value("${app.url:http://example.com}") 44 | private String url = "http://example.com"; 45 | 46 | private static Logger log = LoggerFactory.getLogger(DemoApplication.class); 47 | private RestTemplate restTemplate = new RestTemplate(); 48 | private Scheduler scheduler = Schedulers.elastic(); 49 | 50 | @RequestMapping("/parallel") 51 | public CompletableFuture parallel() { 52 | log.info("Handling /parallel"); 53 | return Flux.range(1, 10) // <1> 54 | .log() // 55 | .flatMap( // <2> 56 | this::fetch, 4 ) // <3> 57 | .collect(Result::new, Result::add) // <4> 58 | .doOnSuccess(Result::stop) // <5> 59 | .toFuture(); 60 | 61 | // <1> make 10 calls 62 | // <2> drop down to a new publisher to process in parallel 63 | // <3> concurrency hint in flatMap 64 | // <4> collect results and aggregate into a single object 65 | // <5> at the end stop the clock 66 | 67 | } 68 | 69 | private Mono fetch(int value) { 70 | 71 | return Mono.fromCallable(() -> block(value)) // <1> 72 | .subscribeOn(this.scheduler); // <2> 73 | 74 | // <1> blocking code here inside a Callable to defer execution 75 | // <2> subscribe to the slow publisher on a background thread 76 | 77 | } 78 | 79 | @RequestMapping("/serial") 80 | public CompletableFuture serial() { 81 | log.info("Handling /serial"); 82 | return Flux.range(1, 10) // <1> 83 | .log() // 84 | .map( // <2> 85 | this::block) // <3> 86 | .collect(Result::new, Result::add) // <4> 87 | .subscribeOn(this.scheduler) // <6> 88 | .doOnSuccess(Result::stop) // <5> 89 | .toFuture(); 90 | // <1> make 10 calls 91 | // <2> stay in the same publisher chain 92 | // <3> blocking call not deferred (no point in this case) 93 | // <4> collect results and aggregate into a single object 94 | // <5> at the end stop the clock 95 | // <6> subscribe on a background thread 96 | } 97 | 98 | private HttpStatus block(int value) { 99 | return this.restTemplate.getForEntity(url, String.class, value).getStatusCode(); 100 | } 101 | 102 | public static void main(String[] args) { 103 | System.setProperty("reactor.io.epoll", "false"); 104 | SpringApplication.run(DemoApplication.class, args); 105 | } 106 | 107 | } 108 | 109 | class Result { 110 | 111 | private ConcurrentMap counts = new ConcurrentHashMap<>(); 112 | 113 | private long timestamp = System.currentTimeMillis(); 114 | 115 | private long duration; 116 | 117 | public long add(HttpStatus status) { 118 | AtomicLong value = this.counts.getOrDefault(status, new AtomicLong()); 119 | this.counts.putIfAbsent(status, value); 120 | return value.incrementAndGet(); 121 | } 122 | 123 | public void stop() { 124 | this.duration = System.currentTimeMillis() - this.timestamp; 125 | } 126 | 127 | public long getDuration() { 128 | return this.duration; 129 | } 130 | 131 | public Map getCounts() { 132 | return this.counts; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /gather/src/main/java/com/example/NettyClientController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.concurrent.CompletableFuture; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.beans.factory.annotation.Value; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.web.bind.annotation.RequestMapping; 25 | import org.springframework.web.bind.annotation.RestController; 26 | 27 | import reactor.core.publisher.Flux; 28 | import reactor.core.publisher.Mono; 29 | import reactor.ipc.netty.http.HttpClient; 30 | 31 | @RestController 32 | public class NettyClientController { 33 | 34 | @Value("${app.url:http://example.com}") 35 | private String url = "http://example.com"; 36 | 37 | private static Logger log = LoggerFactory.getLogger(NettyClientController.class); 38 | private HttpClient client = HttpClient.create(); 39 | 40 | @RequestMapping("/netty") 41 | public CompletableFuture gather() { 42 | log.info("Handling /netty"); 43 | return Flux.range(1, 10) // 44 | .log() // 45 | .flatMap(value -> fetch(value)) // 46 | .collect(Result::new, Result::add) // 47 | .doOnSuccess(Result::stop) // 48 | .toFuture(); 49 | } 50 | 51 | private Mono fetch(int value) { 52 | return this.client.get(url) 53 | .map(inbound -> HttpStatus.valueOf(inbound.status().code())); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /gather/src/main/java/com/example/RestTemplateStoreGatherer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.ArrayList; 19 | import java.util.LinkedHashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.concurrent.CompletableFuture; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import org.springframework.beans.factory.annotation.Value; 27 | import org.springframework.core.ParameterizedTypeReference; 28 | import org.springframework.http.HttpMethod; 29 | import org.springframework.web.bind.annotation.RequestMapping; 30 | import org.springframework.web.bind.annotation.RestController; 31 | import org.springframework.web.client.RestTemplate; 32 | 33 | import com.fasterxml.jackson.annotation.JsonCreator; 34 | import com.fasterxml.jackson.annotation.JsonInclude; 35 | 36 | import reactor.core.publisher.Flux; 37 | import reactor.core.publisher.Mono; 38 | import reactor.core.scheduler.Scheduler; 39 | import reactor.core.scheduler.Schedulers; 40 | 41 | @RestController 42 | @RequestMapping("/stores") 43 | public class RestTemplateStoreGatherer { 44 | 45 | @Value("${app.url:http://stores.cfapps.io}") 46 | private String url = "http://stores.cfapps.io"; 47 | 48 | private static Logger log = LoggerFactory.getLogger(RestTemplateStoreGatherer.class); 49 | private RestTemplate restTemplate = new RestTemplate(); 50 | 51 | @RequestMapping 52 | public CompletableFuture> gather() { 53 | Scheduler scheduler = Schedulers.elastic(); 54 | return Flux.range(0, Integer.MAX_VALUE) // <1> 55 | .flatMap(page -> Flux.defer(() -> page(page)).subscribeOn(scheduler), 2) // <2> 56 | .flatMap(store -> Mono.fromCallable(() -> meta(store)) 57 | .subscribeOn(scheduler), 4) // <3> 58 | .take(50) // <4> 59 | .collectList() // <5> 60 | .toFuture(); 61 | } 62 | 63 | // <1> an "infinite" sequence sine we don't know how many pages there are 64 | // <2> drop to a background thread to process each page, 2 at a time 65 | // <3> for each store drop to a background thread to enhance it with metadata, 4 at a 66 | // time 67 | // <4> take at most 50 68 | // <5> convert to a list 69 | 70 | /** 71 | * Enhance a Store with some metadata. Blocking. 72 | * 73 | * @param store a Store to enhance 74 | * @return the enhanced store 75 | */ 76 | private Store meta(Store store) { 77 | Map map = this.restTemplate 78 | .exchange(url + "/stores/{id}", HttpMethod.GET, null, 79 | new ParameterizedTypeReference>() { 80 | }, store.getId()) 81 | .getBody(); 82 | @SuppressWarnings("unchecked") 83 | Map meta = (Map) map.get("address"); 84 | store.getMeta().putAll(meta); 85 | return store; 86 | } 87 | 88 | /** 89 | * Get a page of stores from the backend, Blocking. 90 | * 91 | * @param page the page number (starting at 0) 92 | * @return a page of stores (or empty) 93 | */ 94 | private Flux page(int page) { 95 | Map map = this.restTemplate 96 | .exchange(url + "/stores?page={page}", HttpMethod.GET, null, 97 | new ParameterizedTypeReference>() { 98 | }, page) 99 | .getBody(); 100 | @SuppressWarnings("unchecked") 101 | List> list = (List>) ((Map) map 102 | .get("_embedded")).get("stores"); 103 | List stores = new ArrayList<>(); 104 | for (Map store : list) { 105 | stores.add(new Store((String) store.get("id"), (String) store.get("name"))); 106 | } 107 | log.info("Fetched " + stores.size() + " stores for page: " + page); 108 | return Flux.fromIterable(stores); 109 | } 110 | 111 | } 112 | 113 | @JsonInclude(JsonInclude.Include.NON_DEFAULT) 114 | class Store { 115 | 116 | private String id; 117 | private String name; 118 | private Map meta = new LinkedHashMap<>(); 119 | 120 | Store() { 121 | } 122 | 123 | @JsonCreator 124 | public Store(String id, String name) { 125 | this.id = id; 126 | this.name = name; 127 | } 128 | 129 | public String getId() { 130 | return this.id; 131 | } 132 | 133 | public String getName() { 134 | return this.name; 135 | } 136 | 137 | public void setId(String id) { 138 | this.id = id; 139 | } 140 | 141 | public void setName(String name) { 142 | this.name = name; 143 | } 144 | 145 | public Map getMeta() { 146 | return this.meta; 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /gather/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | app.url: http://stores.cfapps.io -------------------------------------------------------------------------------- /gather/src/test/java/com/example/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.springframework.boot.test.SpringApplicationConfiguration; 21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 22 | 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | @SpringApplicationConfiguration(classes = DemoApplication.class) 25 | public class DemoApplicationTests { 26 | 27 | @Test 28 | public void contextLoads() { 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /intro.adoc: -------------------------------------------------------------------------------- 1 | :github: https://github.com/dsyer/reactive-notes 2 | :master: {github}/blob/master 3 | :partii: https://spring.io/blog/2016/06/13/notes-on-reactive-programming-part-ii-writing-some-code 4 | 5 | Reactive Programming is interesting (again) and there is a lot of noise about it at the moment, not all of which is very easy to understand for an outsider and simple enterprise Java developer, such as the author. This article (the first in a series) might help to clarify your understanding of what the fuss is about. The approach is as concrete as possible, and there is no mention of "denotational semantics". If you are looking for a more academic approach and loads of code samples in Haskell, the internet is full of them, but you probably don't want to be here. 6 | 7 | Reactive Programming is often conflated with concurrent programming and high performance to such an extent that it's hard to separate those concepts, when actually they are in principle completely different. This inevitably leads to confusion. Reactive Programming is also often referred to as or conflated with Functional Reactive Programming, or FRP (and we use the two interchangeably here). Some people think Reactive is nothing new, and it's what they do all day anyway (mostly they use JavaScript). Others seem to think that it's a gift from Microsoft (who made a big splash about it when they released some C# extensions a while ago). In the Enterprise Java space there has been something of a buzz recently (e.g. see the http://www.reactive-streams.org/[Reactive Streams initiative]), and as with anything shiny and new, there are a lot of easy mistakes to make out there, about when and where it can and should be used. 8 | 9 | == What Is It? 10 | 11 | Reactive Programming is a style of micro-architecture involving intelligent routing and consumption of events, all combining to change behaviour. That's a bit abstract, and so are many of the other definitions you will come across online. We attempt build up some more concrete notions of what it means to be reactive, or why it might be important in what follows. 12 | 13 | The origins of Reactive Programming can probably be traced to the 1970s or even earlier, so there's nothing new about the idea, but they are really resonating with something in the modern enterprise. This resonance has arrived (not accidentally) at the same time as the rise of microservices, and the ubiquity of multi-core processors. Some of the reasons for that will hopefully become clear. 14 | 15 | Here are some useful potted definitions from other sources: 16 | 17 | [quote] 18 | ---- 19 | The basic idea behind reactive programming is that there are certain 20 | datatypes that represent a value "over time". Computations that 21 | involve these changing-over-time values will themselves have values 22 | that change over time. 23 | ---- 24 | 25 | and... 26 | 27 | [quote] 28 | ---- 29 | An easy way of reaching a first intuition about what it's like is to 30 | imagine your program is a spreadsheet and all of your variables are 31 | cells. If any of the cells in a spreadsheet change, any cells that 32 | refer to that cell change as well. It's just the same with FRP. Now 33 | imagine that some of the cells change on their own (or rather, are 34 | taken from the outside world): in a GUI situation, the position of 35 | the mouse would be a good example. 36 | ---- 37 | 38 | (from http://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming[Terminology Question on Stackoverflow]) 39 | 40 | FRP has a strong affinity with high-performance, concurrency, asynchronous operations and non-blocking IO. However, it might be helpful to start with a suspicion that FRP has nothing to do with any of them. It is certainly the case that such concerns can be naturally handled, often transparently to the caller, when using a Reactive model. But the actual benefit, in terms of handling those concerns effectively or efficiently is entirely up to the implementation in question (and therefore should be subject to a high degree of scrutiny). It is also possible to implement a perfectly sane and useful FRP framework in a synchronous, single-threaded way, but that isn't really likely to be helpful in trying to use any of the new tools and libraries. 41 | 42 | == Reactive Use Cases 43 | 44 | The hardest question to get an answer to as a newbie seems to be "what is it good for?" Here are some examples from an enterprise setting that illustrate general patterns of use: 45 | 46 | **External Service Calls** Many backend services these days are REST-ful (i.e. they operate over HTTP) so the underlying protocol is fundamentally blocking and synchronous. Not obvious territory for FRP maybe, but actually it's quite fertile ground because the implementation of such services often involves calling other services, and then yet more services depending on the results from the first calls. With so much IO going on if you were to wait for one call to complete before sending the next request, your poor client would give up in frustration before you managed to assemble a reply. So external service calls, especially complex orchestrations of dependencies between calls, are a good thing to optimize. FRP offers the promise of "composability" of the logic driving those operations, so that it is easier to write for the developer of the calling service. 47 | 48 | **Highly Concurrent Message Consumers** Message processing, in particular when it is highly concurrent, is a common enterprise use case. Reactive frameworks like to measure micro benchmarks, and brag about how many messages per second you can process in the JVM. The results are truly staggering (tens of millions of messages per second are easy to achieve), but possibly somewhat artificial - you wouldn't be so impressed if they said they were benchmarking a simple "for" loop. However, we should not be too quick to write off such work, and it's easy to see that when performance matters, all contributions should be gratefully accepted. Reactive patterns fit naturally with message processing (since an event translates nicely into a message), so if there is a way to process more messages faster we should pay attention. 49 | 50 | **Spreadsheets** Perhaps not really an enterprise use case, but one that everyone in the enterprise can easily relate to, and it nicely captures the philosophy of, and difficulty of implementing FRP. If cell B depends on cell A, and cell C depends on both cells A and B, then how do you propagate changes in A, ensuring that C is updated before any change events are sent to B? If you have a truly reactive framework to build on, then the answer is "you don't care, you just declare the dependencies," and that is really the power of a spreadsheet in a nutshell. It also highlights the difference between FRP and simple event-driven programming -- it puts the "intelligent" in "intelligent routing". 51 | 52 | **Abstraction Over (A)synchronous Processing** This is more of an abstract use case, so straying into the territory we should perhaps be avoiding. There is also some (a lot) of overlap between this and the more concrete use cases already mentioned, but hopefully it is still worth some discussion. The basic claim is a familiar (and justifiable) one, that as long as developers are willing to accept an extra layer of abstraction, they can forget about whether the code they are calling is synchronous or asynchronous. Since it costs precious brain cells to deal with asynchronous programming, there could be some useful ideas there. Reactive Programming is not the only approach to this issue, but some of the implementaters of FRP have thought hard enough about this problem that their tools are useful. 53 | 54 | This Netflix blog has some really useful concrete examples of real-life use cases: http://techblog.netflix.com/2013/02/rxjava-netflix-api.html[Netflix Tech Blog: Functional Reactive in the Netflix API with RxJava] 55 | 56 | == Comparisons 57 | 58 | If you haven't been living in a cave since 1970 you will have come across some other concepts that are relevant to Reactive Programming and the kinds of problems people try and solve with it. Here are a few of them with my personal take on their relevance: 59 | 60 | **Ruby Event-Machine** The https://github.com/eventmachine/eventmachine[Event Machine] is an abstraction over concurrent programming (usually involving non-blocking IO). Rubyists struggled for a long time to turn a language that was designed for single-threaded scripting into something that you could use to write a server application that a) worked, b) performed well, and c) stayed alive under load. Ruby has had threads for quite some time, but they aren't used much and have a bad reputation because they don't always perform very well. The alternative, which is ubiquitous now that it has been promoted (in Ruby 1.9) to the core of the language, is http://www.ruby-doc.org/core-1.9.3/Fiber.html[Fibers](sic). The Fiber programming model is sort of a flavour of coroutines (see below), where a single native thread is used to process large numbers of concurrent requests (usually involving IO). The programming model itself is a bit abstract and hard to reason about, so most people use a wrapper, and the Event Machine is the most common. Event Machine doesn't necessarily use Fibers (it abstracts those concerns), but it is easy to find examples of code using Event Machine with Fibers in Ruby web apps (e.g. http://www.igvita.com/2009/05/13/fibers-cooperative-scheduling-in-ruby[see this article by Ilya Grigorik], or the https://github.com/igrigorik/em-http-request/blob/master/examples/fibered-http.rb[fibered example from em-http-request]). People do this a lot to get the benefit of scalability that comes from using Event Machine in an I/O intensive application, without the ugly programming model that you get with lots of nested callbacks. 61 | 62 | **Actor Model** Similar to Object Oriented Programming, the Actor Model is a deep thread of Computer Science going back to the 1970s. Actors provide an abstraction over computation (as opposed to data and behaviour) that allows for concurrency as a natural consequence, so in practical terms they can form the basis of a concurrent system. Actors send each other messages, so they are reactive in some sense, and there is a lot of overlap between systems that style themselves as Actors or Reactive. Often the distinction is at the level of their implementation (e.g. `Actors` in http://doc.akka.io/docs/akka/current/java.html[Akka] can be distributed across processes, and that is a distinguishing feature of that framework). 63 | 64 | **Deferred results (Futures)** Java 1.5 introduced a rich new set of libraries including Doug Lea's "java.util.concurrent", and part of that is the concept of a deferred result, encapsulated in a `Future`. It's a good example of a simple abstraction over an asynchronous pattern, without forcing the implementation to be asynchronous, or use any particular model of asynchronous processing. As the http://techblog.netflix.com/2013/02/rxjava-netflix-api.html[Netflix Tech Blog: Functional Reactive in the Netflix API with RxJava] shows nicely, `Futures` are great when all you need is concurrent processing of a set of similar tasks, but as soon as any of them want to depend on each other or execute conditionally you get into a form of "nested callback hell". Reactive Programming provides an antidote to that. 65 | 66 | **Map-reduce and fork-join** Abstractions over parallel processing are useful and there are many examples to choose from. Map-reduce and fork-join that have evolved recently in the Java world, driven by massively parallel distributed processing (http://research.google.com/archive/mapreduce-osdi04.pdf[MapReduce] and http://wiki.apache.org/hadoop/MapReduce[Hadoop]) and by the JDK itself in version 1.7 (http://gee.cs.oswego.edu/dl/papers/fj.pdf[Fork-Join]). These are useful abstractions but (like deferred results) they are shallow compared to FRP, which can be used as an abstraction over simple parallel processing, but which reaches beyond that into composability and declarative communication. 67 | 68 | **Coroutines** A https://en.wikipedia.org/wiki/Coroutines["coroutine"] is a generalization of a "subroutine" -- it has an entry point, and exit point(s) like a subroutine, but when it exits it passes control to another coroutine (not necessarily to its caller), and whatever state it accumulated is kept and remembered for the next time it is called. Coroutines can be used as a building block for higher level features like Actors and Streams. One of the goals of Reactive Programming is to provide the same kind of abstraction over communicating parallel processing agents, so coroutines (if they are available) are a useful building block. There are various flavours of coroutines, some of which are more restrictive than the general case, but more flexible than vanilla subroutines. Fibers (see the discussion on Event Machine) are one flavour, and Generators (familiar in Scala and Python) are another. 69 | 70 | == Reactive Programming in Java 71 | 72 | Java is not a "reactive language" in the sense that it doesn't support coroutines natively. There are other languages on the JVM (Scala and Clojure) that support reactive models more natively, but Java itself does not until version 9. Java, however, is a powerhouse of enterprise development, and there has been a lot of activity recently in providing Reactive layers on top of the JDK. We only take a very brief look at a few of them here. 73 | 74 | http://www.reactive-streams.org/[**Reactive Streams**] is a very low level contract, expressed as a handful of Java interfaces (plus a TCK), but also applicable to other languages. The interfaces express the basic building blocks of `Publisher` and `Subscriber` with explicit back pressure, forming a common language for interoperable libraries. Reactive Streams have been incorporated into the JDK as `java.util.concurrent.Flow` in version 9. The project is a collaboration between engineers from Kaazing, Netflix, Pivotal, Red Hat, Twitter, Typesafe and many others. 75 | 76 | 77 | https://github.com/ReactiveX/RxJava/wiki[**RxJava**]: Netflix were using reactive patterns internally for some time and then they released the tools they were using under an open source license as https://github.com/ReactiveX/RxJava/wiki[Netflix/RxJava] (subsequently re-branded as "ReactiveX/RxJava"). Netflix does a lot of programming in Groovy on top of RxJava, but it is open to Java usage and quite well suited to Java 8 through the use of Lambdas. There is a https://github.com/ReactiveX/RxJavaReactiveStreams[bridge to Reactive Streams]. RxJava is a "2nd Generation" library according to David Karnok's http://akarnokd.blogspot.co.uk/2016/03/operator-fusion-part-1.html[Generations of Reactive] classification. 78 | 79 | https://projectreactor.io/[**Reactor**] is a Java framework from the http://www.gopivotal.com/oss[Pivotal] open source team (the one that created Spring). It builds directly on Reactive Streams, so there is no need for a bridge. The Reactor IO project provides wrappers around low-level network runtimes like Netty and Aeron. Reactor is a "4th Generation" library according to David Karnok's http://akarnokd.blogspot.co.uk/2016/03/operator-fusion-part-1.html[Generations of Reactive] classification. 80 | 81 | http://projects.spring.io/spring-framework/[**Spring Framework 5.0**] (first milestone June 2016) has reactive features built into it, including tools for building HTTP servers and clients. Existing users of Spring in the web tier will find a very familiar programming model using annotations to decorate controller methods to handle HTTP requests, for the most part handing off the dispatching of reactive requests and back pressure concerns to the framework. Spring builds on Reactor, but also exposes APIs that allow its features to be expressed using a choice of libraries (e.g. Reactor or RxJava). Users can choose from Tomcat, Jetty, Netty (via Reactor IO) and Undertow for the server side network stack. 82 | 83 | https://ratpack.io[**Ratpack**] is a set of libraries for building high performance services over HTTP. It builds on Netty and implements Reactive Streams for interoperability (so you can use other Reactive Streams implementations higher up the stack, for instance). Spring is supported as a native component, and can be used to provide dependency injection using some simple utility classes. There is also some autoconfiguration so that Spring Boot users can embed Ratpack inside a Spring application, bringing up an HTTP endpoint and listening there instead of using one of the embedded servers supplied directly by Spring Boot. 84 | 85 | http://akka.io/[**Akka**] is a toolkit for building applications using the Actor pattern in Scala or Java, with interprocess communication using Akka Streams, and Reactive Streams contracts are built in. Akka is a "3rd Generation" library according to David Karnok's http://akarnokd.blogspot.co.uk/2016/03/operator-fusion-part-1.html[Generations of Reactive] classification. 86 | 87 | == Why Now? 88 | 89 | What is driving the rise of Reactive in Enterprise Java? Well, it's not (all) just a technology fad -- people jumping on the bandwagon with the shiny new toys. The driver is efficient resource utilization, or in other words, spending less money on servers and data centres. The promise of Reactive is that you can do more with less, specifically you can process higher loads with fewer threads. This is where the intersection of Reactive and non-blocking, asynchronous I/O comes to the foreground. For the right problem, the effects are dramatic. For the wrong problem, the effects might go into reverse (you actually make things worse). Also remember, even if you pick the right problem, there is no such thing as a free lunch, and Reactive doesn't solve the problems for you, it just gives you a toolbox that you can use to implement solutions. 90 | 91 | == Conclusion 92 | 93 | In this article we have taken a very broad and high level look at the Reactive movement, setting it in context in the modern enterprise. There are a number of Reactive libraries or frameworks for the JVM, all under active development. To a large extent they provide similar features, but increasingly, thanks to Reactive Streams, they are interoperable. In the {partii}[next article] in the series we will get down to brass tacks and have a look at some actual code samples, to get a better picture of the specifics of what it means to be Reactive and why it matters. We will also devote some time to understanding why the "F" in FRP is important, and how the concepts of back pressure and non-blocking code have a profound impact on programming style. And most importantly, we will help you to make the important decision about when and how to go Reactive, and when to stay put on the older styles and stacks. 94 | 95 | -------------------------------------------------------------------------------- /io.adoc: -------------------------------------------------------------------------------- 1 | :github: https://github.com/dsyer/reactive-notes 2 | :master: {github}/blob/master 3 | :partii: https://spring.io/blog/2016/06/13/notes-on-reactive-programming-part-ii-writing-some-code 4 | :partiv: {master}/platform.adoc 5 | 6 | In this article we continue the series on {partii}[Reactive Programming], and the focus is less on learning the basic APIs and more on more concrete use cases and writing code that actually does something useful. We will see how Reactive is a useful abstraction for concurrent programming, but also that it has some very low level features that we should learn to treat with respect and caution. If we start to use these features to their full potential we can take control of layers in our application that previously were invisible, hidden by containers, platforms and frameworks. 7 | 8 | == Bridging from Blocking to Reactive with Spring MVC 9 | 10 | Being Reactive forces you to look at the world differently. Instead of asking for something and getting it (or not getting it), everything is delivered as a sequence (`Publisher`) and you have to subscribe to it. Instead of waiting for an answer, you have to register a callback. It's not so hard when you get used to it, but unless the whole world turns on its head and becomes Reactive, you are going to find you need to interact with an old-style blocking API 11 | 12 | Suppose we have a blocking method that returns an `HttpStatus`: 13 | 14 | ```java 15 | private RestTemplate restTemplate = new RestTemplate(); 16 | 17 | private HttpStatus block(int value) { 18 | return this.restTemplate.getForEntity("http://example.com/{value}", String.class, value) 19 | .getStatusCode(); 20 | } 21 | ``` 22 | 23 | and we want to call it repeatedly with different arguments and aggregate the results. It's a classic "scatter-gather" use case, which you would get, for instance, if you had a paginated back end needed to summarize the "top N" items across multiple pages. Since the details of the non-reactive (blocking) operation are not relevant to the scatter-gather pattern, we can push them down into a method called `block()`, and implement it later. Here's a (bad) example that calls the back end and aggregates into an object of type `Result`: 24 | 25 | ```java 26 | Flux.range(1, 10) // <1> 27 | .log() 28 | .map(this::block) // <2> 29 | .collect(Result::new, Result::add) // <3> 30 | .doOnSuccess(Result::stop) // <4> 31 | ``` 32 | <1> make 10 calls 33 | <3> blocking code here 34 | <3> collect results and aggregate into a single object 35 | <4> at the end stop the clock (the result is a `Mono`) 36 | 37 | Don't do this at home. It's a "bad" example because, while the APIs are technically being used correctly, we know that it is going to block the calling thread; this code is more or less equivalent to a for loop with the call to `block()` in the body of the loop. A better implementation would push the call to `block()` onto a background thread. We can do that by wrapping it in a method that returns a `Mono`: 38 | 39 | ```java 40 | private Mono fetch(int value) { 41 | return Mono.fromCallable(() -> block(value)) // <1> 42 | .subscribeOn(this.scheduler); // <2> 43 | } 44 | ``` 45 | <1> blocking code here inside a Callable to defer execution 46 | <2> subscribe to the slow publisher on a background thread 47 | 48 | The `scheduler` is declared separately as a shared field: `Scheduler scheduler = Schedulers.parallel()`. Then we can declare that we want to `flatMap()` the sequence instead of using `map()`: 49 | 50 | ```java 51 | Flux.range(1, 10) 52 | .log() 53 | .flatMap( // <1> 54 | this::fetch, 4) // <2> 55 | .collect(Result::new, Result::add) 56 | .doOnSuccess(Result::stop) 57 | ``` 58 | <1> drop down to a new publisher to process in parallel 59 | <2> concurrency hint in flatMap 60 | 61 | === Embedding in a Non-Reactive Server 62 | 63 | If we wanted to run the scatter-gather code above in a non-reactive server like a servlet container, we could use Spring MVC, like this: 64 | 65 | 66 | ```java 67 | @RequestMapping("/parallel") 68 | public CompletableFuture parallel() { 69 | return Flux.range(1, 10) 70 | ... 71 | .doOnSuccess(Result::stop) 72 | .toFuture(); 73 | } 74 | ``` 75 | 76 | If you read the Javadocs for `@RequestMapping` you will find that a method can return a `CompletableFuture` "which the application uses to produce a return value in a separate thread of its own choosing". The separate thread in this case is provided by "scheduler", which is a thread pool, so the processing is happening on multiple threads, 4 at a time because of the way that `flatMap()` is called. 77 | 78 | === No Such Thing as a Free Lunch 79 | 80 | The scatter-gather with a background thread is a useful pattern but it isn't perfect -- it's not blocking the caller, but it's blocking something, so it's just moving the problem around. There are some practical implications. We have an HTTP server with (probably) non-blocking IO handlers, passing work back to a thread pool, one HTTP request per thread -- all of this is happening inside a servlet container (e.g. Tomcat). The request is processed asynchronously, so the worker thread in Tomcat isn't blocked, and the thread pool that we created in our "scheduler" is processing on up to 4 concurrent threads. We are processing 10 back end requests (calls to `block()`) so there is a maximum, theoretical benefit of using the scheduler of 4 times lower latency. In other words, if processing all 10 requests one after the other in a single thread takes 1000ms, we might see a processing time of 250ms for a single incoming request at our HTTP service. We should emphasise the "might" though: it's only going to go that fast if there is no contention for the processing threads (in both stages, the Tomcat workers, and the application scheduler). If you have a server with a large number of cores, very low concurrency, i.e. a small number of clients connecting to your application, and hardly any chance that two will make a request at the same time, then you will probably see close to the theoretical improvement. As soon as there are multiple clients trying to connect, they will all be competing for the same 4 threads, and the latency will drift up, and could even be worse than that experienced by a single client with no background processing. We can improve the latency for concurrent clients by creating the scheduler with a larger thread pool, e.g. 81 | 82 | ```java 83 | private Scheduler scheduler = Schedulers.newParallel("sub", 16); 84 | ``` 85 | 86 | (16 threads.) Now we are using more memory for the threads and their stacks, and we can expect to see lower latency for low concurrency, but not necessarily for high concurrency if our hardware has fewer than 16 cores. We also do not expect higher throughput under load: if there is contention for the threads, there is a high cost for managing those resources and that has to be reflected somwehere in a metric that matters. If you are interested in more detailed analysis of that kind of trade off, some detailed analyses of how performance metrics scale under load can be found in a blog series by https://robharrop.github.io/[Rob Harrop]. 87 | 88 | TIP: Tomcat allocates 100 threads for processing HTTP requests by default. That is excessive if we know all the processing is going to be on our scheduler thread pool. There is an impedance mismatch: the scheduler thread pool can be a bottleneck because it has fewer threads than the upstream Tomcat thread pool. This highlights the fact that performance tuning can be very hard, and, while you might have control of all the configuration, it's a delicate balance. 89 | 90 | We can do better than a fixed thread pool if we use a scheduler that adjusts its capacity according to demand. Reactor has a convenience for that, so if you try the same code with `Schedulers.elastic()` (you can call it anywhere; there is only one instance), you will see that under load more threads are created. 91 | 92 | 93 | == Reactive all the Way Down 94 | 95 | The bridge from blocking to reactive is a useful pattern, and is easy to implement using available technology in Spring MVC (as shown above). The next stage in the Reactive journey is to break out of blocking in application threads completely, and to do that requires new APIs and new tools. Ultimately we have to be Reactive all the way down the stack, including servers and clients. This is the goal of Spring Reactive, which is a new framework, orthogonal to Spring MVC, but meeting the same needs, and using a similar programming model. 96 | 97 | NOTE: Spring Reactive started as a standalone project, but is folded into the Spring Framework in version 5.0 (first milestone June 2016). 98 | 99 | The first step to fully Reactive in our scatter-gather example would be to replace `spring-boot-starter-web` with `spring-boot-starter-web-reactive` on the classpath. In Maven: 100 | 101 | ```xml 102 | 103 | 104 | org.springframework.boot.experimental 105 | spring-boot-starter-web-reactive 106 | 107 | ... 108 | 109 | 110 | 111 | 112 | org.springframework.boot.experimental 113 | spring-boot-dependencies-web-reactive 114 | 0.1.0.M1 115 | pom 116 | import 117 | 118 | 119 | 120 | ``` 121 | 122 | or in Gradle: 123 | 124 | ```groovy 125 | dependencies { 126 | compile('org.springframework.boot.experimental:spring-boot-starter-web-reactive') 127 | ... 128 | } 129 | dependencyManagement { 130 | imports { 131 | mavenBom "org.springframework.boot.experimental:spring-boot-dependencies-web-reactive:0.1.0.M1" 132 | } 133 | } 134 | ``` 135 | 136 | (At the time of writing there are snapshots and milestones of this experimental library in repo.spring.io, or you could build and install locally from https://github.com/bclozel/spring-boot-reactive-web[GitHub].) 137 | 138 | Then in the controller, we can simply lose the bridge to `CompletableFuture` and return an object of type `Mono`: 139 | 140 | ```java 141 | @RequestMapping("/parallel") 142 | public Mono parallel() { 143 | return Flux.range(1, 10) 144 | .log() 145 | .flatMap(this::fetch, 4) 146 | .collect(Result::new, Result::add) 147 | .doOnSuccess(Result::stop); 148 | } 149 | ``` 150 | 151 | Take this code and put it in a Spring Boot application and it will run in Tomcat, Jetty or Netty, depending on what it finds on the classpath. Tomcat is the default server in that starter, so you have to exclude it and provide a different one if you want to switch. All three have very similar characteristics in terms of startup time, memory usage and runtime resource usage. 152 | 153 | We still have the blocking backend call in `block()`, so we still have to subscribe on a thread pool to avoid blocking the caller. We can change that if we have a non-blocking client, e.g. instead of using `RestTemplate` we use the new `WebClient` then we might do this instead to use a non-blocking client: 154 | 155 | ```java 156 | private WebClient client = new WebClient(new ReactorHttpClientRequestFactory()); 157 | 158 | private Mono fetch(int value) { 159 | return this.client.perform(HttpRequestBuilders.get("http://example.com")) 160 | .extract(WebResponseExtractors.response(String.class)) 161 | .map(response -> response.getStatusCode()); 162 | } 163 | ``` 164 | 165 | Note that the `WebClient.perform()` (or the `WebResponseExtractor` to be precise) has a Reactive return type, which we have transformed into a `Mono`, but we have not subscribed to it. We want the framework to do all the subscribing, so now we are Reactive all the way down. 166 | 167 | WARNING: Methods in Spring Reactive that return a `Publisher` *are* non-blocking, but in general a method that returns a `Publisher` (or `Flux`, `Mono` or `Observable`) is only a hint that it might be non-blocking. If you are writing such methods it is important to analyse (and preferably test) whether they block, and to let callers know explicitly if they might do. 168 | 169 | NOTE: The trick we played just now of using a non-blocking client to simplify the HTTP stack works in regular Spring MVC as well. The result of the `fetch()` method above can be converted to a `CompletableFuture` and passed out of a regular `@RequestMapping` method (in Spring Boot 1.3 for instance). 170 | 171 | === Inversion of Control 172 | 173 | Now we can remove the concurrency hint after the call to `fetch()` in the HTTP request handler: 174 | 175 | ```java 176 | @RequestMapping("/netty") 177 | public Mono netty() { 178 | return Flux.range(1, 10) // <1> 179 | .log() // 180 | .flatMap(this::fetch) // <2> 181 | .collect(Result::new, Result::add) 182 | .doOnSuccess(Result::stop); 183 | } 184 | ``` 185 | <1> make 10 calls 186 | <2> drop down to a new publisher to process in parallel 187 | 188 | Taking into account that we don't need the extra callable and subscriber thread at all, this code is a lot cleaner than when we had to bridge to the blocking client, which can be attributed to the fact that the code is Reactive all the way down. The Reactive `WebClient` returns a `Mono`, and that drives us immediately to select `flatMap()` in the transformation chain, and the code we need just falls out. It's a nicer experience to write it, and it's more readable, so it's easier to maintain. Also, since there is no thread pooling and no concurrency hint, there is no magic factor of 4 to plug into our performance expectations. There is a limit somewhere, but it's not imposed by our choices in the application tier any more, nor is it limited by anything in the server "container". It's not magic, and there are still laws of physics, so the backend calls are all still going to take 100ms or so each, but with low contention we might even see all 10 requests complete in roughly the same time it takes for one. As the load on the server increases latency and throughput will naturally degrade, but in a way that is governed by buffer contention and kernel networking, not by application thread management. It's an inversion of control, to lower levels of the stack below the application code. 189 | 190 | Remember the same application code runs on Tomcat, Jetty or Netty. Currently, the Tomcat and Jetty support is provided on top of Servlet 3.1 asynchronous processing, so it is limited to one request per thread. When the same code runs on the Netty server platform that constraint is lifted, and the server can dispatch requests sympathetically to the web client. As long as the client doesn't block, everyone is happy. Performance metrics for the netty server and client probably show similar characteristics, but the Netty server is not restricted to processing a single request per thread, so it doesn't use a large thread pool and we might expect to see some differences in resource utilization. We will come back to that later in another article in this series. 191 | 192 | TIP: in the {github}[sample code] the "reactive" sample has Maven profiles "tomcat", "tomcatNext" (for Tomcat 8.5), "jetty" and "netty", so you can easily try out all the different server options without changing a line of code. 193 | 194 | NOTE: the blocking code in many applications is not HTTP backend calls, but database interactions. Very few databases support non-blocking clients at this point in time (MongoDB and Couchbase are notable exceptions, but even those are not as mature as the HTTP clients). Thread pools and the blocking-to-reactive pattern will have a long life until all the database vendors catch up on the client side. 195 | 196 | === Still No Free Lunch 197 | 198 | We have whittled down our basic scatter-gather use case until the code is very clean, and very sympathetic to the hardware it runs on. We wrote some simple code and it was stacked up and orchestrated very nicely into a working HTTP service using Spring. On a sunny day everyone is more than happy with the outcome. But as soon as there are errors, e.g. a badly behaved network connection, or a back end service that suffers from poor latency, we are going to suffer. 199 | 200 | The first, most obvious way to suffer is that the code we wrote is declarative, so it's hard to debug. When errors occur the diagnostics can be very opaque. Using the raw, low-level APIs, like Reactor without Spring, or even down to the level of Netty without Reactor would probably make it even worse, because then we would have to build a lot of error handling ourselves, repeating the boiler plate every time we interact with the network. At least with Spring and Reactor in the mix we can expect to see stack traces logged for stray, uncaught exceptions. They might not be easy to understand though because they happen on threads that we don't control, and they sometimes show up as quite low level concerns, from unfamiliar parts of the stack. 201 | 202 | Another source of pain is that if we ever make a mistake and block in one of our Reactive callbacks, we will be holding up *all* requests on the same thread. With the servlet-based containers every request is isolated to a thread, and blocking doesn't hold up other requests because they are be processed on different threads. Blocking all requests is still a recipe for trouble, but it only shows up as increased latency with roughly a constant factor per request. In the Reactive world, blocking a single request can lead to increased latency for all requests, and blocking all requests can bring a server to its knees because the extra layers of buffers and threads are not there to take up the slack. 203 | 204 | == Conclusion 205 | 206 | It's nice to be able to control all the moving parts in our asynchronous processing: every layer has a thread pool size and a queue. We can make some of those layers elastic, and try and adjust them according to how much work they do. But at some point it becomes a burden, and we start looking for something simpler, or leaner. Analysis of scalability leads to the conclusion that it is often better to shed the extra threads, and work with the constraints imposed by the physical hardware. This is an example of "mechanical sympathy", as is famously exploited by LMAX to great effect in the https://lmax-exchange.github.io/disruptor/[Disruptor Pattern]. 207 | 208 | We have begun to see the power of the Reactive approach, but remember that with power comes responsibility. It's radical, and it's fundamental. It's "rip it up and start again" territory. So you will also hopefully appreciate that Reactive isn't a solution to all problems. In fact it isn't a solution to any problem, it merely facilitates the solution of a certain class of problems. The benefits you get from using it might be outweighed by the costs of learning it, modifying your APIs to be Reactive all the way down, and maintaining the code afterwards, so tread carefully. 209 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 1.3.3.RELEASE 9 | 10 | 11 | com.example 12 | parent-demo 13 | 0.0.1-SNAPSHOT 14 | pom 15 | Parent Demo 16 | 17 | 18 | gather 19 | reactive 20 | flux 21 | 22 | 23 | 24 | 1.8 25 | 26 | 27 | 28 | 29 | spring-libs-snapshot 30 | http://repo.spring.io/libs-snapshot 31 | 32 | true 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | spring-libs-snapshot 43 | http://repo.spring.io/libs-snapshot 44 | 45 | true 46 | 47 | 48 | true 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /reactive/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.4.0.M3' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url "https://repo.spring.io/snapshot" } 8 | maven { url "https://repo.spring.io/milestone" } 9 | } 10 | dependencies { 11 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 12 | } 13 | } 14 | 15 | apply plugin: 'java' 16 | apply plugin: 'eclipse' 17 | apply plugin: 'spring-boot' 18 | 19 | jar { 20 | baseName = 'demo' 21 | version = '0.0.1-SNAPSHOT' 22 | } 23 | sourceCompatibility = 1.8 24 | targetCompatibility = 1.8 25 | 26 | repositories { 27 | mavenCentral() 28 | maven { url "https://repo.spring.io/snapshot" } 29 | maven { url "https://repo.spring.io/milestone" } 30 | mavenLocal() 31 | } 32 | 33 | 34 | dependencies { 35 | compile('org.springframework.boot:spring-boot-starter') 36 | compile('io.projectreactor:reactor-core:2.5.0.BUILD-SNAPSHOT') 37 | compile('org.springframework.boot:spring-boot-starter-reactive-web:0.0.1-SNAPSHOT') 38 | testCompile('org.springframework.boot:spring-boot-starter-test') 39 | } 40 | 41 | 42 | eclipse { 43 | classpath { 44 | containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') 45 | containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /reactive/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/reactive-notes/5b574e7a378c048ac906a31ddfa7edd848b843fd/reactive/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /reactive/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip 6 | -------------------------------------------------------------------------------- /reactive/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 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 | -------------------------------------------------------------------------------- /reactive/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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 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 Windows 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 | -------------------------------------------------------------------------------- /reactive/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | reactive 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | reactive 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.BUILD-SNAPSHOT 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 3.0.2.RELEASE 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter 31 | 32 | 33 | io.projectreactor.ipc 34 | reactor-netty 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.boot.experimental 48 | spring-boot-dependencies-web-reactive 49 | 0.1.0.BUILD-SNAPSHOT 50 | pom 51 | import 52 | 53 | 54 | 55 | 56 | 57 | 58 | tomcat 59 | 60 | true 61 | 62 | 63 | 64 | 65 | org.springframework.boot.experimental 66 | spring-boot-starter-web-reactive 67 | 68 | 69 | 70 | 71 | netty 72 | 73 | 74 | 75 | org.springframework.boot.experimental 76 | spring-boot-starter-web-reactive 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-tomcat 81 | 82 | 83 | 84 | 85 | 86 | 87 | jetty 88 | 89 | 90 | 91 | org.springframework.boot.experimental 92 | spring-boot-starter-web-reactive 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-starter-tomcat 97 | 98 | 99 | 100 | 101 | org.eclipse.jetty 102 | jetty-server 103 | 104 | 105 | org.eclipse.jetty 106 | jetty-servlet 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.springframework.boot 116 | spring-boot-maven-plugin 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | spring-snapshots 125 | Spring Snapshots 126 | https://repo.spring.io/snapshot 127 | 128 | true 129 | 130 | 131 | 132 | spring-milestones 133 | Spring Milestones 134 | https://repo.spring.io/milestone 135 | 136 | false 137 | 138 | 139 | 140 | 141 | 142 | spring-snapshots 143 | Spring Snapshots 144 | https://repo.spring.io/snapshot 145 | 146 | true 147 | 148 | 149 | 150 | spring-milestones 151 | Spring Milestones 152 | https://repo.spring.io/milestone 153 | 154 | false 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /reactive/src/main/java/com/example/ReactiveApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import java.util.Map; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.concurrent.ConcurrentMap; 21 | import java.util.concurrent.atomic.AtomicLong; 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.springframework.beans.factory.annotation.Value; 26 | import org.springframework.boot.SpringApplication; 27 | import org.springframework.boot.autoconfigure.SpringBootApplication; 28 | import org.springframework.http.HttpStatus; 29 | import org.springframework.http.client.reactive.ReactorClientHttpConnector; 30 | import org.springframework.web.bind.annotation.RequestMapping; 31 | import org.springframework.web.bind.annotation.RestController; 32 | import org.springframework.web.client.RestTemplate; 33 | import org.springframework.web.client.reactive.ClientWebRequestBuilders; 34 | import org.springframework.web.client.reactive.ResponseExtractors; 35 | import org.springframework.web.client.reactive.WebClient; 36 | 37 | import reactor.core.publisher.Flux; 38 | import reactor.core.publisher.Mono; 39 | import reactor.core.scheduler.Scheduler; 40 | import reactor.core.scheduler.Schedulers; 41 | 42 | @SpringBootApplication 43 | @RestController 44 | public class ReactiveApplication { 45 | 46 | @Value("${app.url:http://example.com}") 47 | private String url = "http://example.com"; 48 | 49 | private static Logger log = LoggerFactory.getLogger(ReactiveApplication.class); 50 | private RestTemplate restTemplate = new RestTemplate(); 51 | private WebClient client = new WebClient(new ReactorClientHttpConnector()); 52 | private Scheduler scheduler = Schedulers.elastic(); 53 | // private Scheduler scheduler = Computations.parallel("sub", 16, 40); 54 | 55 | @RequestMapping("/parallel") 56 | public Mono parallel() { 57 | log.info("Handling /parallel"); 58 | return Flux.range(1, 10) // <1> 59 | .log() // 60 | .flatMap( // <2> 61 | value -> Mono.fromCallable(() -> block(value)) // <3> 62 | .subscribeOn(scheduler), // <4> 63 | 4) // <5> 64 | .collect(Result::new, Result::add) // <6> 65 | .doOnSuccess(Result::stop); // <7> 66 | 67 | // <1> make 10 calls 68 | // <2> drop down to a new publisher to process in parallel 69 | // <3> blocking code here inside a Callable to defer execution 70 | // <4> subscribe to the slow publisher on a background thread 71 | // <5> concurrency hint in flatMap 72 | // <6> collect results and aggregate into a single object 73 | // <7> at the end stop the clock 74 | 75 | } 76 | 77 | @RequestMapping("/serial") 78 | public Mono serial() { 79 | Scheduler scheduler = Schedulers.parallel(); 80 | log.info("Handling /serial"); 81 | return Flux.range(1, 10) // <1> 82 | .log() // 83 | .map( // <2> 84 | this::block) // <3> 85 | .collect(Result::new, Result::add) // <4> 86 | .doOnSuccess(Result::stop) // <5> 87 | .subscribeOn(scheduler); // <6> 88 | // <1> make 10 calls 89 | // <2> stay in the same publisher chain 90 | // <3> blocking call not deferred (no point in this case) 91 | // <4> collect results and aggregate into a single object 92 | // <5> at the end stop the clock 93 | // <6> subscribe on a background thread 94 | } 95 | 96 | @RequestMapping("/netty") 97 | public Mono netty() { 98 | log.info("Handling /netty"); 99 | return Flux.range(1, 10) // <1> 100 | .log() // 101 | .flatMap(this::fetch) // <2> 102 | .collect(Result::new, Result::add) // 103 | .doOnSuccess(Result::stop); // <3> 104 | 105 | // <1> make 10 calls 106 | // <2> drop down to a new publisher to process in parallel 107 | // <3> at the end stop the clock 108 | 109 | } 110 | 111 | private HttpStatus block(int value) { 112 | return this.restTemplate.getForEntity(url, String.class, value).getStatusCode(); 113 | } 114 | 115 | private Mono fetch(int value) { 116 | return this.client.perform(ClientWebRequestBuilders.get(url)).extract(ResponseExtractors.response(String.class)) 117 | .map(response -> response.getStatusCode()); 118 | } 119 | 120 | public static void main(String[] args) { 121 | // System.setProperty("reactor.io.epoll", "false"); 122 | SpringApplication.run(ReactiveApplication.class, args); 123 | } 124 | 125 | } 126 | 127 | class Result { 128 | 129 | private ConcurrentMap counts = new ConcurrentHashMap<>(); 130 | 131 | private long timestamp = System.currentTimeMillis(); 132 | 133 | private long duration; 134 | 135 | public long add(HttpStatus status) { 136 | AtomicLong value = this.counts.getOrDefault(status, new AtomicLong()); 137 | this.counts.putIfAbsent(status, value); 138 | return value.incrementAndGet(); 139 | } 140 | 141 | public void stop() { 142 | this.duration = System.currentTimeMillis() - this.timestamp; 143 | } 144 | 145 | public long getDuration() { 146 | return this.duration; 147 | } 148 | 149 | public Map getCounts() { 150 | return this.counts; 151 | } 152 | 153 | } -------------------------------------------------------------------------------- /reactive/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | app.url: http://stores.cfapps.io -------------------------------------------------------------------------------- /reactive/src/test/java/com/example/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.example; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | import org.junit.Test; 21 | import org.junit.runner.RunWith; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.boot.test.context.SpringBootTest; 24 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 25 | import org.springframework.test.context.junit4.SpringRunner; 26 | 27 | @RunWith(SpringRunner.class) 28 | @SpringBootTest(webEnvironment=WebEnvironment.NONE, properties="server.port=0") 29 | public class DemoApplicationTests { 30 | 31 | @Autowired 32 | private ReactiveApplication application; 33 | 34 | @Test 35 | public void contextLoads() { 36 | assertThat(application.parallel().block().getCounts()).isNotNull(); 37 | } 38 | 39 | @Test 40 | public void nettyEndpoint() { 41 | assertThat(application.netty().block().getCounts()).isNotNull(); 42 | } 43 | 44 | @Test 45 | public void serialEndpoint() { 46 | assertThat(application.serial().block().getCounts()).isNotNull(); 47 | } 48 | 49 | } 50 | --------------------------------------------------------------------------------