├── .gitignore ├── .travis.yml ├── ChangeLog.md ├── LICENSE ├── LICENSE-APACHE2 ├── LICENSE-MPL-RabbitMQ ├── Makefile ├── README.md ├── rebar.config ├── src ├── gen_batch_server.app.src └── gen_batch_server.erl └── test └── gen_batch_server_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | ebin/*.beam 8 | rel/example_project 9 | .concrete/DEV_MODE 10 | .rebar 11 | .erlang.mk 12 | doc 13 | logs 14 | *.d 15 | ebin/ 16 | _build 17 | rebar.lock 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # vim:sw=2:et: 2 | 3 | dist: bionic 4 | 5 | sudo: false 6 | language: erlang 7 | notifications: 8 | email: 9 | on_success: never 10 | on_failure: always 11 | cache: 12 | apt: true 13 | 14 | otp_release: 15 | - "23.3.1" 16 | - "24.0.2" 17 | 18 | 19 | script: 20 | - make test xref dialyzer 21 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Changes Between 0.8.4 and 0.9.0 (in development) 4 | 5 | No changes yet. 6 | 7 | ## Changes Between 0.8.3 and 0.8.4 (July 20th, 2020) 8 | 9 | ### License Change 10 | 11 | The library is now double-licensed under the Apache Software License 2.0 12 | and Mozilla Public License 2.0 (previously: under the ASL2 and Mozilla Public License 1.1). 13 | 14 | ### Minimum Supported Erlang Version Bump 15 | 16 | The library now requires OTP 21.3 or a later version. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This package, gen_batch_server, is dual-licensed under 2 | the Apache License v2 and the Mozilla Public License v2.0. 3 | 4 | For the Apache License, please see the file LICENSE-APACHE2. 5 | 6 | For the Mozilla Public License, please see the file LICENSE-MPL-RabbitMQ. 7 | 8 | For attribution of copyright and other details of provenance, please 9 | refer to the source code. 10 | 11 | If you have any questions regarding licensing, please contact us at 12 | info@rabbitmq.com. 13 | -------------------------------------------------------------------------------- /LICENSE-APACHE2: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017 Pivotal Software Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | https://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE-MPL-RabbitMQ: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EPMD ?= $(shell which epmd) 2 | REBAR ?= $(shell which rebar3) 3 | 4 | clean: 5 | $(REBAR) clean 6 | 7 | xref: 8 | @$(REBAR) xref 9 | 10 | dialyzer: 11 | @$(REBAR) dialyzer 12 | 13 | test: clean 14 | @$(EPMD) -daemon 15 | @$(REBAR) ct --readable=false --sname=batch_test 16 | 17 | tests: test 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gen_batch_server 2 | 3 | [![Build Status](https://travis-ci.org/rabbitmq/gen-batch-server.svg?branch=master)](https://travis-ci.org/rabbitmq/gen-batch-server) 4 | 5 | 6 | A generic batching server for erlang / elixir 7 | 8 | 9 | `gen_batch_server` is a stateful generic server similar to [`gen_server`](https://erlang.org/doc/man/gen_server.html) that instead of processing incoming requests 10 | one by one gathers them into batches before they are passed to the behaviour 11 | implementation. 12 | 13 | Batches are processed _either_ when the Erlang process mailbox has no further 14 | messages to batch _or_ 15 | when the number of messages in the current batch reaches the maximum batch size 16 | limit. 17 | `gen_batch_server` tries to trade latency for throughput by automatically growing the max batch 18 | size limit when message ingress is high and shrinks it down again as ingress 19 | reduces. 20 | 21 | This behaviour makes it suitable for use as a data sink, proxy or other kinds of 22 | aggregator that benefit from processing messages in batches. Examples 23 | would be a log writer that needs to flush messages to disk using `file:sync/1` 24 | without undue delay or a metrics sink that aggregates metrics from multiple 25 | processes and writes them to an external service. It could also be beneficial 26 | to use `gen_batch_server` to proxy a bunch of processes that want to update 27 | some resource (such as a `dets` table) that doesn't handle casts. 28 | 29 | ## Usage 30 | 31 | #### start_link(Name, Mod, Args) -> Result 32 | #### start_link(Name, Mod, Args, Opts) -> Result 33 | 34 | Types: 35 | Name = {local,Name} | {global,GlobalName} | {via,Module,ViaName} 36 | Mod = module() 37 | Args = term() 38 | Opt = {debug, Dbgs} | 39 | {min_batch_size | max_batch_size, non_neg_integer()} | 40 | {reversed_batch, boolean()} 41 | Opts = [Opt] 42 | Opts = [term()] 43 | Result = {ok,Pid} | ignore | {error,Error} 44 | 45 | Creates a `gen_batch_server` as part of a supervision tree. The minimum and 46 | maximum batch sizes that control the bounds of the batch sizes that are processed 47 | can be controlled using the `min_batch_size` (default: 32) 48 | and `max_batch_size` (default: 8192) options. 49 | 50 | The `reversed_batch` option is an advanced option that where the batch that is 51 | passed to `handle_batch/2` is in reversed order to the one the messages were 52 | received in. This avoids a `list:reverse/1` all before the batch handling and is 53 | somewhat more performant. 54 | 55 | 56 | #### cast(ServerRef, Request) -> ok 57 | 58 | Types: 59 | ServerRef = pid() | {Name :: atom(), node()} | Name :: atom() 60 | Request = term() 61 | 62 | Sends an asynchronous request to the `gen_batch_server returning` `ok` immediately. 63 | The request tuple (`{cast, Request}`) is included in the list of operations passed 64 | to `Module:handle_batch/2`. 65 | 66 | #### cast_batch(ServerRef, Batch) -> ok 67 | 68 | Types: 69 | ServerRef = pid() | {Name :: atom(), node()} | Name :: atom() 70 | Batch = [term()] 71 | 72 | Sends an asynchronous batch of requests to the `gen_batch_server returning` `ok` 73 | immediately. The batch is appended in order to the current gen_batch_server batch. 74 | 75 | #### call(ServerRef, Request) -> Reply 76 | #### call(ServerRef, Request, Timeout) -> Reply 77 | 78 | Types: 79 | ServerRef = pid() | {Name :: atom(), node()} | Name :: atom() 80 | Request = term() 81 | Reply = term() 82 | Timeout = non_neg_integer() | infinity. 83 | 84 | Makes a synchronous call to the `gen_batch_server` and waits for the response provided 85 | by `Module:handle_batch/2`. 86 | The timeout is optional and defaults to 5000ms. 87 | 88 | #### Module:init(Args) -> Result 89 | 90 | Types: 91 | Args = term() 92 | Result = {ok, State} | {stop, Reason} 93 | State = term(), 94 | Reason = term() 95 | 96 | 97 | Called whenever a `gen_batch_server` is started with the arguments provided 98 | to `start_link/4`. 99 | 100 | #### Module:handle_batch(Batch, State) -> Result. 101 | 102 | Types: 103 | Batch = [Op] 104 | UserOp = term(), 105 | Op = {cast, UserOp} | 106 | {call, from(), UserOp} | 107 | {info, UserOp}. 108 | Result = {ok, State} | {ok, Actions, State} | {stop, Reason} 109 | State = term() 110 | From = {Pid :: pid(), Tag :: reference()} 111 | Action = {reply, From, Msg} | garbage_collect 112 | Actions = [Action] 113 | Reason = term() 114 | 115 | Called whenever a new batch is ready for processing. The implementation can 116 | optionally return a list of reply actions used to reply to `call` operations. 117 | 118 | #### Module:terminate(Reason, State) -> Result 119 | 120 | Types: 121 | Reason = term() 122 | Result = term() 123 | State = term(), 124 | 125 | 126 | Optional. Called whenever a `gen_batch_server` is terminating. 127 | 128 | #### Module:format_status(State) -> Result 129 | 130 | Types: 131 | Result = term() 132 | State = term(), 133 | 134 | 135 | Optional. Used to provide a custom formatting of the user state. 136 | 137 | 138 | # Copyright 139 | 140 | (c) 2018-2023 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. 141 | 142 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {minimum_otp_vsn, "22.3"}. 2 | {deps, []}. 3 | {project_plugins, [rebar3_hex]}. 4 | {profiles, 5 | [{test, [{deps, [meck, proper]}]}] 6 | }. 7 | {dist_node, [ 8 | {sname, 'aten'} 9 | ]}. 10 | {erl_opts, [debug_info, 11 | recv_opt_info, 12 | warn_export_vars, 13 | warn_shadow_vars, 14 | warn_obsolete_guard]}. 15 | {dialyzer, [{warnings, 16 | [error_handling, 17 | unmatched_returns 18 | ]}]}. 19 | {xref_extra_paths, ["test"]}. 20 | {xref_checks,[undefined_function_calls, 21 | undefined_functions, 22 | locals_not_used, 23 | % exports_not_used, 24 | deprecated_function_calls, 25 | deprecated_functions]}. 26 | -------------------------------------------------------------------------------- /src/gen_batch_server.app.src: -------------------------------------------------------------------------------- 1 | {application,gen_batch_server, 2 | [{description,"Generic batching server"}, 3 | {licenses,["Apache-2.0","MPL-2.0"]}, 4 | {links,[{"github", 5 | "https://github.com/rabbitmq/gen-batch-server"}]}, 6 | {vsn,"0.8.9"}, 7 | {modules,[gen_batch_server]}, 8 | {registered,[]}, 9 | {applications,[kernel,stdlib]}, 10 | {env,[]}]}. 11 | -------------------------------------------------------------------------------- /src/gen_batch_server.erl: -------------------------------------------------------------------------------- 1 | %% This Source Code Form is subject to the terms of the Mozilla Public 2 | %% License, v. 2.0. If a copy of the MPL was not distributed with this 3 | %% file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | %% 5 | %% Copyright (c) 2018-2023 Broadcom. All Rights Reserved. The term Broadcom refers to Broadcom Inc. and/or its subsidiaries. 6 | %% 7 | -module(gen_batch_server). 8 | -export([start_link/2, 9 | start_link/3, 10 | start_link/4, 11 | init_it/6, 12 | stop/1, 13 | stop/3, 14 | cast/2, 15 | cast_batch/2, 16 | call/2, 17 | call/3, 18 | system_continue/3, 19 | system_terminate/4, 20 | system_get_state/1, 21 | write_debug/3, 22 | format_status/2, 23 | %% needed for hibernation 24 | loop_wait/2 25 | ]). 26 | 27 | -define(MIN_MAX_BATCH_SIZE, 32). 28 | -define(MAX_MAX_BATCH_SIZE, 8192). 29 | 30 | -type server_ref() :: pid() | 31 | (LocalName :: atom()) | 32 | {Name :: atom(), Node :: atom()} | 33 | {'global', term()} | 34 | {'via', Module :: module(), Name :: term()}. 35 | 36 | -type from() :: {Pid :: pid(), Tag :: reference()}. 37 | 38 | -type op() :: {cast, pid(), UserOp :: term()} | 39 | {call, from(), UserOp :: term()} | 40 | {info, UserOp :: term()}. 41 | 42 | -record(config, {batch_size :: non_neg_integer(), 43 | min_batch_size :: non_neg_integer(), 44 | max_batch_size :: non_neg_integer(), 45 | parent :: pid(), 46 | name :: atom(), 47 | module :: module(), 48 | hibernate_after = infinity :: non_neg_integer(), 49 | reversed_batch = false :: boolean()}). 50 | 51 | -record(state, {batch = [] :: [op()], 52 | batch_count = 0 :: non_neg_integer(), 53 | config = #config{} :: #config{}, 54 | state :: term(), 55 | needs_gc = false :: boolean(), 56 | debug :: list()}). 57 | 58 | % -type state() :: #state{}. 59 | 60 | -export_type([from/0, op/0, 61 | action/0, server_ref/0]). 62 | 63 | %%% Behaviour 64 | 65 | -type action() :: 66 | {reply, from(), Msg :: term()} | 67 | garbage_collect. 68 | %% an action that can be returned from handle_batch/2 69 | 70 | -callback init(Args :: term()) -> 71 | {ok, State} | 72 | {ok, State, {continue, term()}} | 73 | {stop, Reason :: term()} 74 | when State :: term(). 75 | 76 | -callback handle_batch([op()], State) -> 77 | {ok, State} | 78 | {ok, State, {continue, term()}} | 79 | {ok, [action()], State} | 80 | {ok, [action()], State, {continue, term()}} | 81 | {stop, Reason :: term()} 82 | when State :: term(). 83 | 84 | -callback handle_continue(Continue :: term(), State :: term()) -> term(). 85 | 86 | -callback terminate(Reason :: term(), State :: term()) -> term(). 87 | 88 | -callback format_status(State :: term()) -> term(). 89 | 90 | %% TODO: code_change 91 | 92 | -optional_callbacks([handle_continue/2, 93 | format_status/1, 94 | terminate/2]). 95 | 96 | 97 | %%% 98 | %%% API 99 | %%% 100 | 101 | -spec start_link(Mod, Args) -> Result when 102 | Mod :: module(), 103 | Args :: term(), 104 | Result :: {ok,pid()} | {error, Reason :: term()}. 105 | start_link(Mod, Args) -> 106 | gen:start(?MODULE, link, Mod, {[], Args}, []). 107 | 108 | -spec start_link(Name, Mod, Args) -> Result when 109 | Name :: {local, atom()} | 110 | {global, term()} | 111 | {via, atom(), term()} | 112 | undefined, 113 | Mod :: module(), 114 | Args :: term(), 115 | Result :: {ok,pid()} | {error, Reason :: term()}. 116 | start_link(Name, Mod, Args) -> 117 | gen_start(Name, Mod, Args, []). 118 | 119 | -spec start_link(Name, Mod, Args, Options) -> Result when 120 | Name :: {local, atom()} | 121 | {global, term()} | 122 | {via, atom(), term()} | 123 | undefined, 124 | Mod :: module(), 125 | Args :: term(), 126 | Options :: list(), 127 | Result :: {ok,pid()} | {error, Reason :: term()}. 128 | start_link(Name, Mod, Args, Opts0) -> 129 | gen_start(Name, Mod, Args, Opts0). 130 | 131 | %% pretty much copied wholesale from gen_server 132 | init_it(Starter, self, Name, Mod, Args, Options) -> 133 | init_it(Starter, self(), Name, Mod, Args, Options); 134 | init_it(Starter, Parent, Name0, Mod, {GBOpts, Args}, Options) -> 135 | Name = gen:name(Name0), 136 | Debug = gen:debug_options(Name, Options), 137 | HibernateAfter = gen:hibernate_after(Options), 138 | MaxBatchSize = proplists:get_value(max_batch_size, GBOpts, 139 | ?MAX_MAX_BATCH_SIZE), 140 | MinBatchSize = proplists:get_value(min_batch_size, GBOpts, 141 | ?MIN_MAX_BATCH_SIZE), 142 | ReverseBatch = proplists:get_value(reversed_batch, GBOpts, false), 143 | Conf = #config{module = Mod, 144 | parent = Parent, 145 | name = Name, 146 | batch_size = MinBatchSize, 147 | min_batch_size = MinBatchSize, 148 | max_batch_size = MaxBatchSize, 149 | hibernate_after = HibernateAfter, 150 | reversed_batch = ReverseBatch}, 151 | case catch Mod:init(Args) of 152 | {ok, Inner0} -> 153 | proc_lib:init_ack(Starter, {ok, self()}), 154 | State = #state{config = Conf, 155 | state = Inner0, 156 | debug = Debug}, 157 | loop_wait(State, Parent); 158 | {ok, Inner0, {continue, Continue}} -> 159 | proc_lib:init_ack(Starter, {ok, self()}), 160 | State0 = #state{config = Conf, 161 | state = Inner0, 162 | debug = Debug}, 163 | State = handle_continue(Continue, State0), 164 | loop_wait(State, Parent); 165 | {stop, Reason} -> 166 | %% For consistency, we must make sure that the 167 | %% registered name (if any) is unregistered before 168 | %% the parent process is notified about the failure. 169 | %% (Otherwise, the parent process could get 170 | %% an 'already_started' error if it immediately 171 | %% tried starting the process again.) 172 | gen:unregister_name(Name0), 173 | proc_lib:init_ack(Starter, {error, Reason}), 174 | exit(Reason); 175 | % ignore -> 176 | % gen:unregister_name(Name0), 177 | % proc_lib:init_ack(Starter, ignore), 178 | % exit(normal); 179 | {'EXIT', Reason} -> 180 | gen:unregister_name(Name0), 181 | proc_lib:init_ack(Starter, {error, Reason}), 182 | exit(Reason); 183 | Else -> 184 | Error = {bad_return_value, Else}, 185 | proc_lib:init_ack(Starter, {error, Error}), 186 | exit(Error) 187 | end. 188 | 189 | stop(Name) -> 190 | gen:stop(Name). 191 | 192 | stop(Name, Reason, Timeout) -> 193 | gen:stop(Name, Reason, Timeout). 194 | 195 | -spec cast(server_ref(), term()) -> ok. 196 | cast({global,Name}, Request) -> 197 | catch global:send(Name, cast_msg(Request)), 198 | ok; 199 | cast({via, Mod, Name}, Request) -> 200 | catch Mod:send(Name, cast_msg(Request)), 201 | ok; 202 | cast({Name,Node}=Dest, Request) when is_atom(Name), is_atom(Node) -> 203 | do_cast(Dest, Request); 204 | cast(Dest, Request) when is_atom(Dest) -> 205 | do_cast(Dest, Request); 206 | cast(Dest, Request) when is_pid(Dest) -> 207 | do_cast(Dest, Request). 208 | 209 | 210 | do_cast(Dest, Request) -> 211 | do_send(Dest, cast_msg(Request)), 212 | ok. 213 | 214 | cast_msg(Request) -> 215 | {'$gen_cast', Request}. 216 | 217 | -spec cast_batch(server_ref(), [term()]) -> ok. 218 | cast_batch({global,Name}, Batch) -> 219 | catch global:send(Name, cast_batch_msg(Batch)), 220 | ok; 221 | cast_batch({via, Mod, Name}, Batch) -> 222 | catch Mod:send(Name, cast_batch_msg(Batch)), 223 | ok; 224 | cast_batch({Name,Node}=Dest, Batch) when is_atom(Name), is_atom(Node) -> 225 | do_cast_batch(Dest, Batch); 226 | cast_batch(Dest, Batch) when is_atom(Dest) -> 227 | do_cast_batch(Dest, Batch); 228 | cast_batch(Dest, Batch) when is_pid(Dest) -> 229 | do_cast_batch(Dest, Batch). 230 | 231 | do_cast_batch(Dest, Batch) -> 232 | do_send(Dest, cast_batch_msg(Batch)), 233 | ok. 234 | 235 | cast_batch_msg(Msgs0) when is_list(Msgs0) -> 236 | Msgs = lists:foldl(fun (Msg, Acc) -> 237 | [{cast, Msg} | Acc] 238 | end, [], Msgs0), 239 | {'$gen_cast_batch', Msgs, length(Msgs)}; 240 | cast_batch_msg(_) -> 241 | erlang:error(badarg). 242 | 243 | -spec call(server_ref(), Request :: term()) -> term(). 244 | call(Name, Request) -> 245 | call(Name, Request, 5000). 246 | 247 | -spec call(pid() | atom(), term(), non_neg_integer() | infinity) -> term(). 248 | call(Name, Request, Timeout) -> 249 | case catch gen:call(Name, '$gen_call', Request, Timeout) of 250 | {ok, Res} -> 251 | Res; 252 | {'EXIT', Reason} -> 253 | exit({Reason, {?MODULE, call, [Name, Request, Timeout]}}) 254 | end. 255 | 256 | 257 | %% Internal 258 | 259 | loop_wait(#state{config = #config{hibernate_after = Hib}} = State00, Parent) -> 260 | %% batches can accumulate a lot of garbage, collect it here 261 | %% evaluate gc state 262 | State0 = case State00 of 263 | #state{needs_gc = true} -> 264 | _ = garbage_collect(), 265 | State00#state{needs_gc = false}; 266 | _ -> 267 | State00 268 | end, 269 | receive 270 | Msg -> 271 | case Msg of 272 | {system, From, Request} -> 273 | sys:handle_system_msg(Request, From, Parent, 274 | ?MODULE, State0#state.debug, State0); 275 | {'EXIT', Parent, Reason} -> 276 | terminate(Reason, State0), 277 | exit(Reason); 278 | _ -> 279 | enter_loop_batched(Msg, Parent, State0) 280 | end 281 | after Hib -> 282 | proc_lib:hibernate(?MODULE, ?FUNCTION_NAME, [State0, Parent]) 283 | end. 284 | 285 | append_msg({'$gen_cast', Msg}, 286 | #state{batch = Batch, 287 | batch_count = BatchCount} = State0) -> 288 | State0#state{batch = [{cast, Msg} | Batch], 289 | batch_count = BatchCount + 1}; 290 | append_msg({'$gen_cast_batch', Msgs, Count}, 291 | #state{batch = Batch, 292 | batch_count = BatchCount} = State0) -> 293 | State0#state{batch = Msgs ++ Batch, 294 | batch_count = BatchCount + Count}; 295 | append_msg({'$gen_call', From, Msg}, 296 | #state{batch = Batch, 297 | batch_count = BatchCount} = State0) -> 298 | State0#state{batch = [{call, From, Msg} | Batch], 299 | batch_count = BatchCount + 1}; 300 | append_msg(Msg, #state{batch = Batch, 301 | batch_count = BatchCount} = State0) -> 302 | State0#state{batch = [{info, Msg} | Batch], 303 | batch_count = BatchCount + 1}. 304 | 305 | enter_loop_batched(Msg, Parent, #state{debug = []} = State0) -> 306 | loop_batched(append_msg(Msg, State0), Parent); 307 | enter_loop_batched(Msg, Parent, State0) -> 308 | State = handle_debug_in(State0, Msg), 309 | %% append to batch 310 | loop_batched(append_msg(Msg, State), Parent). 311 | 312 | loop_batched(#state{config = #config{batch_size = BatchSize, 313 | max_batch_size = Max} = Config, 314 | batch_count = BatchCount} = State0, 315 | Parent) when BatchCount >= BatchSize -> 316 | % complete batch after seeing batch_size writes 317 | State = complete_batch(State0), 318 | % grow max batch size 319 | NewBatchSize = min(Max, BatchSize * 2), 320 | loop_wait(State#state{config = Config#config{batch_size = NewBatchSize}}, 321 | Parent); 322 | loop_batched(#state{debug = Debug} = State0, Parent) -> 323 | receive 324 | Msg -> 325 | case Msg of 326 | {system, From, Request} -> 327 | sys:handle_system_msg(Request, From, Parent, 328 | ?MODULE, Debug, State0); 329 | {'EXIT', Parent, Reason} -> 330 | terminate(Reason, State0), 331 | exit(Reason); 332 | _ -> 333 | enter_loop_batched(Msg, Parent, State0) 334 | end 335 | after 0 -> 336 | State = complete_batch(State0), 337 | Config = State#state.config, 338 | NewBatchSize = max(Config#config.min_batch_size, 339 | Config#config.batch_size / 2), 340 | loop_wait(State#state{config = 341 | Config#config{batch_size = NewBatchSize}}, 342 | Parent) 343 | end. 344 | 345 | terminate(Reason, #state{config = #config{module = Mod}, state = Inner}) -> 346 | catch Mod:terminate(Reason, Inner), 347 | ok. 348 | 349 | 350 | complete_batch(#state{batch = []} = State) -> 351 | State; 352 | complete_batch(#state{batch = Batch0, 353 | config = #config{module = Mod, 354 | reversed_batch = ReverseBatch}, 355 | state = Inner0, 356 | debug = Debug0} = State0) -> 357 | Batch = case ReverseBatch of 358 | false -> 359 | %% reversing restores the received order 360 | lists:reverse(Batch0); 361 | true -> 362 | %% accepting the batch in reverse order means we can avoid 363 | %% reversing the list 364 | Batch0 365 | end, 366 | 367 | case catch Mod:handle_batch(Batch, Inner0) of 368 | {ok, Inner} -> 369 | State0#state{batch = [], 370 | state = Inner, 371 | batch_count = 0}; 372 | {ok, Inner, {continue, Continue}} -> 373 | handle_continue(Continue, 374 | State0#state{batch = [], 375 | state = Inner, 376 | batch_count = 0}); 377 | {ok, Actions, Inner} when is_list(Actions) -> 378 | {ShouldGc, Debug} = handle_actions(Actions, Debug0), 379 | State0#state{batch = [], 380 | batch_count = 0, 381 | state = Inner, 382 | needs_gc = ShouldGc, 383 | debug = Debug}; 384 | {ok, Actions, Inner, {continue, Continue}} when is_list(Actions) -> 385 | {ShouldGc, Debug} = handle_actions(Actions, Debug0), 386 | handle_continue(Continue, 387 | State0#state{batch = [], 388 | batch_count = 0, 389 | state = Inner, 390 | needs_gc = ShouldGc, 391 | debug = Debug}); 392 | {stop, Reason} -> 393 | terminate(Reason, State0), 394 | exit(Reason); 395 | {'EXIT', Reason} -> 396 | terminate(Reason, State0), 397 | exit(Reason) 398 | end. 399 | 400 | handle_actions(Actions, Debug0) -> 401 | lists:foldl(fun ({reply, {Pid, Tag}, Msg}, 402 | {ShouldGc, Dbg}) -> 403 | Pid ! {Tag, Msg}, 404 | {ShouldGc, 405 | handle_debug_out(Pid, Msg, Dbg)}; 406 | (garbage_collect, {_, Dbg}) -> 407 | {true, Dbg} 408 | end, {false, Debug0}, Actions). 409 | 410 | 411 | handle_continue(Continue, #state{config = #config{module = Mod}, 412 | state = Inner0} = State0) -> 413 | case catch Mod:handle_continue(Continue, Inner0) of 414 | {ok, Inner} -> 415 | State0#state{state = Inner}; 416 | {ok, Inner, {continue, Continue}} -> 417 | handle_continue(Continue, State0#state{state = Inner}); 418 | {stop, Reason} -> 419 | terminate(Reason, State0), 420 | exit(Reason); 421 | {'EXIT', Reason} -> 422 | terminate(Reason, State0), 423 | exit(Reason) 424 | end. 425 | 426 | handle_debug_in(#state{debug = Dbg0} = State, Msg) -> 427 | Dbg = sys:handle_debug(Dbg0, fun write_debug/3, ?MODULE, {in, Msg}), 428 | State#state{debug = Dbg}. 429 | 430 | handle_debug_out(_, _, []) -> 431 | []; 432 | handle_debug_out(Pid, Msg, Dbg) -> 433 | Evt = {out, {self(), Msg}, Pid}, 434 | sys:handle_debug(Dbg, fun write_debug/3, ?MODULE, Evt). 435 | 436 | %% Here are the sys call back functions 437 | 438 | system_continue(Parent, Debug, State) -> 439 | % TODO check if we've written to the current batch or not 440 | loop_batched(State#state{debug = Debug}, Parent). 441 | 442 | -spec system_terminate(term(), pid(), list(), term()) -> no_return(). 443 | system_terminate(Reason, _Parent, _Debug, State) -> 444 | terminate(Reason, State), 445 | exit(Reason). 446 | 447 | system_get_state(State) -> 448 | {ok, State}. 449 | 450 | format_status(_Reason, [_PDict, SysState, Parent, Debug, 451 | #state{config = #config{name = Name, 452 | module = Mod}, 453 | state = State }]) -> 454 | Header = gen:format_status_header("Status for batching server", Name), 455 | Log = sys_get_log(Debug), 456 | 457 | [{header, Header}, 458 | {data, 459 | [ 460 | {"Status", SysState}, 461 | {"Parent", Parent}, 462 | {"Logged Events", Log}]} | 463 | case catch Mod:format_status(State) of 464 | L when is_list(L) -> L; 465 | {'EXIT', {undef, _}} -> 466 | %% not implemented just return the state 467 | [State]; 468 | T -> [T] 469 | end]. 470 | 471 | sys_get_log(Debug) -> 472 | sys:get_log(Debug). 473 | 474 | write_debug(Dev, Event, Name) -> 475 | io:format(Dev, "~p event = ~p~n", [Name, Event]). 476 | 477 | %% Send function 478 | 479 | do_send(Dest, Msg) -> 480 | try erlang:send(Dest, Msg) 481 | catch 482 | error:_ -> ok 483 | end, 484 | ok. 485 | 486 | gen_start(undefined, Mod, Args, Opts0) -> 487 | %% filter out gen batch server specific options as the options type in gen 488 | %% is closed and dialyzer would complain. 489 | {GBOpts, Opts} = lists:splitwith(fun ({Key, _}) -> 490 | Key == max_batch_size orelse 491 | Key == min_batch_size orelse 492 | Key == reversed_batch 493 | end, Opts0), 494 | gen:start(?MODULE, link, Mod, {GBOpts, Args}, Opts); 495 | gen_start(Name, Mod, Args, Opts0) -> 496 | %% filter out gen batch server specific options as the options type in gen 497 | %% is closed and dialyzer would complain. 498 | {GBOpts, Opts} = lists:splitwith(fun ({Key, _}) -> 499 | Key == max_batch_size orelse 500 | Key == min_batch_size orelse 501 | Key == reversed_batch 502 | end, Opts0), 503 | gen:start(?MODULE, link, Name, Mod, {GBOpts, Args}, Opts). 504 | 505 | -------------------------------------------------------------------------------- /test/gen_batch_server_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2018-Present Pivotal Software, Inc. All Rights Reserved. 2 | -module(gen_batch_server_SUITE). 3 | 4 | -compile(nowarn_export_all). 5 | -compile(export_all). 6 | 7 | -export([ 8 | ]). 9 | 10 | -include_lib("common_test/include/ct.hrl"). 11 | -include_lib("eunit/include/eunit.hrl"). 12 | 13 | %%%=================================================================== 14 | %%% Common Test callbacks 15 | %%%=================================================================== 16 | 17 | all() -> 18 | [ 19 | {group, tests} 20 | ]. 21 | 22 | 23 | all_tests() -> 24 | [ 25 | start_link_calls_init, 26 | simple_start_link_calls_init, 27 | handle_continue, 28 | cast_calls_handle_batch, 29 | info_calls_handle_batch, 30 | cast_many, 31 | cast_batch, 32 | ordering, 33 | ordering_reversed, 34 | call_calls_handle_batch, 35 | returning_stop_calls_terminate, 36 | terminate_is_optional, 37 | sys_get_status_calls_format_status, 38 | format_status_is_optional, 39 | max_batch_size, 40 | stop_calls_terminate, 41 | process_hibernates 42 | ]. 43 | 44 | groups() -> 45 | [ 46 | {tests, [], all_tests()} 47 | ]. 48 | 49 | init_per_suite(Config) -> 50 | Config. 51 | 52 | end_per_suite(_Config) -> 53 | ok. 54 | 55 | init_per_group(_Group, Config) -> 56 | Config. 57 | 58 | end_per_group(_Group, _Config) -> 59 | ok. 60 | 61 | init_per_testcase(TestCase, Config) -> 62 | 63 | [{mod, TestCase} | Config]. 64 | 65 | end_per_testcase(_TestCase, _Config) -> 66 | meck:unload(), 67 | ok. 68 | 69 | %%%=================================================================== 70 | %%% Test cases 71 | %%%=================================================================== 72 | 73 | start_link_calls_init(Config) -> 74 | Mod = ?config(mod, Config), 75 | meck:new(Mod, [non_strict]), 76 | meck:expect(Mod, init, fun([{some_arg, argh}]) -> 77 | {ok, #{}} 78 | end), 79 | Args = [{some_arg, argh}], 80 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args), 81 | ?assertEqual(true, meck:called(Mod, init, '_', Pid)), 82 | {ok, Pid2} = gen_batch_server:start_link(Mod, Args), 83 | ?assertEqual(true, meck:called(Mod, init, '_', Pid2)), 84 | {ok, Pid3} = gen_batch_server:start_link(undefined, Mod, Args), 85 | ?assertEqual(true, meck:called(Mod, init, '_', Pid3)), 86 | Opts = [{reversed_batch, true}], 87 | {ok, Pid4} = gen_batch_server:start_link(undefined, Mod, Args, Opts), 88 | ?assertEqual(true, meck:called(Mod, init, '_', Pid4)), 89 | ?assert(meck:validate(Mod)), 90 | gen_batch_server:stop(Pid), 91 | gen_batch_server:stop(Pid2), 92 | gen_batch_server:stop(Pid3), 93 | gen_batch_server:stop(Pid4), 94 | ok. 95 | 96 | simple_start_link_calls_init(Config) -> 97 | Mod = ?config(mod, Config), 98 | meck:new(Mod, [non_strict]), 99 | meck:expect(Mod, init, fun([{some_arg, argh}]) -> 100 | {ok, #{}} 101 | end), 102 | Args = [{some_arg, argh}], 103 | {ok, Pid} = gen_batch_server:start_link(Mod, Args), 104 | %% having to wildcard the args as they don't seem to 105 | %% validate correctly 106 | ?assertEqual(true, meck:called(Mod, init, '_', Pid)), 107 | ?assert(meck:validate(Mod)), 108 | ok. 109 | 110 | handle_continue(Config) -> 111 | Self = self(), 112 | Mod = ?config(mod, Config), 113 | meck:new(Mod, [non_strict]), 114 | meck:expect(Mod, init, fun([{some_arg, argh}]) -> 115 | {ok, #{}, {continue, post_init}} 116 | end), 117 | meck:expect(Mod, handle_continue, fun(Cont, #{}) -> 118 | Self ! {continue_called, Cont}, 119 | {ok, #{}} 120 | end), 121 | meck:expect(Mod, handle_batch, fun(_Batch, #{}) -> 122 | {ok, #{}, {continue, batch}} 123 | end), 124 | Args = [{some_arg, argh}], 125 | {ok, Pid} = gen_batch_server:start_link(Mod, Args), 126 | %% having to wildcard the args as they don't seem to 127 | %% validate correctly 128 | ?assertEqual(true, meck:called(Mod, init, '_', Pid)), 129 | ?assertEqual(true, meck:called(Mod, handle_continue, '_', Pid)), 130 | receive 131 | {continue_called, post_init} -> ok 132 | after 2000 -> 133 | exit(continue_timeout) 134 | end, 135 | ok = gen_batch_server:cast(Pid, msg), 136 | receive 137 | {continue_called, batch} -> ok 138 | after 2000 -> 139 | exit(continue_timeout_2) 140 | end, 141 | meck:expect(Mod, handle_batch, fun(_Batch, #{}) -> 142 | {ok, [garbage_collect], #{}, {continue, batch}} 143 | end), 144 | ok = gen_batch_server:cast(Pid, msg), 145 | receive 146 | {continue_called, batch} -> ok 147 | after 2000 -> 148 | exit(continue_timeout_3) 149 | end, 150 | ?assert(meck:validate(Mod)), 151 | ok. 152 | 153 | cast_calls_handle_batch(Config) -> 154 | Mod = ?config(mod, Config), 155 | meck:new(Mod, [non_strict]), 156 | meck:expect(Mod, init, fun(Init) -> {ok, Init} end), 157 | Args = #{}, 158 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args, []), 159 | Msg = {put, k, v}, 160 | Self = self(), 161 | meck:expect(Mod, handle_batch, 162 | fun([{cast, {put, k, v}}], State) -> 163 | Self ! continue, 164 | {ok, [garbage_collect], maps:put(k, v, State)} 165 | end), 166 | ok = gen_batch_server:cast(Pid, Msg), 167 | receive continue -> ok after 2000 -> exit(timeout) end, 168 | ?assertEqual(true, meck:called(Mod, handle_batch, '_', Pid)), 169 | {ok, Pid1} = gen_batch_server:start_link({global, Mod}, Mod, Args, []), 170 | ok = gen_batch_server:cast({global, Mod}, Msg), 171 | receive continue -> ok after 2000 -> exit(timeout) end, 172 | ?assertEqual(true, meck:called(Mod, handle_batch, '_', Pid1)), 173 | {ok, Pid2} = gen_batch_server:start_link({via, global, test_via_cast}, Mod, Args, []), 174 | ok = gen_batch_server:cast({via, global, test_via_cast}, Msg), 175 | receive continue -> ok after 2000 -> exit(timeout) end, 176 | ?assertEqual(true, meck:called(Mod, handle_batch, '_', Pid2)), 177 | ?assert(meck:validate(Mod)), 178 | ok. 179 | 180 | info_calls_handle_batch(Config) -> 181 | Mod = ?config(mod, Config), 182 | meck:new(Mod, [non_strict]), 183 | meck:expect(Mod, init, fun(Init) -> {ok, Init} end), 184 | Args = #{}, 185 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args, []), 186 | Msg = {put, k, v}, 187 | Self = self(), 188 | meck:expect(Mod, handle_batch, 189 | fun([{info, {put, k, v}}], State) -> 190 | Self ! continue, 191 | {ok, [], maps:put(k, v, State)} 192 | end), 193 | Pid ! Msg, 194 | receive continue -> ok after 2000 -> exit(timeout) end, 195 | ?assertEqual(true, meck:called(Mod, handle_batch, '_', Pid)), 196 | ?assert(meck:validate(Mod)), 197 | ok. 198 | 199 | process_hibernates(Config) -> 200 | Mod = ?config(mod, Config), 201 | meck:new(Mod, [non_strict]), 202 | meck:expect(Mod, init, fun(Init) -> {ok, Init} end), 203 | Args = #{}, 204 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args, 205 | [{hibernate_after, 10}]), 206 | %% sleep longer than hibernation wait 207 | timer:sleep(20), 208 | ?assertEqual({current_function, {erlang, hibernate, 3}}, 209 | erlang:process_info(Pid, current_function)), 210 | Msg = {put, k, v}, 211 | Self = self(), 212 | meck:expect(Mod, handle_batch, 213 | fun([{info, {put, k, v}}], State) -> 214 | Self ! continue, 215 | {ok, [], maps:put(k, v, State)} 216 | end), 217 | Pid ! Msg, 218 | receive continue -> ok after 2000 -> exit(timeout) end, 219 | ?assertEqual(true, meck:called(Mod, handle_batch, '_', Pid)), 220 | ?assert(meck:validate(Mod)), 221 | timer:sleep(20), 222 | ?assertEqual({current_function, {erlang, hibernate, 3}}, 223 | erlang:process_info(Pid, current_function)), 224 | ok. 225 | 226 | cast_many(Config) -> 227 | Mod = ?config(mod, Config), 228 | meck:new(Mod, [non_strict]), 229 | meck:expect(Mod, init, fun(Init) -> {ok, Init} end), 230 | Args = #{}, 231 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args, []), 232 | Self = self(), 233 | meck:expect(Mod, handle_batch, 234 | fun(Ops, State) -> 235 | {cast, {put, K, V}} = lists:last(Ops), 236 | Self ! {done, K, V}, 237 | {ok, [], maps:put(K, V, State)} 238 | end), 239 | Num = 20000, 240 | [gen_batch_server:cast(Pid, {put, I, I}) || I <- lists:seq(1, Num)], 241 | receive {done, Num, Num} -> 242 | ok 243 | after 5000 -> 244 | exit(timeout) 245 | end, 246 | ?assert(meck:validate(Mod)), 247 | ok. 248 | 249 | cast_batch(Config) -> 250 | Mod = ?config(mod, Config), 251 | meck:new(Mod, [non_strict]), 252 | meck:expect(Mod, init, fun(Init) -> {ok, Init} end), 253 | Args = #{}, 254 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args, []), 255 | Self = self(), 256 | meck:expect(Mod, handle_batch, 257 | fun(Ops, State) -> 258 | {cast, {put, K, V}} = lists:last(Ops), 259 | Self ! {done, K, V}, 260 | {ok, [], maps:put(K, V, State)} 261 | end), 262 | Num = 20000, 263 | gen_batch_server:cast_batch(Pid, [{put, I, I} || I <- lists:seq(1, Num)]), 264 | receive {done, Num, Num} -> 265 | ok 266 | after 5000 -> 267 | exit(timeout) 268 | end, 269 | ?assert(meck:validate(Mod)), 270 | ok. 271 | 272 | ordering(Config) -> 273 | test_ordering(Config, false). 274 | 275 | ordering_reversed(Config) -> 276 | test_ordering(Config, true). 277 | 278 | test_ordering(Config, Reverse) -> 279 | Mod = ?config(mod, Config), 280 | meck:new(Mod, [non_strict]), 281 | meck:expect(Mod, init, fun(Init) -> {ok, Init} end), 282 | Args = #{}, 283 | Opts = [{reversed_batch, Reverse}], 284 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args, Opts), 285 | Self = self(), 286 | ExpectedOps = [{cast,1}, {cast,2}, {cast,3}, {cast,4}, {cast,5}], 287 | Expected = case Reverse of 288 | false -> 289 | ExpectedOps; 290 | true -> 291 | lists:reverse(ExpectedOps) 292 | end, 293 | 294 | meck:expect(Mod, handle_batch, 295 | fun([{cast, block}], State) -> 296 | timer:sleep(100), 297 | {ok, State}; 298 | (Ops, State) -> 299 | case Ops of 300 | Expected -> 301 | Self ! in_order; 302 | _ -> 303 | Self ! {out_of_order, Ops} 304 | end, 305 | {ok, State} 306 | end), 307 | gen_batch_server:cast(Pid, block), 308 | timer:sleep(10), 309 | gen_batch_server:cast(Pid, 1), 310 | gen_batch_server:cast_batch(Pid, [I || I <- lists:seq(2, 4)]), 311 | gen_batch_server:cast(Pid, 5), 312 | 313 | receive in_order -> 314 | ok; 315 | {out_of_order, Ops} -> 316 | ct:pal("out of order ops ~w", [Ops]), 317 | exit({outof_order_assertion, Ops}) 318 | after 5000 -> 319 | exit(timeout) 320 | end, 321 | ?assert(meck:validate(Mod)), 322 | ok. 323 | 324 | call_calls_handle_batch(Config) -> 325 | Mod = ?config(mod, Config), 326 | meck:new(Mod, [non_strict]), 327 | meck:expect(Mod, init, fun(Init) -> {ok, Init} end), 328 | Args = #{}, 329 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args, []), 330 | Msg = {put, k, v}, 331 | meck:expect(Mod, handle_batch, 332 | fun([{call, From, {put, k, v}}], State) -> 333 | {ok, [{reply, From, {ok, k}}, garbage_collect], 334 | maps:put(k, v, State)} 335 | end), 336 | {ok, k} = gen_batch_server:call(Pid, Msg), 337 | ?assertEqual(true, meck:called(Mod, handle_batch, '_', Pid)), 338 | {ok, Pid1} = gen_batch_server:start_link({global, Mod}, Mod, Args, []), 339 | {ok, k} = gen_batch_server:call({global, Mod}, Msg), 340 | ?assertEqual(true, meck:called(Mod, handle_batch, '_', Pid1)), 341 | {ok, Pid2} = gen_batch_server:start_link({via, global, test_via_call}, Mod, Args, []), 342 | {ok, k} = gen_batch_server:call({via, global, test_via_call}, Msg), 343 | ?assertEqual(true, meck:called(Mod, handle_batch, '_', Pid2)), 344 | ?assert(meck:validate(Mod)), 345 | ok. 346 | 347 | returning_stop_calls_terminate(Config) -> 348 | Mod = ?config(mod, Config), 349 | %% as we are linked the test process need to also trap exits for this test 350 | process_flag(trap_exit, true), 351 | meck:new(Mod, [non_strict]), 352 | meck:expect(Mod, init, fun(Init) -> 353 | process_flag(trap_exit, true), 354 | {ok, Init} 355 | end), 356 | Args = #{}, 357 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, 358 | Args, []), 359 | Msg = {put, k, v}, 360 | meck:expect(Mod, handle_batch, 361 | fun([{cast, {put, k, v}}], _) -> 362 | {stop, because} 363 | end), 364 | meck:expect(Mod, terminate, fun(because, S) -> S end), 365 | ok = gen_batch_server:cast(Pid, Msg), 366 | %% wait for process exit signal 367 | receive {'EXIT', Pid, because} -> ok after 2000 -> exit(timeout) end, 368 | %% sleep a little to allow meck to register results 369 | timer:sleep(10), 370 | ?assertEqual(true, meck:called(Mod, terminate, '_')), 371 | ?assert(meck:validate(Mod)), 372 | ok. 373 | 374 | terminate_is_optional(Config) -> 375 | Mod = ?config(mod, Config), 376 | %% as we are linked the test process need to also trap exits for this test 377 | process_flag(trap_exit, true), 378 | meck:new(Mod, [non_strict]), 379 | meck:expect(Mod, init, fun(Init) -> 380 | process_flag(trap_exit, true), 381 | {ok, Init} 382 | end), 383 | Args = #{}, 384 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, 385 | Args, []), 386 | Msg = {put, k, v}, 387 | meck:expect(Mod, handle_batch, 388 | fun([{cast, {put, k, v}}], _) -> 389 | {stop, because} 390 | end), 391 | ok = gen_batch_server:cast(Pid, Msg), 392 | %% wait for process exit signal 393 | receive {'EXIT', Pid, because} -> ok after 2000 -> exit(timeout) end, 394 | %% sleep a little to allow meck to register results 395 | timer:sleep(10), 396 | ?assertEqual(false, meck:called(Mod, terminate, '_')), 397 | ?assert(meck:validate(Mod)), 398 | ok. 399 | 400 | sys_get_status_calls_format_status(Config) -> 401 | Mod = ?config(mod, Config), 402 | meck:new(Mod, [non_strict]), 403 | meck:expect(Mod, init, fun(Init) -> 404 | {ok, Init} 405 | end), 406 | meck:expect(Mod, format_status, 407 | fun(S) -> 408 | {format_status, S} 409 | end), 410 | {ok, _Pid} = gen_batch_server:start_link({local, Mod}, Mod, 411 | #{}, []), 412 | 413 | {_, _, _, [_, _, _, _, [_, _ ,S]]} = sys:get_status(Mod), 414 | ?assertEqual({format_status, #{}}, S), 415 | 416 | ?assertEqual(true, meck:called(Mod, format_status, '_')), 417 | ?assert(meck:validate(Mod)), 418 | ok. 419 | 420 | format_status_is_optional(Config) -> 421 | Mod = ?config(mod, Config), 422 | meck:new(Mod, [non_strict]), 423 | Args = bananas, 424 | meck:expect(Mod, init, fun(Init) -> 425 | {ok, Init} 426 | end), 427 | {ok, _Pid} = gen_batch_server:start_link({local, Mod}, Mod, 428 | Args, []), 429 | 430 | {_, _, _, [_, _, _, _, [_, _ ,S]]} = sys:get_status(Mod), 431 | ?assertEqual(Args, S), 432 | 433 | ?assertEqual(false, meck:called(Mod, format_status, '_')), 434 | ?assert(meck:validate(Mod)), 435 | ok. 436 | 437 | max_batch_size(Config) -> 438 | Mod = ?config(mod, Config), 439 | meck:new(Mod, [non_strict]), 440 | meck:expect(Mod, init, fun(Init) -> {ok, Init} end), 441 | Args = #{}, 442 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, Args, [{max_batch_size, 5000}]), 443 | Self = self(), 444 | Num = 20000, 445 | meck:expect(Mod, handle_batch, 446 | fun(Ops, State) -> 447 | {cast, {put, K, V}} = lists:last(Ops), 448 | ct:pal("cast_batch: batch size ~b~n", [length(Ops)]), 449 | Self ! {last, K, V}, 450 | case K of 451 | Num -> 452 | Self ! done; 453 | _ -> 454 | ok 455 | end, 456 | {ok, [], maps:put(K, V, State)} 457 | end), 458 | [gen_batch_server:cast(Pid, {put, I, I}) || I <- lists:seq(1, Num)], 459 | [Num | _] = BatchResult = wait_batch(), 460 | ?assert(BatchResult >= 4), 461 | ?assert(meck:validate(Mod)), 462 | ok. 463 | 464 | stop_calls_terminate(Config) -> 465 | Mod = ?config(mod, Config), 466 | %% as we are linked the test process need to also trap exits for this test 467 | process_flag(trap_exit, true), 468 | meck:new(Mod, [non_strict]), 469 | meck:expect(Mod, init, fun(Init) -> 470 | process_flag(trap_exit, true), 471 | {ok, Init} 472 | end), 473 | Args = #{}, 474 | {ok, Pid} = gen_batch_server:start_link({local, Mod}, Mod, 475 | Args, []), 476 | meck:expect(Mod, terminate, fun(because, S) -> S end), 477 | ok = gen_batch_server:stop(Pid, because, infinity), 478 | %% wait for process exit signal 479 | receive {'EXIT', Pid, because} -> ok after 2000 -> exit(timeout) end, 480 | %% sleep a little to allow meck to register results 481 | timer:sleep(10), 482 | ?assertEqual(true, meck:called(Mod, terminate, '_')), 483 | ?assert(meck:validate(Mod)), 484 | ok. 485 | 486 | %% Utility 487 | wait_batch() -> 488 | wait_batch([]). 489 | 490 | wait_batch(Acc) -> 491 | receive 492 | {last, Num, Num} -> 493 | wait_batch([Num | Acc]); 494 | done -> 495 | Acc 496 | after 5000 -> 497 | exit(timeout) 498 | end. 499 | --------------------------------------------------------------------------------