├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle ├── coverage.gradle ├── publishing.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── asciidoc │ └── index.adoc └── java │ └── io │ └── engagingspaces │ └── vertx │ └── dataloader │ ├── BatchLoader.java │ ├── CacheKey.java │ ├── CacheMap.java │ ├── DataLoader.java │ ├── DataLoaderOptions.java │ ├── impl │ └── DefaultCacheMap.java │ └── package-info.java └── test └── java └── io └── engagingspaces └── vertx └── dataloader └── DataLoaderTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source 2 | *.com 3 | *.class 4 | *.dll 5 | *.exe 6 | *.o 7 | *.so 8 | 9 | # Packages 10 | *.7z 11 | *.dmg 12 | *.gz 13 | *.iso 14 | *.jar 15 | *.rar 16 | *.tar 17 | *.zip 18 | 19 | # Logs and databases 20 | *.log 21 | 22 | # OS generated files 23 | .DS_Store* 24 | ehthumbs.db 25 | Icon? 26 | Thumbs.db 27 | 28 | # Editor Files 29 | *~ 30 | *.swp 31 | 32 | # Gradle Files 33 | .gradle 34 | 35 | # Build output directies 36 | /target 37 | **/target 38 | /build/** 39 | **/build 40 | /node_modules 41 | **/node_modules 42 | 43 | # IntelliJ specific files/directories 44 | out 45 | .idea 46 | *.ipr 47 | *.iws 48 | *.iml 49 | 50 | # Eclipse specific files/directories 51 | .classpath 52 | .project 53 | .settings 54 | .metadata 55 | 56 | # NetBeans specific files/directories 57 | .nbattrs 58 | 59 | # Vagrant files 60 | .vagrant 61 | 62 | # Vert.x 63 | .vertx -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | 4 | jdk: 5 | - oraclejdk8 6 | 7 | addons: 8 | apt: 9 | packages: 10 | - oracle-java8-installer 11 | 12 | notifications: 13 | email: false 14 | 15 | branches: 16 | only: 17 | - master 18 | 19 | script: 20 | - ./gradlew clean test 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vert.x `DataLoader` 2 | 3 | [![Build Status](https://travis-ci.org/engagingspaces/vertx-dataloader.svg?branch=master)](https://travis-ci.org/engagingspaces/vertx-dataloader/)   4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/ec906aa3a12147e28b69b93e3a9d9bf7)](https://www.codacy.com/app/engagingspaces/vertx-dataloader?utm_source=github.com&utm_medium=referral&utm_content=engagingspaces/vertx-dataloader&utm_campaign=Badge_Grade)   5 | [![Apache licensed](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/engagingspaces/vertx-dataloader/blob/master/LICENSE)   6 | [ ![Download](https://api.bintray.com/packages/engagingspaces/maven/vertx-dataloader/images/download.svg) ](https://bintray.com/engagingspaces/maven/vertx-dataloader/_latestVersion) 7 | 8 | **Note**: A pure java 8 (non-Vert.x) port of this project is now an official [graphql-java](https://github.com/graphql-java/awesome-graphql-java#batch-loading) project: [java-dataloader](https://github.com/graphql-java/java-dataloader) 9 | 10 | ![vertx-dataloader-concepts](https://cloud.githubusercontent.com/assets/5111931/17837825/f5748bfc-67bd-11e6-9c7a-d407bb92c3d9.png) 11 | 12 | This small and simple utility library is a port of [Facebook DataLoader](https://github.com/facebook/dataloader) 13 | to Java 8 for use with [Vert.x](http://vertx.io). It can serve as integral part of your application's data layer to provide a 14 | consistent API over various back-ends and reduce message communication overhead through batching and caching. 15 | 16 | An important use case for `DataLoader` is improving the efficiency of GraphQL query execution, but there are 17 | many other use cases where you can benefit from using this utility. 18 | 19 | Most of the code is ported directly from Facebook's reference implementation, with one IMPORTANT adaptation to make 20 | it work for Java 8 and Vert.x. ([more on this below](manual-dispatching)). 21 | 22 | But before reading on, be sure to take a short dive into the 23 | [original documentation](https://github.com/facebook/dataloader/blob/master/README.md) provided by Lee Byron (@leebyron) 24 | and Nicholas Schrock (@schrockn) from [Facebook](https://www.facebook.com/), the creators of the original data loader. 25 | 26 | ## Table of contents 27 | 28 | - [Features](#features) 29 | - [Differences to reference implementation](#differences-to-reference-implementation) 30 | - [Manual dispatching](#manual-dispatching) 31 | - [Let's get started!](#lets-get-started) 32 | - [Installing](#installing) 33 | - [Building](#building) 34 | - [Project plans](#project-plans) 35 | - [Current releases](#current-releases) 36 | - [Known issues](#known-issues) 37 | - [Future ideas](#future-ideas) 38 | - [Other information sources](#other-information-sources) 39 | - [Contributing](#contributing) 40 | - [Acknowledgements](#acknowledgements) 41 | - [Licensing](#licensing) 42 | 43 | ## Features 44 | 45 | Vert.x `DataLoader` is a feature-complete port of the Facebook reference implementation with [one major difference](#manual-dispatching). These features are: 46 | 47 | - Simple, intuitive API, using generics and fluent coding 48 | - Define batch load function with lambda expression 49 | - Schedule a load request in queue for batching 50 | - Add load requests from anywhere in code 51 | - Request returns [`Future`](http://vertx.io/docs/apidocs/io/vertx/core/Future.html) of requested value 52 | - Can create multiple requests at once, returns [`CompositeFuture`](http://vertx.io/docs/apidocs/io/vertx/core/CompositeFuture.html) 53 | - Caches load requests, so data is only fetched once 54 | - Can clear individual cache keys, so data is fetched on next batch queue dispatch 55 | - Can prime the cache with key/values, to avoid data being fetched needlessly 56 | - Can configure cache key function with lambda expression to extract cache key from complex data loader key types 57 | - Dispatch load request queue after batch is prepared, also returns [`CompositeFuture`](http://vertx.io/docs/apidocs/io/vertx/core/CompositeFuture.html) 58 | - Individual batch futures complete / resolve as batch is processed 59 | - `CompositeFuture`s results are ordered according to insertion order of load requests 60 | - Deals with partial errors when a batch future fails 61 | - Can disable batching and/or caching in configuration 62 | - Can supply your own [`CacheMap`](https://github.com/engagingspaces/vertx-dataloader/blob/master/src/main/java/io/engagingspaces/vertx/dataloader/CacheMap.java) implementations 63 | - Has very high test coverage (see [Acknowledgements](#acknowlegdements)) 64 | 65 | ## Differences to reference implementation 66 | 67 | ### Manual dispatching 68 | 69 | The original data loader was written in Javascript for NodeJS. NodeJS is single-threaded in nature, but simulates 70 | asynchronous logic by invoking functions on separate threads in an event loop, as explained 71 | [in this post](http://stackoverflow.com/a/19823583/3455094) on StackOverflow. 72 | 73 | [Vert.x](http://vertx.io) on the other hand also uses an event loop ([that you should not block!!](http://vertx.io/docs/vertx-core/java/#golden_rule)), but comes 74 | with actor-like [`Verticle`](http://vertx.io/docs/vertx-core/java/#_verticles)s and a 75 | distributed [`EventBus`](http://vertx.io/docs/vertx-core/java/#event_bus) that make it inherently asynchronous, and non-blocking. 76 | 77 | Now in NodeJS generates so-call 'ticks' in which queued functions are dispatched for execution, and Facebook `DataLoader` uses 78 | the `nextTick()` function in NodeJS to _automatically_ dequeue load requests and send them to the batch execution function for processing. 79 | 80 | And here there is an **IMPORTANT DIFFERENCE** compared to how _this_ data loader operates!! 81 | 82 | In NodeJS the batch preparation will not affect the asynchronous processing behaviour in any way. It will just prepare 83 | batches in 'spare time' as it were. 84 | 85 | This is different in Vert.x as you will actually _delay_ the execution of your load requests, until the moment where you make a call 86 | to `dataLoader.dispatch()` in comparison to when you would just handle futures directly. 87 | 88 | Does this make Java `DataLoader` any less useful than the reference implementation? I would argue this is not the case, 89 | and there are also gains to this different mode of operation: 90 | 91 | - In contrast to the NodeJS implementation _you_ as developer are in full control of when batches are dispatched 92 | - You can attach any logic that determines when a dispatch takes place 93 | - You still retain all other features, full caching support and batching (e.g. to optimize message bus traffic, GraphQL query execution time, etc.) 94 | 95 | However, with batch execution control comes responsibility! If you forget to make the call to `dispatch()` then the futures 96 | in the load request queue will never be batched, and thus _will never complete_! So be careful when crafting your loader designs. 97 | 98 | ## Let's get started! 99 | 100 | ### Installing 101 | 102 | Gradle users configure the `vertx-dataloader` dependency in `build.gradle`: 103 | 104 | ``` 105 | repositories { 106 | maven { 107 | jcenter() 108 | } 109 | } 110 | 111 | dependencies { 112 | compile 'io.engagingspaces:vertx-dataloader:1.0.0' 113 | } 114 | ``` 115 | 116 | ### Building 117 | 118 | To build from source use the Gradle wrapper: 119 | 120 | ``` 121 | ./gradlew clean build 122 | ``` 123 | 124 | Or when using Maven add the following repository to your `pom.xml`: 125 | 126 | ``` 127 | 128 | 129 | 130 | false 131 | 132 | central 133 | bintray 134 | http://jcenter.bintray.com 135 | 136 | 137 | ``` 138 | 139 | And add the dependency to `vertx-dataloader`: 140 | 141 | ``` 142 | 143 | io.engagingspaces 144 | vertx-dataloader 145 | 1.0.0 146 | pom 147 | 148 | ``` 149 | 150 | ### Using 151 | 152 | Please take a look at the example project [vertx-graphql-example](https://github.com/bmsantos/vertx-graphql-example) 153 | created by [Bruno Santos](https://github.com/bmsantos). 154 | 155 | ## Project plans 156 | 157 | ### Current releases 158 | 159 | - `1.0.0` Initial release 160 | 161 | ### Known issues 162 | 163 | - Tests on job queue ordering need refactoring to Futures, one test currently omitted 164 | 165 | ### Future ideas 166 | 167 | - `CompletableFuture` implementation 168 | 169 | ## Other information sources 170 | 171 | - [Facebook DataLoader Github repo](https://github.com/facebook/dataloader) 172 | - [Facebook DataLoader code walkthrough on YouTube](https://youtu.be/OQTnXNCDywA) 173 | - [Using DataLoader and GraphQL to batch requests](https://github.com/gajus/gajus.com-blog/blob/master/posts/using-dataloader-to-batch-requests/index.md) 174 | 175 | ## Contributing 176 | 177 | All your feedback and help to improve this project is very welcome. Please create issues for your bugs, ideas and 178 | enhancement requests, or better yet, contribute directly by creating a PR. 179 | 180 | When reporting an issue, please add a detailed instruction, and if possible a code snippet or test that can be used 181 | as a reproducer of your problem. 182 | 183 | When creating a pull request, please adhere to the Vert.x coding style where possible, and create tests with your 184 | code so it keeps providing an excellent test coverage level. PR's without tests may not be accepted unless they only 185 | deal with minor changes. 186 | 187 | ## Acknowledgements 188 | 189 | This library is entirely inspired by the great works of [Lee Byron](https://github.com/leebyron) and 190 | [Nicholas Schrock](https://github.com/schrockn) from [Facebook](https://www.facebook.com/) whom I like to thank, and 191 | especially @leebyron for taking the time and effort to provide 100% coverage on the codebase. A set of tests which 192 | I also ported. 193 | 194 | ## Licensing 195 | 196 | This project [vertx-dataloader](https://github.com/engagingspaces/vertx-dataloader) is licensed under the 197 | [Apache Commons v2.0](https://github.com/engagingspaces/vertx-dataloader/LICENSE) license. 198 | 199 | Copyright © 2016 Arnold Schrijver and other 200 | [contributors](https://github.com/engagingspaces/vertx-dataloader/graphs/contributors) 201 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' 7 | } 8 | } 9 | 10 | apply plugin: 'maven-publish' 11 | 12 | publishing { 13 | repositories { 14 | maven { 15 | url 'https://dl.bintray.com/engagingspaces/maven' 16 | } 17 | } 18 | } 19 | 20 | ext { 21 | vertxVersion = '3.4.0' 22 | junitVersion = '4.12' 23 | } 24 | 25 | apply plugin: 'java' 26 | apply from: "$projectDir/gradle/coverage.gradle" 27 | apply from: "$projectDir/gradle/publishing.gradle" 28 | 29 | repositories { 30 | mavenLocal() 31 | mavenCentral() 32 | jcenter() 33 | maven { 34 | url 'https://dl.bintray.com/engagingspaces/maven' 35 | } 36 | } 37 | 38 | group = 'io.engagingspaces' 39 | version = '1.0.0' 40 | 41 | task docProcessing(type: JavaCompile, group: 'build') { 42 | source = sourceSets.main.java 43 | classpath = configurations.compile + configurations.compileOnly 44 | destinationDir = project.file('src/main/asciidoc') 45 | options.compilerArgs = [ 46 | "-proc:only", 47 | "-processor", "io.vertx.docgen.JavaDocGenProcessor", 48 | "-Adocgen.output=$buildDir/asciidoc", 49 | "-Adocgen.source=$projectDir/src/main/asciidoc/index.adoc" 50 | ] 51 | } 52 | 53 | compileJava { 54 | sourceCompatibility = 1.8 55 | targetCompatibility = 1.8 56 | 57 | dependsOn project.tasks.docProcessing 58 | options.compilerArgs = [ "-Xlint:unchecked", "-Xdiags:verbose" ] 59 | } 60 | 61 | dependencies { 62 | compile "io.vertx:vertx-core:$vertxVersion" 63 | compileOnly "io.vertx:vertx-docgen:$vertxVersion" 64 | 65 | testCompile "junit:junit:$junitVersion" 66 | testCompile 'org.awaitility:awaitility:2.0.0' 67 | testCompile "io.vertx:vertx-unit:$vertxVersion" 68 | } 69 | 70 | task wrapper(type: Wrapper) { 71 | gradleVersion = '2.14' 72 | distributionUrl = "http://services.gradle.org/distributions/gradle-2.14-all.zip" 73 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | projectTitle = Vert.x Dataloader 2 | projectDescription = Port of Facebook Dataloader for Java and Vert.x -------------------------------------------------------------------------------- /gradle/coverage.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'jacoco' 2 | 3 | jacoco { 4 | toolVersion = '0.7.6.201602180812' 5 | } 6 | 7 | jacocoTestReport { 8 | dependsOn "test" 9 | 10 | group = "Reporting" 11 | description = "Generate code coverage results using JaCoCo." 12 | 13 | reports { 14 | xml.enabled = true 15 | csv.enabled = false 16 | html.enabled = true 17 | html.destination "$buildDir/jacocoHtml" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle/publishing.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | apply plugin: 'com.jfrog.bintray' 3 | 4 | publishing { 5 | repositories { 6 | maven { 7 | url 'https://dl.bintray.com/engagingspaces/maven' 8 | } 9 | } 10 | } 11 | 12 | task sourcesJar(type: Jar, dependsOn: classes) { 13 | classifier = 'sources' 14 | from sourceSets.main.allSource 15 | } 16 | 17 | task javadocJar(type: Jar, dependsOn: javadoc) { 18 | classifier = 'javadoc' 19 | from javadoc.destinationDir 20 | } 21 | 22 | artifacts { 23 | archives sourcesJar 24 | archives javadocJar 25 | } 26 | 27 | publishing { 28 | publications { 29 | maven(MavenPublication) { 30 | artifactId "$project.name" 31 | from components.java 32 | 33 | artifact sourcesJar { 34 | classifier 'sources' 35 | } 36 | artifact javadocJar { 37 | classifier 'javadoc' 38 | } 39 | pom.withXml { 40 | asNode().children().last() + { 41 | def builder = delegate 42 | builder.name projectTitle 43 | builder.description projectDescription 44 | builder.url 'https://github.com/engagingspaces/vertx-dataloader' 45 | builder.licenses { 46 | builder.license { 47 | builder.name 'The Apache Software License, Version 2.0' 48 | builder.url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 49 | builder.distribution 'repo' 50 | } 51 | } 52 | builder.scm { 53 | builder.url 'scm:git@github.com/engagingspaces/vertx-dataloader.git' 54 | builder.connection 'git@github.com/engagingspaces/vertx-dataloader.git' 55 | builder.developerConnection 'git@github.com/engagingspaces/vertx-dataloader.git' 56 | } 57 | builder.developers { 58 | builder.developer { 59 | builder.id 'aschrijver' 60 | builder.name 'Arnold Schrijver' 61 | builder.url 'https://github.com/aschrijver/' 62 | builder.organization 'engagingspaces' 63 | builder.organizationUrl 'https://github.com/engagingspaces/' 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | bintray { 73 | user = System.getenv('BINTRAY_USER') 74 | key = System.getenv('BINTRAY_KEY') 75 | publications = ['maven'] 76 | publish = true 77 | pkg { 78 | repo = 'maven' 79 | name = "$project.name" 80 | desc = projectDescription 81 | userOrg = 'engagingspaces' 82 | websiteUrl = 'https://github.com/engagingspaces/vertx-dataloader' 83 | issueTrackerUrl = 'https://github.com/engagingspaces/vertx-dataloader/issues' 84 | licenses = ['Apache-2.0'] 85 | vcsUrl = 'https://github.com/engagingspaces/vertx-dataloader.git' 86 | labels = ['vertx', 'batch', 'batch-loading', 'completablefuture', 'future', 'asynchronous', 87 | 'data-loader', 'dataloader', 'java', 'port', 'facebook', 'graphql'] 88 | githubRepo = 'engagingspaces/vertx-dataloader' 89 | githubReleaseNotesFile = 'README.md' 90 | version { 91 | released = new Date() 92 | vcsTag = project.version 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engagingspaces/vertx-dataloader/420d972292fece1c0bec92b62e036dd0a9ab8c25/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 20 09:54:51 CEST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engagingspaces/vertx-dataloader/420d972292fece1c0bec92b62e036dd0a9ab8c25/settings.gradle -------------------------------------------------------------------------------- /src/main/asciidoc/index.adoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engagingspaces/vertx-dataloader/420d972292fece1c0bec92b62e036dd0a9ab8c25/src/main/asciidoc/index.adoc -------------------------------------------------------------------------------- /src/main/java/io/engagingspaces/vertx/dataloader/BatchLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.engagingspaces.vertx.dataloader; 18 | 19 | import io.vertx.core.CompositeFuture; 20 | 21 | import java.util.Collection; 22 | 23 | /** 24 | * Function that is invoked for batch loading the list of data values indicated by the provided list of keys. The 25 | * function returns a {@link CompositeFuture} to aggregate results of individual load requests. 26 | * 27 | * @param type parameter indicating the type of keys to use for data load requests. 28 | * 29 | * @author Arnold Schrijver 30 | */ 31 | @FunctionalInterface 32 | public interface BatchLoader { 33 | 34 | /** 35 | * Batch load the provided keys and return a composite future of the result. 36 | * 37 | * @param keys the list of keys to load 38 | * @return the composite future 39 | */ 40 | CompositeFuture load(Collection keys); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/engagingspaces/vertx/dataloader/CacheKey.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.engagingspaces.vertx.dataloader; 18 | 19 | /** 20 | * Function that is invoked on input keys of type {@code K} to derive keys that are required by the {@link CacheMap} 21 | * implementation. 22 | * 23 | * @param type parameter indicating the type of the input key 24 | * @author Arnold Schrijver 25 | */ 26 | @FunctionalInterface 27 | public interface CacheKey { 28 | 29 | /** 30 | * Returns the cache key that is created from the provided input key. 31 | * 32 | * @param input the input key 33 | * @return the cache key 34 | */ 35 | Object getKey(K input); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/engagingspaces/vertx/dataloader/CacheMap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.engagingspaces.vertx.dataloader; 18 | 19 | import io.engagingspaces.vertx.dataloader.impl.DefaultCacheMap; 20 | import io.vertx.core.Future; 21 | 22 | /** 23 | * Cache map interface for data loaders that use caching. 24 | *

25 | * The default implementation used by the data loader is based on a {@link java.util.LinkedHashMap}. Note that the 26 | * implementation could also have used a regular {@link java.util.Map} instead of this {@link CacheMap}, but 27 | * this aligns better to the reference data loader implementation provided by Facebook 28 | *

29 | * Also it doesn't require you to implement the full set of map overloads, just the required methods. 30 | * 31 | * @param type parameter indicating the type of the cache keys 32 | * @param type parameter indicating the type of the data that is cached 33 | * 34 | * @author Arnold Schrijver 35 | */ 36 | public interface CacheMap { 37 | 38 | /** 39 | * Creates a new cache map, using the default implementation that is based on a {@link java.util.LinkedHashMap}. 40 | * 41 | * @param type parameter indicating the type of the cache keys 42 | * @param type parameter indicating the type of the data that is cached 43 | * @return the cache map 44 | */ 45 | static CacheMap> simpleMap() { 46 | return new DefaultCacheMap<>(); 47 | } 48 | 49 | /** 50 | * Checks whether the specified key is contained in the cach map. 51 | * 52 | * @param key the key to check 53 | * @return {@code true} if the cache contains the key, {@code false} otherwise 54 | */ 55 | boolean containsKey(U key); 56 | 57 | /** 58 | * Gets the specified key from the cache map. 59 | *

60 | * May throw an exception if the key does not exists, depending on the cache map implementation that is used, 61 | * so be sure to check {@link CacheMap#containsKey(Object)} first. 62 | * 63 | * @param key the key to retrieve 64 | * @return the cached value, or {@code null} if not found (depends on cache implementation) 65 | */ 66 | V get(U key); 67 | 68 | /** 69 | * Creates a new cache map entry with the specified key and value, or updates the value if the key already exists. 70 | * 71 | * @param key the key to cache 72 | * @param value the value to cache 73 | * @return the cache map for fluent coding 74 | */ 75 | CacheMap set(U key, V value); 76 | 77 | /** 78 | * Deletes the entry with the specified key from the cache map, if it exists. 79 | * 80 | * @param key the key to delete 81 | * @return the cache map for fluent coding 82 | */ 83 | CacheMap delete(U key); 84 | 85 | /** 86 | * Clears all entries of the cache map 87 | * 88 | * @return the cache map for fluent coding 89 | */ 90 | CacheMap clear(); 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/io/engagingspaces/vertx/dataloader/DataLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.engagingspaces.vertx.dataloader; 18 | 19 | import io.vertx.core.CompositeFuture; 20 | import io.vertx.core.Future; 21 | 22 | import java.util.Collections; 23 | import java.util.LinkedHashMap; 24 | import java.util.List; 25 | import java.util.Objects; 26 | import java.util.concurrent.atomic.AtomicInteger; 27 | import java.util.stream.Collectors; 28 | 29 | /** 30 | * Data loader is a utility class that allows batch loading of data that is identified by a set of unique keys. For 31 | * each key that is loaded a separate {@link Future} is returned, that completes as the batch function completes. 32 | * Besides individual futures a {@link CompositeFuture} of the batch is available as well. 33 | *

34 | * With batching enabled the execution will start after calling {@link DataLoader#dispatch()}, causing the queue of 35 | * loaded keys to be sent to the batch function, clears the queue, and returns the {@link CompositeFuture}. 36 | *

37 | * As batch functions are executed the resulting futures are cached using a cache implementation of choice, so they 38 | * will only execute once. Individual cache keys can be cleared, so they will be re-fetched when referred to again. 39 | * It is also possible to clear the cache entirely, and prime it with values before they are used. 40 | *

41 | * Both caching and batching can be disabled. Configuration of the data loader is done by providing a 42 | * {@link DataLoaderOptions} instance on creation. 43 | * 44 | * @param type parameter indicating the type of the data load keys 45 | * @param type parameter indicating the type of the data that is returned 46 | * 47 | * @author Arnold Schrijver 48 | */ 49 | public class DataLoader { 50 | 51 | private final BatchLoader batchLoadFunction; 52 | private final DataLoaderOptions loaderOptions; 53 | private final CacheMap> futureCache; 54 | private final LinkedHashMap> loaderQueue; 55 | private final LinkedHashMap>> dispatchedQueues; 56 | 57 | /** 58 | * Creates a new data loader with the provided batch load function, and default options. 59 | * 60 | * @param batchLoadFunction the batch load function to use 61 | */ 62 | public DataLoader(BatchLoader batchLoadFunction) { 63 | this(batchLoadFunction, null); 64 | } 65 | 66 | /** 67 | * Creates a new data loader with the provided batch load function and options. 68 | * 69 | * @param batchLoadFunction the batch load function to use 70 | * @param options the batch load options 71 | */ 72 | @SuppressWarnings("unchecked") 73 | public DataLoader(BatchLoader batchLoadFunction, DataLoaderOptions options) { 74 | Objects.requireNonNull(batchLoadFunction, "Batch load function cannot be null"); 75 | this.batchLoadFunction = batchLoadFunction; 76 | this.loaderOptions = options == null ? new DataLoaderOptions() : options; 77 | this.futureCache = loaderOptions.cacheMap().isPresent() ? (CacheMap>) loaderOptions.cacheMap().get() : CacheMap.simpleMap(); 78 | this.loaderQueue = new LinkedHashMap<>(); 79 | this.dispatchedQueues = new LinkedHashMap<>(); 80 | } 81 | 82 | /** 83 | * Requests to load the data with the specified key asynchronously, and returns a future of the resulting value. 84 | *

85 | * If batching is enabled (the default), you'll have to call {@link DataLoader#dispatch()} at a later stage to 86 | * start batch execution. If you forget this call the future will never be completed (unless already completed, 87 | * and returned from cache). 88 | * 89 | * @param key the key to load 90 | * @return the future of the value 91 | */ 92 | public Future load(K key) { 93 | Objects.requireNonNull(key, "Key cannot be null"); 94 | Object cacheKey = getCacheKey(key); 95 | if (loaderOptions.cachingEnabled() && futureCache.containsKey(cacheKey)) { 96 | return futureCache.get(cacheKey); 97 | } 98 | 99 | Future future = Future.future(); 100 | if (loaderOptions.batchingEnabled()) { 101 | loaderQueue.put(key, future); 102 | } else { 103 | CompositeFuture compositeFuture = batchLoadFunction.load(Collections.singleton(key)); 104 | if (compositeFuture.succeeded()) { 105 | future.complete(compositeFuture.result().resultAt(0)); 106 | } else { 107 | future.fail(compositeFuture.cause()); 108 | } 109 | } 110 | if (loaderOptions.cachingEnabled()) { 111 | futureCache.set(cacheKey, future); 112 | } 113 | return future; 114 | } 115 | 116 | /** 117 | * Requests to load the list of data provided by the specified keys asynchronously, and returns a composite future 118 | * of the resulting values. 119 | *

120 | * If batching is enabled (the default), you'll have to call {@link DataLoader#dispatch()} at a later stage to 121 | * start batch execution. If you forget this call the future will never be completed (unless already completed, 122 | * and returned from cache). 123 | * 124 | * @param keys the list of keys to load 125 | * @return the composite future of the list of values 126 | */ 127 | public CompositeFuture loadMany(List keys) { 128 | return CompositeFuture.join(keys.stream().map(this::load).collect(Collectors.toList())); 129 | } 130 | 131 | /** 132 | * Dispatches the queued load requests to the batch execution function and returns a composite future of the result. 133 | *

134 | * If batching is disabled, or there are no queued requests, then a succeeded composite future is returned. 135 | * 136 | * @return the composite future of the queued load requests 137 | */ 138 | public CompositeFuture dispatch() { 139 | if (!loaderOptions.batchingEnabled() || loaderQueue.size() == 0) { 140 | return CompositeFuture.join(Collections.emptyList()); 141 | } 142 | CompositeFuture batch = batchLoadFunction.load(loaderQueue.keySet()); 143 | dispatchedQueues.put(batch, new LinkedHashMap<>(loaderQueue)); 144 | batch.setHandler(rh -> { 145 | AtomicInteger index = new AtomicInteger(0); 146 | dispatchedQueues.get(batch).forEach((key, future) -> { 147 | if (batch.succeeded(index.get())) { 148 | future.complete(batch.resultAt(index.get())); 149 | } else { 150 | future.fail(batch.cause(index.get())); 151 | } 152 | index.incrementAndGet(); 153 | }); 154 | dispatchedQueues.remove(batch); 155 | }); 156 | loaderQueue.clear(); 157 | return batch; 158 | } 159 | 160 | /** 161 | * Clears the future with the specified key from the cache, if caching is enabled, so it will be re-fetched 162 | * on the next load request. 163 | * 164 | * @param key the key to remove 165 | * @return the data loader for fluent coding 166 | */ 167 | public DataLoader clear(K key) { 168 | Object cacheKey = getCacheKey(key); 169 | futureCache.delete(cacheKey); 170 | return this; 171 | } 172 | 173 | /** 174 | * Clears the entire cache map of the loader. 175 | * 176 | * @return the data loader for fluent coding 177 | */ 178 | public DataLoader clearAll() { 179 | futureCache.clear(); 180 | return this; 181 | } 182 | 183 | /** 184 | * Primes the cache with the given key and value. 185 | * 186 | * @param key the key 187 | * @param value the value 188 | * @return the data loader for fluent coding 189 | */ 190 | public DataLoader prime(K key, V value) { 191 | Object cacheKey = getCacheKey(key); 192 | if (!futureCache.containsKey(cacheKey)) { 193 | futureCache.set(cacheKey, Future.succeededFuture(value)); 194 | } 195 | return this; 196 | } 197 | 198 | /** 199 | * Primes the cache with the given key and error. 200 | * 201 | * @param key the key 202 | * @param error the exception to prime instead of a value 203 | * @return the data loader for fluent coding 204 | */ 205 | public DataLoader prime(K key, Exception error) { 206 | Object cacheKey = getCacheKey(key); 207 | if (!futureCache.containsKey(cacheKey)) { 208 | futureCache.set(cacheKey, Future.failedFuture(error)); 209 | } 210 | return this; 211 | } 212 | 213 | /** 214 | * Gets the object that is used in the internal cache map as key, by applying the cache key function to 215 | * the provided key. 216 | *

217 | * If no cache key function is present in {@link DataLoaderOptions}, then the returned value equals the input key. 218 | * 219 | * @param key the input key 220 | * @return the cache key after the input is transformed with the cache key function 221 | */ 222 | @SuppressWarnings("unchecked") 223 | public Object getCacheKey(K key) { 224 | return loaderOptions.cacheKeyFunction().isPresent() ? 225 | loaderOptions.cacheKeyFunction().get().getKey(key) : key; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/main/java/io/engagingspaces/vertx/dataloader/DataLoaderOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.engagingspaces.vertx.dataloader; 18 | 19 | import io.vertx.core.json.JsonObject; 20 | 21 | import java.util.Objects; 22 | import java.util.Optional; 23 | 24 | /** 25 | * Configuration options for {@link DataLoader} instances. 26 | * 27 | * @author Arnold Schrijver 28 | */ 29 | public class DataLoaderOptions { 30 | 31 | private boolean batchingEnabled; 32 | private boolean cachingEnabled; 33 | private CacheKey cacheKeyFunction; 34 | private CacheMap cacheMap; 35 | 36 | /** 37 | * Creates a new data loader options with default settings. 38 | */ 39 | public DataLoaderOptions() { 40 | batchingEnabled = true; 41 | cachingEnabled = true; 42 | } 43 | 44 | public static DataLoaderOptions create() { 45 | return new DataLoaderOptions(); 46 | } 47 | 48 | /** 49 | * Clones the provided data loader options. 50 | * 51 | * @param other the other options instance 52 | */ 53 | public DataLoaderOptions(DataLoaderOptions other) { 54 | Objects.requireNonNull(other, "Other data loader options cannot be null"); 55 | this.batchingEnabled = other.batchingEnabled; 56 | this.cachingEnabled = other.cachingEnabled; 57 | this.cacheKeyFunction = other.cacheKeyFunction; 58 | this.cacheMap = other.cacheMap; 59 | } 60 | 61 | /** 62 | * Creates a new data loader options with values provided as JSON. 63 | *

64 | * Note that only json-serializable options can be set with this constructor. Others, 65 | * like {@link DataLoaderOptions#cacheKeyFunction} must be set manually after creation. 66 | *

67 | * Note also that this makes it incompatible with true Vert.x data objects, so beware if you use it that way. 68 | * 69 | * @param json the serialized data loader options to set 70 | */ 71 | public DataLoaderOptions(JsonObject json) { 72 | Objects.requireNonNull(json, "Json cannot be null"); 73 | this.batchingEnabled = json.getBoolean("batchingEnabled"); 74 | this.batchingEnabled = json.getBoolean("cachingEnabled"); 75 | } 76 | 77 | /** 78 | * Option that determines whether to use batching (the default), or not. 79 | * 80 | * @return {@code true} when batching is enabled, {@code false} otherwise 81 | */ 82 | public boolean batchingEnabled() { 83 | return batchingEnabled; 84 | } 85 | 86 | /** 87 | * Sets the option that determines whether batch loading is enabled. 88 | * 89 | * @param batchingEnabled {@code true} to enable batch loading, {@code false} otherwise 90 | * @return the data loader options for fluent coding 91 | */ 92 | public DataLoaderOptions setBatchingEnabled(boolean batchingEnabled) { 93 | this.batchingEnabled = batchingEnabled; 94 | return this; 95 | } 96 | 97 | /** 98 | * Option that determines whether to use caching of futures (the default), or not. 99 | * 100 | * @return {@code true} when caching is enabled, {@code false} otherwise 101 | */ 102 | public boolean cachingEnabled() { 103 | return cachingEnabled; 104 | } 105 | 106 | /** 107 | * Sets the option that determines whether caching is enabled. 108 | * 109 | * @param cachingEnabled {@code true} to enable caching, {@code false} otherwise 110 | * @return the data loader options for fluent coding 111 | */ 112 | public DataLoaderOptions setCachingEnabled(boolean cachingEnabled) { 113 | this.cachingEnabled = cachingEnabled; 114 | return this; 115 | } 116 | 117 | /** 118 | * Gets an (optional) function to invoke for creation of the cache key, if caching is enabled. 119 | *

120 | * If missing the cache key defaults to the {@code key} type parameter of the data loader of type {@code K}. 121 | * 122 | * @return an optional with the function, or empty optional 123 | */ 124 | public Optional cacheKeyFunction() { 125 | return Optional.ofNullable(cacheKeyFunction); 126 | } 127 | 128 | /** 129 | * Sets the function to use for creating the cache key, if caching is enabled. 130 | * 131 | * @param cacheKeyFunction the cache key function to use 132 | * @return the data loader options for fluent coding 133 | */ 134 | public DataLoaderOptions setCacheKeyFunction(CacheKey cacheKeyFunction) { 135 | this.cacheKeyFunction = cacheKeyFunction; 136 | return this; 137 | } 138 | 139 | /** 140 | * Gets the (optional) cache map implementation that is used for caching, if caching is enabled. 141 | *

142 | * If missing a standard {@link java.util.LinkedHashMap} will be used as the cache implementation. 143 | * 144 | * @return an optional with the cache map instance, or empty 145 | */ 146 | public Optional cacheMap() { 147 | return Optional.ofNullable(cacheMap); 148 | } 149 | 150 | /** 151 | * Sets the cache map implementation to use for caching, if caching is enabled. 152 | * 153 | * @param cacheMap the cache map instance 154 | * @return the data loader options for fluent coding 155 | */ 156 | public DataLoaderOptions setCacheMap(CacheMap cacheMap) { 157 | this.cacheMap = cacheMap; 158 | return this; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/io/engagingspaces/vertx/dataloader/impl/DefaultCacheMap.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.engagingspaces.vertx.dataloader.impl; 18 | 19 | import io.engagingspaces.vertx.dataloader.CacheMap; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | /** 25 | * Default implementation of {@link CacheMap} that is based on a regular {@link java.util.LinkedHashMap}. 26 | * 27 | * @param type parameter indicating the type of the cache keys 28 | * @param type parameter indicating the type of the data that is cached 29 | * 30 | * @author Arnold Schrijver 31 | */ 32 | public class DefaultCacheMap implements CacheMap { 33 | 34 | private Map cache; 35 | 36 | /** 37 | * Default constructor 38 | */ 39 | public DefaultCacheMap() { 40 | cache = new HashMap<>(); 41 | } 42 | 43 | /** 44 | * {@inheritDoc} 45 | */ 46 | @Override 47 | public boolean containsKey(U key) { 48 | return cache.containsKey(key); 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | @Override 55 | public V get(U key) { 56 | return cache.get(key); 57 | } 58 | 59 | /** 60 | * {@inheritDoc} 61 | */ 62 | @Override 63 | public CacheMap set(U key, V value) { 64 | cache.put(key, value); 65 | return this; 66 | } 67 | 68 | /** 69 | * {@inheritDoc} 70 | */ 71 | @Override 72 | public CacheMap delete(U key) { 73 | cache.remove(key); 74 | return this; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | @Override 81 | public CacheMap clear() { 82 | cache.clear(); 83 | return this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/engagingspaces/vertx/dataloader/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * = Vert.x DataLoader 3 | * 4 | */ 5 | @Document(fileName = "index.adoc") 6 | package io.engagingspaces.vertx.dataloader; 7 | 8 | import io.vertx.docgen.Document; -------------------------------------------------------------------------------- /src/test/java/io/engagingspaces/vertx/dataloader/DataLoaderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The original author or authors 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | 17 | package io.engagingspaces.vertx.dataloader; 18 | 19 | import io.vertx.core.CompositeFuture; 20 | import io.vertx.core.Future; 21 | import io.vertx.core.json.JsonObject; 22 | import io.vertx.ext.unit.junit.RunTestOnContext; 23 | import io.vertx.ext.unit.junit.VertxUnitRunner; 24 | import org.junit.Before; 25 | import org.junit.Ignore; 26 | import org.junit.Rule; 27 | import org.junit.Test; 28 | import org.junit.runner.RunWith; 29 | 30 | import java.util.*; 31 | import java.util.concurrent.Callable; 32 | import java.util.concurrent.atomic.AtomicBoolean; 33 | import java.util.stream.Collectors; 34 | 35 | import static java.util.Arrays.asList; 36 | import static org.awaitility.Awaitility.await; 37 | import static org.hamcrest.Matchers.*; 38 | import static org.junit.Assert.assertArrayEquals; 39 | import static org.junit.Assert.assertThat; 40 | 41 | /** 42 | * Tests for {@link DataLoader}. 43 | *

44 | * The tests are a port of the existing tests in 45 | * the facebook/dataloader project. 46 | *

47 | * Acknowledgments go to Lee Byron for providing excellent coverage. 48 | * 49 | * @author Arnold Schrijver 50 | */ 51 | @RunWith(VertxUnitRunner.class) 52 | public class DataLoaderTest { 53 | 54 | @Rule 55 | public RunTestOnContext rule = new RunTestOnContext(); 56 | 57 | DataLoader identityLoader; 58 | 59 | @Before 60 | public void setUp() { 61 | identityLoader = idLoader(new DataLoaderOptions(), new ArrayList<>()); 62 | } 63 | 64 | @Test 65 | public void should_Build_a_really_really_simple_data_loader() { 66 | AtomicBoolean success = new AtomicBoolean(); 67 | DataLoader identityLoader = new DataLoader<>(keys -> 68 | CompositeFuture.join(keys.stream() 69 | .map(Future::succeededFuture) 70 | .collect(Collectors.toCollection(ArrayList::new)))); 71 | 72 | Future future1 = identityLoader.load(1); 73 | future1.setHandler(rh -> { 74 | assertThat(rh.result(), equalTo(1)); 75 | success.set(rh.succeeded()); 76 | }); 77 | identityLoader.dispatch(); 78 | await().untilAtomic(success, is(true)); 79 | } 80 | 81 | @Test 82 | public void should_Support_loading_multiple_keys_in_one_call() { 83 | AtomicBoolean success = new AtomicBoolean(); 84 | DataLoader identityLoader = new DataLoader<>(keys -> 85 | CompositeFuture.join(keys.stream() 86 | .map(Future::succeededFuture) 87 | .collect(Collectors.toCollection(ArrayList::new)))); 88 | 89 | CompositeFuture futureAll = identityLoader.loadMany(asList(1, 2)); 90 | futureAll.setHandler(rh -> { 91 | assertThat(rh.result().size(), is(2)); 92 | success.set(rh.succeeded()); 93 | }); 94 | identityLoader.dispatch(); 95 | await().untilAtomic(success, is(true)); 96 | assertThat(futureAll.list(), equalTo(asList(1, 2))); 97 | } 98 | 99 | @Test 100 | public void should_Resolve_to_empty_list_when_no_keys_supplied() { 101 | AtomicBoolean success = new AtomicBoolean(); 102 | CompositeFuture futureEmpty = identityLoader.loadMany(Collections.emptyList()); 103 | futureEmpty.setHandler(rh -> { 104 | assertThat(rh.result().size(), is(0)); 105 | success.set(rh.succeeded()); 106 | }); 107 | identityLoader.dispatch(); 108 | await().untilAtomic(success, is(true)); 109 | assertThat(futureEmpty.list(), empty()); 110 | } 111 | 112 | @Test 113 | public void should_Batch_multiple_requests() { 114 | ArrayList loadCalls = new ArrayList<>(); 115 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 116 | 117 | Future future1 = identityLoader.load(1); 118 | Future future2 = identityLoader.load(2); 119 | identityLoader.dispatch(); 120 | 121 | await().until(() -> future1.isComplete() && future2.isComplete()); 122 | assertThat(future1.result(), equalTo(1)); 123 | assertThat(future2.result(), equalTo(2)); 124 | assertThat(loadCalls, equalTo(Collections.singletonList(asList(1, 2)))); 125 | } 126 | 127 | @Test 128 | public void should_Coalesce_identical_requests() { 129 | ArrayList loadCalls = new ArrayList<>(); 130 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 131 | 132 | Future future1a = identityLoader.load(1); 133 | Future future1b = identityLoader.load(1); 134 | assertThat(future1a, equalTo(future1b)); 135 | identityLoader.dispatch(); 136 | 137 | await().until(future1a::isComplete); 138 | assertThat(future1a.result(), equalTo(1)); 139 | assertThat(future1b.result(), equalTo(1)); 140 | assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList(1)))); 141 | } 142 | 143 | @Test 144 | public void should_Cache_repeated_requests() { 145 | ArrayList loadCalls = new ArrayList<>(); 146 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 147 | 148 | Future future1 = identityLoader.load("A"); 149 | Future future2 = identityLoader.load("B"); 150 | identityLoader.dispatch(); 151 | 152 | await().until(() -> future1.isComplete() && future2.isComplete()); 153 | assertThat(future1.result(), equalTo("A")); 154 | assertThat(future2.result(), equalTo("B")); 155 | assertThat(loadCalls, equalTo(Collections.singletonList(asList("A", "B")))); 156 | 157 | Future future1a = identityLoader.load("A"); 158 | Future future3 = identityLoader.load("C"); 159 | identityLoader.dispatch(); 160 | 161 | await().until(() -> future1a.isComplete() && future3.isComplete()); 162 | assertThat(future1a.result(), equalTo("A")); 163 | assertThat(future3.result(), equalTo("C")); 164 | assertThat(loadCalls, equalTo(asList(asList("A", "B"), Collections.singletonList("C")))); 165 | 166 | Future future1b = identityLoader.load("A"); 167 | Future future2a = identityLoader.load("B"); 168 | Future future3a = identityLoader.load("C"); 169 | identityLoader.dispatch(); 170 | 171 | await().until(() -> future1b.isComplete() && future2a.isComplete() && future3a.isComplete()); 172 | assertThat(future1b.result(), equalTo("A")); 173 | assertThat(future2a.result(), equalTo("B")); 174 | assertThat(future3a.result(), equalTo("C")); 175 | assertThat(loadCalls, equalTo(asList(asList("A", "B"), Collections.singletonList("C")))); 176 | } 177 | 178 | @Test 179 | public void should_Not_redispatch_previous_load() { 180 | ArrayList loadCalls = new ArrayList<>(); 181 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 182 | 183 | Future future1 = identityLoader.load("A"); 184 | identityLoader.dispatch(); 185 | 186 | Future future2 = identityLoader.load("B"); 187 | identityLoader.dispatch(); 188 | 189 | await().until(() -> future1.isComplete() && future2.isComplete()); 190 | assertThat(future1.result(), equalTo("A")); 191 | assertThat(future2.result(), equalTo("B")); 192 | assertThat(loadCalls, equalTo(asList(asList("A"), asList("B")))); 193 | } 194 | 195 | @Test 196 | public void should_Cache_on_redispatch() { 197 | ArrayList loadCalls = new ArrayList<>(); 198 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 199 | 200 | Future future1 = identityLoader.load("A"); 201 | identityLoader.dispatch(); 202 | 203 | CompositeFuture future2 = identityLoader.loadMany(asList("A", "B")); 204 | identityLoader.dispatch(); 205 | 206 | await().until(() -> future1.isComplete() && future2.isComplete()); 207 | assertThat(future1.result(), equalTo("A")); 208 | assertThat(future2.list(), equalTo(asList("A", "B"))); 209 | assertThat(loadCalls, equalTo(asList(asList("A"), asList("B")))); 210 | } 211 | 212 | @Test 213 | public void should_Clear_single_value_in_loader() { 214 | ArrayList loadCalls = new ArrayList<>(); 215 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 216 | 217 | Future future1 = identityLoader.load("A"); 218 | Future future2 = identityLoader.load("B"); 219 | identityLoader.dispatch(); 220 | 221 | await().until(() -> future1.isComplete() && future2.isComplete()); 222 | assertThat(future1.result(), equalTo("A")); 223 | assertThat(future2.result(), equalTo("B")); 224 | assertThat(loadCalls, equalTo(Collections.singletonList(asList("A", "B")))); 225 | 226 | identityLoader.clear("A"); 227 | 228 | Future future1a = identityLoader.load("A"); 229 | Future future2a = identityLoader.load("B"); 230 | identityLoader.dispatch(); 231 | 232 | await().until(() -> future1a.isComplete() && future2a.isComplete()); 233 | assertThat(future1a.result(), equalTo("A")); 234 | assertThat(future2a.result(), equalTo("B")); 235 | assertThat(loadCalls, equalTo(asList(asList("A", "B"), Collections.singletonList("A")))); 236 | } 237 | 238 | @Test 239 | public void should_Clear_all_values_in_loader() { 240 | ArrayList loadCalls = new ArrayList<>(); 241 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 242 | 243 | Future future1 = identityLoader.load("A"); 244 | Future future2 = identityLoader.load("B"); 245 | identityLoader.dispatch(); 246 | 247 | await().until(() -> future1.isComplete() && future2.isComplete()); 248 | assertThat(future1.result(), equalTo("A")); 249 | assertThat(future2.result(), equalTo("B")); 250 | assertThat(loadCalls, equalTo(Collections.singletonList(asList("A", "B")))); 251 | 252 | identityLoader.clearAll(); 253 | 254 | Future future1a = identityLoader.load("A"); 255 | Future future2a = identityLoader.load("B"); 256 | identityLoader.dispatch(); 257 | 258 | await().until(() -> future1a.isComplete() && future2a.isComplete()); 259 | assertThat(future1a.result(), equalTo("A")); 260 | assertThat(future2a.result(), equalTo("B")); 261 | assertThat(loadCalls, equalTo(asList(asList("A", "B"), asList("A", "B")))); 262 | } 263 | 264 | @Test 265 | public void should_Allow_priming_the_cache() { 266 | ArrayList loadCalls = new ArrayList<>(); 267 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 268 | 269 | identityLoader.prime("A", "A"); 270 | 271 | Future future1 = identityLoader.load("A"); 272 | Future future2 = identityLoader.load("B"); 273 | identityLoader.dispatch(); 274 | 275 | await().until(() -> future1.isComplete() && future2.isComplete()); 276 | assertThat(future1.result(), equalTo("A")); 277 | assertThat(future2.result(), equalTo("B")); 278 | assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList("B")))); 279 | } 280 | 281 | @Test 282 | public void should_Not_prime_keys_that_already_exist() { 283 | ArrayList loadCalls = new ArrayList<>(); 284 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 285 | 286 | identityLoader.prime("A", "X"); 287 | 288 | Future future1 = identityLoader.load("A"); 289 | Future future2 = identityLoader.load("B"); 290 | CompositeFuture composite = identityLoader.dispatch(); 291 | 292 | await().until((Callable) composite::succeeded); 293 | assertThat(future1.result(), equalTo("X")); 294 | assertThat(future2.result(), equalTo("B")); 295 | 296 | identityLoader.prime("A", "Y"); 297 | identityLoader.prime("B", "Y"); 298 | 299 | Future future1a = identityLoader.load("A"); 300 | Future future2a = identityLoader.load("B"); 301 | CompositeFuture composite2 = identityLoader.dispatch(); 302 | 303 | await().until((Callable) composite2::succeeded); 304 | assertThat(future1a.result(), equalTo("X")); 305 | assertThat(future2a.result(), equalTo("B")); 306 | assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList("B")))); 307 | } 308 | 309 | @Test 310 | public void should_Allow_to_forcefully_prime_the_cache() { 311 | ArrayList loadCalls = new ArrayList<>(); 312 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 313 | 314 | identityLoader.prime("A", "X"); 315 | 316 | Future future1 = identityLoader.load("A"); 317 | Future future2 = identityLoader.load("B"); 318 | CompositeFuture composite = identityLoader.dispatch(); 319 | 320 | await().until((Callable) composite::succeeded); 321 | assertThat(future1.result(), equalTo("X")); 322 | assertThat(future2.result(), equalTo("B")); 323 | 324 | identityLoader.clear("A").prime("A", "Y"); 325 | identityLoader.clear("B").prime("B", "Y"); 326 | 327 | Future future1a = identityLoader.load("A"); 328 | Future future2a = identityLoader.load("B"); 329 | CompositeFuture composite2 = identityLoader.dispatch(); 330 | 331 | await().until((Callable) composite2::succeeded); 332 | assertThat(future1a.result(), equalTo("Y")); 333 | assertThat(future2a.result(), equalTo("Y")); 334 | assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList("B")))); 335 | } 336 | 337 | @Test 338 | public void should_Resolve_to_error_to_indicate_failure() { 339 | ArrayList loadCalls = new ArrayList<>(); 340 | DataLoader evenLoader = idLoaderWithErrors(new DataLoaderOptions(), loadCalls); 341 | 342 | Future future1 = evenLoader.load(1); 343 | evenLoader.dispatch(); 344 | 345 | await().until(future1::isComplete); 346 | assertThat(future1.failed(), is(true)); 347 | assertThat(future1.cause(), instanceOf(IllegalStateException.class)); 348 | 349 | Future future2 = evenLoader.load(2); 350 | evenLoader.dispatch(); 351 | 352 | await().until(future2::isComplete); 353 | assertThat(future2.result(), equalTo(2)); 354 | assertThat(loadCalls, equalTo(asList(Collections.singletonList(1), Collections.singletonList(2)))); 355 | } 356 | 357 | @Test 358 | public void should_Represent_failures_and_successes_simultaneously() { 359 | AtomicBoolean success = new AtomicBoolean(); 360 | ArrayList loadCalls = new ArrayList<>(); 361 | DataLoader evenLoader = idLoaderWithErrors(new DataLoaderOptions(), loadCalls); 362 | 363 | Future future1 = evenLoader.load(1); 364 | Future future2 = evenLoader.load(2); 365 | Future future3 = evenLoader.load(3); 366 | Future future4 = evenLoader.load(4); 367 | CompositeFuture result = evenLoader.dispatch(); 368 | result.setHandler(rh -> success.set(true)); 369 | 370 | await().untilAtomic(success, is(true)); 371 | assertThat(future1.failed(), is(true)); 372 | assertThat(future1.cause(), instanceOf(IllegalStateException.class)); 373 | assertThat(future2.result(), equalTo(2)); 374 | assertThat(future3.failed(), is(true)); 375 | assertThat(future4.result(), equalTo(4)); 376 | 377 | assertThat(loadCalls, equalTo(Collections.singletonList(asList(1, 2, 3, 4)))); 378 | } 379 | 380 | @Test 381 | public void should_Cache_failed_fetches() { 382 | ArrayList loadCalls = new ArrayList<>(); 383 | DataLoader errorLoader = idLoaderAllErrors(new DataLoaderOptions(), loadCalls); 384 | 385 | Future future1 = errorLoader.load(1); 386 | errorLoader.dispatch(); 387 | 388 | await().until(future1::isComplete); 389 | assertThat(future1.failed(), is(true)); 390 | assertThat(future1.cause(), instanceOf(IllegalStateException.class)); 391 | 392 | Future future2 = errorLoader.load(1); 393 | errorLoader.dispatch(); 394 | 395 | await().until(future2::isComplete); 396 | assertThat(future2.failed(), is(true)); 397 | assertThat(future2.cause(), instanceOf(IllegalStateException.class)); 398 | assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList(1)))); 399 | } 400 | 401 | @Test 402 | public void should_Handle_priming_the_cache_with_an_error() { 403 | ArrayList loadCalls = new ArrayList<>(); 404 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 405 | 406 | identityLoader.prime(1, new IllegalStateException("Error")); 407 | 408 | Future future1 = identityLoader.load(1); 409 | identityLoader.dispatch(); 410 | 411 | await().until(future1::isComplete); 412 | assertThat(future1.failed(), is(true)); 413 | assertThat(future1.cause(), instanceOf(IllegalStateException.class)); 414 | assertThat(loadCalls, equalTo(Collections.emptyList())); 415 | } 416 | 417 | @Test 418 | public void should_Clear_values_from_cache_after_errors() { 419 | ArrayList loadCalls = new ArrayList<>(); 420 | DataLoader errorLoader = idLoaderAllErrors(new DataLoaderOptions(), loadCalls); 421 | 422 | Future future1 = errorLoader.load(1); 423 | future1.setHandler(rh -> { 424 | if (rh.failed()) { 425 | // Presumably determine if this error is transient, and only clear the cache in that case. 426 | errorLoader.clear(1); 427 | } 428 | }); 429 | errorLoader.dispatch(); 430 | 431 | await().until(future1::isComplete); 432 | assertThat(future1.failed(), is(true)); 433 | assertThat(future1.cause(), instanceOf(IllegalStateException.class)); 434 | 435 | Future future2 = errorLoader.load(1); 436 | future2.setHandler(rh -> { 437 | if (rh.failed()) { 438 | // Again, only do this if you can determine the error is transient. 439 | errorLoader.clear(1); 440 | } 441 | }); 442 | errorLoader.dispatch(); 443 | 444 | await().until(future2::isComplete); 445 | assertThat(future2.failed(), is(true)); 446 | assertThat(future2.cause(), instanceOf(IllegalStateException.class)); 447 | assertThat(loadCalls, equalTo(asList(Collections.singletonList(1), Collections.singletonList(1)))); 448 | } 449 | 450 | @Test 451 | public void should_Propagate_error_to_all_loads() { 452 | ArrayList loadCalls = new ArrayList<>(); 453 | DataLoader errorLoader = idLoaderAllErrors(new DataLoaderOptions(), loadCalls); 454 | 455 | Future future1 = errorLoader.load(1); 456 | Future future2 = errorLoader.load(2); 457 | errorLoader.dispatch(); 458 | 459 | await().until(future1::isComplete); 460 | assertThat(future1.failed(), is(true)); 461 | Throwable cause = future1.cause(); 462 | assertThat(cause, instanceOf(IllegalStateException.class)); 463 | assertThat(cause.getMessage(), equalTo("Error")); 464 | 465 | await().until(future2::isComplete); 466 | cause = future2.cause(); 467 | assertThat(cause.getMessage(), equalTo(cause.getMessage())); 468 | assertThat(loadCalls, equalTo(Collections.singletonList(asList(1, 2)))); 469 | } 470 | 471 | // Accept any kind of key. 472 | 473 | @Test 474 | public void should_Accept_objects_as_keys() { 475 | ArrayList loadCalls = new ArrayList<>(); 476 | DataLoader identityLoader = idLoader(new DataLoaderOptions(), loadCalls); 477 | 478 | Object keyA = new Object(); 479 | Object keyB = new Object(); 480 | 481 | // Fetches as expected 482 | 483 | identityLoader.load(keyA); 484 | identityLoader.load(keyB); 485 | 486 | identityLoader.dispatch().setHandler(rh -> { 487 | assertThat(rh.succeeded(), is(true)); 488 | assertThat(rh.result().resultAt(0), equalTo(keyA)); 489 | assertThat(rh.result().resultAt(1), equalTo(keyB)); 490 | }); 491 | 492 | assertThat(loadCalls.size(), equalTo(1)); 493 | assertThat(loadCalls.get(0).size(), equalTo(2)); 494 | assertThat(loadCalls.get(0).toArray()[0], equalTo(keyA)); 495 | assertThat(loadCalls.get(0).toArray()[1], equalTo(keyB)); 496 | 497 | // Caching 498 | identityLoader.clear(keyA); 499 | //noinspection SuspiciousMethodCalls 500 | loadCalls.remove(keyA); 501 | 502 | identityLoader.load(keyA); 503 | identityLoader.load(keyB); 504 | 505 | identityLoader.dispatch().setHandler(rh -> { 506 | assertThat(rh.succeeded(), is(true)); 507 | assertThat(rh.result().resultAt(0), equalTo(keyA)); 508 | assertThat(identityLoader.getCacheKey(keyB), equalTo(keyB)); 509 | }); 510 | 511 | assertThat(loadCalls.size(), equalTo(2)); 512 | assertThat(loadCalls.get(1).size(), equalTo(1)); 513 | assertThat(loadCalls.get(1).toArray()[0], equalTo(keyA)); 514 | } 515 | 516 | // Accepts options 517 | 518 | @Test 519 | public void should_Disable_caching() { 520 | ArrayList loadCalls = new ArrayList<>(); 521 | DataLoader identityLoader = 522 | idLoader(DataLoaderOptions.create().setCachingEnabled(false), loadCalls); 523 | 524 | Future future1 = identityLoader.load("A"); 525 | Future future2 = identityLoader.load("B"); 526 | identityLoader.dispatch(); 527 | 528 | await().until(() -> future1.isComplete() && future2.isComplete()); 529 | assertThat(future1.result(), equalTo("A")); 530 | assertThat(future2.result(), equalTo("B")); 531 | assertThat(loadCalls, equalTo(Collections.singletonList(asList("A", "B")))); 532 | 533 | Future future1a = identityLoader.load("A"); 534 | Future future3 = identityLoader.load("C"); 535 | identityLoader.dispatch(); 536 | 537 | await().until(() -> future1a.isComplete() && future3.isComplete()); 538 | assertThat(future1a.result(), equalTo("A")); 539 | assertThat(future3.result(), equalTo("C")); 540 | assertThat(loadCalls, equalTo(asList(asList("A", "B"), asList("A", "C")))); 541 | 542 | Future future1b = identityLoader.load("A"); 543 | Future future2a = identityLoader.load("B"); 544 | Future future3a = identityLoader.load("C"); 545 | identityLoader.dispatch(); 546 | 547 | await().until(() -> future1b.isComplete() && future2a.isComplete() && future3a.isComplete()); 548 | assertThat(future1b.result(), equalTo("A")); 549 | assertThat(future2a.result(), equalTo("B")); 550 | assertThat(future3a.result(), equalTo("C")); 551 | assertThat(loadCalls, equalTo(asList(asList("A", "B"), 552 | asList("A", "C"), asList("A", "B", "C")))); 553 | } 554 | 555 | // Accepts object key in custom cacheKey function 556 | 557 | @Test 558 | public void should_Accept_objects_with_a_complex_key() { 559 | ArrayList loadCalls = new ArrayList<>(); 560 | DataLoaderOptions options = DataLoaderOptions.create().setCacheKeyFunction(getJsonObjectCacheMapFn()); 561 | DataLoader identityLoader = idLoader(options, loadCalls); 562 | 563 | JsonObject key1 = new JsonObject().put("id", 123); 564 | JsonObject key2 = new JsonObject().put("id", 123); 565 | 566 | Future future1 = identityLoader.load(key1); 567 | Future future2 = identityLoader.load(key2); 568 | identityLoader.dispatch(); 569 | 570 | await().until(() -> future1.isComplete() && future2.isComplete()); 571 | assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList(key1)))); 572 | assertThat(future1.result(), equalTo(key1)); 573 | assertThat(future2.result(), equalTo(key1)); 574 | } 575 | 576 | @Test 577 | public void should_Clear_objects_with_complex_key() { 578 | ArrayList loadCalls = new ArrayList<>(); 579 | DataLoaderOptions options = DataLoaderOptions.create().setCacheKeyFunction(getJsonObjectCacheMapFn()); 580 | DataLoader identityLoader = idLoader(options, loadCalls); 581 | 582 | JsonObject key1 = new JsonObject().put("id", 123); 583 | JsonObject key2 = new JsonObject().put("id", 123); 584 | 585 | Future future1 = identityLoader.load(key1); 586 | identityLoader.dispatch(); 587 | 588 | await().until(future1::isComplete); 589 | identityLoader.clear(key2); // clear equivalent object key 590 | 591 | Future future2 = identityLoader.load(key1); 592 | identityLoader.dispatch(); 593 | 594 | await().until(future2::isComplete); 595 | assertThat(loadCalls, equalTo(asList(Collections.singletonList(key1), Collections.singletonList(key1)))); 596 | assertThat(future1.result(), equalTo(key1)); 597 | assertThat(future2.result(), equalTo(key1)); 598 | } 599 | 600 | @Test 601 | public void should_Accept_objects_with_different_order_of_keys() { 602 | ArrayList loadCalls = new ArrayList<>(); 603 | DataLoaderOptions options = DataLoaderOptions.create().setCacheKeyFunction(getJsonObjectCacheMapFn()); 604 | DataLoader identityLoader = idLoader(options, loadCalls); 605 | 606 | JsonObject key1 = new JsonObject().put("a", 123).put("b", 321); 607 | JsonObject key2 = new JsonObject().put("b", 321).put("a", 123); 608 | 609 | // Fetches as expected 610 | 611 | Future future1 = identityLoader.load(key1); 612 | Future future2 = identityLoader.load(key2); 613 | identityLoader.dispatch(); 614 | 615 | await().until(() -> future1.isComplete() && future2.isComplete()); 616 | assertThat(loadCalls, equalTo(Collections.singletonList(Collections.singletonList(key1)))); 617 | assertThat(loadCalls.size(), equalTo(1)); 618 | assertThat(future1.result(), equalTo(key1)); 619 | assertThat(future2.result(), equalTo(key1)); 620 | } 621 | 622 | @Test 623 | public void should_Allow_priming_the_cache_with_an_object_key() { 624 | ArrayList loadCalls = new ArrayList<>(); 625 | DataLoaderOptions options = DataLoaderOptions.create().setCacheKeyFunction(getJsonObjectCacheMapFn()); 626 | DataLoader identityLoader = idLoader(options, loadCalls); 627 | 628 | JsonObject key1 = new JsonObject().put("id", 123); 629 | JsonObject key2 = new JsonObject().put("id", 123); 630 | 631 | identityLoader.prime(key1, key1); 632 | 633 | Future future1 = identityLoader.load(key1); 634 | Future future2 = identityLoader.load(key2); 635 | identityLoader.dispatch(); 636 | 637 | await().until(() -> future1.isComplete() && future2.isComplete()); 638 | assertThat(loadCalls, equalTo(Collections.emptyList())); 639 | assertThat(future1.result(), equalTo(key1)); 640 | assertThat(future2.result(), equalTo(key1)); 641 | } 642 | 643 | @Test 644 | public void should_Accept_a_custom_cache_map_implementation() { 645 | CustomCacheMap customMap = new CustomCacheMap(); 646 | ArrayList loadCalls = new ArrayList<>(); 647 | DataLoaderOptions options = DataLoaderOptions.create().setCacheMap(customMap); 648 | DataLoader identityLoader = idLoader(options, loadCalls); 649 | 650 | // Fetches as expected 651 | 652 | Future future1 = identityLoader.load("a"); 653 | Future future2 = identityLoader.load("b"); 654 | CompositeFuture composite = identityLoader.dispatch(); 655 | 656 | await().until((Callable) composite::isComplete); 657 | assertThat(future1.result(), equalTo("a")); 658 | assertThat(future2.result(), equalTo("b")); 659 | 660 | assertThat(loadCalls, equalTo(Collections.singletonList(asList("a", "b")))); 661 | assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "b").toArray()); 662 | 663 | Future future3 = identityLoader.load("c"); 664 | Future future2a = identityLoader.load("b"); 665 | composite = identityLoader.dispatch(); 666 | 667 | await().until((Callable) composite::isComplete); 668 | assertThat(future3.result(), equalTo("c")); 669 | assertThat(future2a.result(), equalTo("b")); 670 | 671 | assertThat(loadCalls, equalTo(asList(asList("a", "b"), Collections.singletonList("c")))); 672 | assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "b", "c").toArray()); 673 | 674 | // Supports clear 675 | 676 | identityLoader.clear("b"); 677 | assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "c").toArray()); 678 | 679 | Future future2b = identityLoader.load("b"); 680 | composite = identityLoader.dispatch(); 681 | 682 | await().until((Callable) composite::isComplete); 683 | assertThat(future2b.result(), equalTo("b")); 684 | assertThat(loadCalls, equalTo(asList(asList("a", "b"), 685 | Collections.singletonList("c"), Collections.singletonList("b")))); 686 | assertArrayEquals(customMap.stash.keySet().toArray(), asList("a", "c", "b").toArray()); 687 | 688 | // Supports clear all 689 | 690 | identityLoader.clearAll(); 691 | assertArrayEquals(customMap.stash.keySet().toArray(), Collections.emptyList().toArray()); 692 | } 693 | 694 | // It is resilient to job queue ordering 695 | 696 | @Test 697 | public void should_Batch_loads_occurring_within_futures() { 698 | ArrayList loadCalls = new ArrayList<>(); 699 | DataLoader identityLoader = idLoader(DataLoaderOptions.create(), loadCalls); 700 | 701 | Future.future().setHandler(rh -> { 702 | identityLoader.load("a"); 703 | Future.future().setHandler(rh2 -> { 704 | identityLoader.load("b"); 705 | Future.future().setHandler(rh3 -> { 706 | identityLoader.load("c"); 707 | Future.future().setHandler(rh4 -> 708 | identityLoader.load("d")).complete(); 709 | }).complete(); 710 | }).complete(); 711 | }).complete(); 712 | CompositeFuture composite = identityLoader.dispatch(); 713 | 714 | await().until((Callable) composite::isComplete); 715 | assertThat(loadCalls, equalTo( 716 | Collections.singletonList(asList("a", "b", "c", "d")))); 717 | } 718 | 719 | @Test 720 | @Ignore 721 | public void should_Call_a_loader_from_a_loader() { 722 | // TODO Provide implementation with Futures 723 | } 724 | 725 | // Helper methods 726 | 727 | private static CacheKey getJsonObjectCacheMapFn() { 728 | return key -> key.stream() 729 | .map(entry -> entry.getKey() + ":" + entry.getValue()) 730 | .sorted() 731 | .collect(Collectors.joining()); 732 | } 733 | 734 | public class CustomCacheMap implements CacheMap { 735 | 736 | public Map stash; 737 | 738 | public CustomCacheMap() { 739 | stash = new LinkedHashMap<>(); 740 | } 741 | 742 | @Override 743 | public boolean containsKey(String key) { 744 | return stash.containsKey(key); 745 | } 746 | 747 | @Override 748 | public Object get(String key) { 749 | return stash.get(key); 750 | } 751 | 752 | @Override 753 | public CacheMap set(String key, Object value) { 754 | stash.put(key, value); 755 | return this; 756 | } 757 | 758 | @Override 759 | public CacheMap delete(String key) { 760 | stash.remove(key); 761 | return this; 762 | } 763 | 764 | @Override 765 | public CacheMap clear() { 766 | stash.clear(); 767 | return this; 768 | } 769 | } 770 | 771 | @SuppressWarnings("unchecked") 772 | private static DataLoader idLoader(DataLoaderOptions options, List loadCalls) { 773 | return new DataLoader<>(keys -> { 774 | loadCalls.add(new ArrayList(keys)); 775 | List futures = keys.stream().map(Future::succeededFuture).collect(Collectors.toList()); 776 | return CompositeFuture.join(futures); 777 | }, options); 778 | } 779 | 780 | @SuppressWarnings("unchecked") 781 | private static DataLoader idLoaderAllErrors( 782 | DataLoaderOptions options, List loadCalls) { 783 | return new DataLoader<>(keys -> { 784 | loadCalls.add(new ArrayList(keys)); 785 | List futures = keys.stream() 786 | .map(key -> Future.failedFuture(new IllegalStateException("Error"))) 787 | .collect(Collectors.toList()); 788 | return CompositeFuture.join(futures); 789 | }, options); 790 | } 791 | 792 | @SuppressWarnings("unchecked") 793 | private static DataLoader idLoaderWithErrors( 794 | DataLoaderOptions options, List loadCalls) { 795 | return new DataLoader<>(keys -> { 796 | loadCalls.add(new ArrayList(keys)); 797 | List futures = keys.stream() 798 | .map(key -> key % 2 == 0 ? Future.succeededFuture(key) : 799 | Future.failedFuture(new IllegalStateException("Error"))) 800 | .collect(Collectors.toList()); 801 | return CompositeFuture.join(futures); 802 | }, options); 803 | } 804 | } 805 | --------------------------------------------------------------------------------