├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── catalog-info.yaml ├── copyright-header.txt ├── pom.xml └── src ├── main └── java │ └── com │ └── spotify │ └── futures │ ├── CombinedFutures.java │ ├── CompletableFutures.java │ ├── ConcurrencyReducer.java │ ├── Function3.java │ ├── Function4.java │ ├── Function5.java │ ├── Function6.java │ └── package-info.java ├── site └── site.xml └── test └── java └── com └── spotify └── futures ├── CompletableFuturesTest.java ├── ConcurrencyReducerTest.java ├── FunctionsTest.java └── jmh └── AllAsListBenchmark.java /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | name: Build/Test 14 | 15 | runs-on: ubuntu-latest 16 | 17 | # Test on each Java LTS version 18 | strategy: 19 | matrix: 20 | java_version: [8, 11, 17] 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 2 25 | - name: Set up JDK ${{ matrix.java_version }} 26 | uses: actions/setup-java@v2 27 | with: 28 | distribution: 'zulu' 29 | java-version: ${{ matrix.java_version }} 30 | - name: Cache Maven packages 31 | uses: actions/cache@v2 32 | with: 33 | path: ~/.m2 34 | key: ${{ runner.os }}-java-${{ matrix.java_version }}-m2-${{ hashFiles('**/pom.xml') }} 35 | restore-keys: ${{ runner.os }}-java-${{ matrix.java_version }}-m2 36 | - name: Build with Maven 37 | run: mvn --batch-mode --update-snapshots verify 38 | - name: Codecov 39 | uses: codecov/codecov-action@v1 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.git* 3 | !.travis.yml 4 | target/ 5 | pom.xml.releaseBackup 6 | release.properties 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2013-2016 Spotify AB 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | completable-futures 2 | Copyright 2016 Spotify AB 3 | 4 | This product includes software developed at 5 | Spotify AB (http://www.spotify.com/). 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # completable-futures 2 | [![Build Status](https://github.com/spotify/completable-futures/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/spotify/completable-futures/actions/workflows/ci.yaml) 3 | [![Test Coverage](https://img.shields.io/codecov/c/github/spotify/completable-futures/master.svg)](https://codecov.io/github/spotify/completable-futures?branch=master) 4 | [![Maven Central](https://img.shields.io/maven-central/v/com.spotify/completable-futures.svg)](https://maven-badges.herokuapp.com/maven-central/com.spotify/completable-futures) 5 | [![License](https://img.shields.io/github/license/spotify/completable-futures.svg)](LICENSE) 6 | 7 | completable-futures is a set of utility functions to simplify working with asynchronous code in 8 | Java8. 9 | 10 | ## Usage 11 | 12 | Using `completable-futures` requires Java 8 but has no additional dependencies. It is meant to be 13 | included as a library in other software. To import it with maven, add this to your pom: 14 | 15 | ```xml 16 | 17 | com.spotify 18 | completable-futures 19 | 0.3.1 20 | 21 | ``` 22 | 23 | ## Features 24 | 25 | ### Combining more than two things 26 | 27 | The builtin CompletableFuture API includes `future.thenCombine(otherFuture, function)` but if you 28 | want to combine more than two things it gets trickier. The `CompletableFutures` class contains the 29 | following APIs to simplify this use-case: 30 | 31 | #### allAsList 32 | 33 | If you want to join a list of futures of uniform type, use `allAsList`. This returns a future which 34 | completes to a list of all values of its inputs: 35 | 36 | ```java 37 | List> futures = asList(completedFuture("a"), completedFuture("b")); 38 | CompletableFuture> joined = CompletableFutures.allAsList(futures); 39 | ``` 40 | 41 | #### allAsMap 42 | 43 | If you want to join a map of key and value-futures, each of uniform type, use `allAsMap`. This 44 | returns a future which completes to a map of all key values of its inputs: 45 | 46 | ```java 47 | Map> futures = new HashMap() {{ 48 | put("key", completedFuture("value")); 49 | }}; 50 | CompletableFuture> joined = CompletableFutures.allAsMap(futures); 51 | ``` 52 | 53 | #### successfulAsList 54 | 55 | Works like `allAsList`, but futures that fail will not fail the joined future. Instead, the 56 | defaultValueMapper function will be called once for each failed future and value returned will be 57 | put in the resulting list on the place corresponding to the failed future. The default value 58 | returned by the function may be anything, such as `null` or `Optional.empty()`. 59 | 60 | ```java 61 | List> input = asList( 62 | completedFuture("a"), 63 | exceptionallyCompletedFuture(new RuntimeException("boom"))); 64 | CompletableFuture> joined = CompletableFutures.successfulAsList(input, t -> "default"); 65 | ``` 66 | 67 | #### joinList 68 | 69 | `joinList` is a stream collector that combines multiple futures into a list. This is handy if you 70 | apply an asynchronous operation to a collection of entities: 71 | 72 | ```java 73 | collection.stream() 74 | .map(this::someAsyncFunction) 75 | .collect(CompletableFutures.joinList()) 76 | .thenApply(this::consumeList) 77 | ``` 78 | 79 | #### joinMap 80 | 81 | `joinMap` is a stream collector that applies an asynchronous operation to each element of the 82 | stream, and associates the result of that operation to a key derived from the original element. 83 | This is useful when you need to keep the association between the entity that triggered the 84 | asynchronous operation and the result of that operation: 85 | 86 | ```java 87 | collection.stream() 88 | .collect(joinMap(this::toKey, this::someAsyncFunc)) 89 | .thenApply(this::consumeMap) 90 | ``` 91 | 92 | #### combine 93 | 94 | If you want to combine more than two futures of different types, use the `combine` method: 95 | 96 | ```java 97 | CompletableFutures.combine(f1, f2, (a, b) -> a + b); 98 | CompletableFutures.combine(f1, f2, f3, (a, b, c) -> a + b + c); 99 | CompletableFutures.combine(f1, f2, f3, f4, (a, b, c, d) -> a + b + c + d); 100 | CompletableFutures.combine(f1, f2, f3, f4, f5, (a, b, c, d, e) -> a + b + c + d + e); 101 | ``` 102 | 103 | #### combineFutures 104 | 105 | If you want to combine multiple futures into another future, use `combineFutures`: 106 | 107 | ```java 108 | CompletableFutures.combineFutures(f1, f2, (a, b) -> completedFuture(a + b)); 109 | CompletableFutures.combineFutures(f1, f2, f3, (a, b, c) -> completedFuture(a + b + c)); 110 | CompletableFutures.combineFutures(f1, f2, f3, f4, (a, b, c, d) -> completedFuture(a + b + c + d)); 111 | CompletableFutures.combineFutures(f1, f2, f3, f4, f5, (a, b, c, d, e) -> completedFuture(a + b + c + d + e)); 112 | ``` 113 | 114 | #### Combine an arbitrary number of futures 115 | 116 | If you want to combine more than six futures of different types, use the other `combine` method. 117 | Since it supports vararg usage, the function is now the first argument. 118 | The `CombinedFutures` object that is input to the function can be used to extract values from the input functions. 119 | 120 | This is effectively the same thing as calling `join()` on the input future, but it's safer because 121 | calling `.get(f)` on a future that is not part of the combine, you will get an `IllegalArgumentException`. 122 | 123 | This prevents accidental misuse where you would join on a future that is either not complete, or might never complete 124 | at all. 125 | 126 | ```java 127 | CompletionStage f1; 128 | CompletionStage f2; 129 | CompletionStage result = combine(combined -> combined.get(f1) + combined.get(f2), f1, f2); 130 | ``` 131 | 132 | If you want to do this in a `combineFutures` form, you can do that like this: 133 | 134 | ```java 135 | CompletionStage f1; 136 | CompletionStage f2; 137 | CompletionStage result = dereference(combine(combined -> completedFuture(combined.get(f1) + combined.get(f2)), f1, f2)); 138 | ``` 139 | 140 | ### Scheduling 141 | 142 | #### Polling an external resource 143 | 144 | If you are dealing with a long-running external task that only exposes a polling API, you can 145 | transform that into a future like so: 146 | 147 | ```java 148 | Supplier> pollingTask = () -> Optional.ofNullable(resource.result()); 149 | Duration frequency = Duration.ofSeconds(2); 150 | CompletableFuture result = CompletableFutures.poll(pollingTask, frequency, executor); 151 | ``` 152 | 153 | ### Missing parts of the CompletableFuture API 154 | 155 | The `CompletableFutures` class includes utility functions for operating on futures that is missing 156 | from the builtin API. 157 | 158 | #### handleCompose 159 | 160 | Like `CompletableFuture.handle` but lets you return a new `CompletionStage` instead of a 161 | direct value. 162 | 163 | ```java 164 | CompletionStage composed = handleCompose(future, (value, throwable) -> completedFuture("hello")); 165 | ``` 166 | 167 | #### exceptionallyCompose 168 | 169 | Like `CompletableFuture.exceptionally` but lets you return a new `CompletionStage` instead of a 170 | direct value. 171 | 172 | ```java 173 | CompletionStage composed = CompletableFutures.exceptionallyCompose(future, throwable -> completedFuture("fallback")); 174 | ``` 175 | 176 | #### dereference 177 | 178 | Unwrap a `CompletionStage>` to a plain `CompletionStage`. 179 | 180 | ```java 181 | CompletionStage> wrapped = completedFuture(completedFuture("hello")); 182 | CompletionStage unwrapped = CompletableFutures.dereference(wrapped); 183 | ``` 184 | 185 | #### supplyAsyncCompose 186 | 187 | Like `CompletableFuture.supplyAsync` but unwraps a `CompletionStage>` to a plain `CompletionStage` when 188 | the `Supplier` returns a `CompletionStage`. 189 | 190 | ```java 191 | CompletionStage suppliedStage = completedFuture("hello").thenApply(stage -> stage + "-chained"); 192 | CompletionStage outputStage = CompletableFutures.supplyAsyncCompose(suppliedStage); 193 | ``` 194 | 195 | #### exceptionallyCompletedFuture 196 | 197 | Creates a new future that is already exceptionally completed with the given exception. 198 | 199 | ```java 200 | return CompletableFutures.exceptionallyCompletedFuture(new RuntimeException("boom")); 201 | ``` 202 | 203 | ## License 204 | 205 | Copyright 2016 Spotify AB. 206 | Licensed under the [Apache License, Version 2.0](LICENSE). 207 | 208 | ## Code of Conduct 209 | 210 | This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are 211 | expected to honor this code. 212 | 213 | ## Releases 214 | 215 | See the instructions in [spotify/foss-root][foss-root] 216 | 217 | [code-of-conduct]: https://github.com/spotify/code-of-conduct/blob/master/code-of-conduct.md 218 | [foss-root]: https://github.com/spotify/foss-root 219 | 220 | ## Ownership 221 | 222 | The Weaver squad is currently owning this project internally. 223 | We are currently in the evaluating process of the ownership of this and other OSS Java libraries. 224 | The ownership takes into account **ONLY** security maintenance. 225 | 226 | This repo is also co-owned by other people: 227 | 228 | * [mattnworb](https://github.com/mattnworb) 229 | * [spkrka](https://github.com/spkrka) 230 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Resource 3 | metadata: 4 | name: completable-futures 5 | spec: 6 | type: library 7 | owner: weaver 8 | -------------------------------------------------------------------------------- /copyright-header.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Spotify AB 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * 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, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.spotify 7 | foss-root 8 | 16 9 | 10 | 11 | completable-futures 12 | 0.3.7-SNAPSHOT 13 | 14 | completable-futures 15 | Convenience utilities for working with asynchronous Java 8 16 | https://spotify.github.io/completable-futures 17 | 18 | 19 | github 20 | 21 | 22 | 23 | scm:git:https://github.com/spotify/completable-futures.git 24 | scm:git:ssh://git@github.com/spotify/completable-futures.git 25 | HEAD 26 | scm:https://github.com/spotify/completable-futures 27 | 28 | 29 | 30 | 31 | The Apache Software License, Version 2.0 32 | http://www.apache.org/licenses/LICENSE-2.0.txt 33 | repo 34 | 35 | 36 | 37 | 38 | Spotify AB 39 | http://www.spotify.com 40 | 41 | 42 | 43 | 44 | marcbr 45 | Marc Bruggmann 46 | marcbr@spotify.com 47 | Spotify AB 48 | http://www.spotify.com 49 | 50 | developer 51 | 52 | +1 53 | 54 | 55 | krka 56 | Kristofer Karlsson 57 | krka@spotify.com 58 | Spotify AB 59 | http://www.spotify.com 60 | 61 | developer 62 | 63 | +1 64 | 65 | 66 | dflemstr 67 | David Flemström 68 | dflemstr@spotify.com 69 | Spotify AB 70 | http://www.spotify.com 71 | 72 | developer 73 | 74 | +1 75 | 76 | 77 | 78 | 79 | 80 | 81 | com.google.code.findbugs 82 | jsr305 83 | 3.0.2 84 | provided 85 | 86 | 87 | 88 | 89 | org.junit.jupiter 90 | junit-jupiter 91 | 5.9.3 92 | test 93 | 94 | 95 | org.hamcrest 96 | hamcrest-core 97 | 2.2 98 | test 99 | 100 | 101 | org.openjdk.jmh 102 | jmh-core 103 | 1.21 104 | test 105 | 106 | 107 | org.openjdk.jmh 108 | jmh-generator-annprocess 109 | 1.21 110 | test 111 | 112 | 113 | org.mockito 114 | mockito-core 115 | 4.11.0 116 | test 117 | 118 | 119 | org.jmock 120 | jmock 121 | 2.12.0 122 | test 123 | 124 | 125 | 126 | 127 | 128 | 129 | maven-checkstyle-plugin 130 | 131 | 132 | maven-enforcer-plugin 133 | 134 | 135 | maven-failsafe-plugin 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-jar-plugin 140 | 141 | 142 | 143 | com.spotify.completablefutures 144 | 145 | 146 | 147 | 148 | 149 | org.apache.maven.plugins 150 | maven-javadoc-plugin 151 | 152 | private 153 | false 154 | 155 | 156 | 157 | attach-javadocs 158 | 159 | jar 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/futures/CombinedFutures.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import java.util.IdentityHashMap; 23 | import java.util.List; 24 | import java.util.concurrent.CompletionStage; 25 | 26 | public final class CombinedFutures { 27 | 28 | private static final Object NULL_PLACEHOLDER = new Object(); 29 | 30 | private final IdentityHashMap, Object> map = new IdentityHashMap<>(); 31 | 32 | CombinedFutures(List> stages) { 33 | for (final CompletionStage stage : stages) { 34 | Object value = stage.toCompletableFuture().join(); 35 | if (value == null) { 36 | value = NULL_PLACEHOLDER; 37 | } 38 | map.put(stage, value); 39 | } 40 | } 41 | 42 | public T get(CompletionStage stage) { 43 | final Object value = map.get(stage); 44 | if (value == null) { 45 | throw new IllegalArgumentException( 46 | "Can not resolve values for futures that were not part of the combine"); 47 | } 48 | if (value == NULL_PLACEHOLDER) { 49 | return null; 50 | } 51 | return (T) value; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/futures/CompletableFutures.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import static java.util.stream.Collectors.collectingAndThen; 23 | import static java.util.stream.Collectors.toList; 24 | import static java.util.stream.Collectors.toMap; 25 | 26 | import java.time.Duration; 27 | import java.util.ArrayList; 28 | import java.util.Arrays; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.Optional; 33 | import java.util.concurrent.CancellationException; 34 | import java.util.concurrent.CompletableFuture; 35 | import java.util.concurrent.CompletionException; 36 | import java.util.concurrent.CompletionStage; 37 | import java.util.concurrent.Executor; 38 | import java.util.concurrent.ScheduledExecutorService; 39 | import java.util.concurrent.ScheduledFuture; 40 | import java.util.concurrent.TimeUnit; 41 | import java.util.function.BiFunction; 42 | import java.util.function.Function; 43 | import java.util.function.Supplier; 44 | import java.util.stream.Collector; 45 | 46 | /** 47 | * A collection of static utility methods that extend the 48 | * {@link java.util.concurrent.CompletableFuture Java completable future} API. 49 | * 50 | * @since 0.1.0 51 | */ 52 | public final class CompletableFutures { 53 | 54 | private CompletableFutures() { 55 | throw new IllegalAccessError("This class must not be instantiated."); 56 | } 57 | 58 | /** 59 | * Returns a new {@link CompletableFuture} which completes to a list of all values of its input 60 | * stages, if all succeed. The list of results is in the same order as the input stages. 61 | * 62 | *

As soon as any of the given stages complete exceptionally, then the returned future also does so, 63 | * with a {@link CompletionException} holding this exception as its cause. 64 | * 65 | *

If no stages are provided, returns a future holding an empty list. 66 | * 67 | * @param stages the stages to combine 68 | * @param the common super-type of all of the input stages, that determines the monomorphic 69 | * type of the output future 70 | * @return a future that completes to a list of the results of the supplied stages 71 | * @throws NullPointerException if the stages list or any of its elements are {@code null} 72 | * @since 0.1.0 73 | */ 74 | public static CompletableFuture> allAsList( 75 | List> stages) { 76 | // We use traditional for-loops instead of streams here for performance reasons, 77 | // see AllAsListBenchmark 78 | 79 | @SuppressWarnings("unchecked") // generic array creation 80 | final CompletableFuture[] all = new CompletableFuture[stages.size()]; 81 | for (int i = 0; i < stages.size(); i++) { 82 | all[i] = stages.get(i).toCompletableFuture(); 83 | } 84 | 85 | CompletableFuture allOf = CompletableFuture.allOf(all); 86 | 87 | for (int i = 0; i < all.length; i++) { 88 | all[i].exceptionally(throwable -> { 89 | if (!allOf.isDone()) { 90 | allOf.completeExceptionally(throwable); 91 | } 92 | return null; // intentionally unused 93 | }); 94 | } 95 | 96 | return allOf 97 | .thenApply(ignored -> { 98 | final List result = new ArrayList<>(all.length); 99 | for (int i = 0; i < all.length; i++) { 100 | result.add(all[i].join()); 101 | } 102 | return result; 103 | }); 104 | } 105 | 106 | /** 107 | * Returns a new {@link CompletableFuture} which completes to a map of all values of its input 108 | * stages, if all succeed. 109 | * 110 | *

If any of the given stages complete exceptionally, then the returned future also does so, 111 | * with a {@link CompletionException} holding this exception as its cause. 112 | * 113 | *

If no stages are provided, returns a future holding an empty map. 114 | * 115 | * @param map the map of stages to combine 116 | * @param the common super-type of the keys 117 | * @param the common super-type of all of the input value-stages, that determines the 118 | * monomorphic type of the output future 119 | * @return a future that completes to a map of the results of the supplied keys and value-stages 120 | * @throws NullPointerException if value-stages or any of its elements are {@code null} 121 | * @since 0.3.3 122 | */ 123 | public static CompletableFuture> allAsMap( 124 | Map> map) { 125 | 126 | final List keys = new ArrayList<>(map.keySet()); 127 | @SuppressWarnings("unchecked") // generic array creation 128 | final CompletableFuture[] values = new CompletableFuture[keys.size()]; 129 | for (int i = 0; i < keys.size(); i++) { 130 | values[i] = map.get(keys.get(i)).toCompletableFuture(); 131 | } 132 | return CompletableFuture.allOf(values) 133 | .thenApply( 134 | ignored -> { 135 | final Map result = new HashMap<>(values.length); 136 | for (int i = 0; i < values.length; i++) { 137 | result.put(keys.get(i), values[i].join()); 138 | } 139 | return result; 140 | }); 141 | } 142 | 143 | /** 144 | * Returns a new {@link CompletableFuture} which completes to a list of values of those input 145 | * stages that succeeded. The list of results is in the same order as the input stages. For failed 146 | * stages, the defaultValueMapper will be called, and the value returned from that function will 147 | * be put in the resulting list. 148 | * 149 | *

If no stages are provided, returns a future holding an empty list. 150 | * 151 | * @param stages the stages to combine. 152 | * @param defaultValueMapper a function that will be called when a future completes exceptionally 153 | * to provide a default value to place in the resulting list 154 | * @param the common type of all of the input stages, that determines the type of the 155 | * output future 156 | * @return a future that completes to a list of the results of the supplied stages 157 | * @throws NullPointerException if the stages list or any of its elements are {@code null} 158 | */ 159 | public static CompletableFuture> successfulAsList( 160 | List> stages, 161 | Function defaultValueMapper) { 162 | return stages.stream() 163 | .map(f -> f.exceptionally(defaultValueMapper)) 164 | .collect(joinList()); 165 | } 166 | 167 | /** 168 | * Returns a new {@code CompletableFuture} that is already exceptionally completed with 169 | * the given exception. 170 | * 171 | * @param throwable the exception 172 | * @param an arbitrary type for the returned future; can be anything since the future 173 | * will be exceptionally completed and thus there will never be a value of type 174 | * {@code T} 175 | * @return a future that exceptionally completed with the supplied exception 176 | * @throws NullPointerException if the supplied throwable is {@code null} 177 | * @since 0.1.0 178 | */ 179 | public static CompletableFuture exceptionallyCompletedFuture(Throwable throwable) { 180 | final CompletableFuture future = new CompletableFuture<>(); 181 | future.completeExceptionally(throwable); 182 | return future; 183 | } 184 | 185 | /** 186 | * Collect a stream of {@link CompletionStage}s into a single future holding a list of the 187 | * joined entities. 188 | * 189 | *

Usage: 190 | * 191 | *

{@code
192 |    * collection.stream()
193 |    *     .map(this::someAsyncFunc)
194 |    *     .collect(joinList())
195 |    *     .thenApply(this::consumeList)
196 |    * }
197 | * 198 | *

The generated {@link CompletableFuture} will complete to a list of all entities, in the 199 | * order they were encountered in the original stream. Similar to 200 | * {@link CompletableFuture#allOf(CompletableFuture[])}, if any of the input futures complete 201 | * exceptionally, then the returned CompletableFuture also does so, with a 202 | * {@link CompletionException} holding this exception as its cause. 203 | * 204 | * @param the common super-type of all of the input stages, that determines the monomorphic 205 | * type of the output future 206 | * @param the implementation of {@link CompletionStage} that the stream contains 207 | * @return a new {@link CompletableFuture} according to the rules outlined in the method 208 | * description 209 | * @throws NullPointerException if any future in the stream is {@code null} 210 | * @since 0.1.0 211 | */ 212 | public static > 213 | Collector>> joinList() { 214 | return collectingAndThen(toList(), CompletableFutures::allAsList); 215 | } 216 | 217 | /** 218 | * Collect a stream of Objects into a single future holding a Map whose keys are the result of 219 | * applying the provided mapping function to the input elements, and whose values are the 220 | * corresponding joined entities. 221 | * 222 | *

Usage: 223 | * 224 | *

{@code
225 |    * collection.stream()
226 |    *     .collect(joinMap(this::toKey, this::someAsyncFunc))
227 |    *     .thenApply(this::consumeMap)
228 |    * }
229 | * 230 | *

The generated {@link CompletableFuture} will complete to a Map of all entities. Similar 231 | * to {@link CompletableFuture#allOf(CompletableFuture[])}, if any of the input futures 232 | * complete exceptionally, then the returned CompletableFuture also does so, with a 233 | * {@link CompletionException} holding this exception as its cause. 234 | * 235 | * @param keyMapper a mapping function to produce keys 236 | * @param valueFutureMapper a mapping function to produce futures that will eventually produce 237 | * the values 238 | * @param the type of the input elements 239 | * @param the output type of the key mapping function 240 | * @param the common super-type of the stages returned by the value future mapping function 241 | * @return a new {@link CompletableFuture} according to the rules outlined in the method 242 | * description 243 | * @throws NullPointerException if valueFutureMapper returns {@code null} for any input element 244 | * @since 0.3.4 245 | */ 246 | public static Collector>> joinMap( 247 | Function keyMapper, 248 | Function> valueFutureMapper){ 249 | Collector>> collector = 250 | toMap(keyMapper, valueFutureMapper); 251 | return collectingAndThen(collector, CompletableFutures::allAsMap); 252 | } 253 | 254 | /** 255 | * Checks that a stage is completed. 256 | * 257 | * @param stage the {@link CompletionStage} to check 258 | * @throws IllegalStateException if the stage is not completed 259 | * @since 0.1.0 260 | */ 261 | public static void checkCompleted(CompletionStage stage) { 262 | if (!stage.toCompletableFuture().isDone()) { 263 | throw new IllegalStateException("future was not completed"); 264 | } 265 | } 266 | 267 | /** 268 | * Gets the value of a completed stage. 269 | * 270 | * @param stage a completed {@link CompletionStage} 271 | * @param the type of the value that the stage completes into 272 | * @return the value of the stage if it has one 273 | * @throws IllegalStateException if the stage is not completed 274 | * @since 0.1.0 275 | */ 276 | public static T getCompleted(CompletionStage stage) { 277 | CompletableFuture future = stage.toCompletableFuture(); 278 | checkCompleted(future); 279 | return future.join(); 280 | } 281 | 282 | /** 283 | * Gets the exception from an exceptionally completed future 284 | * @param stage an exceptionally completed {@link CompletionStage} 285 | * @param the type of the value that the stage completes into 286 | * @return the exception the stage has completed with 287 | * @throws IllegalStateException if the stage is not completed exceptionally 288 | * @throws CancellationException if the stage was cancelled 289 | * @throws UnsupportedOperationException if the {@link CompletionStage} does not 290 | * support the {@link CompletionStage#toCompletableFuture()} operation 291 | */ 292 | public static Throwable getException(CompletionStage stage) { 293 | CompletableFuture future = stage.toCompletableFuture(); 294 | if (!future.isCompletedExceptionally()) { 295 | throw new IllegalStateException("future was not completed exceptionally"); 296 | } 297 | try { 298 | future.join(); 299 | return null; 300 | } catch (CompletionException x) { 301 | return x.getCause(); 302 | } 303 | } 304 | 305 | /** 306 | * Returns a new stage that, when this stage completes either normally or exceptionally, is 307 | * executed with this stage's result and exception as arguments to the supplied function. 308 | * 309 | *

When this stage is complete, the given function is invoked with the result (or {@code null} 310 | * if none) and the exception (or {@code null} if none) of this stage as arguments, and the 311 | * function's result is used to complete the returned stage. 312 | * 313 | *

This differs from 314 | * {@link java.util.concurrent.CompletionStage#handle(java.util.function.BiFunction)} in that the 315 | * function should return a {@link java.util.concurrent.CompletionStage} rather than the value 316 | * directly. 317 | * 318 | * @param stage the {@link CompletionStage} to compose 319 | * @param fn the function to use to compute the value of the 320 | * returned {@link CompletionStage} 321 | * @param the type of the input stage's value. 322 | * @param the function's return type 323 | * @return the new {@link CompletionStage} 324 | * @since 0.1.0 325 | */ 326 | public static CompletionStage handleCompose( 327 | CompletionStage stage, 328 | BiFunction> fn) { 329 | return dereference(stage.handle(fn)); 330 | } 331 | 332 | /** 333 | * Returns a new stage that, when this stage completes 334 | * exceptionally, is executed with this stage's exception as the 335 | * argument to the supplied function. Otherwise, if this stage 336 | * completes normally, then the returned stage also completes 337 | * normally with the same value. 338 | * 339 | *

This differs from 340 | * {@link java.util.concurrent.CompletionStage#exceptionally(java.util.function.Function)} 341 | * in that the function should return a {@link java.util.concurrent.CompletionStage} rather than 342 | * the value directly. 343 | * 344 | * @param stage the {@link CompletionStage} to compose 345 | * @param fn the function to use to compute the value of the 346 | * returned {@link CompletionStage} if this stage completed 347 | * exceptionally 348 | * @param the type of the input stage's value. 349 | * @return the new {@link CompletionStage} 350 | * @since 0.1.0 351 | */ 352 | public static CompletionStage exceptionallyCompose( 353 | CompletionStage stage, 354 | Function> fn) { 355 | return dereference(wrap(stage).exceptionally(fn)); 356 | } 357 | 358 | /** 359 | * This allows for a stage to be the return type of supplier when using supplyAsync and returns a plain stage. 360 | * 361 | *

This differs from {@link 362 | * java.util.concurrent.CompletableFuture#supplyAsync(Supplier)} in that the 363 | * function should return a {@link java.util.concurrent.CompletionStage} rather than the {@link CompletionStage} of 364 | * a {@link CompletionStage} of a value when the return value of the {@link Supplier} is a {@link CompletionStage}. 365 | * 366 | * @param supplier a {@link Supplier} with a return value of {@link CompletionStage} 367 | * @param the type of the supplied stage's value 368 | * @return the new {@link CompletionStage} 369 | * @since 0.3.6 370 | */ 371 | public static CompletionStage supplyAsyncCompose(Supplier> supplier) { 372 | return dereference(CompletableFuture.supplyAsync(supplier)); 373 | } 374 | 375 | /** 376 | * This allows for a stage to be the return type of supplier when using supplyAsync and returns a plain stage. 377 | * 378 | *

This differs from {@link 379 | * java.util.concurrent.CompletableFuture#supplyAsync(Supplier, Executor)} in that the 380 | * function should return a {@link java.util.concurrent.CompletionStage} rather than the {@link CompletionStage} of 381 | * a {@link CompletionStage} of a value when the return value of the {@link Supplier} is a {@link CompletionStage}. 382 | * 383 | * @param supplier a {@link Supplier} with a return value of {@link CompletionStage} 384 | * @param executor a {@link Executor} the executor to use for asynchronous execution 385 | * @param the type of the supplied stage's value 386 | * @return the new {@link CompletionStage} 387 | * @since 0.3.6 388 | */ 389 | public static CompletionStage supplyAsyncCompose( 390 | Supplier> supplier, Executor executor) { 391 | return dereference(CompletableFuture.supplyAsync(supplier, executor)); 392 | } 393 | 394 | /** 395 | * This takes a stage of a stage of a value and returns a plain stage of a value. 396 | * 397 | * @param stage a {@link CompletionStage} of a {@link CompletionStage} of a value 398 | * @param the type of the inner stage's value. 399 | * @return the {@link CompletionStage} of the value 400 | * @since 0.1.0 401 | */ 402 | public static CompletionStage dereference( 403 | CompletionStage> stage) { 404 | return stage.thenCompose(Function.identity()); 405 | } 406 | 407 | private static CompletionStage> wrap(CompletionStage future) { 408 | //noinspection unchecked 409 | return future.thenApply(CompletableFuture::completedFuture); 410 | } 411 | 412 | /** 413 | * Combines multiple stages by applying a function. 414 | * 415 | * @param a the first stage. 416 | * @param b the second stage. 417 | * @param function the combining function. 418 | * @param the type of the combining function's return value. 419 | * @param the type of the first stage's value. 420 | * @param the type of the second stage's value. 421 | * @return a stage that completes into the return value of the supplied function. 422 | * @since 0.1.0 423 | */ 424 | public static CompletionStage combine( 425 | CompletionStage a, CompletionStage b, 426 | BiFunction function) { 427 | return a.thenCombine(b, function); 428 | } 429 | 430 | /** 431 | * Combines multiple stages by applying a function. 432 | * 433 | * @param a the first stage. 434 | * @param b the second stage. 435 | * @param c the third stage. 436 | * @param function the combining function. 437 | * @param the type of the combining function's return value. 438 | * @param the type of the first stage's value. 439 | * @param the type of the second stage's value. 440 | * @param the type of the third stage's value. 441 | * @return a stage that completes into the return value of the supplied function. 442 | * @since 0.1.0 443 | */ 444 | public static CompletionStage combine( 445 | CompletionStage a, CompletionStage b, CompletionStage c, 446 | Function3 function) { 447 | final CompletableFuture af = a.toCompletableFuture(); 448 | final CompletableFuture bf = b.toCompletableFuture(); 449 | final CompletableFuture cf = c.toCompletableFuture(); 450 | 451 | return CompletableFuture.allOf(af, bf, cf) 452 | .thenApply(ignored -> function.apply(af.join(), bf.join(), cf.join())); 453 | } 454 | 455 | /** 456 | * Combines multiple stages by applying a function. 457 | * 458 | * @param a the first stage. 459 | * @param b the second stage. 460 | * @param c the third stage. 461 | * @param d the fourth stage. 462 | * @param function the combining function. 463 | * @param the type of the combining function's return value. 464 | * @param the type of the first stage's value. 465 | * @param the type of the second stage's value. 466 | * @param the type of the third stage's value. 467 | * @param the type of the fourth stage's value. 468 | * @return a stage that completes into the return value of the supplied function. 469 | * @since 0.1.0 470 | */ 471 | public static CompletionStage combine( 472 | CompletionStage a, CompletionStage b, CompletionStage c, CompletionStage d, 473 | Function4 function) { 474 | final CompletableFuture af = a.toCompletableFuture(); 475 | final CompletableFuture bf = b.toCompletableFuture(); 476 | final CompletableFuture cf = c.toCompletableFuture(); 477 | final CompletableFuture df = d.toCompletableFuture(); 478 | 479 | return CompletableFuture.allOf(af, bf, cf, df) 480 | .thenApply(ignored -> function.apply(af.join(), bf.join(), cf.join(), df.join())); 481 | } 482 | 483 | /** 484 | * Combines multiple stages by applying a function. 485 | * 486 | * @param a the first stage. 487 | * @param b the second stage. 488 | * @param c the third stage. 489 | * @param d the fourth stage. 490 | * @param e the fifth stage. 491 | * @param function the combining function. 492 | * @param the type of the combining function's return value. 493 | * @param the type of the first stage's value. 494 | * @param the type of the second stage's value. 495 | * @param the type of the third stage's value. 496 | * @param the type of the fourth stage's value. 497 | * @param the type of the fifth stage's value. 498 | * @return a stage that completes into the return value of the supplied function. 499 | * @since 0.1.0 500 | */ 501 | public static CompletionStage combine( 502 | CompletionStage a, CompletionStage b, CompletionStage c, 503 | CompletionStage d, CompletionStage e, 504 | Function5 function) { 505 | final CompletableFuture af = a.toCompletableFuture(); 506 | final CompletableFuture bf = b.toCompletableFuture(); 507 | final CompletableFuture cf = c.toCompletableFuture(); 508 | final CompletableFuture df = d.toCompletableFuture(); 509 | final CompletableFuture ef = e.toCompletableFuture(); 510 | 511 | return CompletableFuture.allOf(af, bf, cf, df, ef) 512 | .thenApply(ignored -> 513 | function.apply(af.join(), bf.join(), cf.join(), df.join(), ef.join())); 514 | } 515 | 516 | /** 517 | * Combines multiple stages by applying a function. 518 | * 519 | * @param a the first stage. 520 | * @param b the second stage. 521 | * @param c the third stage. 522 | * @param d the fourth stage. 523 | * @param e the fifth stage. 524 | * @param f the sixth stage. 525 | * @param function the combining function. 526 | * @param the type of the combining function's return value. 527 | * @param the type of the first stage's value. 528 | * @param the type of the second stage's value. 529 | * @param the type of the third stage's value. 530 | * @param the type of the fourth stage's value. 531 | * @param the type of the fifth stage's value. 532 | * @param the type of the sixth stage's value. 533 | * @return a stage that completes into the return value of the supplied function. 534 | * @since 0.3.2 535 | */ 536 | public static CompletionStage combine( 537 | CompletionStage a, CompletionStage b, CompletionStage c, 538 | CompletionStage d, CompletionStage e, CompletionStage f, 539 | Function6 function) { 540 | final CompletableFuture af = a.toCompletableFuture(); 541 | final CompletableFuture bf = b.toCompletableFuture(); 542 | final CompletableFuture cf = c.toCompletableFuture(); 543 | final CompletableFuture df = d.toCompletableFuture(); 544 | final CompletableFuture ef = e.toCompletableFuture(); 545 | final CompletableFuture ff = f.toCompletableFuture(); 546 | 547 | return CompletableFuture.allOf(af, bf, cf, df, ef, ff) 548 | .thenApply(ignored -> 549 | function.apply(af.join(), 550 | bf.join(), 551 | cf.join(), 552 | df.join(), 553 | ef.join(), 554 | ff.join())); 555 | } 556 | 557 | /** 558 | * Combines multiple stages by applying a function. 559 | * 560 | * @param function the combining function. 561 | * @param stages the stages to combine 562 | * @param the type of the combining function's return value. 563 | * @return a stage that completes into the return value of the supplied function. 564 | * @since 0.4.0 565 | */ 566 | public static CompletionStage combine( 567 | Function function, CompletionStage... stages) { 568 | return combine(function, Arrays.asList(stages)); 569 | } 570 | 571 | /** 572 | * Combines multiple stages by applying a function. 573 | * 574 | * @param function the combining function. 575 | * @param stages the stages to combine 576 | * @param the type of the combining function's return value. 577 | * @return a stage that completes into the return value of the supplied function. 578 | * @since 0.4.0 579 | */ 580 | public static CompletionStage combine( 581 | Function function, List> stages) { 582 | @SuppressWarnings("unchecked") // generic array creation 583 | final CompletableFuture[] all = new CompletableFuture[stages.size()]; 584 | for (int i = 0; i < stages.size(); i++) { 585 | all[i] = stages.get(i).toCompletableFuture(); 586 | } 587 | return CompletableFuture.allOf(all).thenApply(ignored -> function.apply(new CombinedFutures(stages))); 588 | } 589 | 590 | /** 591 | * Composes multiple stages into another stage using a function. 592 | * 593 | * @param a the first stage. 594 | * @param b the second stage. 595 | * @param function the combining function. 596 | * @param the type of the composed {@link CompletionStage}. 597 | * @param the type of the first stage's value. 598 | * @param the type of the second stage's value. 599 | * @return a stage that is composed from the input stages using the function. 600 | * @throws UnsupportedOperationException if any of the {@link CompletionStage}s 601 | * do not interoperate with CompletableFuture 602 | */ 603 | public static CompletionStage combineFutures( 604 | CompletionStage a, 605 | CompletionStage b, 606 | BiFunction> function) { 607 | final CompletableFuture af = a.toCompletableFuture(); 608 | final CompletableFuture bf = b.toCompletableFuture(); 609 | 610 | return CompletableFuture.allOf(af, bf) 611 | .thenCompose(ignored -> function.apply(af.join(), bf.join())); 612 | } 613 | 614 | /** 615 | * Composes multiple stages into another stage using a function. 616 | * 617 | * @param a the first stage. 618 | * @param b the second stage. 619 | * @param c the third stage. 620 | * @param function the combining function. 621 | * @param the type of the composed {@link CompletionStage}. 622 | * @param the type of the first stage's value. 623 | * @param the type of the second stage's value. 624 | * @param the type of the third stage's value. 625 | * @return a stage that is composed from the input stages using the function. 626 | * @throws UnsupportedOperationException if any of the {@link CompletionStage}s 627 | * do not interoperate with CompletableFuture 628 | */ 629 | public static CompletionStage combineFutures( 630 | CompletionStage a, 631 | CompletionStage b, 632 | CompletionStage c, 633 | Function3> function) { 634 | final CompletableFuture af = a.toCompletableFuture(); 635 | final CompletableFuture bf = b.toCompletableFuture(); 636 | final CompletableFuture cf = c.toCompletableFuture(); 637 | 638 | return CompletableFuture.allOf(af, bf, cf) 639 | .thenCompose(ignored -> function.apply(af.join(), 640 | bf.join(), 641 | cf.join())); 642 | } 643 | 644 | /** 645 | * Composes multiple stages into another stage using a function. 646 | * 647 | * @param a the first stage. 648 | * @param b the second stage. 649 | * @param c the third stage. 650 | * @param d the fourth stage. 651 | * @param function the combining function. 652 | * @param the type of the composed {@link CompletionStage}. 653 | * @param the type of the first stage's value. 654 | * @param the type of the second stage's value. 655 | * @param the type of the third stage's value. 656 | * @param the type of the fourth stage's value. 657 | * @return a stage that is composed from the input stages using the function. 658 | * @throws UnsupportedOperationException if any of the {@link CompletionStage}s 659 | * do not interoperate with CompletableFuture 660 | */ 661 | public static CompletionStage combineFutures( 662 | CompletionStage a, 663 | CompletionStage b, 664 | CompletionStage c, 665 | CompletionStage d, 666 | Function4> function) { 667 | final CompletableFuture af = a.toCompletableFuture(); 668 | final CompletableFuture bf = b.toCompletableFuture(); 669 | final CompletableFuture cf = c.toCompletableFuture(); 670 | final CompletableFuture df = d.toCompletableFuture(); 671 | 672 | return CompletableFuture.allOf(af, bf, cf, df) 673 | .thenCompose(ignored -> function.apply(af.join(), bf.join(), cf.join(), df.join())); 674 | } 675 | 676 | /** 677 | * Composes multiple stages into another stage using a function. 678 | * 679 | * @param a the first stage. 680 | * @param b the second stage. 681 | * @param c the third stage. 682 | * @param d the fourth stage. 683 | * @param e the fifth stage. 684 | * @param function the combining function. 685 | * @param the type of the composed {@link CompletionStage}. 686 | * @param the type of the first stage's value. 687 | * @param the type of the second stage's value. 688 | * @param the type of the third stage's value. 689 | * @param the type of the fourth stage's value. 690 | * @param the type of the fifth stage's value. 691 | * @return a stage that is composed from the input stages using the function. 692 | * @throws UnsupportedOperationException if any of the {@link CompletionStage}s 693 | * do not interoperate with CompletableFuture 694 | */ 695 | public static CompletionStage combineFutures( 696 | CompletionStage a, 697 | CompletionStage b, 698 | CompletionStage c, 699 | CompletionStage d, 700 | CompletionStage e, 701 | Function5> function) { 702 | final CompletableFuture af = a.toCompletableFuture(); 703 | final CompletableFuture bf = b.toCompletableFuture(); 704 | final CompletableFuture cf = c.toCompletableFuture(); 705 | final CompletableFuture df = d.toCompletableFuture(); 706 | final CompletableFuture ef = e.toCompletableFuture(); 707 | 708 | return CompletableFuture.allOf(af, bf, cf, df, ef) 709 | .thenCompose(ignored -> function.apply(af.join(), 710 | bf.join(), 711 | cf.join(), 712 | df.join(), 713 | ef.join())); 714 | } 715 | 716 | /** 717 | * Composes multiple stages into another stage using a function. 718 | * 719 | * @param a the first stage. 720 | * @param b the second stage. 721 | * @param c the third stage. 722 | * @param d the fourth stage. 723 | * @param e the fifth stage. 724 | * @param f the sixth stage. 725 | * @param function the combining function. 726 | * @param the type of the composed {@link CompletionStage}. 727 | * @param the type of the first stage's value. 728 | * @param the type of the second stage's value. 729 | * @param the type of the third stage's value. 730 | * @param the type of the fourth stage's value. 731 | * @param the type of the fifth stage's value. 732 | * @param the type of the sixth stage's value. 733 | * @return a stage that is composed from the input stages using the function. 734 | * @throws UnsupportedOperationException if any of the {@link CompletionStage}s 735 | * do not interoperate with CompletableFuture 736 | */ 737 | public static CompletionStage combineFutures( 738 | CompletionStage a, 739 | CompletionStage b, 740 | CompletionStage c, 741 | CompletionStage d, 742 | CompletionStage e, 743 | CompletionStage f, 744 | Function6> function) { 745 | final CompletableFuture af = a.toCompletableFuture(); 746 | final CompletableFuture bf = b.toCompletableFuture(); 747 | final CompletableFuture cf = c.toCompletableFuture(); 748 | final CompletableFuture df = d.toCompletableFuture(); 749 | final CompletableFuture ef = e.toCompletableFuture(); 750 | final CompletableFuture ff = f.toCompletableFuture(); 751 | 752 | return CompletableFuture.allOf(af, bf, cf, df, ef, ff) 753 | .thenCompose(ignored -> function.apply(af.join(), 754 | bf.join(), 755 | cf.join(), 756 | df.join(), 757 | ef.join(), 758 | ff.join())); 759 | } 760 | 761 | /** 762 | * Polls an external resource periodically until it returns a non-empty result. 763 | * 764 | *

The polling task should return {@code Optional.empty()} until it becomes available, and 765 | * then {@code Optional.of(result)}. If the polling task throws an exception or returns null, 766 | * that will cause the result future to complete exceptionally. 767 | * 768 | *

Canceling the returned future will cancel the scheduled polling task as well. 769 | * 770 | *

Note that on a ScheduledThreadPoolExecutor the polling task might remain allocated for up 771 | * to {@code frequency} time after completing or being cancelled. If you have lots of polling 772 | * operations or a long polling frequency, consider setting {@code removeOnCancelPolicy} to true. 773 | * See {@link java.util.concurrent.ScheduledThreadPoolExecutor#setRemoveOnCancelPolicy(boolean)}. 774 | * 775 | * @param pollingTask the polling task 776 | * @param frequency the frequency to run the polling task at 777 | * @param executorService the executor service to schedule the polling task on 778 | * @param the type of the result of the polling task, that will be returned when 779 | * the task succeeds. 780 | * @return a future completing to the result of the polling task once that becomes available 781 | */ 782 | public static CompletableFuture poll( 783 | final Supplier> pollingTask, 784 | final Duration frequency, 785 | final ScheduledExecutorService executorService) { 786 | final CompletableFuture result = new CompletableFuture<>(); 787 | final ScheduledFuture scheduled = executorService.scheduleAtFixedRate( 788 | () -> pollTask(pollingTask, result), 0, frequency.toMillis(), TimeUnit.MILLISECONDS); 789 | result.whenComplete((r, ex) -> scheduled.cancel(true)); 790 | return result; 791 | } 792 | 793 | private static void pollTask( 794 | final Supplier> pollingTask, 795 | final CompletableFuture resultFuture) { 796 | try { 797 | pollingTask.get().ifPresent(resultFuture::complete); 798 | } catch (Exception ex) { 799 | resultFuture.completeExceptionally(ex); 800 | } 801 | } 802 | 803 | } 804 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/futures/ConcurrencyReducer.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import static java.util.Objects.requireNonNull; 23 | 24 | import java.util.concurrent.ArrayBlockingQueue; 25 | import java.util.concurrent.BlockingQueue; 26 | import java.util.concurrent.Callable; 27 | import java.util.concurrent.CompletableFuture; 28 | import java.util.concurrent.CompletionStage; 29 | import java.util.concurrent.Semaphore; 30 | 31 | /** 32 | * {@link ConcurrencyReducer} is used to queue tasks which will be 33 | * executed in a manner reducing the number of concurrent tasks. 34 | * 35 | * Note: This is a port of ConcurrencyLimiter from futures-extra for use with CompletionStages 36 | */ 37 | public class ConcurrencyReducer { 38 | 39 | private final BlockingQueue> queue; 40 | private final Semaphore limit; 41 | private final int maxQueueSize; 42 | 43 | private final int maxConcurrency; 44 | 45 | private ConcurrencyReducer(int maxConcurrency, int maxQueueSize) { 46 | this.maxConcurrency = maxConcurrency; 47 | this.maxQueueSize = maxQueueSize; 48 | if (maxConcurrency <= 0) { 49 | throw new IllegalArgumentException("maxConcurrency must be at least 0"); 50 | } 51 | 52 | if (maxQueueSize <= 0) { 53 | throw new IllegalArgumentException("maxQueueSize must be at least 0"); 54 | } 55 | this.queue = new ArrayBlockingQueue<>(maxQueueSize); 56 | this.limit = new Semaphore(maxConcurrency); 57 | } 58 | 59 | /** 60 | * @param maxConcurrency maximum number of futures in progress, 61 | * @param maxQueueSize maximum number of jobs in queue. This is a soft bound and may be 62 | * temporarily exceeded if add() is called concurrently. 63 | * @return a new concurrency limiter 64 | */ 65 | public static ConcurrencyReducer create(int maxConcurrency, int maxQueueSize) { 66 | return new ConcurrencyReducer<>(maxConcurrency, maxQueueSize); 67 | } 68 | 69 | /** 70 | * the callable function will run as soon as the currently active set of futures is less than the 71 | * maxConcurrency limit. 72 | * 73 | * @param callable - a function that creates a future. 74 | * @return a proxy future that completes with the future created by the input function. This 75 | * future will be immediately failed with {@link CapacityReachedException} if the soft queue 76 | * size limit is exceeded. 77 | */ 78 | public CompletableFuture add(final Callable> callable) { 79 | requireNonNull(callable); 80 | final CompletableFuture response = new CompletableFuture<>(); 81 | final Job job = new Job<>(callable, response); 82 | if (!queue.offer(job)) { 83 | final String message = "Queue size has reached capacity: " + maxQueueSize; 84 | return CompletableFutures.exceptionallyCompletedFuture(new CapacityReachedException(message)); 85 | } 86 | pump(); 87 | return response; 88 | } 89 | 90 | /** 91 | * @return the number of callables that are queued up and haven't started yet. 92 | */ 93 | public int numQueued() { 94 | return queue.size(); 95 | } 96 | 97 | /** 98 | * @return the number of currently active futures that have not yet completed. 99 | */ 100 | public int numActive() { 101 | return maxConcurrency - limit.availablePermits(); 102 | } 103 | 104 | /** 105 | * @return the number of additional callables that can be queued before failing. 106 | */ 107 | public int remainingQueueCapacity() { 108 | return queue.remainingCapacity(); 109 | } 110 | 111 | /** 112 | * @return the number of additional callables that can be run without queueing. 113 | */ 114 | public int remainingActiveCapacity() { 115 | return limit.availablePermits(); 116 | } 117 | 118 | /** 119 | * Does one of two things: 1) return a job and acquire a permit from the semaphore 2) return null 120 | * and does not acquire a permit from the semaphore 121 | */ 122 | private Job grabJob() { 123 | if (!limit.tryAcquire()) { 124 | return null; 125 | } 126 | 127 | final Job job = queue.poll(); 128 | if (job != null) { 129 | return job; 130 | } 131 | 132 | limit.release(); 133 | return null; 134 | } 135 | 136 | private void pump() { 137 | Job job; 138 | while ((job = grabJob()) != null) { 139 | final CompletableFuture response = job.response; 140 | 141 | if (response.isCancelled()) { 142 | limit.release(); 143 | } else { 144 | invoke(response, job.callable); 145 | } 146 | } 147 | } 148 | 149 | private void invoke(final CompletableFuture response, 150 | final Callable> callable) { 151 | final CompletionStage future; 152 | try { 153 | future = callable.call(); 154 | if (future == null) { 155 | limit.release(); 156 | response.completeExceptionally(new NullPointerException()); 157 | return; 158 | } 159 | } catch (Throwable e) { 160 | limit.release(); 161 | response.completeExceptionally(e); 162 | return; 163 | } 164 | 165 | future.whenComplete( 166 | (result, t) -> { 167 | if (t != null) { 168 | limit.release(); 169 | response.completeExceptionally(t); 170 | pump(); 171 | } else { 172 | limit.release(); 173 | response.complete(result); 174 | pump(); 175 | } 176 | }); 177 | } 178 | 179 | private static class Job { 180 | 181 | private final Callable> callable; 182 | private final CompletableFuture response; 183 | 184 | public Job(Callable> callable, CompletableFuture response) { 185 | this.callable = callable; 186 | this.response = response; 187 | } 188 | } 189 | 190 | public static class CapacityReachedException extends RuntimeException { 191 | 192 | public CapacityReachedException(String errorMessage) { 193 | super(errorMessage); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/futures/Function3.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import java.util.Objects; 23 | import java.util.function.Function; 24 | 25 | /** 26 | * Represents a function that accepts three arguments and produces a result. This is the 27 | * three-arity specialization of {@link Function}. 28 | * 29 | *

This is a functional interface whose functional method is 30 | * {@link #apply(Object, Object, Object)}. 31 | * 32 | * @param the type of the first argument to the function 33 | * @param the type of the second argument to the function 34 | * @param the type of the third argument to the function 35 | * @param the type of the result of the function 36 | * @see Function 37 | * @since 0.1.0 38 | */ 39 | @FunctionalInterface 40 | public interface Function3 { 41 | 42 | /** 43 | * Applies this function to the given arguments. 44 | * 45 | * @param a the first function argument 46 | * @param b the second function argument 47 | * @param c the third function argument 48 | * @return the function result 49 | * @since 0.1.0 50 | */ 51 | R apply(A a, B b, C c); 52 | 53 | /** 54 | * Returns a composed function that first applies this function to 55 | * its input, and then applies the {@code after} function to the result. 56 | * If evaluation of either function throws an exception, it is relayed to 57 | * the caller of the composed function. 58 | * 59 | * @param the type of output of the {@code after} function, and of the 60 | * composed function 61 | * @param after the function to apply after this function is applied 62 | * @return a composed function that first applies this function and then 63 | * applies the {@code after} function 64 | * @throws NullPointerException if after is null 65 | * @since 0.1.0 66 | */ 67 | default Function3 andThen(Function after) { 68 | Objects.requireNonNull(after); 69 | return (A a, B b, C c) -> after.apply(apply(a, b, c)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/futures/Function4.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import java.util.Objects; 23 | import java.util.function.Function; 24 | 25 | /** 26 | * Represents a function that accepts four arguments and produces a result. This is the four-arity 27 | * specialization of {@link Function}. 28 | * 29 | *

This is a functional interface whose functional method is 30 | * {@link #apply(Object, Object, Object, Object)}. 31 | * 32 | * @param the type of the first argument to the function 33 | * @param the type of the second argument to the function 34 | * @param the type of the third argument to the function 35 | * @param the type of the fourth argument to the function 36 | * @param the type of the result of the function 37 | * @see Function 38 | * @since 0.1.0 39 | */ 40 | @FunctionalInterface 41 | public interface Function4 { 42 | 43 | /** 44 | * Applies this function to the given arguments. 45 | * 46 | * @param a the first function argument 47 | * @param b the second function argument 48 | * @param c the third function argument 49 | * @param d the fourth function argument 50 | * @return the function result 51 | * @since 0.1.0 52 | */ 53 | R apply(A a, B b, C c, D d); 54 | 55 | /** 56 | * Returns a composed function that first applies this function to 57 | * its input, and then applies the {@code after} function to the result. 58 | * If evaluation of either function throws an exception, it is relayed to 59 | * the caller of the composed function. 60 | * 61 | * @param the type of output of the {@code after} function, and of the 62 | * composed function 63 | * @param after the function to apply after this function is applied 64 | * @return a composed function that first applies this function and then 65 | * applies the {@code after} function 66 | * @throws NullPointerException if after is null 67 | * @since 0.1.0 68 | */ 69 | default Function4 andThen(Function after) { 70 | Objects.requireNonNull(after); 71 | return (A a, B b, C c, D d) -> after.apply(apply(a, b, c, d)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/futures/Function5.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import java.util.Objects; 23 | import java.util.function.Function; 24 | 25 | /** 26 | * Represents a function that accepts five arguments and produces a result. This is the five-arity 27 | * specialization of {@link Function}. 28 | * 29 | *

This is a functional interface whose functional method is 30 | * {@link #apply(Object, Object, Object, Object, Object)}. 31 | * 32 | * @param the type of the first argument to the function 33 | * @param the type of the second argument to the function 34 | * @param the type of the third argument to the function 35 | * @param the type of the fourth argument to the function 36 | * @param the type of the fifth argument to the function 37 | * @param the type of the result of the function 38 | * @see Function 39 | * @since 0.1.0 40 | */ 41 | @FunctionalInterface 42 | public interface Function5 { 43 | 44 | /** 45 | * Applies this function to the given arguments. 46 | * 47 | * @param a the first function argument 48 | * @param b the second function argument 49 | * @param c the third function argument 50 | * @param d the fourth function argument 51 | * @param e the firth function argument 52 | * @return the function result 53 | * @since 0.1.0 54 | */ 55 | R apply(A a, B b, C c, D d, E e); 56 | 57 | /** 58 | * Returns a composed function that first applies this function to 59 | * its input, and then applies the {@code after} function to the result. 60 | * If evaluation of either function throws an exception, it is relayed to 61 | * the caller of the composed function. 62 | * 63 | * @param the type of output of the {@code after} function, and of the 64 | * composed function 65 | * @param after the function to apply after this function is applied 66 | * @return a composed function that first applies this function and then 67 | * applies the {@code after} function 68 | * @throws NullPointerException if after is null 69 | * @since 0.1.0 70 | */ 71 | default Function5 andThen(Function after) { 72 | Objects.requireNonNull(after); 73 | return (A a, B b, C c, D d, E e) -> after.apply(apply(a, b, c, d, e)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/futures/Function6.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import java.util.Objects; 23 | import java.util.function.Function; 24 | 25 | /** 26 | * Represents a function that accepts six arguments and produces a result. This is the six-arity 27 | * specialization of {@link Function}. 28 | * 29 | *

This is a functional interface whose functional method is 30 | * {@link #apply(Object, Object, Object, Object, Object, Object)}. 31 | * 32 | * @param the type of the first argument to the function 33 | * @param the type of the second argument to the function 34 | * @param the type of the third argument to the function 35 | * @param the type of the fourth argument to the function 36 | * @param the type of the fifth argument to the function 37 | * @param the type of the sixth argument to the function 38 | * @param the type of the result of the function 39 | * @see Function 40 | * @since 0.3.2 41 | */ 42 | @FunctionalInterface 43 | public interface Function6 { 44 | 45 | /** 46 | * Applies this function to the given arguments. 47 | * 48 | * @param a the first function argument 49 | * @param b the second function argument 50 | * @param c the third function argument 51 | * @param d the fourth function argument 52 | * @param e the fifth function argument 53 | * @param f the sixth function argument 54 | * @return the function result 55 | * @since 0.3.2 56 | */ 57 | R apply(A a, B b, C c, D d, E e, F f); 58 | 59 | /** 60 | * Returns a composed function that first applies this function to 61 | * its input, and then applies the {@code after} function to the result. 62 | * If evaluation of either function throws an exception, it is relayed to 63 | * the caller of the composed function. 64 | * 65 | * @param the type of output of the {@code after} function, and of the 66 | * composed function 67 | * @param after the function to apply after this function is applied 68 | * @return a composed function that first applies this function and then 69 | * applies the {@code after} function 70 | * @throws NullPointerException if after is null 71 | * @since 0.3.2 72 | */ 73 | default Function6 andThen(Function after) { 74 | Objects.requireNonNull(after); 75 | return (A a, B b, C c, D d, E e, F f) -> after.apply(apply(a, b, c, d, e, f)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/spotify/futures/package-info.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | 21 | /** 22 | * Extends the {@link java.util.concurrent.CompletableFuture Java completable future} API. The main 23 | * entry point of this package is the {@link com.spotify.futures.CompletableFutures} class, that 24 | * contains useful static utility methods. 25 | * 26 | *

This package uses the convention that all method parameters are non-null by default, i.e. 27 | * unless indicated by a {@link javax.annotation.Nullable @Nullable} annotation. 28 | * 29 | * @since 0.1.0 30 | */ 31 | @ParametersAreNonnullByDefault 32 | package com.spotify.futures; 33 | 34 | import javax.annotation.ParametersAreNonnullByDefault; 35 | -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | Spotify Completable Futures 7 | # 8 | 9 | 10 | 11 | org.apache.maven.skins 12 | maven-fluido-skin 13 | 1.5 14 | 15 | 16 | 17 | 18 | false 19 | true 20 | 21 | 22 | spotify/completable-futures 23 | right 24 | black 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | if (window.location.protocol != 'https:' && window.location.host.indexOf('github.io') > -1) { 33 | window.location.protocol = 'https'; 34 | } 35 | ]]> 36 | 37 |

38 | 39 | 40 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/futures/CompletableFuturesTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2023 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import static com.spotify.futures.CompletableFutures.allAsList; 23 | import static com.spotify.futures.CompletableFutures.allAsMap; 24 | import static com.spotify.futures.CompletableFutures.combine; 25 | import static com.spotify.futures.CompletableFutures.combineFutures; 26 | import static com.spotify.futures.CompletableFutures.dereference; 27 | import static com.spotify.futures.CompletableFutures.exceptionallyCompletedFuture; 28 | import static com.spotify.futures.CompletableFutures.exceptionallyCompose; 29 | import static com.spotify.futures.CompletableFutures.getCompleted; 30 | import static com.spotify.futures.CompletableFutures.getException; 31 | import static com.spotify.futures.CompletableFutures.handleCompose; 32 | import static com.spotify.futures.CompletableFutures.joinList; 33 | import static com.spotify.futures.CompletableFutures.joinMap; 34 | import static com.spotify.futures.CompletableFutures.poll; 35 | import static com.spotify.futures.CompletableFutures.successfulAsList; 36 | import static java.util.Arrays.asList; 37 | import static java.util.Collections.emptyList; 38 | import static java.util.Collections.emptyMap; 39 | import static java.util.Collections.singletonList; 40 | import static java.util.Collections.singletonMap; 41 | import static java.util.concurrent.CompletableFuture.completedFuture; 42 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 43 | import static java.util.concurrent.TimeUnit.SECONDS; 44 | import static java.util.function.Function.identity; 45 | import static java.util.stream.Collectors.toList; 46 | import static org.hamcrest.MatcherAssert.assertThat; 47 | import static org.hamcrest.Matchers.contains; 48 | import static org.hamcrest.Matchers.equalTo; 49 | import static org.hamcrest.Matchers.hasEntry; 50 | import static org.hamcrest.Matchers.hasSize; 51 | import static org.hamcrest.Matchers.instanceOf; 52 | import static org.hamcrest.Matchers.nullValue; 53 | import static org.hamcrest.core.Is.is; 54 | import static org.hamcrest.core.IsNot.not; 55 | import static org.junit.jupiter.api.Assertions.assertEquals; 56 | import static org.junit.jupiter.api.Assertions.assertNull; 57 | import static org.junit.jupiter.api.Assertions.assertThrows; 58 | import static org.mockito.ArgumentMatchers.any; 59 | import static org.mockito.ArgumentMatchers.anyLong; 60 | import static org.mockito.ArgumentMatchers.eq; 61 | import static org.mockito.Mockito.mock; 62 | import static org.mockito.Mockito.verify; 63 | import static org.mockito.Mockito.when; 64 | 65 | import java.lang.reflect.Constructor; 66 | import java.lang.reflect.InvocationTargetException; 67 | import java.time.Duration; 68 | import java.util.HashMap; 69 | import java.util.List; 70 | import java.util.Map; 71 | import java.util.Optional; 72 | import java.util.concurrent.CancellationException; 73 | import java.util.concurrent.CompletableFuture; 74 | import java.util.concurrent.CompletionException; 75 | import java.util.concurrent.CompletionStage; 76 | import java.util.concurrent.ExecutionException; 77 | import java.util.concurrent.ScheduledExecutorService; 78 | import java.util.concurrent.ScheduledFuture; 79 | import java.util.concurrent.TimeoutException; 80 | import java.util.function.Supplier; 81 | import java.util.stream.Stream; 82 | import org.hamcrest.CustomTypeSafeMatcher; 83 | import org.hamcrest.Matcher; 84 | import org.jmock.lib.concurrent.DeterministicScheduler; 85 | import org.junit.jupiter.api.BeforeEach; 86 | import org.junit.jupiter.api.Test; 87 | 88 | public class CompletableFuturesTest { 89 | 90 | private DeterministicScheduler executor; 91 | 92 | @BeforeEach 93 | public void setUp() { 94 | executor = new DeterministicScheduler(); 95 | } 96 | 97 | @Test 98 | public void allAsList_empty() { 99 | final List> input = emptyList(); 100 | assertThat(allAsList(input), completesTo(emptyList())); 101 | } 102 | 103 | @Test 104 | public void allAsList_one() { 105 | final String value = "a"; 106 | final List> input = singletonList(completedFuture(value)); 107 | assertThat(allAsList(input), completesTo(singletonList(value))); 108 | } 109 | 110 | @Test 111 | public void allAsList_multiple() { 112 | final List values = asList("a", "b", "c"); 113 | final List> input = 114 | values.stream().map(CompletableFuture::completedFuture).collect(toList()); 115 | assertThat(allAsList(input), completesTo(values)); 116 | } 117 | 118 | @Test 119 | public void allAsList_exceptional() { 120 | final RuntimeException boom = new RuntimeException("boom"); 121 | final List> input = 122 | asList(completedFuture("a"), exceptionallyCompletedFuture(boom), completedFuture("b")); 123 | 124 | final ExecutionException e = assertThrows(ExecutionException.class, allAsList(input)::get); 125 | assertThat(e.getCause(), is(equalTo(boom))); 126 | } 127 | 128 | @Test 129 | public void allAsList_exceptional_failFast() { 130 | final CompletableFuture incomplete = incompleteFuture(); 131 | final CompletableFuture failed = exceptionallyCompletedFuture(new TimeoutException()); 132 | final List> input = asList(incomplete, failed); 133 | 134 | final CompletionException e = 135 | assertThrows(CompletionException.class, () -> allAsList(input).join()); 136 | assertThat(e.getCause(), instanceOf(TimeoutException.class)); 137 | } 138 | 139 | @Test 140 | public void allAsList_null() { 141 | assertThrows(NullPointerException.class, () -> allAsList(null)); 142 | } 143 | 144 | @Test 145 | public void allAsList_containsNull() { 146 | final List> input = 147 | asList(completedFuture("a"), null, completedFuture("b")); 148 | 149 | assertThrows(NullPointerException.class, () -> allAsList(input)); 150 | } 151 | 152 | @Test 153 | public void allAsMap_empty() { 154 | final Map> input = emptyMap(); 155 | assertThat(allAsMap(input), completesTo(emptyMap())); 156 | } 157 | 158 | @Test 159 | public void allAsMap_one() { 160 | final String key = "1"; 161 | final String value = "a"; 162 | final Map> input = singletonMap(key, completedFuture(value)); 163 | assertThat(allAsMap(input), completesTo(singletonMap(key, value))); 164 | } 165 | 166 | @Test 167 | public void allAsMap_multiple() { 168 | final List keys = asList("1", "2", "3"); 169 | final List values = asList("a", "b", "c"); 170 | final List> stagedValues = 171 | values.stream().map(CompletableFuture::completedFuture).collect(toList()); 172 | final Map> input = asMap(keys, stagedValues); 173 | assertThat(allAsMap(input), completesTo(asMap(keys, values))); 174 | } 175 | 176 | @Test 177 | public void allAsMap_exceptional() throws Exception { 178 | final List keys = asList("1", "2", "3"); 179 | final RuntimeException boom = new RuntimeException("boom"); 180 | final List> values = 181 | asList(completedFuture("a"), exceptionallyCompletedFuture(boom), completedFuture("b")); 182 | Map> input = asMap(keys, values); 183 | final ExecutionException e = assertThrows(ExecutionException.class, allAsMap(input)::get); 184 | assertThat(e.getCause(), is(equalTo(boom))); 185 | } 186 | 187 | @Test 188 | public void allAsMap_null() { 189 | assertThrows(NullPointerException.class, () -> allAsMap(null)); 190 | } 191 | 192 | @Test 193 | public void allAsMap_valueContainsNull() { 194 | final List keys = asList("1", "2", "3"); 195 | final List> values = 196 | asList(completedFuture("a"), null, completedFuture("b")); 197 | 198 | final Map> input = asMap(keys, values); 199 | assertThrows(NullPointerException.class, () -> allAsMap(input)); 200 | } 201 | 202 | @Test 203 | public void allAsMap_keyContainsNull() { 204 | final List keys = asList("1", null, "3"); 205 | final List values = asList("a", "b", "c"); 206 | final List> stagedValues = 207 | values.stream().map(CompletableFuture::completedFuture).collect(toList()); 208 | ; 209 | 210 | final Map> input = asMap(keys, stagedValues); 211 | assertThat(allAsMap(input), completesTo(asMap(keys, values))); 212 | } 213 | 214 | @Test 215 | public void successfulAsList_exceptionalAndNull() { 216 | final List> input = 217 | asList( 218 | completedFuture("a"), 219 | exceptionallyCompletedFuture(new RuntimeException("boom")), 220 | completedFuture(null), 221 | completedFuture("d")); 222 | final List expected = asList("a", "default", null, "d"); 223 | assertThat(successfulAsList(input, t -> "default"), completesTo(expected)); 224 | } 225 | 226 | @Test 227 | public void getCompleted_done() { 228 | final CompletionStage future = completedFuture("hello"); 229 | assertThat(getCompleted(future), is("hello")); 230 | } 231 | 232 | @Test 233 | public void getCompleted_exceptional() { 234 | final Exception ex = new Exception("boom"); 235 | final CompletionStage future = exceptionallyCompletedFuture(ex); 236 | final Exception e = assertThrows(Exception.class, () -> getCompleted(future)); 237 | assertThat(e.getCause(), is(equalTo(ex))); 238 | } 239 | 240 | @Test 241 | public void getCompleted_nilResult() { 242 | final CompletableFuture future = completedFuture(null); 243 | assertNull(getCompleted(future)); 244 | } 245 | 246 | @Test 247 | public void getCompleted_pending() { 248 | final CompletionStage future = new CompletableFuture<>(); 249 | 250 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 251 | } 252 | 253 | @Test 254 | public void getException_completedExceptionally() { 255 | final Exception ex = new Exception("boom"); 256 | final CompletionStage future = exceptionallyCompletedFuture(ex); 257 | assertThat(getException(future), is(ex)); 258 | } 259 | 260 | @Test 261 | public void getException_completedNormally() { 262 | final CompletionStage future = completedFuture("hello"); 263 | assertThrows(IllegalStateException.class, () -> getException(future)); 264 | } 265 | 266 | @Test 267 | public void getException_pending() { 268 | final CompletionStage future = new CompletableFuture<>(); 269 | assertThrows(IllegalStateException.class, () -> getException(future)); 270 | } 271 | 272 | @Test 273 | public void getException_cancelled() { 274 | final CompletionStage future = new CompletableFuture<>(); 275 | future.toCompletableFuture().cancel(true); 276 | assertThrows(CancellationException.class, () -> getException(future)); 277 | } 278 | 279 | @Test 280 | public void getException_returnsNullIfImplementationDoesNotThrow() { 281 | final CompletableFuture future = new NonThrowingFuture<>(); 282 | future.completeExceptionally(new NullPointerException()); 283 | assertNull(getException(future)); 284 | } 285 | 286 | @Test 287 | public void exceptionallyCompletedFuture_completed() { 288 | final CompletableFuture future = exceptionallyCompletedFuture(new Exception("boom")); 289 | assertThat(future.isCompletedExceptionally(), is(true)); 290 | } 291 | 292 | @Test 293 | public void exceptionallyCompletedFuture_throws() throws Exception { 294 | final Exception ex = new Exception("boom"); 295 | final CompletableFuture future = exceptionallyCompletedFuture(ex); 296 | 297 | final ExecutionException e = assertThrows(ExecutionException.class, future::get); 298 | assertThat(e.getCause(), is(equalTo(ex))); 299 | } 300 | 301 | @Test 302 | public void exceptionallyCompletedFuture_null() { 303 | assertThrows(NullPointerException.class, () -> exceptionallyCompletedFuture(null)); 304 | } 305 | 306 | @Test 307 | public void joinList_empty() throws Exception { 308 | final List result = Stream.>of().collect(joinList()).get(); 309 | 310 | assertThat(result, not(nullValue())); 311 | assertThat(result, hasSize(0)); 312 | } 313 | 314 | @Test 315 | public void joinList_one() throws Exception { 316 | final List result = Stream.of(completedFuture("a")).collect(joinList()).get(); 317 | 318 | assertThat(result, hasSize(1)); 319 | assertThat(result, contains("a")); 320 | } 321 | 322 | @Test 323 | public void joinList_two() throws Exception { 324 | final CompletableFuture a = completedFuture("hello"); 325 | final CompletableFuture b = completedFuture("world"); 326 | 327 | final List result = Stream.of(a, b).collect(joinList()).get(); 328 | assertThat(result, contains("hello", "world")); 329 | } 330 | 331 | @Test 332 | public void joinList_mixedStageTypes() throws Exception { 333 | // Note that a and b use different subclasses of CompletionStage 334 | final CompletionStage a = completedFuture("hello"); 335 | final CompletableFuture b = completedFuture("world"); 336 | 337 | final List result = Stream.of(a, b).collect(joinList()).get(); 338 | assertThat(result, contains("hello", "world")); 339 | } 340 | 341 | @Test 342 | public void joinList_mixedValueTypes() throws Exception { 343 | // Note that a and b have different result types 344 | final CompletionStage a = completedFuture(3); 345 | final CompletableFuture b = completedFuture(4L); 346 | 347 | final Stream> s = Stream.of(a, b); 348 | 349 | final List result = s.collect(joinList()).get(); 350 | assertThat(result, contains(3, 4L)); 351 | } 352 | 353 | @Test 354 | public void joinList_exceptional() throws Exception { 355 | final RuntimeException ex = new RuntimeException("boom"); 356 | final CompletableFuture a = completedFuture("hello"); 357 | final CompletableFuture b = exceptionallyCompletedFuture(ex); 358 | 359 | final CompletableFuture> result = Stream.of(a, b).collect(joinList()); 360 | 361 | final ExecutionException e = assertThrows(ExecutionException.class, result::get); 362 | assertThat(e.getCause(), is(equalTo(ex))); 363 | } 364 | 365 | @Test 366 | public void joinList_containsNull() { 367 | final CompletableFuture a = completedFuture("hello"); 368 | final CompletableFuture b = null; 369 | final Stream> stream = Stream.of(a, b); 370 | 371 | assertThrows(NullPointerException.class, () -> stream.collect(joinList())); 372 | } 373 | 374 | @Test 375 | public void joinMap_empty() throws Exception { 376 | final Map result = 377 | Stream.of().collect(joinMap(identity(), CompletableFuture::completedFuture)).get(); 378 | 379 | assertThat(result, not(nullValue())); 380 | assertThat(result.values(), hasSize(0)); 381 | } 382 | 383 | @Test 384 | public void joinMap_one() throws Exception { 385 | final Map result = 386 | Stream.of("a").collect(joinMap(identity(), k -> completedFuture(k + "v"))).get(); 387 | 388 | assertThat(result.values(), hasSize(1)); 389 | assertThat(result.values(), contains("av")); 390 | assertThat(result.keySet(), contains("a")); 391 | } 392 | 393 | @Test 394 | public void joinMap_two() throws Exception { 395 | final Map result = 396 | Stream.of("hello", "world") 397 | .collect(joinMap(e -> "k " + e, e -> completedFuture("v " + e))) 398 | .get(); 399 | assertThat(result.entrySet(), hasSize(2)); 400 | assertThat(result, hasEntry("k hello", "v hello")); 401 | assertThat(result, hasEntry("k world", "v world")); 402 | } 403 | 404 | @Test 405 | public void joinMap_exceptional() throws Exception { 406 | final RuntimeException ex = new RuntimeException("boom"); 407 | final CompletableFuture a = completedFuture("hello"); 408 | final CompletableFuture b = exceptionallyCompletedFuture(ex); 409 | 410 | final CompletableFuture> result = 411 | Stream.of("a", "b").collect(joinMap(identity(), v -> v.equals("a") ? a : b)); 412 | 413 | final ExecutionException e = assertThrows(ExecutionException.class, result::get); 414 | assertThat(e.getCause(), is(equalTo(ex))); 415 | } 416 | 417 | @Test 418 | public void joinMap_containsNull() { 419 | final CompletableFuture a = completedFuture("hello"); 420 | final CompletableFuture b = null; 421 | final Stream stream = Stream.of("a", "b"); 422 | 423 | assertThrows( 424 | NullPointerException.class, 425 | () -> stream.collect(joinMap(identity(), v -> v.equals("a") ? a : b))); 426 | } 427 | 428 | @Test 429 | public void dereference_completed() { 430 | final CompletionStage future = completedFuture("hello"); 431 | final CompletionStage dereferenced = dereference(completedFuture(future)); 432 | 433 | assertThat(dereferenced, completesTo("hello")); 434 | } 435 | 436 | @Test 437 | public void dereference_exceptional() { 438 | final IllegalArgumentException ex = new IllegalArgumentException(); 439 | final CompletionStage future = exceptionallyCompletedFuture(ex); 440 | final CompletionStage dereferenced = dereference(completedFuture(future)); 441 | 442 | final CompletionException e = 443 | assertThrows(CompletionException.class, () -> getCompleted(dereferenced)); 444 | assertThat(e.getCause(), is(equalTo(ex))); 445 | } 446 | 447 | @Test 448 | public void dereference_null() { 449 | final CompletionStage dereferenced = dereference(completedFuture(null)); 450 | 451 | final CompletionException e = 452 | assertThrows(CompletionException.class, () -> getCompleted(dereferenced)); 453 | assertThat(e.getCause(), is(instanceOf(NullPointerException.class))); 454 | } 455 | 456 | @Test 457 | public void exceptionallyCompose_complete() { 458 | final CompletionStage future = exceptionallyCompletedFuture(new Exception("boom")); 459 | final CompletableFuture fallback = completedFuture("hello"); 460 | 461 | final CompletionStage composed = exceptionallyCompose(future, throwable -> fallback); 462 | 463 | assertThat(composed, completesTo("hello")); 464 | } 465 | 466 | @Test 467 | public void exceptionallyCompose_exceptional() { 468 | final CompletionStage future = exceptionallyCompletedFuture(new Exception("boom")); 469 | final IllegalStateException fallbackException = new IllegalStateException(); 470 | final CompletableFuture fallback = exceptionallyCompletedFuture(fallbackException); 471 | 472 | final CompletionStage composed = exceptionallyCompose(future, throwable -> fallback); 473 | 474 | final CompletionException e = 475 | assertThrows(CompletionException.class, () -> getCompleted(composed)); 476 | assertThat(e.getCause(), is(equalTo(fallbackException))); 477 | } 478 | 479 | @Test 480 | public void exceptionallyCompose_unused() { 481 | final CompletionStage future = completedFuture("hello"); 482 | final IllegalStateException fallbackException = new IllegalStateException(); 483 | final CompletableFuture fallback = exceptionallyCompletedFuture(fallbackException); 484 | 485 | final CompletionStage composed = exceptionallyCompose(future, throwable -> fallback); 486 | assertThat(composed, completesTo("hello")); 487 | } 488 | 489 | @Test 490 | public void exceptionallyCompose_throws() { 491 | final CompletionStage future = exceptionallyCompletedFuture(new Exception("boom")); 492 | final IllegalStateException ex = new IllegalStateException(); 493 | 494 | final CompletionStage composed = 495 | exceptionallyCompose( 496 | future, 497 | throwable -> { 498 | throw ex; 499 | }); 500 | 501 | final CompletionException e = 502 | assertThrows(CompletionException.class, () -> getCompleted(composed)); 503 | assertThat(e.getCause(), is(equalTo(ex))); 504 | } 505 | 506 | @Test 507 | public void exceptionallyCompose_returnsNull() { 508 | final CompletionStage future = exceptionallyCompletedFuture(new Exception("boom")); 509 | 510 | final CompletionStage composed = exceptionallyCompose(future, throwable -> null); 511 | 512 | final CompletionException e = 513 | assertThrows(CompletionException.class, () -> getCompleted(composed)); 514 | assertThat(e.getCause(), is(instanceOf(NullPointerException.class))); 515 | } 516 | 517 | @Test 518 | public void supplyAsyncCompose_completed() { 519 | final CompletionStage future = completedFuture("hello"); 520 | final CompletionStage composed = CompletableFutures.supplyAsyncCompose(() -> future); 521 | 522 | assertThat(composed, completesTo("hello")); 523 | } 524 | 525 | @Test 526 | public void supplyAsyncCompose_chain_completed() { 527 | final CompletionStage future = completedFuture("hello").thenApply(previous -> previous + "-chained"); 528 | final CompletionStage composed = CompletableFutures.supplyAsyncCompose(() -> future); 529 | 530 | assertThat(composed, completesTo("hello-chained")); 531 | } 532 | 533 | @Test 534 | public void supplyAsyncCompose_failure() { 535 | final IllegalStateException ex = new IllegalStateException(); 536 | final CompletionStage future = exceptionallyCompletedFuture(ex); 537 | final CompletionStage composed = CompletableFutures.supplyAsyncCompose(() -> future); 538 | 539 | final CompletionException e = 540 | assertThrows(CompletionException.class, () -> getCompleted(composed)); 541 | assertThat(e.getCause(), is(equalTo(ex))); 542 | } 543 | 544 | @Test 545 | public void handleCompose_completed() { 546 | final CompletionStage future = exceptionallyCompletedFuture(new Exception("boom")); 547 | 548 | final CompletionStage composed = 549 | handleCompose(future, (s, t) -> completedFuture("hello")); 550 | 551 | assertThat(composed, completesTo("hello")); 552 | } 553 | 554 | @Test 555 | public void handleCompose_failure() { 556 | final CompletionStage future = exceptionallyCompletedFuture(new Exception("boom")); 557 | final IllegalStateException ex = new IllegalStateException(); 558 | 559 | final CompletionStage composed = 560 | handleCompose(future, (s, t) -> exceptionallyCompletedFuture(ex)); 561 | 562 | final CompletionException e = 563 | assertThrows(CompletionException.class, () -> getCompleted(composed)); 564 | assertThat(e.getCause(), is(equalTo(ex))); 565 | } 566 | 567 | @Test 568 | public void handleCompose_throws() { 569 | final CompletionStage future = exceptionallyCompletedFuture(new Exception("boom")); 570 | final IllegalStateException ex = new IllegalStateException(); 571 | 572 | final CompletionStage composed = 573 | handleCompose( 574 | future, 575 | (s, throwable) -> { 576 | throw ex; 577 | }); 578 | 579 | final CompletionException e = 580 | assertThrows(CompletionException.class, () -> getCompleted(composed)); 581 | assertThat(e.getCause(), is(equalTo(ex))); 582 | } 583 | 584 | @Test 585 | public void handleCompose_returnsNull() { 586 | final CompletionStage future = exceptionallyCompletedFuture(new Exception("boom")); 587 | final CompletionStage composed = handleCompose(future, (s, throwable) -> null); 588 | 589 | final CompletionException e = 590 | assertThrows(CompletionException.class, () -> getCompleted(composed)); 591 | assertThat(e.getCause(), is(instanceOf(NullPointerException.class))); 592 | } 593 | 594 | @Test 595 | public void combine2_completed() { 596 | final CompletionStage future = 597 | combine(completedFuture("a"), completedFuture("b"), (a, b) -> a + b); 598 | 599 | assertThat(future, completesTo("ab")); 600 | } 601 | 602 | @Test 603 | public void combine2_exceptional() { 604 | final CompletionStage future = 605 | combine( 606 | completedFuture("a"), 607 | exceptionallyCompletedFuture(new IllegalStateException()), 608 | (a, b) -> a + b); 609 | 610 | final CompletionException e = 611 | assertThrows(CompletionException.class, () -> getCompleted(future)); 612 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 613 | } 614 | 615 | @Test 616 | public void combine3_completed() { 617 | final CompletionStage future = 618 | combine( 619 | completedFuture("a"), 620 | completedFuture("b"), 621 | completedFuture("c"), 622 | (a, b, c) -> a + b + c); 623 | 624 | assertThat(future, completesTo("abc")); 625 | } 626 | 627 | @Test 628 | public void combine3_exceptional() { 629 | final CompletionStage future = 630 | combine( 631 | completedFuture("a"), 632 | completedFuture("b"), 633 | exceptionallyCompletedFuture(new IllegalStateException()), 634 | (a, b, c) -> a + b + c); 635 | 636 | final CompletionException e = 637 | assertThrows(CompletionException.class, () -> getCompleted(future)); 638 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 639 | } 640 | 641 | @Test 642 | public void combine4_completed() { 643 | final CompletionStage future = 644 | combine( 645 | completedFuture("a"), 646 | completedFuture("b"), 647 | completedFuture("c"), 648 | completedFuture("d"), 649 | (a, b, c, d) -> a + b + c + d); 650 | 651 | assertThat(future, completesTo("abcd")); 652 | } 653 | 654 | @Test 655 | public void combine4_exceptional() { 656 | final CompletionStage future = 657 | combine( 658 | completedFuture("a"), 659 | completedFuture("b"), 660 | completedFuture("c"), 661 | exceptionallyCompletedFuture(new IllegalStateException()), 662 | (a, b, c, d) -> a + b + c + d); 663 | 664 | final CompletionException e = 665 | assertThrows(CompletionException.class, () -> getCompleted(future)); 666 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 667 | } 668 | 669 | @Test 670 | public void combine4_incomplete() { 671 | final CompletionStage future = 672 | combine( 673 | completedFuture("a"), 674 | completedFuture("b"), 675 | completedFuture("c"), 676 | incompleteFuture(), 677 | (a, b, c, d) -> a + b + c + d); 678 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 679 | } 680 | 681 | @Test 682 | public void combine5_completed() { 683 | final CompletionStage future = 684 | combine( 685 | completedFuture("a"), 686 | completedFuture("b"), 687 | completedFuture("c"), 688 | completedFuture("d"), 689 | completedFuture("e"), 690 | (a, b, c, d, e) -> a + b + c + d + e); 691 | 692 | assertThat(future, completesTo("abcde")); 693 | } 694 | 695 | @Test 696 | public void combine5_exceptional() { 697 | final CompletionStage future = 698 | combine( 699 | completedFuture("a"), 700 | completedFuture("b"), 701 | completedFuture("c"), 702 | completedFuture("d"), 703 | exceptionallyCompletedFuture(new IllegalStateException()), 704 | (a, b, c, d, e) -> a + b + c + d + e); 705 | 706 | final CompletionException e = 707 | assertThrows(CompletionException.class, () -> getCompleted(future)); 708 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 709 | } 710 | 711 | @Test 712 | public void combine5_incomplete() { 713 | final CompletionStage future = 714 | combine( 715 | completedFuture("a"), 716 | completedFuture("b"), 717 | completedFuture("c"), 718 | completedFuture("d"), 719 | incompleteFuture(), 720 | (a, b, c, d, e) -> a + b + c + d + e); 721 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 722 | } 723 | 724 | @Test 725 | public void combine6_completed() { 726 | final CompletionStage future = 727 | combine( 728 | completedFuture("a"), 729 | completedFuture("b"), 730 | completedFuture("c"), 731 | completedFuture("d"), 732 | completedFuture("e"), 733 | completedFuture("f"), 734 | (a, b, c, d, e, f) -> a + b + c + d + e + f); 735 | 736 | assertThat(future, completesTo("abcdef")); 737 | } 738 | 739 | @Test 740 | public void combine6_exceptional() { 741 | final CompletionStage future = 742 | combine( 743 | completedFuture("a"), 744 | completedFuture("b"), 745 | completedFuture("c"), 746 | completedFuture("d"), 747 | completedFuture("e"), 748 | exceptionallyCompletedFuture(new IllegalStateException()), 749 | (a, b, c, d, e, f) -> a + b + c + d + e + f); 750 | 751 | final CompletionException e = 752 | assertThrows(CompletionException.class, () -> getCompleted(future)); 753 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 754 | } 755 | 756 | @Test 757 | public void combine6_incomplete() { 758 | final CompletionStage future = 759 | combine( 760 | completedFuture("a"), 761 | completedFuture("b"), 762 | completedFuture("c"), 763 | completedFuture("d"), 764 | completedFuture("e"), 765 | incompleteFuture(), 766 | (a, b, c, d, e, f) -> a + b + c + d + e + f); 767 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 768 | } 769 | 770 | @Test 771 | public void combineFutures2_completed() { 772 | final CompletionStage future = 773 | combineFutures( 774 | completedFuture("a"), completedFuture("b"), (a, b) -> completedFuture(a + b)); 775 | 776 | assertThat(future, completesTo("ab")); 777 | } 778 | 779 | @Test 780 | public void combineFutures2_incomplete() { 781 | final CompletionStage future = 782 | combineFutures(completedFuture("a"), incompleteFuture(), (a, b) -> completedFuture(a + b)); 783 | 784 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 785 | } 786 | 787 | @Test 788 | public void combineFutures2_exceptional() { 789 | final CompletionStage future = 790 | combineFutures( 791 | completedFuture("a"), 792 | exceptionallyCompletedFuture(new IllegalStateException()), 793 | (a, b) -> completedFuture(a + b)); 794 | 795 | final CompletionException e = 796 | assertThrows(CompletionException.class, () -> getCompleted(future)); 797 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 798 | } 799 | 800 | @Test 801 | public void combineFutures3_completed() { 802 | final CompletionStage future = 803 | combineFutures( 804 | completedFuture("a"), 805 | completedFuture("b"), 806 | completedFuture("c"), 807 | (a, b, c) -> completedFuture(a + b + c)); 808 | 809 | assertThat(future, completesTo("abc")); 810 | } 811 | 812 | @Test 813 | public void combineFutures3_incomplete() { 814 | final CompletionStage future = 815 | combineFutures( 816 | completedFuture("a"), 817 | completedFuture("b"), 818 | incompleteFuture(), 819 | (a, b, c) -> completedFuture(a + b + c)); 820 | 821 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 822 | } 823 | 824 | @Test 825 | public void combineFutures3_exceptional() { 826 | final CompletionStage future = 827 | combineFutures( 828 | completedFuture("a"), 829 | completedFuture("b"), 830 | exceptionallyCompletedFuture(new IllegalStateException()), 831 | (a, b, c) -> completedFuture(a + b + c)); 832 | 833 | final CompletionException e = 834 | assertThrows(CompletionException.class, () -> getCompleted(future)); 835 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 836 | } 837 | 838 | @Test 839 | public void combineFutures4_completed() { 840 | final CompletionStage future = 841 | combineFutures( 842 | completedFuture("a"), 843 | completedFuture("b"), 844 | completedFuture("c"), 845 | completedFuture("d"), 846 | (a, b, c, d) -> completedFuture(a + b + c + d)); 847 | 848 | assertThat(future, completesTo("abcd")); 849 | } 850 | 851 | @Test 852 | public void combineFutures4_incomplete() { 853 | final CompletionStage future = 854 | combineFutures( 855 | completedFuture("a"), 856 | completedFuture("b"), 857 | completedFuture("c"), 858 | incompleteFuture(), 859 | (a, b, c, d) -> completedFuture(a + b + c + d)); 860 | 861 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 862 | } 863 | 864 | @Test 865 | public void combineFutures4_exceptional() { 866 | final CompletionStage future = 867 | combineFutures( 868 | completedFuture("a"), 869 | completedFuture("b"), 870 | completedFuture("c"), 871 | exceptionallyCompletedFuture(new IllegalStateException()), 872 | (a, b, c, d) -> completedFuture(a + b + c + d)); 873 | 874 | final CompletionException e = 875 | assertThrows(CompletionException.class, () -> getCompleted(future)); 876 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 877 | } 878 | 879 | @Test 880 | public void combineFutures5_completed() { 881 | final CompletionStage future = 882 | combineFutures( 883 | completedFuture("a"), 884 | completedFuture("b"), 885 | completedFuture("c"), 886 | completedFuture("d"), 887 | completedFuture("e"), 888 | (a, b, c, d, e) -> completedFuture(a + b + c + d + e)); 889 | 890 | assertThat(future, completesTo("abcde")); 891 | } 892 | 893 | @Test 894 | public void combineFutures5_incomplete() { 895 | final CompletionStage future = 896 | combineFutures( 897 | completedFuture("a"), 898 | completedFuture("b"), 899 | completedFuture("c"), 900 | completedFuture("d"), 901 | incompleteFuture(), 902 | (a, b, c, d, e) -> completedFuture(a + b + c + d + e)); 903 | 904 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 905 | } 906 | 907 | @Test 908 | public void combineFutures5_exceptional() { 909 | final CompletionStage future = 910 | combineFutures( 911 | completedFuture("a"), 912 | completedFuture("b"), 913 | completedFuture("c"), 914 | completedFuture("d"), 915 | exceptionallyCompletedFuture(new IllegalStateException()), 916 | (a, b, c, d, e) -> completedFuture(a + b + c + d + e)); 917 | 918 | final CompletionException e = 919 | assertThrows(CompletionException.class, () -> getCompleted(future)); 920 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 921 | } 922 | 923 | @Test 924 | public void combineFutures6_completed() { 925 | final CompletionStage future = 926 | combineFutures( 927 | completedFuture("a"), 928 | completedFuture("b"), 929 | completedFuture("c"), 930 | completedFuture("d"), 931 | completedFuture("e"), 932 | completedFuture("f"), 933 | (a, b, c, d, e, f) -> completedFuture(a + b + c + d + e + f)); 934 | 935 | assertThat(future, completesTo("abcdef")); 936 | } 937 | 938 | @Test 939 | public void combineFutures6_incomplete() { 940 | final CompletionStage future = 941 | combineFutures( 942 | completedFuture("a"), 943 | completedFuture("b"), 944 | completedFuture("c"), 945 | completedFuture("d"), 946 | completedFuture("e"), 947 | incompleteFuture(), 948 | (a, b, c, d, e, f) -> completedFuture(a + b + c + d + e + f)); 949 | 950 | assertThrows(IllegalStateException.class, () -> getCompleted(future)); 951 | } 952 | 953 | @Test 954 | public void combineFutures6_exceptional() { 955 | final CompletionStage future = 956 | combineFutures( 957 | completedFuture("a"), 958 | completedFuture("b"), 959 | completedFuture("c"), 960 | completedFuture("d"), 961 | completedFuture("e"), 962 | exceptionallyCompletedFuture(new IllegalStateException()), 963 | (a, b, c, d, e, f) -> completedFuture(a + b + c + d + e + f)); 964 | 965 | final CompletionException e = 966 | assertThrows(CompletionException.class, () -> getCompleted(future)); 967 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 968 | } 969 | 970 | @Test 971 | public void combineVararg_success() { 972 | final CompletionStage first = completedFuture("a"); 973 | final CompletionStage second = completedFuture("b"); 974 | final CompletionStage future = 975 | combine(combined -> combined.get(first) + combined.get(second), first, second); 976 | 977 | assertEquals("ab", getCompleted(future)); 978 | } 979 | 980 | @Test 981 | public void combineList_success() { 982 | final CompletionStage first = completedFuture("a"); 983 | final CompletionStage second = completedFuture("b"); 984 | final CompletionStage future = 985 | combine(combined -> combined.get(first) + combined.get(second), asList(first, second)); 986 | 987 | assertEquals("ab", getCompleted(future)); 988 | } 989 | 990 | @Test 991 | public void combineList_nullValues_success() { 992 | final CompletionStage first = completedFuture(null); 993 | final CompletionStage future = combine(combined -> combined.get(first), first); 994 | 995 | assertNull(getCompleted(future)); 996 | } 997 | 998 | @Test 999 | public void combineVararg_exceptional() { 1000 | final CompletionStage first = completedFuture("a"); 1001 | final CompletionStage second = 1002 | exceptionallyCompletedFuture(new IllegalStateException()); 1003 | final CompletionStage future = 1004 | combine(combined -> combined.get(first) + combined.get(second), first, second); 1005 | 1006 | final CompletionException e = 1007 | assertThrows(CompletionException.class, () -> getCompleted(future)); 1008 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 1009 | } 1010 | 1011 | @Test 1012 | public void combineList_exceptional() { 1013 | final CompletionStage first = completedFuture("a"); 1014 | final CompletionStage second = 1015 | exceptionallyCompletedFuture(new IllegalStateException()); 1016 | final CompletionStage future = 1017 | combine(combined -> combined.get(first) + combined.get(second), asList(first, second)); 1018 | 1019 | final CompletionException e = 1020 | assertThrows(CompletionException.class, () -> getCompleted(future)); 1021 | assertThat(e.getCause(), is(instanceOf(IllegalStateException.class))); 1022 | } 1023 | 1024 | @Test 1025 | public void combineVararg_misuse() { 1026 | final CompletionStage first = completedFuture("a"); 1027 | final CompletionStage second = completedFuture("b"); 1028 | final CompletionStage third = completedFuture("c"); 1029 | final CompletionStage future = 1030 | combine( 1031 | combined -> combined.get(first) + combined.get(second) + combined.get(third), 1032 | first, 1033 | second); 1034 | 1035 | final CompletionException e = 1036 | assertThrows(CompletionException.class, () -> getCompleted(future)); 1037 | assertThat(e.getCause(), is(instanceOf(IllegalArgumentException.class))); 1038 | } 1039 | 1040 | @Test 1041 | public void combineList_misuse() { 1042 | final CompletionStage first = completedFuture("a"); 1043 | final CompletionStage second = completedFuture("b"); 1044 | final CompletionStage third = completedFuture("c"); 1045 | final CompletionStage future = 1046 | combine( 1047 | combined -> combined.get(first) + combined.get(second) + combined.get(third), 1048 | asList(first, second)); 1049 | 1050 | final CompletionException e = 1051 | assertThrows(CompletionException.class, () -> getCompleted(future)); 1052 | assertThat(e.getCause(), is(instanceOf(IllegalArgumentException.class))); 1053 | } 1054 | 1055 | @Test 1056 | public void ctor_preventInstantiation() throws Exception { 1057 | final Constructor ctor = CompletableFutures.class.getDeclaredConstructor(); 1058 | ctor.setAccessible(true); 1059 | final InvocationTargetException e = 1060 | assertThrows(InvocationTargetException.class, ctor::newInstance); 1061 | assertThat(e.getCause(), is(instanceOf(IllegalAccessError.class))); 1062 | } 1063 | 1064 | @Test 1065 | public void poll_done() { 1066 | final Supplier> supplier = () -> Optional.of("done"); 1067 | final CompletableFuture future = poll(supplier, Duration.ofMillis(2), executor); 1068 | 1069 | executor.runNextPendingCommand(); 1070 | assertThat(future, completesTo("done")); 1071 | } 1072 | 1073 | @Test 1074 | public void poll_twice() { 1075 | final List> results = asList(Optional.empty(), Optional.of("done")); 1076 | final Supplier> supplier = results.iterator()::next; 1077 | final CompletableFuture future = poll(supplier, Duration.ofMillis(2), executor); 1078 | 1079 | executor.tick(1, MILLISECONDS); 1080 | assertThat(future.isDone(), is(false)); 1081 | 1082 | executor.tick(10, MILLISECONDS); 1083 | assertThat(future, completesTo("done")); 1084 | } 1085 | 1086 | @Test 1087 | public void poll_taskReturnsNull() throws Exception { 1088 | final Supplier> supplier = () -> null; 1089 | final CompletableFuture future = poll(supplier, Duration.ofMillis(2), executor); 1090 | 1091 | executor.runNextPendingCommand(); 1092 | final ExecutionException e = assertThrows(ExecutionException.class, future::get); 1093 | assertThat(e.getCause(), is(instanceOf(NullPointerException.class))); 1094 | } 1095 | 1096 | @Test 1097 | public void poll_taskThrows() throws Exception { 1098 | final RuntimeException ex = new RuntimeException("boom"); 1099 | final Supplier> supplier = 1100 | () -> { 1101 | throw ex; 1102 | }; 1103 | final CompletableFuture future = poll(supplier, Duration.ofMillis(2), executor); 1104 | 1105 | executor.runNextPendingCommand(); 1106 | final ExecutionException e = assertThrows(ExecutionException.class, future::get); 1107 | assertThat(e.getCause(), is(equalTo(ex))); 1108 | } 1109 | 1110 | @Test 1111 | public void poll_scheduled() { 1112 | final ScheduledExecutorService executor = mock(ScheduledExecutorService.class); 1113 | final Supplier> supplier = () -> Optional.of("hello"); 1114 | poll(supplier, Duration.ofMillis(2), executor); 1115 | 1116 | verify(executor).scheduleAtFixedRate(any(), eq(0L), eq(2L), eq(MILLISECONDS)); 1117 | } 1118 | 1119 | @Test 1120 | @SuppressWarnings("unchecked") 1121 | public void poll_resultFutureCanceled() { 1122 | final ScheduledFuture scheduledFuture = mock(ScheduledFuture.class); 1123 | final ScheduledExecutorService executor = mock(ScheduledExecutorService.class); 1124 | when(executor.scheduleAtFixedRate(any(), anyLong(), anyLong(), any())) 1125 | .thenReturn(scheduledFuture); 1126 | 1127 | final CompletableFuture future = poll(Optional::empty, Duration.ofMillis(2), executor); 1128 | future.cancel(true); 1129 | 1130 | verify(scheduledFuture).cancel(true); 1131 | } 1132 | 1133 | @Test 1134 | public void poll_notRunningAfterCancel() { 1135 | final CompletableFuture future = poll(Optional::empty, Duration.ofMillis(2), executor); 1136 | 1137 | future.cancel(true); 1138 | 1139 | executor.tick(5, MILLISECONDS); 1140 | assertThat(executor.isIdle(), is(true)); 1141 | } 1142 | 1143 | private static CompletableFuture incompleteFuture() { 1144 | return new CompletableFuture<>(); 1145 | } 1146 | 1147 | private static Matcher> completesTo(final T expected) { 1148 | return completesTo(is(expected)); 1149 | } 1150 | 1151 | private static Matcher> completesTo(final Matcher expected) { 1152 | return new CustomTypeSafeMatcher>( 1153 | "completes to " + String.valueOf(expected)) { 1154 | @Override 1155 | protected boolean matchesSafely(CompletionStage item) { 1156 | try { 1157 | final T value = item.toCompletableFuture().get(1, SECONDS); 1158 | return expected.matches(value); 1159 | } catch (Exception ex) { 1160 | return false; 1161 | } 1162 | } 1163 | }; 1164 | } 1165 | 1166 | private static Map asMap(final List keys, final List input) { 1167 | Map map = new HashMap<>(); 1168 | for (int i = 0; i < keys.size(); i++) { 1169 | map.put(keys.get(i), input.get(i)); 1170 | } 1171 | return map; 1172 | } 1173 | 1174 | private static class NonThrowingFuture extends CompletableFuture { 1175 | @Override 1176 | public T join() { 1177 | if (this.isCompletedExceptionally()) { 1178 | return null; 1179 | } 1180 | return super.join(); 1181 | } 1182 | } 1183 | } 1184 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/futures/ConcurrencyReducerTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2023 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import static com.spotify.futures.CompletableFutures.getException; 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.hamcrest.Matchers.instanceOf; 25 | import static org.junit.jupiter.api.Assertions.assertEquals; 26 | import static org.junit.jupiter.api.Assertions.assertFalse; 27 | import static org.junit.jupiter.api.Assertions.assertThrows; 28 | import static org.junit.jupiter.api.Assertions.assertTrue; 29 | 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | import java.util.concurrent.Callable; 33 | import java.util.concurrent.CompletableFuture; 34 | import java.util.concurrent.CompletionStage; 35 | import java.util.concurrent.atomic.AtomicBoolean; 36 | import java.util.concurrent.atomic.AtomicInteger; 37 | import java.util.function.Supplier; 38 | import org.junit.jupiter.api.Test; 39 | 40 | public class ConcurrencyReducerTest { 41 | 42 | @Test 43 | public void testTooLowConcurrency() { 44 | assertThrows(IllegalArgumentException.class, () -> ConcurrencyReducer.create(0, 10)); 45 | } 46 | 47 | @Test 48 | public void testTooLowQueueSize() { 49 | assertThrows(IllegalArgumentException.class, () -> ConcurrencyReducer.create(10, 0)); 50 | } 51 | 52 | @Test 53 | public void testNullJob() { 54 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(1, 10); 55 | assertThrows(NullPointerException.class, () -> limiter.add(null)); 56 | } 57 | 58 | @Test() 59 | public void testVoidJob() { 60 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(1, 10); 61 | final CompletionStage task = CompletableFuture.completedFuture(null); 62 | assertTrue(task.toCompletableFuture().isDone()); 63 | 64 | final CompletableFuture stage = limiter.add(() -> task); 65 | assertTrue(stage.isDone()); 66 | } 67 | 68 | @Test 69 | public void testJobReturnsNull() { 70 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(1, 10); 71 | final CompletableFuture response = limiter.add(job(null)); 72 | assertTrue(response.isDone()); 73 | final Throwable exception = getException(response); 74 | assertThat(exception, instanceOf(NullPointerException.class)); 75 | } 76 | 77 | @Test 78 | public void testJobThrows() { 79 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(1, 10); 80 | final CompletableFuture response = 81 | limiter.add( 82 | () -> { 83 | throw new IllegalStateException(); 84 | }); 85 | 86 | assertTrue(response.isDone()); 87 | final Throwable exception = getException(response); 88 | assertThat(exception, instanceOf(IllegalStateException.class)); 89 | } 90 | 91 | @Test 92 | public void testJobReturnsFailure() { 93 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(1, 10); 94 | final CompletionStage response = 95 | limiter.add( 96 | job(CompletableFutures.exceptionallyCompletedFuture(new IllegalStateException()))); 97 | 98 | assertTrue(response.toCompletableFuture().isDone()); 99 | final Throwable exception = getException(response); 100 | assertThat(exception, instanceOf(IllegalStateException.class)); 101 | } 102 | 103 | @Test 104 | public void testCancellation() { 105 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(2, 10); 106 | final CompletableFuture request1 = new CompletableFuture<>(); 107 | final CompletableFuture request2 = new CompletableFuture<>(); 108 | 109 | final CompletableFuture response1 = limiter.add(job(request1)); 110 | final CompletableFuture response2 = limiter.add(job(request2)); 111 | 112 | final AtomicBoolean wasInvoked = new AtomicBoolean(); 113 | final CompletableFuture response3 = 114 | limiter.add( 115 | () -> { 116 | wasInvoked.set(true); 117 | return null; 118 | }); 119 | 120 | response3.toCompletableFuture().cancel(false); 121 | 122 | // 1 and 2 are in progress, 3 is cancelled 123 | 124 | assertFalse(response1.isDone()); 125 | assertFalse(response2.isDone()); 126 | assertTrue(response3.isDone()); 127 | assertEquals(2, limiter.numActive()); 128 | assertEquals(1, limiter.numQueued()); 129 | 130 | request2.complete("2"); 131 | 132 | assertFalse(response1.isDone()); 133 | assertTrue(response2.isDone()); 134 | assertTrue(response3.isDone()); 135 | assertEquals(1, limiter.numActive()); 136 | assertEquals(0, limiter.numQueued()); 137 | 138 | request1.complete("1"); 139 | 140 | assertTrue(response1.isDone()); 141 | assertTrue(response2.isDone()); 142 | assertTrue(response3.isDone()); 143 | assertEquals(0, limiter.numActive()); 144 | assertEquals(0, limiter.numQueued()); 145 | 146 | assertFalse(wasInvoked.get()); 147 | } 148 | 149 | @Test 150 | public void testSimple() { 151 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(2, 10); 152 | final CompletableFuture request1 = new CompletableFuture<>(); 153 | final CompletableFuture request2 = new CompletableFuture<>(); 154 | final CompletableFuture request3 = new CompletableFuture<>(); 155 | final CompletableFuture response1 = limiter.add(job(request1)); 156 | final CompletableFuture response2 = limiter.add(job(request2)); 157 | final CompletableFuture response3 = limiter.add(job(request3)); 158 | 159 | request3.complete("3"); 160 | 161 | // 1 and 2 are in progress, 3 is still blocked 162 | 163 | assertFalse(response1.isDone()); 164 | assertFalse(response2.isDone()); 165 | assertFalse(response3.isDone()); 166 | assertEquals(2, limiter.numActive()); 167 | assertEquals(1, limiter.numQueued()); 168 | 169 | request2.complete("2"); 170 | 171 | assertFalse(response1.isDone()); 172 | assertTrue(response2.isDone()); 173 | assertTrue(response3.isDone()); 174 | assertEquals(1, limiter.numActive()); 175 | assertEquals(0, limiter.numQueued()); 176 | 177 | request1.complete("1"); 178 | 179 | assertTrue(response1.isDone()); 180 | assertTrue(response2.isDone()); 181 | assertTrue(response3.isDone()); 182 | assertEquals(0, limiter.numActive()); 183 | assertEquals(0, limiter.numQueued()); 184 | } 185 | 186 | @Test 187 | public void testLongRunning() { 188 | final AtomicInteger activeCount = new AtomicInteger(); 189 | final AtomicInteger maxCount = new AtomicInteger(); 190 | final int queueSize = 11; 191 | final int maxConcurrency = 10; 192 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(maxConcurrency, queueSize); 193 | List jobs = new ArrayList<>(); 194 | List> responses = new ArrayList<>(); 195 | for (int i = 0; i < queueSize; i++) { 196 | final CountingJob job = new CountingJob(limiter::numActive, maxCount); 197 | jobs.add(job); 198 | responses.add(limiter.add(job)); 199 | } 200 | 201 | for (int i = 0; i < jobs.size(); i++) { 202 | final CountingJob job = jobs.get(i); 203 | if (i % 2 == 0) { 204 | job.future.complete("success"); 205 | } else { 206 | job.future.completeExceptionally(new IllegalStateException()); 207 | } 208 | } 209 | responses.forEach(response -> assertTrue(response.isDone())); 210 | assertEquals(0, activeCount.get()); 211 | assertEquals(0, limiter.numActive()); 212 | assertEquals(0, limiter.numQueued()); 213 | assertEquals(maxConcurrency, limiter.remainingActiveCapacity()); 214 | assertEquals(maxConcurrency, maxCount.get()); 215 | assertEquals(queueSize, limiter.remainingQueueCapacity()); 216 | } 217 | 218 | @Test 219 | public void testQueueSize() { 220 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(10, 10); 221 | for (int i = 0; i < 20; i++) { 222 | limiter.add(job(new CompletableFuture<>())); 223 | } 224 | 225 | final CompletableFuture future = limiter.add(job(new CompletableFuture<>())); 226 | assertTrue(future.isDone()); 227 | final Throwable e = getException(future); 228 | assertThat(e, instanceOf(ConcurrencyReducer.CapacityReachedException.class)); 229 | } 230 | 231 | @Test 232 | public void testQueueSizeCounter() { 233 | final CompletableFuture future = new CompletableFuture<>(); 234 | 235 | final ConcurrencyReducer limiter = ConcurrencyReducer.create(10, 10); 236 | for (int i = 0; i < 20; i++) { 237 | limiter.add(job(future)); 238 | } 239 | 240 | assertEquals(10, limiter.numActive()); 241 | assertEquals(10, limiter.numQueued()); 242 | assertEquals(0, limiter.remainingActiveCapacity()); 243 | assertEquals(0, limiter.remainingQueueCapacity()); 244 | 245 | future.complete(""); 246 | 247 | assertEquals(0, limiter.numActive()); 248 | assertEquals(0, limiter.numQueued()); 249 | assertEquals(10, limiter.remainingActiveCapacity()); 250 | assertEquals(10, limiter.remainingQueueCapacity()); 251 | } 252 | 253 | private Callable> job(final CompletionStage future) { 254 | return () -> future; 255 | } 256 | 257 | private static class CountingJob implements Callable> { 258 | 259 | private final Supplier activeCount; 260 | private final AtomicInteger maxCount; 261 | 262 | final CompletableFuture future = new CompletableFuture<>(); 263 | 264 | public CountingJob(Supplier activeCount, AtomicInteger maxCount) { 265 | this.activeCount = activeCount; 266 | this.maxCount = maxCount; 267 | } 268 | 269 | @Override 270 | public CompletionStage call() { 271 | if (activeCount.get() > maxCount.get()) { 272 | maxCount.set(activeCount.get()); 273 | } 274 | return future; 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/futures/FunctionsTest.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2023 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures; 21 | 22 | import static org.hamcrest.CoreMatchers.is; 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.junit.jupiter.api.Assertions.assertThrows; 25 | 26 | import org.junit.jupiter.api.Test; 27 | 28 | public class FunctionsTest { 29 | 30 | @Test 31 | public void function3_andThen() { 32 | final Function3 f = (a, b, c) -> 1; 33 | assertThat(f.andThen(i -> i + 1).apply("", "", ""), is(2)); 34 | } 35 | 36 | @Test 37 | public void function3_andThenNull() { 38 | final Function3 f = (a, b, c) -> 1; 39 | assertThrows(NullPointerException.class, () -> f.andThen(null)); 40 | } 41 | 42 | @Test 43 | public void function4_andThen() { 44 | final Function4 f = (a, b, c, d) -> 1; 45 | assertThat(f.andThen(i -> i + 1).apply("", "", "", ""), is(2)); 46 | } 47 | 48 | @Test 49 | public void function4_andThenNull() { 50 | final Function4 f = (a, b, c, d) -> 1; 51 | assertThrows(NullPointerException.class, () -> f.andThen(null)); 52 | } 53 | 54 | @Test 55 | public void function5_andThen() { 56 | final Function5 f = (a, b, c, d, e) -> 1; 57 | assertThat(f.andThen(i -> i + 1).apply("", "", "", "", ""), is(2)); 58 | } 59 | 60 | @Test 61 | public void function5_andThenNull() { 62 | final Function5 f = (a, b, c, d, e) -> 1; 63 | assertThrows(NullPointerException.class, () -> f.andThen(null)); 64 | } 65 | 66 | @Test 67 | public void function6_andThen() { 68 | final Function6 ff = 69 | (a, b, c, d, e, f) -> 1; 70 | assertThat(ff.andThen(i -> i + 1).apply("", "", "", "", "", ""), is(2)); 71 | } 72 | 73 | @Test 74 | public void function6_andThenNull() { 75 | final Function6 ff = 76 | (a, b, c, d, e, f) -> 1; 77 | assertThrows(NullPointerException.class, () -> ff.andThen(null)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/spotify/futures/jmh/AllAsListBenchmark.java: -------------------------------------------------------------------------------- 1 | /*- 2 | * -\-\- 3 | * completable-futures 4 | * -- 5 | * Copyright (C) 2016 - 2020 Spotify AB 6 | * -- 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * 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, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * -/-/- 19 | */ 20 | package com.spotify.futures.jmh; 21 | 22 | import static java.util.concurrent.CompletableFuture.completedFuture; 23 | import static java.util.stream.Collectors.toList; 24 | 25 | import com.spotify.futures.CompletableFutures; 26 | import java.util.ArrayList; 27 | import java.util.Collections; 28 | import java.util.List; 29 | import java.util.concurrent.CompletableFuture; 30 | import java.util.concurrent.CompletionStage; 31 | import java.util.concurrent.TimeUnit; 32 | import java.util.stream.Stream; 33 | import org.openjdk.jmh.annotations.Benchmark; 34 | import org.openjdk.jmh.annotations.BenchmarkMode; 35 | import org.openjdk.jmh.annotations.Mode; 36 | import org.openjdk.jmh.annotations.OutputTimeUnit; 37 | import org.openjdk.jmh.annotations.Param; 38 | import org.openjdk.jmh.annotations.Scope; 39 | import org.openjdk.jmh.annotations.Setup; 40 | import org.openjdk.jmh.annotations.State; 41 | import org.openjdk.jmh.runner.Runner; 42 | import org.openjdk.jmh.runner.options.Options; 43 | import org.openjdk.jmh.runner.options.OptionsBuilder; 44 | 45 | @BenchmarkMode(Mode.AverageTime) 46 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 47 | public class AllAsListBenchmark { 48 | 49 | @State(Scope.Benchmark) 50 | public static class Input { 51 | 52 | @Param({"4", "16", "64", "256", "1024"}) 53 | int inputSize; 54 | 55 | List> stages; 56 | 57 | @Setup 58 | public void setup() { 59 | stages = Collections.nCopies(inputSize, completedFuture("hello")); 60 | } 61 | } 62 | 63 | @Benchmark 64 | public List actual(final Input input) throws Exception { 65 | final List> stages = input.stages; 66 | final CompletableFuture> future = CompletableFutures.allAsList(stages); 67 | return future.get(); 68 | } 69 | 70 | @Benchmark 71 | public List stream(final Input input) throws Exception { 72 | final List> stages = input.stages; 73 | 74 | @SuppressWarnings("unchecked") // generic array creation 75 | final CompletableFuture[] all = stages.stream() 76 | .map(CompletionStage::toCompletableFuture) 77 | .toArray(CompletableFuture[]::new); 78 | final CompletableFuture> future = CompletableFuture.allOf(all) 79 | .thenApply(i -> Stream.of(all) 80 | .map(CompletableFuture::join) 81 | .collect(toList())); 82 | 83 | return future.get(); 84 | } 85 | 86 | @Benchmark 87 | public List instantiateAndFor(final Input input) throws Exception { 88 | final List> stages = input.stages; 89 | 90 | @SuppressWarnings("unchecked") // generic array creation 91 | final CompletableFuture[] all = new CompletableFuture[stages.size()]; 92 | for (int i = 0; i < stages.size(); i++) { 93 | all[i] = stages.get(i).toCompletableFuture(); 94 | } 95 | final CompletableFuture> future = CompletableFuture.allOf(all) 96 | .thenApply(ignored -> { 97 | final List result = new ArrayList<>(all.length); 98 | for (int i = 0; i < all.length; i++) { 99 | result.add(all[i].join()); 100 | } 101 | return result; 102 | }); 103 | 104 | return future.get(); 105 | } 106 | 107 | @Benchmark 108 | public List instantiateAndForeach(final Input input) throws Exception { 109 | final List> stages = input.stages; 110 | 111 | @SuppressWarnings("unchecked") // generic array creation 112 | final CompletableFuture[] all = new CompletableFuture[stages.size()]; 113 | for (int i = 0; i < stages.size(); i++) { 114 | all[i] = stages.get(i).toCompletableFuture(); 115 | } 116 | 117 | final CompletableFuture> future = CompletableFuture.allOf(all) 118 | .thenApply(ignored -> { 119 | final List result = new ArrayList<>(all.length); 120 | for (CompletableFuture entry : all) { 121 | result.add(entry.join()); 122 | } 123 | return result; 124 | }); 125 | 126 | return future.get(); 127 | } 128 | 129 | public static void main(String[] args) throws Exception { 130 | final Options opt = new OptionsBuilder() 131 | .include(AllAsListBenchmark.class.getSimpleName()) 132 | .build(); 133 | new Runner(opt).run(); 134 | } 135 | 136 | } 137 | --------------------------------------------------------------------------------