├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── RELEASING.md ├── checkstyle ├── checkstyle-config.xml └── checkstyle-suppressions.xml ├── common-kafka-connect ├── README.md ├── pom.xml └── src │ ├── etc │ └── findbugs-exclude.xml │ ├── main │ ├── java │ │ └── com │ │ │ └── cerner │ │ │ └── common │ │ │ └── kafka │ │ │ └── connect │ │ │ ├── Version.java │ │ │ └── kafka │ │ │ ├── KafkaSinkConnector.java │ │ │ └── KafkaSinkTask.java │ └── resources │ │ └── common-kafka-connect.properties │ └── test │ ├── java │ └── com │ │ └── cerner │ │ └── common │ │ └── kafka │ │ └── connect │ │ ├── VersionTest.java │ │ └── kafka │ │ ├── KafkaSinkConnectorTest.java │ │ └── KafkaSinkTaskTest.java │ └── resources │ └── log4j.properties ├── common-kafka-test ├── README.md ├── pom.xml └── src │ ├── etc │ └── findbugs-exclude.xml │ └── main │ └── java │ └── com │ └── cerner │ └── common │ └── kafka │ └── testing │ ├── AbstractKafkaTests.java │ ├── KafkaBrokerTestHarness.java │ └── KafkaTestUtils.java ├── common-kafka ├── README.md ├── pom.xml └── src │ ├── etc │ └── findbugs-exclude.xml │ ├── main │ └── java │ │ └── com │ │ └── cerner │ │ └── common │ │ └── kafka │ │ ├── KafkaExecutionException.java │ │ ├── TopicPartitionComparator.java │ │ ├── consumer │ │ ├── ConsumerOffsetClient.java │ │ ├── ProcessingConfig.java │ │ ├── ProcessingKafkaConsumer.java │ │ ├── ProcessingPartition.java │ │ └── assignors │ │ │ └── FairAssignor.java │ │ ├── metrics │ │ └── MeterPool.java │ │ └── producer │ │ ├── KafkaProducerPool.java │ │ ├── KafkaProducerWrapper.java │ │ └── partitioners │ │ └── FairPartitioner.java │ └── test │ ├── java │ └── com │ │ └── cerner │ │ └── common │ │ └── kafka │ │ ├── KafkaTests.java │ │ ├── StandaloneTests.java │ │ ├── consumer │ │ ├── ConsumerOffsetClientTest.java │ │ ├── ProcessingConfigTest.java │ │ ├── ProcessingKafkaConsumerITest.java │ │ ├── ProcessingKafkaConsumerRebalanceIT.java │ │ ├── ProcessingKafkaConsumerTest.java │ │ ├── ProcessingPartitionTest.java │ │ └── assignors │ │ │ └── FairAssignorTest.java │ │ ├── metrics │ │ └── MeterPoolTest.java │ │ └── producer │ │ ├── KafkaProducerPoolTest.java │ │ ├── KafkaProducerWrapperTest.java │ │ └── partitioners │ │ └── FairPartitionerTest.java │ └── resources │ └── log4j.properties └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | *.iml 4 | */generated/ 5 | */dependency-reduced-pom.xml 6 | **/.settings 7 | **/.project 8 | **/.classpath 9 | .ruby-version 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: trusty 3 | jdk: oraclejdk8 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Help us to make this project better by contributing. Whether it's new features, bug fixes, or simply improving documentation, your contributions are welcome. Please start with logging a [github issue][1] or submit a pull request. 4 | 5 | Before you contribute, please review these guidelines to help ensure a smooth process for everyone. 6 | 7 | Thanks. 8 | 9 | ## Issue reporting 10 | 11 | * Please browse our [existing issues][1] before logging new issues. 12 | * Check that the issue has not already been fixed in the `master` branch. 13 | * Open an issue with a descriptive title and a summary. 14 | * Please be as clear and explicit as you can in your description of the problem. 15 | * Please state the version of common-kafka and kafka you are using in the description. 16 | * Include any relevant code in the issue summary. 17 | 18 | ## Pull requests 19 | 20 | * Read [how to properly contribute to open source projects on Github][2]. 21 | * Fork the project. 22 | * Use a feature branch. 23 | * Write [good commit messages][3]. 24 | * Use the same coding conventions as the rest of the project. 25 | * Commit locally and push to your fork until you are happy with your contribution. 26 | * Make sure to add tests and verify all the tests are passing when merging upstream. 27 | * Add an entry to the [Changelog][4] accordingly. 28 | * Please add your name to the CONTRIBUTORS.md file. Adding your name to the CONTRIBUTORS.md file signifies agreement to all rights and reservations provided by the [License][5]. 29 | * [Squash related commits together][6]. 30 | * Open a [pull request][7]. 31 | * The pull request will be reviewed by the community and merged by the project committers. 32 | 33 | [1]: https://github.com/cerner/common-kafka/issues 34 | [2]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request 35 | [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 36 | [4]: ./CHANGELOG.md 37 | [5]: ./LICENSE.txt 38 | [6]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html 39 | [7]: https://help.github.com/articles/using-pull-requests 40 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * Cerner Corporation 2 | * Bryan Baugher [@bbaugher][bryan-baugher] 3 | * Andrew Olson [@noslowerdna][andrew-olson] 4 | * Micah Whitacre [@mkwhitacre][micah-whitacre] 5 | * Cole Skoviak [@EdwardSkoviak][cole-skoviak] 6 | * Pooja Dhondge [@dhondgepooja][pooja-dhondge] 7 | * Brian Tieman 8 | * Stephen Durfey 9 | * Greg Whitsitt 10 | * Matt Garlock [@matt-garlock][matt-garlock] 11 | 12 | [bryan-baugher]: https://github.com/bbaugher 13 | [andrew-olson]: https://github.com/noslowerdna 14 | [cole-skoviak]: https://github.com/EdwardSkoviak 15 | [micah-whitacre]: https://github.com/mkwhitacre 16 | [pooja-dhondge]: https://github.com/dhondgepooja 17 | [matt-garlock]: https://github.com/matt-garlock -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://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 [yyyy] [name of copyright owner] 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 | http://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 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 Cerner Innovation, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Common Kafka [![Build Status](https://travis-ci.com/cerner/common-kafka.svg?branch=master)](https://travis-ci.com/cerner/common-kafka) 2 | 3 | 4 | This repository contains common Kafka code supporting Cerner's cloud-based solutions. 5 | 6 | For Maven, add the following, 7 | 8 | ``` 9 | 10 | 11 | com.cerner.common.kafka 12 | common-kafka 13 | 3.0 14 | 15 | 16 | 17 | com.cerner.common.kafka 18 | common-kafka-connect 19 | 3.0 20 | 21 | 22 | 23 | com.cerner.common.kafka 24 | common-kafka-test 25 | 3.0 26 | 27 | ``` 28 | 29 | ## Project Inventory 30 | 31 | The following modules are available for use, 32 | 33 | * [common-kafka](common-kafka/README.md): Lightweight wrapper for 34 | [producers](http://kafka.apache.org/32/javadoc/org/apache/kafka/clients/producer/KafkaProducer.html) 35 | and [consumers](http://kafka.apache.org/32/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html) 36 | in the [kafka-clients](https://github.com/apache/kafka/tree/trunk/clients) library. 37 | * [common-kafka-connect](common-kafka-connect/README.md): Contains 38 | [Kafka Connect](http://kafka.apache.org/documentation.html#connect) component implementations. 39 | * [common-kafka-test](common-kafka-test/README.md): Provides infrastructure for integration or "heavy" 40 | unit tests that need a running Kafka broker and ZooKeeper service. 41 | 42 | Please refer to the project-specific README documentation for content details. 43 | 44 | ## Version Requirements 45 | 46 | The 3.0 release of common-kafka uses the following dependency versions. 47 | 48 | * [Kafka](http://kafka.apache.org/): 3.2.0 49 | * [Metrics](http://metrics.dropwizard.io/): 2.2.0 50 | * [Scala](https://scala-lang.org/): 2.13.5 51 | 52 | Note that the Scala dependency is only applicable for common-kafka-test. 53 | 54 | ## Upgrading from common-kafka 2.x to 3.0 55 | 56 | * The common-kafka-admin module has been removed, KafkaAdminClient can be replaced by Kafka's [admin client](https://github.com/apache/kafka/blob/3.2.0/clients/src/main/java/org/apache/kafka/clients/admin/AdminClient.java) 57 | * KafkaBrokerTestHarness has been updated to require TestInfo in the setup method, which requires junit5. 58 | By extending AbstractKafkaTests instead of using KafkaBrokerTestHarness directly you can avoid needing to add a junit5 dependency. 59 | 60 | ## Contribute 61 | 62 | You are welcome to contribute to Common-Kafka. 63 | 64 | Read our [Contribution guidelines](CONTRIBUTING.md). 65 | 66 | ## License 67 | 68 | ``` 69 | Copyright 2017 Cerner Innovation, Inc. 70 | 71 | Licensed under the Apache License, Version 2.0 (the "License"); 72 | you may not use this file except in compliance with the License. 73 | You may obtain a copy of the License at 74 | 75 | http://www.apache.org/licenses/LICENSE-2.0 76 | 77 | Unless required by applicable law or agreed to in writing, software 78 | distributed under the License is distributed on an "AS IS" BASIS, 79 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 80 | See the License for the specific language governing permissions and 81 | limitations under the License. 82 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ========= 3 | 4 | Setup / First Time 5 | ------------------ 6 | 7 | If you have never released before you will need to do the following, 8 | 9 | * Your user will need access to the maven central repo for our group (`com.cerner.common.kafka`) 10 | * Create a [JIRA account](https://issues.sonatype.org/secure/Signup!default.jspa) 11 | * Log a ticket ([like this one](https://issues.sonatype.org/browse/OSSRH-37290)) to get access to the repo. You will need one of the owners of the project to approve the JIRA 12 | * Install gpg (for Mac `brew install gnupg`) 13 | * Setup gpg key (http://central.sonatype.org/pages/working-with-pgp-signatures.html) 14 | * Create new key `gpg --gen-key`. Follow instructions 15 | * Share public key `gpg --keyserver pgp.mit.edu --send-keys KEY_ID` 16 | * Add the following to `~/.m2/settings.xml` 17 | 18 | ``` 19 | 20 | 21 | 22 | ossrh 23 | JIRA_USER 24 | JIRA_PASSWORD 25 | 26 | 27 | 28 | ``` 29 | 30 | * You can also setup your gpg passphrase into `settings.xml` following [this](http://central.sonatype.org/pages/apache-maven.html#gpg-signed-components) but I was unable to get it to work 31 | 32 | Releasing the Project 33 | --------------------- 34 | 35 | If you've done the setup to release the project do the following, 36 | 37 | `mvn release:clean release:prepare release:perform -P ossrh` 38 | 39 | This will, 40 | 41 | * Drop `-SNAPSHOT` from version 42 | * Create a git tag 43 | * Bump version and add `-SNAPSHOT` for next development 44 | * Push artifact to [staging](https://oss.sonatype.org) 45 | 46 | At this point you can check the artifacts if you would like in the 47 | [staging repo](https://oss.sonatype.org). If everything looks good you can then do, 48 | 49 | `mvn nexus-staging:release -P ossrh -DstagingRepositoryId=REPO_ID` 50 | 51 | This promotes the artifacts from staging to public maven central. 52 | 53 | You can get the `REPO_ID` either look for, 54 | `Closing staging repository with ID "comcernercommonkafka-1002"` 55 | in the maven logs or from the [staging repo](https://oss.sonatype.org). 56 | 57 | ### Common Issues 58 | 59 | #### gpg: signing failed: Inappropriate ioctl for device 60 | 61 | If the gpg maven plugin gives you the error `gpg: signing failed: Inappropriate ioctl for device` 62 | you can try doing, 63 | 64 | ``` 65 | GPG_TTY=$(tty) 66 | export GPG_TTY 67 | ``` 68 | 69 | #### Maven Fails to Commit/Push 70 | 71 | If the maven release plugin fails to commit things to git or create tags you can try 72 | the following, 73 | 74 | `git config --add status.displayCommentPrefix true` 75 | 76 | #### Unhandled: Repository: comcernercommonkafka-XXXX has invalid state: open 77 | 78 | If the staging repository promotion command fails with 79 | `Unhandled: Repository: comcernercommonkafka-1001 has invalid state: open` 80 | this likely means the project doesn't meet some requirement of the sonatype 81 | repo. You can attempt to manually close the repo via the 82 | [staging repo](https://oss.sonatype.org) webUI which will prep it to be 83 | promoted. It should run the staged artifacts through a check process and 84 | fail for any issues. 85 | 86 | #### Rule failure: No public key: Key with id: XXXXXX 87 | 88 | This means that one of the sonatype repository rules about having a valid GPG key 89 | failed and they were unable to find your key in any of the public key servers. 90 | 91 | Pushing your gpg key to a public key server should likely fix the issue, 92 | 93 | ``` 94 | gpg --send-key KEY_ID 95 | ``` 96 | 97 | Then verify with, 98 | 99 | ``` 100 | gpg --recv-key KEY_ID 101 | ``` 102 | 103 | If this isn't working you can try manually uploading, 104 | 105 | ``` 106 | gpg --armor --export KEY_ID 107 | ``` 108 | 109 | Then going to [https://pgp.mit.edu/](https://pgp.mit.edu/) and uploading the key 110 | into the UI. 111 | 112 | Another option I've found that worked when the others didn't was to use another 113 | keyserver, 114 | 115 | ``` 116 | gpg --keyserver hkp://keyserver.ubuntu.com --send-key KEY_ID 117 | ``` 118 | 119 | And verify, 120 | 121 | ``` 122 | gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys KEY_ID 123 | ``` 124 | 125 | Additionally this can still fail even if the key is found on the key server if 126 | your key is actually a sub key. You can see this with, 127 | 128 | ``` 129 | gpg --list-keys 130 | ``` 131 | 132 | If your key contains the `sub` section. If it does you likely need to delete your 133 | key(s), 134 | 135 | ``` 136 | gpg --delete-secret-key KEY_ID 137 | gpg --delete-key KEY_ID 138 | ``` 139 | 140 | Then generate a new key that is a 'sign only' key, 141 | 142 | ``` 143 | gpg --full-generate-key 144 | ``` 145 | 146 | Make sure to select the 'Sign Only' and 'RSA' options. Then follow the same options 147 | above to upload your key and verify its on the public key servers and try the 148 | release again. Release can be started again with, 149 | 150 | ``` 151 | mvn release:perform -P ossrh 152 | ``` -------------------------------------------------------------------------------- /checkstyle/checkstyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /checkstyle/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /common-kafka-connect/README.md: -------------------------------------------------------------------------------- 1 | # common-kafka-connect 2 | 3 | This project contains [Kafka Connect]((http://kafka.apache.org/documentation.html#connect)) component 4 | implementations. 5 | 6 | The primary classes available for use are listed below. 7 | 8 | ## Sources 9 | 10 | No Kafka Connect Sources are currently provided. 11 | 12 | ## Sinks 13 | 14 | ### [KafkaSinkConnector](src/main/java/com/cerner/common/kafka/connect/kafka/KafkaSinkConnector.java) 15 | 16 | * A simple Kafka Connect Sink Connector that sends data to another Kafka cluster without any transformation or filtering. 17 | * Requires the use of the `org.apache.kafka.connect.converters.ByteArrayConverter` converter. 18 | * To run this connector in kafka-connect add the `common-kafka` and `commmon-kafka-connect` jars to the kafka-connect classpath. 19 | 20 | #### Configuration 21 | 22 | * `bootstrap.servers`: The list of broker host/port for the kafka cluster to send data to (Required) 23 | * `producer.*`: Kafka producer configuration. To add a particular producer config prefix it with `producer.` (i.e. `producer.acks`) 24 | 25 | By default the following is set to ensure everything is delivered, 26 | 27 | * `acks`: `all` 28 | * `max.in.flight.requests.per.connection`: `1` 29 | * `retries`: `2147483647` (i.e. `Integer.MAX_VALUE`) 30 | 31 | ### [KafkaSinkTask](src/main/java/com/cerner/common/kafka/connect/kafka/KafkaSinkTask.java) 32 | 33 | * Sink Task implementation used by the `KafkaSinkConnector`. 34 | * Does not do any deserialization or serialization of data. 35 | * Sets producer configuration to ensure strong consistency. 36 | * Writes data to the same topic and partition in the destination Kafka cluster that it was read from the source Kafka cluster. -------------------------------------------------------------------------------- /common-kafka-connect/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.cerner.common.kafka 7 | common-kafka-parent 8 | 3.1-SNAPSHOT 9 | 10 | 11 | common-kafka-connect 12 | Common Kafka Connect 13 | Common Kafka Connect 14 | ${cloud.site.url} 15 | jar 16 | 17 | 18 | .. 19 | 20 | 21 | 22 | 23 | ${cloud.site.id} 24 | ${cloud.site.name} 25 | ${cloud.site.deploy.url} 26 | 27 | 28 | 29 | 30 | 31 | org.apache.kafka 32 | connect-api 33 | 34 | 35 | com.cerner.common.kafka 36 | common-kafka 37 | 38 | 39 | org.apache.kafka 40 | kafka-clients 41 | 42 | 43 | org.slf4j 44 | slf4j-api 45 | 46 | 47 | 48 | 49 | org.junit.jupiter 50 | junit-jupiter-api 51 | test 52 | 53 | 54 | org.hamcrest 55 | hamcrest-core 56 | test 57 | 58 | 59 | org.mockito 60 | mockito-core 61 | test 62 | 63 | 64 | org.mockito 65 | mockito-junit-jupiter 66 | test 67 | 68 | 69 | org.slf4j 70 | slf4j-log4j12 71 | test 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | src/main/resources/ 80 | true 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-dependency-plugin 87 | 88 | 89 | org.slf4j:slf4j-log4j12:jar:${slf4j.version} 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /common-kafka-connect/src/etc/findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /common-kafka-connect/src/main/java/com/cerner/common/kafka/connect/Version.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.connect; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Properties; 9 | 10 | /** 11 | * Class used to determine the version of the project 12 | */ 13 | // This is used in our connectors/sinks to display the version of the connector 14 | public class Version { 15 | 16 | private static final Logger LOGGER = LoggerFactory.getLogger(Version.class); 17 | 18 | private static String version = "unknown"; 19 | 20 | static { 21 | try { 22 | Properties props = new Properties(); 23 | try (InputStream stream = Version.class.getResourceAsStream("/common-kafka-connect.properties")) { 24 | props.load(stream); 25 | } 26 | version = props.getProperty("version", version).trim(); 27 | } catch (IOException | RuntimeException e) { 28 | LOGGER.warn("Error while loading version:", e); 29 | } 30 | } 31 | 32 | /** 33 | * @return the version of the project 34 | */ 35 | public static String getVersion() { 36 | return version; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common-kafka-connect/src/main/java/com/cerner/common/kafka/connect/kafka/KafkaSinkConnector.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.connect.kafka; 2 | 3 | import com.cerner.common.kafka.connect.Version; 4 | import org.apache.kafka.clients.consumer.ConsumerConfig; 5 | import org.apache.kafka.common.config.ConfigDef; 6 | import org.apache.kafka.connect.connector.Task; 7 | import org.apache.kafka.connect.sink.SinkConnector; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.Collections; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.IntStream; 17 | 18 | /** 19 | * A Kafka sink connector which forwards the configured data to another Kafka cluster 20 | */ 21 | public class KafkaSinkConnector extends SinkConnector { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(KafkaSinkConnector.class); 24 | 25 | public static final String PRODUCER_PREFIX = "producer."; 26 | 27 | private static final ConfigDef CONFIG_DEF = new ConfigDef() 28 | .define(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, ConfigDef.Type.STRING, ConfigDef.Importance.HIGH, 29 | "Bootstrap brokers for mirror'ed cluster"); 30 | 31 | // Visible for testing 32 | protected Map config; 33 | 34 | @Override 35 | public String version() { 36 | return Version.getVersion(); 37 | } 38 | 39 | @Override 40 | public void start(Map taskConfig) { 41 | config = new HashMap<>(); 42 | 43 | taskConfig.forEach((key, value) -> { 44 | if (key.startsWith(PRODUCER_PREFIX)) { 45 | config.put(key.substring(PRODUCER_PREFIX.length()), value); 46 | } 47 | }); 48 | 49 | config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, taskConfig.get(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG)); 50 | config = Collections.unmodifiableMap(config); 51 | 52 | LOGGER.debug("Using {} for config", config); 53 | } 54 | 55 | @Override 56 | public Class taskClass() { 57 | return KafkaSinkTask.class; 58 | } 59 | 60 | @Override 61 | public List> taskConfigs(int totalConfigs) { 62 | return IntStream.range(0, totalConfigs).mapToObj(i -> config).collect(Collectors.toList()); 63 | } 64 | 65 | @Override 66 | public void stop() { 67 | } 68 | 69 | @Override 70 | public ConfigDef config() { 71 | return CONFIG_DEF; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /common-kafka-connect/src/main/java/com/cerner/common/kafka/connect/kafka/KafkaSinkTask.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.connect.kafka; 2 | 3 | import com.cerner.common.kafka.connect.Version; 4 | import com.cerner.common.kafka.producer.KafkaProducerWrapper; 5 | import org.apache.kafka.clients.consumer.OffsetAndMetadata; 6 | import org.apache.kafka.clients.producer.KafkaProducer; 7 | import org.apache.kafka.clients.producer.Producer; 8 | import org.apache.kafka.clients.producer.ProducerConfig; 9 | import org.apache.kafka.clients.producer.ProducerRecord; 10 | import org.apache.kafka.common.KafkaException; 11 | import org.apache.kafka.common.TopicPartition; 12 | import org.apache.kafka.common.serialization.ByteArraySerializer; 13 | import org.apache.kafka.connect.data.Schema; 14 | import org.apache.kafka.connect.errors.RetriableException; 15 | import org.apache.kafka.connect.sink.SinkRecord; 16 | import org.apache.kafka.connect.sink.SinkTask; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import java.io.IOException; 21 | import java.util.Collection; 22 | import java.util.Map; 23 | import java.util.Properties; 24 | 25 | /** 26 | * The sink task associated to the {@link KafkaSinkConnector} used to replicate Kafka data from one cluster to another 27 | */ 28 | public class KafkaSinkTask extends SinkTask { 29 | 30 | private static final Logger LOGGER = LoggerFactory.getLogger(KafkaSinkTask.class); 31 | 32 | // We maintain a variable for the Kafka producer since the wrapper will not close it 33 | private Producer kafkaProducer; 34 | 35 | private KafkaProducerWrapper producer; 36 | 37 | @Override 38 | public String version() { 39 | return Version.getVersion(); 40 | } 41 | 42 | @Override 43 | public void start(Map taskConfig) { 44 | Properties properties = new Properties(); 45 | 46 | // Ensures all data is written successfully and received by all in-sync replicas. This gives us strong consistency 47 | properties.setProperty(ProducerConfig.ACKS_CONFIG, "all"); 48 | 49 | // Ensures messages are effectively written in order and we maintain strong consistency 50 | properties.setProperty(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, "1"); 51 | 52 | // Tell producer to effectively try forever to avoid connect task stopping due to transient issues 53 | properties.setProperty(ProducerConfig.RETRIES_CONFIG, Integer.toString(Integer.MAX_VALUE)); 54 | 55 | // We avoid any serialization by just leaving it in the format we read it as 56 | properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getName()); 57 | properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class.getName()); 58 | 59 | // Apply all connector configuration (includes bootstrap config and any other overrides) 60 | properties.putAll(taskConfig); 61 | 62 | kafkaProducer = buildProducer(properties); 63 | producer = new KafkaProducerWrapper<>(kafkaProducer); 64 | } 65 | 66 | // Visible for testing 67 | protected Producer buildProducer(Properties properties) { 68 | return new KafkaProducer<>(properties); 69 | } 70 | 71 | @Override 72 | public void put(Collection collection) { 73 | // Any retriable exception thrown here will be attempted again and not cause the task to pause 74 | for(SinkRecord sinkRecord : collection) { 75 | if (sinkRecord.keySchema() != Schema.OPTIONAL_BYTES_SCHEMA || sinkRecord.valueSchema() != Schema.OPTIONAL_BYTES_SCHEMA) 76 | throw new IllegalStateException("Expected sink record key/value to be optional bytes, but saw instead key: " 77 | + sinkRecord.keySchema() + " value: " + sinkRecord.valueSchema() + ". Must use converter: " + 78 | "org.apache.kafka.connect.converters.ByteArrayConverter"); 79 | 80 | LOGGER.debug("Sending record {}", sinkRecord); 81 | 82 | try { 83 | producer.send(new ProducerRecord<>(sinkRecord.topic(), sinkRecord.kafkaPartition(), (byte[]) sinkRecord.key(), 84 | (byte[]) sinkRecord.value())); 85 | } catch (KafkaException e) { 86 | // If send throws an exception ensure we always retry the record/collection 87 | throw new RetriableException(e); 88 | } 89 | } 90 | } 91 | 92 | @Override 93 | public void flush(Map offsets) { 94 | LOGGER.debug("Flushing kafka sink"); 95 | 96 | try { 97 | producer.flush(); 98 | } catch (IOException e) { 99 | LOGGER.debug("IOException on flush, re-throwing as retriable", e); 100 | // Re-throw exception as connect retriable since we just want connect to keep retrying forever 101 | throw new RetriableException(e); 102 | } 103 | 104 | super.flush(offsets); 105 | } 106 | 107 | @Override 108 | public void stop() { 109 | try { 110 | producer.close(); 111 | } catch (IOException e) { 112 | LOGGER.warn("Failed to close producer wrapper", e); 113 | } 114 | 115 | try { 116 | kafkaProducer.close(); 117 | } catch (KafkaException e) { 118 | LOGGER.warn("Failed to close producer", e); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /common-kafka-connect/src/main/resources/common-kafka-connect.properties: -------------------------------------------------------------------------------- 1 | version=${project.version} -------------------------------------------------------------------------------- /common-kafka-connect/src/test/java/com/cerner/common/kafka/connect/VersionTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.connect; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.hamcrest.core.Is.is; 7 | import static org.hamcrest.core.IsNot.not; 8 | import static org.hamcrest.core.IsNull.nullValue; 9 | 10 | public class VersionTest { 11 | 12 | @Test 13 | public void getVersion() { 14 | // We can't make a lot of assertions without writing/using the same code in getVersion so just verify it is 15 | // not any of the bad cases 16 | assertThat(Version.getVersion(), is(not("unknown"))); 17 | assertThat(Version.getVersion(), is(not(nullValue()))); 18 | assertThat(Version.getVersion(), is(not(""))); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common-kafka-connect/src/test/java/com/cerner/common/kafka/connect/kafka/KafkaSinkConnectorTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.connect.kafka; 2 | 3 | import org.apache.kafka.clients.producer.ProducerConfig; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import static org.hamcrest.CoreMatchers.is; 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | 14 | public class KafkaSinkConnectorTest { 15 | 16 | private Map startConfig; 17 | private Map taskConfig; 18 | private KafkaSinkConnector connector; 19 | 20 | @BeforeEach 21 | public void before() { 22 | connector = new KafkaSinkConnector(); 23 | 24 | startConfig = new HashMap<>(); 25 | // This should be included because its required 26 | startConfig.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "broker01:9092"); 27 | 28 | // These should be included because it has the producer prefix 29 | startConfig.put(KafkaSinkConnector.PRODUCER_PREFIX + ProducerConfig.BATCH_SIZE_CONFIG, "123"); 30 | startConfig.put(KafkaSinkConnector.PRODUCER_PREFIX + ProducerConfig.BUFFER_MEMORY_CONFIG, "321"); 31 | 32 | // This should be ignored 33 | startConfig.put("other.config", "some-value"); 34 | 35 | taskConfig = new HashMap<>(); 36 | taskConfig.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, startConfig.get(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG)); 37 | taskConfig.put(ProducerConfig.BATCH_SIZE_CONFIG, "123"); 38 | taskConfig.put(ProducerConfig.BUFFER_MEMORY_CONFIG, "321"); 39 | } 40 | 41 | @Test 42 | public void start() { 43 | connector.start(startConfig); 44 | 45 | assertThat(connector.config, is(taskConfig)); 46 | } 47 | 48 | @Test 49 | public void taskConfigs() { 50 | // Configures the connector 51 | connector.start(startConfig); 52 | 53 | assertThat(connector.taskConfigs(3), is(Arrays.asList(taskConfig, taskConfig, taskConfig))); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /common-kafka-connect/src/test/java/com/cerner/common/kafka/connect/kafka/KafkaSinkTaskTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.connect.kafka; 2 | 3 | import org.apache.kafka.clients.producer.Producer; 4 | import org.apache.kafka.clients.producer.RecordMetadata; 5 | import org.apache.kafka.common.KafkaException; 6 | import org.apache.kafka.connect.data.Schema; 7 | import org.apache.kafka.connect.errors.RetriableException; 8 | import org.apache.kafka.connect.sink.SinkRecord; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import java.util.Collections; 16 | import java.util.Properties; 17 | import java.util.concurrent.Future; 18 | 19 | import static org.junit.jupiter.api.Assertions.assertThrows; 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.Mockito.doThrow; 22 | import static org.mockito.Mockito.verify; 23 | import static org.mockito.Mockito.when; 24 | 25 | @ExtendWith(MockitoExtension.class) 26 | public class KafkaSinkTaskTest { 27 | 28 | @Mock 29 | private Producer kafkaProducer; 30 | 31 | @Mock 32 | private Future recordMetadataFuture; 33 | 34 | private SinkRecord sinkRecord; 35 | private KafkaSinkTaskTester task; 36 | 37 | @BeforeEach 38 | public void before() { 39 | task = new KafkaSinkTaskTester(); 40 | 41 | // Doesn't matter what we provide it since everything is mocked just need to call start 42 | task.start(Collections.emptyMap()); 43 | 44 | sinkRecord = new SinkRecord("topic", 0, Schema.OPTIONAL_BYTES_SCHEMA, "key".getBytes(), 45 | Schema.OPTIONAL_BYTES_SCHEMA, "value".getBytes(), 0L); 46 | } 47 | 48 | @Test 49 | public void put() { 50 | when(kafkaProducer.send(any())).thenReturn(recordMetadataFuture); 51 | task.put(Collections.singletonList(sinkRecord)); 52 | verify(kafkaProducer).send(any()); 53 | } 54 | 55 | @Test 56 | public void put_recordKeyIsNotNullOrBytes() { 57 | sinkRecord = new SinkRecord("topic", 0, Schema.STRING_SCHEMA, "key", 58 | Schema.OPTIONAL_BYTES_SCHEMA, "value".getBytes(), 0L); 59 | assertThrows(IllegalStateException.class, 60 | () -> task.put(Collections.singletonList(sinkRecord))); 61 | } 62 | 63 | @Test 64 | public void put_recordValueIsNotNullOrBytes() { 65 | sinkRecord = new SinkRecord("topic", 0, Schema.OPTIONAL_BYTES_SCHEMA, "key".getBytes(), 66 | Schema.STRING_SCHEMA, "value", 0L); 67 | assertThrows(IllegalStateException.class, 68 | () -> task.put(Collections.singletonList(sinkRecord))); 69 | } 70 | 71 | @Test 72 | public void put_producerThrowsException() { 73 | when(kafkaProducer.send(any())).thenReturn(recordMetadataFuture); 74 | when(kafkaProducer.send(any())).thenThrow(new KafkaException()); 75 | assertThrows(RetriableException.class, 76 | () -> task.put(Collections.singletonList(sinkRecord))); 77 | } 78 | 79 | @Test 80 | public void flush() { 81 | when(kafkaProducer.send(any())).thenReturn(recordMetadataFuture); 82 | task.put(Collections.singletonList(sinkRecord)); 83 | 84 | // Doesn't matter what the offset map is just need to call flush 85 | task.flush(Collections.emptyMap()); 86 | 87 | verify(kafkaProducer).flush(); 88 | } 89 | 90 | @Test 91 | public void flush_producerThrowsException() { 92 | when(kafkaProducer.send(any())).thenReturn(recordMetadataFuture); 93 | doThrow(new KafkaException()).when(kafkaProducer).flush(); 94 | 95 | task.put(Collections.singletonList(sinkRecord)); 96 | 97 | // Doesn't matter what the offset map is just need to call flush 98 | assertThrows(RetriableException.class, 99 | () -> task.flush(Collections.emptyMap())); 100 | } 101 | 102 | private class KafkaSinkTaskTester extends KafkaSinkTask { 103 | 104 | protected Properties producerProperties; 105 | 106 | @Override 107 | protected Producer buildProducer(Properties properties) { 108 | producerProperties = properties; 109 | return kafkaProducer; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /common-kafka-connect/src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, A1 2 | log4j.appender.A1=org.apache.log4j.ConsoleAppender 3 | log4j.appender.A1.layout=org.apache.log4j.PatternLayout 4 | 5 | # Print the date in ISO 8601 format 6 | log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n 7 | 8 | # Limit Kafka logging 9 | log4j.logger.kafka=WARN 10 | log4j.logger.org.apache.kafka=WARN 11 | log4j.logger.org.apache.kafka.clients.consumer.ConsumerConfig=ERROR 12 | log4j.logger.org.apache.kafka.clients.producer.ProducerConfig=ERROR 13 | log4j.logger.org.apache.kafka.clients.NetworkClient=ERROR 14 | 15 | # Limit Zookeeper logging 16 | log4j.logger.org.apache.zookeeper=WARN 17 | log4j.logger.org.I0Itec.zkclient=WARN -------------------------------------------------------------------------------- /common-kafka-test/README.md: -------------------------------------------------------------------------------- 1 | # common-kafka-test 2 | 3 | This project contains testing infrastructure that is useful when developing integration or "heavy" unit 4 | tests that need a running Kafka broker and ZooKeeper service. 5 | 6 | The primary classes available for use are listed below. 7 | 8 | ## Kafka 9 | 10 | ### [KafkaBrokerTestHarness](src/main/java/com/cerner/common/kafka/testing/KafkaBrokerTestHarness.java) 11 | 12 | * Creates a cluster of one or more Kafka brokers which run within the process. 13 | * Adjusts broker configuration to minimize runtime resource utilization. 14 | * Cleans up data on broker shutdown. 15 | 16 | ### [AbstractKafkaTests](src/main/java/com/cerner/common/kafka/testing/AbstractKafkaTests.java) 17 | 18 | * Provides a simple framework for coordinating the `KafkaBrokerTestHarness` with a suite of unit tests. 19 | -------------------------------------------------------------------------------- /common-kafka-test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.cerner.common.kafka 7 | common-kafka-parent 8 | 3.1-SNAPSHOT 9 | 10 | 11 | common-kafka-test 12 | Common Kafka Testing Utilities 13 | Common Kafka Testing Utilities 14 | ${cloud.site.url} 15 | jar 16 | 17 | 18 | .. 19 | 20 | 21 | 22 | 23 | ${cloud.site.id} 24 | ${cloud.site.name} 25 | ${cloud.site.deploy.url} 26 | 27 | 28 | 29 | 30 | 31 | org.apache.kafka 32 | kafka-clients 33 | 34 | 35 | org.apache.kafka 36 | kafka_${scala.major.version} 37 | 38 | 39 | org.apache.kafka 40 | kafka_${scala.major.version} 41 | test 42 | 43 | 44 | commons-io 45 | commons-io 46 | 47 | 48 | org.apache.commons 49 | commons-lang3 50 | 51 | 52 | org.junit.jupiter 53 | junit-jupiter-api 54 | 55 | 56 | org.scala-lang 57 | scala-library 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /common-kafka-test/src/etc/findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /common-kafka-test/src/main/java/com/cerner/common/kafka/testing/AbstractKafkaTests.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.testing; 2 | 3 | import kafka.utils.EmptyTestInfo; 4 | import org.junit.jupiter.api.AfterAll; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Timeout; 7 | 8 | import java.io.IOException; 9 | import java.util.Properties; 10 | 11 | /** 12 | * An abstract class for coordinating a suite of tests requiring an internal Kafka + Zookeeper cluster to be started up. 13 | *

14 | * Typical usage: 15 | *

 16 |  *     @RunWith(Suite.class)
 17 |  *     @SuiteClasses({
 18 |  *         MyClass.class, MyOtherClass.class
 19 |  *     })
 20 |  *     public class MySuiteOfKafkaTests extends AbstractKafkaTests {
 21 |  *     }
 22 |  * 
23 | *

24 | * Each member of the suite should contain code like, 25 | *

 26 |  *     @BeforeClass
 27 |  *     public static void startup() throws Exception {
 28 |  *         MySuiteOfKafkaTests.startTest();
 29 |  *     }
 30 |  *
 31 |  *     @AfterClass
 32 |  *     public static void shutdown() throws Exception {
 33 |  *         MySuiteOfKafkaTests.endTest();
 34 |  *     }
 35 |  * 
36 | *

37 | * A similar standalone test suite that does not extend this class can also be created if applicable, i.e. if there are tests 38 | * that do not require a running Kafka + Zookeeper cluster. 39 | *

40 | * The test suites are then configured to run with Maven build configuration like, 41 | *

 42 |  *     <build>
 43 |  *         <plugins>
 44 |  *             <plugin>
 45 |  *                 <groupId>org.apache.maven.plugins</groupId>
 46 |  *                 <artifactId>maven-surefire-plugin</artifactId>
 47 |  *                 <configuration>
 48 |  *                     <includes>
 49 |  *                         <include>**/MySuiteOfKafkaTests.java</include>
 50 |  *                         <include>**/MySuiteOfStandaloneTests.java</include>
 51 |  *                     </includes>
 52 |  *                 </configuration>
 53 |  *             </plugin>
 54 |  *         </plugins>
 55 |  *     </build>
 56 |  * 
57 | * 58 | * @author Brandon Inman 59 | */ 60 | @Timeout(600) 61 | public abstract class AbstractKafkaTests { 62 | 63 | private static KafkaBrokerTestHarness kafka = null; 64 | private static boolean runAsSuite = false; 65 | 66 | @BeforeAll 67 | public static void startSuite() throws IOException { 68 | runAsSuite = true; 69 | startKafka(); 70 | } 71 | 72 | @AfterAll 73 | public static void endSuite() throws IOException { 74 | stopKafka(); 75 | } 76 | 77 | public static void startTest() throws IOException { 78 | if (!runAsSuite) 79 | startKafka(); 80 | } 81 | 82 | public static void startTest(Properties props) throws IOException { 83 | if (!runAsSuite) 84 | startKafka(props); 85 | } 86 | 87 | public static void endTest() throws IOException { 88 | if (!runAsSuite) 89 | stopKafka(); 90 | } 91 | 92 | public static void startKafka() throws IOException { 93 | Properties properties = new Properties(); 94 | startKafka(properties); 95 | } 96 | 97 | public static void startKafka(Properties props) throws IOException { 98 | kafka = new KafkaBrokerTestHarness(1,"kafka", props); 99 | kafka.setUp(new EmptyTestInfo()); 100 | } 101 | 102 | public static void stopKafka() throws IOException { 103 | if (kafka != null) { 104 | kafka.tearDown(); 105 | } 106 | } 107 | 108 | public static void startOnlyKafkaBrokers(){ 109 | kafka.startKafkaCluster(); 110 | } 111 | 112 | public static void stopOnlyKafkaBrokers() throws IOException { 113 | kafka.stopKafkaCluster(); 114 | } 115 | 116 | public static Properties getProps() { 117 | return (kafka != null) ? kafka.getProps() : null; 118 | } 119 | } -------------------------------------------------------------------------------- /common-kafka-test/src/main/java/com/cerner/common/kafka/testing/KafkaBrokerTestHarness.java: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Licensed to the Apache Software Foundation (ASF) under one or more 4 | * contributor license agreements. See the NOTICE file distributed with 5 | * this work for additional information regarding copyright ownership. 6 | * The ASF licenses this file to You under the Apache License, Version 2.0 7 | * (the "License"); you may not use this file except in compliance with 8 | * the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package com.cerner.common.kafka.testing; 20 | 21 | import kafka.server.KafkaConfig; 22 | import kafka.server.KafkaServer; 23 | import kafka.server.QuorumTestHarness; 24 | import org.apache.commons.io.FileUtils; 25 | import org.apache.kafka.clients.consumer.ConsumerConfig; 26 | import org.apache.kafka.clients.producer.ProducerConfig; 27 | import org.apache.kafka.common.utils.Time; 28 | import org.junit.jupiter.api.TestInfo; 29 | import scala.Option; 30 | import scala.jdk.javaapi.CollectionConverters; 31 | 32 | import java.io.File; 33 | import java.io.IOException; 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | import java.util.Properties; 37 | import java.util.stream.Collectors; 38 | 39 | /** 40 | * A test harness that brings up some number of Kafka broker nodes. 41 | *

42 | * Adapted from the {@code kafka.integration.KafkaServerTestHarness} class. 43 | *

44 | * 45 | * @author A. Olson 46 | */ 47 | public class KafkaBrokerTestHarness extends QuorumTestHarness { 48 | 49 | /** 50 | * Default number of brokers in the Kafka cluster. 51 | */ 52 | public static final int DEFAULT_BROKERS = 1; 53 | 54 | /** 55 | * Default number of partitions per Kafka topic. 56 | */ 57 | public static final int PARTITIONS_PER_TOPIC = 4; 58 | 59 | private int numOfBrokers; 60 | private List brokers; 61 | private boolean setUp; 62 | private boolean tornDown; 63 | private String clusterId; 64 | private Properties props; 65 | private List brokerConfigs; 66 | 67 | /** 68 | * Creates a new Kafka broker test harness using the {@link #DEFAULT_BROKERS default} number of brokers. 69 | */ 70 | public KafkaBrokerTestHarness() { 71 | this(DEFAULT_BROKERS, new Properties()); 72 | } 73 | 74 | /** 75 | * Creates a new Kafka broker test harness using the {@link #DEFAULT_BROKERS default} number of brokers and the supplied 76 | * {@link Properties} which will be applied to the brokers. 77 | * 78 | * @param properties 79 | * the additional {@link Properties} supplied to the brokers 80 | * @throws IllegalArgumentException 81 | * if {@code properties} is {@code null} 82 | */ 83 | public KafkaBrokerTestHarness(Properties properties) { 84 | this(DEFAULT_BROKERS, properties); 85 | } 86 | 87 | /** 88 | * Creates a new Kafka broker test harness using the given number of brokers and Zookeeper port. 89 | * 90 | * @param numOfBrokers Number of Kafka brokers to start up. 91 | * 92 | * @throws IllegalArgumentException if {@code brokers} is less than 1. 93 | */ 94 | public KafkaBrokerTestHarness(int numOfBrokers) { 95 | this(numOfBrokers, "", new Properties()); 96 | } 97 | 98 | /** 99 | * Creates a new Kafka broker test harness using the given number of brokers and Zookeeper port. 100 | * 101 | * @param numOfBrokers 102 | * Number of Kafka brokers to start up. 103 | * @param properties 104 | * the additional {@link Properties} supplied to the brokers. 105 | * 106 | * @throws IllegalArgumentException 107 | * if {@code brokers} is less than 1 or if {@code baseProperties} is {@code null} 108 | */ 109 | public KafkaBrokerTestHarness(int numOfBrokers, Properties properties) { 110 | this(numOfBrokers, properties.getProperty("cluster.id", ""), properties); 111 | } 112 | 113 | /** 114 | * Creates a new Kafka broker test harness using the given broker configuration properties and Zookeeper port. 115 | * 116 | * @param numOfBrokers the number of brokers to configure. 117 | * @param clusterId the Kafka cluster id. 118 | * @param properties properties to be used when initializing the KafkaServer objects (brokers). 119 | * 120 | */ 121 | public KafkaBrokerTestHarness(final int numOfBrokers, final String clusterId, final Properties properties) { 122 | super(); 123 | this.numOfBrokers = numOfBrokers; 124 | this.brokers = null; 125 | this.setUp = false; 126 | this.tornDown = false; 127 | this.clusterId = clusterId; 128 | this.props = properties; 129 | this.brokerConfigs = new ArrayList<>(numOfBrokers); 130 | } 131 | 132 | /** 133 | * Get the cluster id. 134 | * 135 | * @return the cluster id. 136 | */ 137 | public String getClusterId() { 138 | return clusterId; 139 | } 140 | 141 | /** 142 | * Start up the Kafka broker cluster. 143 | * @param testInfo test info object passed along to {@link QuorumTestHarness} setup method. Used to determine if 144 | * the test will run with zookeeper or Kraft. If the testinof.displayName() contains "quorum=kraft" 145 | * it will run in Kraft mode, if it contains "quorum=zk" it will run in zookeeper mode. 146 | * If the displayName contains neither it will also run in zk mode. 147 | * 148 | * @throws IllegalStateException if the Kafka broker cluster has already been {@link #setUp(TestInfo) setup}. 149 | */ 150 | @Override 151 | public void setUp(TestInfo testInfo) { 152 | if (setUp) { 153 | throw new IllegalStateException("Already setup, cannot setup again"); 154 | } 155 | setUp = true; 156 | 157 | super.setUp(testInfo); 158 | 159 | brokerConfigs.addAll(getBrokerConfig(numOfBrokers, this.zkPort(), props)); 160 | 161 | startKafkaCluster(); 162 | } 163 | 164 | /** 165 | * Shutdown the Kafka broker cluster. Attempting to {@link #setUp(TestInfo)} a cluster again after calling 166 | * this method is not allowed, a new {@code KafkaBrokerTestHarness} must be created instead. 167 | * 168 | * @throws IllegalStateException if the Kafka broker cluster has already been {@link #tearDown() torn down} or has not been 169 | * {@link #setUp(TestInfo)}. 170 | */ 171 | @Override 172 | public void tearDown() { 173 | if (!setUp) { 174 | throw new IllegalStateException("Not set up, cannot tear down"); 175 | } 176 | if (tornDown) { 177 | throw new IllegalStateException("Already torn down, cannot tear down again"); 178 | } 179 | tornDown = true; 180 | 181 | try { 182 | stopKafkaCluster(); 183 | } catch (IOException e) { 184 | throw new RuntimeException(e); 185 | } 186 | 187 | // Shutdown zookeeper 188 | super.tearDown(); 189 | } 190 | 191 | /** 192 | * Start only the Kafka brokers and not the Zookeepers. 193 | * 194 | * @throws IllegalStateException if already started 195 | */ 196 | public void startKafkaCluster() { 197 | if((brokers != null) && (!brokers.isEmpty())) 198 | throw new IllegalStateException("Kafka brokers are already running."); 199 | if(brokerConfigs.isEmpty()) 200 | throw new IllegalStateException("Kafka broker configuration isn't found. Was setup() invoked yet?"); 201 | 202 | brokers = new ArrayList<>(brokerConfigs.size()); 203 | for (KafkaConfig config : brokerConfigs) { 204 | brokers.add(startBroker(config)); 205 | } 206 | } 207 | 208 | /** 209 | * Stop only the Kafka brokers not the Zookeepers. 210 | * 211 | * @throws IllegalStateException if already stopped 212 | * @throws IOException if an error occurs during Kafka broker shutdown. 213 | */ 214 | public void stopKafkaCluster() throws IOException { 215 | 216 | if (brokers == null) { 217 | throw new IllegalStateException("Kafka brokers are already stopped."); 218 | } 219 | 220 | for (KafkaServer broker : brokers) { 221 | broker.shutdown(); 222 | } 223 | 224 | for (KafkaServer broker : brokers) { 225 | for (String logDir : CollectionConverters.asJava(broker.config().logDirs())) { 226 | FileUtils.deleteDirectory(new File(logDir)); 227 | } 228 | } 229 | 230 | brokers = null; 231 | } 232 | 233 | 234 | private String getBootstrapServers() { 235 | 236 | return brokerConfigs.stream() 237 | .map(i-> 238 | i.effectiveAdvertisedListeners().head().host() + ":" + 239 | i.effectiveAdvertisedListeners().head().port()) 240 | .collect(Collectors.joining(",")); 241 | } 242 | 243 | /** 244 | * Returns the configs for all brokers in the test cluster 245 | * 246 | * @return Broker Configs 247 | */ 248 | public List getBrokerConfigs() { 249 | return brokerConfigs; 250 | } 251 | 252 | /** 253 | * Returns properties for a Kafka producer. 254 | * 255 | * @return Producer properties. 256 | */ 257 | public Properties getProducerProps() { 258 | Properties props = new Properties(); 259 | props.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, getBootstrapServers()); 260 | 261 | return props; 262 | } 263 | 264 | /** 265 | * Returns properties for a Kafka consumer. 266 | * 267 | * @return Consumer properties. 268 | */ 269 | public Properties getConsumerProps() { 270 | Properties props = new Properties(); 271 | props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, getBootstrapServers()); 272 | 273 | return props; 274 | } 275 | 276 | /** 277 | * Returns properties for either a Kafka producer or consumer. 278 | * 279 | * @return Combined producer and consumer properties. 280 | */ 281 | public Properties getProps() { 282 | 283 | // Combine producer and consumer properties. 284 | Properties props = getProducerProps(); 285 | props.putAll(getConsumerProps()); 286 | 287 | return props; 288 | } 289 | 290 | /** 291 | * Creates a collection of Kafka Broker configurations based on the number of brokers and zookeeper. 292 | * @param brokers the number of brokers to create configuration for. 293 | * @param zookeeperPort the zookeeper port for the brokers to connect to. 294 | * @return configuration for a collection of brokers. 295 | * @throws IllegalArgumentException if {@code brokers} is less than 1 296 | */ 297 | public List getBrokerConfig(int brokers, int zookeeperPort) { 298 | return getBrokerConfig(brokers, zookeeperPort, new Properties()); 299 | } 300 | 301 | /** 302 | * Creates a collection of Kafka Broker configurations based on the number of brokers and zookeeper. 303 | * @param brokers the number of brokers to create configuration for. 304 | * @param zookeeperPort the zookeeper port for the brokers to connect to. 305 | * @param properties properties that should be applied for each broker config. These properties will be 306 | * honored in favor of any default properties. 307 | * @return configuration for a collection of brokers. 308 | * @throws IllegalArgumentException if {@code brokers} is less than 1 or {@code properties} is {@code null}. 309 | */ 310 | public List getBrokerConfig(int brokers, int zookeeperPort, Properties properties) { 311 | if (brokers < 1) { 312 | throw new IllegalArgumentException("Invalid broker count: " + brokers); 313 | } 314 | if(properties == null){ 315 | throw new IllegalArgumentException("The 'properties' cannot be 'null'."); 316 | } 317 | 318 | int ports[] = KafkaTestUtils.getPorts(brokers); 319 | 320 | List configs = new ArrayList<>(brokers); 321 | for (int i = 0; i < brokers; ++i) { 322 | Properties props = new Properties(); 323 | props.setProperty(KafkaConfig.ZkConnectProp(), "localhost:" + zookeeperPort); 324 | props.setProperty(KafkaConfig.BrokerIdProp(), String.valueOf(i + 1)); 325 | props.setProperty(KafkaConfig.ListenersProp(), "PLAINTEXT://localhost:" + ports[i]); 326 | props.setProperty(KafkaConfig.LogDirProp(), KafkaTestUtils.getTempDir().getAbsolutePath()); 327 | props.setProperty(KafkaConfig.LogFlushIntervalMessagesProp(), String.valueOf(1)); 328 | props.setProperty(KafkaConfig.AutoCreateTopicsEnableProp(), String.valueOf(false)); 329 | props.setProperty(KafkaConfig.NumPartitionsProp(), String.valueOf(PARTITIONS_PER_TOPIC)); 330 | props.setProperty(KafkaConfig.OffsetsTopicReplicationFactorProp(), String.valueOf(brokers)); 331 | props.setProperty(KafkaConfig.DefaultReplicationFactorProp(), String.valueOf(brokers)); 332 | props.setProperty(KafkaConfig.DeleteTopicEnableProp(), String.valueOf(true)); 333 | props.setProperty(KafkaConfig.OffsetsTopicPartitionsProp(), String.valueOf(PARTITIONS_PER_TOPIC)); 334 | props.setProperty(KafkaConfig.LogIndexSizeMaxBytesProp(), String.valueOf(1024 * 1024)); 335 | props.setProperty(KafkaConfig.LogCleanerEnableProp(), String.valueOf(false)); 336 | props.setProperty(KafkaConfig.GroupInitialRebalanceDelayMsProp(), String.valueOf(100)); 337 | 338 | props.putAll(properties); 339 | 340 | configs.add(new KafkaConfig(props)); 341 | } 342 | return configs; 343 | } 344 | 345 | private static KafkaServer startBroker(KafkaConfig config) { 346 | KafkaServer server = new KafkaServer(config, Time.SYSTEM, Option.empty(), false); 347 | server.startup(); 348 | return server; 349 | } 350 | } 351 | 352 | -------------------------------------------------------------------------------- /common-kafka-test/src/main/java/com/cerner/common/kafka/testing/KafkaTestUtils.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.testing; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.net.ServerSocket; 6 | import java.nio.file.Files; 7 | import java.util.HashSet; 8 | import java.util.Map; 9 | import java.util.Map.Entry; 10 | import java.util.Set; 11 | 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | import org.apache.kafka.common.TopicPartition; 15 | 16 | /** 17 | * Assorted Kafka testing utility methods. 18 | * 19 | * @author A. Olson 20 | */ 21 | public class KafkaTestUtils { 22 | 23 | private static final String TEMP_DIR_PREFIX = "kafka-"; 24 | 25 | private static final Set USED_PORTS = new HashSet<>(); 26 | 27 | /** 28 | * Creates and returns a new randomly named temporary directory. It will be deleted upon JVM exit. 29 | * 30 | * @return a new temporary directory. 31 | * 32 | * @throws RuntimeException if a new temporary directory could not be created. 33 | */ 34 | public static File getTempDir() { 35 | try { 36 | File tempDir = Files.createTempDirectory(TEMP_DIR_PREFIX).toFile(); 37 | tempDir.deleteOnExit(); 38 | return tempDir; 39 | } catch (IOException e) { 40 | throw new RuntimeException("could not create temp directory", e); 41 | } 42 | } 43 | 44 | /** 45 | * Returns an array containing the specified number of available local ports. 46 | * 47 | * @param count Number of local ports to identify and return. 48 | * 49 | * @return an array of available local port numbers. 50 | * 51 | * @throws RuntimeException if an I/O error occurs opening or closing a socket. 52 | */ 53 | public static int[] getPorts(int count) { 54 | int[] ports = new int[count]; 55 | Set openSockets = new HashSet<>(count + USED_PORTS.size()); 56 | 57 | for (int i = 0; i < count;) { 58 | try { 59 | ServerSocket socket = new ServerSocket(0); 60 | int port = socket.getLocalPort(); 61 | openSockets.add(socket); 62 | 63 | // Disallow port reuse. 64 | if (!USED_PORTS.contains(port)) { 65 | ports[i++] = port; 66 | USED_PORTS.add(port); 67 | } 68 | } catch (IOException e) { 69 | throw new RuntimeException("could not open socket", e); 70 | } 71 | } 72 | 73 | // Close the sockets so that their port numbers can be used by the caller. 74 | for (ServerSocket socket : openSockets) { 75 | try { 76 | socket.close(); 77 | } catch (IOException e) { 78 | throw new RuntimeException("could not close socket", e); 79 | } 80 | } 81 | 82 | return ports; 83 | } 84 | 85 | /** 86 | * Sums the messages for the given map of {@link TopicPartition} to offsets for the specified topic. This map can be 87 | * retrieved using {@code com.cerner.common.kafka.consumer.ConsumerOffsetClient#getEndOffsets(Collection)}. 88 | * 89 | * @param topic the topic to count messages for 90 | * @param offsets the map of broker offset values 91 | * @return the sum of the offsets for a particular topic 92 | * @throws IllegalArgumentException if topic is {@code null}, empty or blank, or if offsets is {@code null} 93 | */ 94 | public static long getTopicAndPartitionOffsetSum(String topic, Map offsets) { 95 | if (StringUtils.isBlank(topic)) 96 | throw new IllegalArgumentException("topic cannot be null, empty or blank"); 97 | if (offsets == null) 98 | throw new IllegalArgumentException("offsets cannot be null"); 99 | 100 | long count = 0; 101 | for (Entry entry : offsets.entrySet()) { 102 | Long value = entry.getValue(); 103 | if (topic.equals(entry.getKey().topic()) && value != null) { 104 | count += value; 105 | } 106 | } 107 | return count; 108 | } 109 | } 110 | 111 | 112 | -------------------------------------------------------------------------------- /common-kafka/README.md: -------------------------------------------------------------------------------- 1 | # common-kafka 2 | 3 | This project provides a lightweight wrapper for 4 | [producers](http://kafka.apache.org/0102/javadoc/org/apache/kafka/clients/producer/KafkaProducer.html) 5 | and [consumers](http://kafka.apache.org/0102/javadoc/org/apache/kafka/clients/consumer/KafkaConsumer.html) 6 | in the [kafka-clients](https://github.com/apache/kafka/tree/trunk/clients) library. It is intended to 7 | simplify and standardize basic Kafka interactions, without introducing any extraneous dependencies. 8 | 9 | The primary classes available for use are listed below. 10 | 11 | ## Producer 12 | 13 | ### [KafkaProducerPool](src/main/java/com/cerner/common/kafka/producer/KafkaProducerPool.java) 14 | 15 | * Manages a thread-safe pool of producers to improve performance for highly concurrent applications. 16 | * Provides a set of reasonable default configuration properties for producers. 17 | * Creates multiple pools as needed to accommodate producers with differing configuration. 18 | 19 | ### [KafkaProducerWrapper](src/main/java/com/cerner/common/kafka/producer/KafkaProducerWrapper.java) 20 | 21 | * Simplifies the sending of Kafka messages and corresponding error-handling concerns. 22 | * Supports both synchronous and asynchronous usage patterns. 23 | 24 | ### [FairPartitioner](src/main/java/com/cerner/common/kafka/producer/partitioners/FairPartitioner.java) 25 | 26 | * Maximizes message throughput and broker storage efficiency for high-volume applications by grouping 27 | messages produced in a window of time to the same partition. 28 | * Retains some functionality of the default partitioner such as preference of partitions with an 29 | available leader. 30 | * Allows a supplemental key-based hash value to be supplied. 31 | 32 | ## Consumer 33 | 34 | ### [ProcessingKafkaConsumer](src/main/java/com/cerner/common/kafka/consumer/ProcessingKafkaConsumer.java) 35 | 36 | * Manages consumer group topic subscriptions and the resulting partition assignments. 37 | * Allows consumed messages to be either acknowledged as successfully processed or marked as failed and 38 | scheduled for future retried consumption. 39 | * Identifies the acked message offsets that are eligible to commit for each assigned partition. 40 | * Periodically commits the offset marking the end of a contiguous range of acked messages, triggered by 41 | either the number of commit-pending messages or elapsed time since the last commit 42 | * Tracks a number of important consumer processing metrics, to assist with monitoring and troubleshooting 43 | needs. 44 | * Simplifies consumer error-handling logic by catching and dealing with certain commonly encountered 45 | exceptions. 46 | * Handles the processing state changes that occur when the consumer group partition assignments are 47 | rebalanced due to group membership or topic subscription changes. 48 | * Dynamically pauses and resumes message consumption from partitions based on configurable thresholds to 49 | limit the rate of processing failures. 50 | 51 | ### [ProcessingConfig](src/main/java/com/cerner/common/kafka/consumer/ProcessingConfig.java) 52 | 53 | * Encapsulates the configuration for a `ProcessingKafkaConsumer` including offset commit thresholds and 54 | failure handling behavior. 55 | 56 | ### [ConsumerOffsetClient](src/main/java/com/cerner/common/kafka/consumer/ConsumerOffsetClient.java) 57 | 58 | * Wraps consumer offset management functionality including the following capabilities: 59 | * Retrieve the earliest or latest available broker log offsets for each partition of a collection of 60 | topics. 61 | * Retrieve the broker log offset of the first message written after a specified timestamp for each 62 | partition of a collection of topics. 63 | * Retrieve the committed processing offset for a consumer group for each partition of a collection of 64 | topics, or a specific partition. 65 | * Commit specific processing offsets for a collection of partitions for a consumer group. 66 | * Identify the existing partitions for a collection of topics. 67 | 68 | ### [FairAssignor](src/main/java/com/cerner/common/kafka/consumer/assignors/FairAssignor.java) 69 | 70 | * Balances assigned partitions across the members of a consumer group such that each group member is 71 | assigned approximately the same number of partitions, even if the consumer topic subscriptions are 72 | substantially different. 73 | 74 | ## Miscellany 75 | 76 | ### [TopicPartitionComparator](src/main/java/com/cerner/common/kafka/TopicPartitionComparator.java) 77 | 78 | * A comparator for sorting collections of `TopicPartition` objects first by topic name and then partition 79 | number. -------------------------------------------------------------------------------- /common-kafka/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | com.cerner.common.kafka 7 | common-kafka-parent 8 | 3.1-SNAPSHOT 9 | 10 | 11 | common-kafka 12 | Common Kafka 13 | Common Kafka 14 | ${cloud.site.url} 15 | jar 16 | 17 | 18 | .. 19 | 20 | 21 | 22 | 23 | ${cloud.site.id} 24 | ${cloud.site.name} 25 | ${cloud.site.deploy.url} 26 | 27 | 28 | 29 | 30 | 31 | org.apache.kafka 32 | kafka-clients 33 | 34 | 35 | commons-io 36 | commons-io 37 | 38 | 39 | org.slf4j 40 | slf4j-api 41 | 42 | 43 | com.yammer.metrics 44 | metrics-core 45 | 46 | 47 | 48 | 49 | org.apache.kafka 50 | kafka-clients 51 | test 52 | test 53 | 54 | 55 | com.cerner.common.kafka 56 | common-kafka-test 57 | test 58 | 59 | 60 | org.junit.jupiter 61 | junit-jupiter-api 62 | test 63 | 64 | 65 | org.junit.jupiter 66 | junit-jupiter-engine 67 | test 68 | 69 | 70 | org.junit.platform 71 | junit-platform-suite-api 72 | test 73 | 74 | 75 | org.junit.platform 76 | junit-platform-suite-engine 77 | test 78 | 79 | 80 | org.hamcrest 81 | hamcrest-library 82 | test 83 | 84 | 85 | org.hamcrest 86 | hamcrest-core 87 | test 88 | 89 | 90 | org.mockito 91 | mockito-core 92 | test 93 | 94 | 95 | org.mockito 96 | mockito-junit-jupiter 97 | test 98 | 99 | 100 | log4j 101 | log4j 102 | test 103 | 104 | 105 | org.slf4j 106 | slf4j-log4j12 107 | test 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-surefire-plugin 117 | 118 | 119 | **/KafkaTests.java 120 | **/StandaloneTests.java 121 | 122 | always 123 | -Xmx2048m -Djava.net.preferIPv4Stack=true 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-dependency-plugin 129 | 130 | 131 | org.slf4j:slf4j-log4j12:jar:${slf4j.version} 132 | org.junit.platform:junit-platform-suite-engine:jar:${junit.platform.version} 133 | org.junit.jupiter:junit-jupiter-engine:jar:${junit.version} 134 | org.apache.kafka:kafka-clients:jar:${kafka.version} 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /common-kafka/src/etc/findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/KafkaExecutionException.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka; 2 | 3 | import java.io.IOException; 4 | import java.util.concurrent.ExecutionException; 5 | 6 | /** 7 | * Wrapper exception to indicate an error occurred while trying to submit to the destination 8 | * brokers. The exception may or not be recoverable. To see the list of possible exceptions encompassed can be found at 9 | * {@link org.apache.kafka.clients.producer.Callback#onCompletion(org.apache.kafka.clients.producer.RecordMetadata, Exception)}. 10 | * 11 | * If the exception is recoverable, retrying the message might fix the issue. 12 | * 13 | * @author Stephen Durfey 14 | */ 15 | public class KafkaExecutionException extends IOException { 16 | 17 | /** 18 | * Serial version UID 19 | */ 20 | private static final long serialVersionUID = -6449179138732948352L; 21 | 22 | public KafkaExecutionException(ExecutionException e) { 23 | super(e); 24 | } 25 | } -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/TopicPartitionComparator.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka; 2 | 3 | import org.apache.kafka.common.TopicPartition; 4 | 5 | import java.io.Serializable; 6 | import java.util.Comparator; 7 | 8 | /** 9 | * A {@link Comparator} for {@link TopicPartition} objects. 10 | */ 11 | public class TopicPartitionComparator implements Comparator, Serializable { 12 | private static final long serialVersionUID = -2400690278866168934L; 13 | 14 | @Override 15 | public int compare(TopicPartition a, TopicPartition b) { 16 | int compareTopics = a.topic().compareTo(b.topic()); 17 | return compareTopics != 0 ? compareTopics : Integer.compare(a.partition(), b.partition()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/consumer/ConsumerOffsetClient.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.consumer; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.apache.kafka.clients.consumer.Consumer; 5 | import org.apache.kafka.clients.consumer.ConsumerConfig; 6 | import org.apache.kafka.clients.consumer.KafkaConsumer; 7 | import org.apache.kafka.clients.consumer.OffsetAndMetadata; 8 | import org.apache.kafka.clients.consumer.OffsetAndTimestamp; 9 | import org.apache.kafka.common.PartitionInfo; 10 | import org.apache.kafka.common.TopicPartition; 11 | import org.apache.kafka.common.serialization.BytesDeserializer; 12 | 13 | import java.io.Closeable; 14 | import java.util.Collection; 15 | import java.util.Collections; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Properties; 20 | import java.util.UUID; 21 | import java.util.function.Function; 22 | import java.util.stream.Collectors; 23 | 24 | /** 25 | * Simple API to fetch offsets from Kafka 26 | */ 27 | public class ConsumerOffsetClient implements Closeable { 28 | 29 | protected final boolean providedByUser; 30 | protected final Consumer consumer; 31 | 32 | /** 33 | * Constructs the consumer offset client 34 | * 35 | * @param properties 36 | * the properties used to connect to Kafka. The {@link ConsumerConfig#BOOTSTRAP_SERVERS_CONFIG} must be provided while 37 | * {@link ConsumerConfig#GROUP_ID_CONFIG} is optional and a random group will be provided if not supplied 38 | * @throws org.apache.kafka.common.KafkaException 39 | * if there is an issue creating the client 40 | */ 41 | public ConsumerOffsetClient(Properties properties) { 42 | this(getConsumer(properties), false); 43 | } 44 | 45 | /** 46 | * Constructs the consumer offset client 47 | * 48 | * @param consumer 49 | * the consumer used to interact with Kafka. It is up to the user to maintain the lifecycle of this consumer 50 | */ 51 | public ConsumerOffsetClient(Consumer consumer) { 52 | this(consumer, true); 53 | } 54 | 55 | ConsumerOffsetClient(Consumer consumer, boolean providedByUser) { 56 | if (consumer == null) 57 | throw new IllegalArgumentException("consumer cannot be null"); 58 | 59 | this.consumer = consumer; 60 | this.providedByUser = providedByUser; 61 | } 62 | 63 | private static Consumer getConsumer(Properties properties) { 64 | if (properties == null) 65 | throw new IllegalArgumentException("properties cannot be null"); 66 | 67 | Properties consumerProperties = new Properties(); 68 | consumerProperties.putAll(properties); 69 | 70 | // Ensure the serializer configuration is set though its not needed 71 | consumerProperties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class.getName()); 72 | consumerProperties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, BytesDeserializer.class.getName()); 73 | 74 | String group = consumerProperties.getProperty(ConsumerConfig.GROUP_ID_CONFIG); 75 | 76 | // Add some random consumer group name to avoid any issues 77 | if (group == null) 78 | consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, "kafka-consumer-offset-client-" + UUID.randomUUID()); 79 | 80 | return new KafkaConsumer<>(consumerProperties); 81 | } 82 | 83 | /** 84 | * Returns the end/latests offsets for the provided topics 85 | * 86 | * @param topics 87 | * collection of Kafka topics 88 | * @return the end/latests offsets for the provided topics 89 | * @throws org.apache.kafka.common.KafkaException 90 | * if there is an issue fetching the end offsets 91 | * @throws IllegalArgumentException 92 | * if topics is null 93 | */ 94 | public Map getEndOffsets(Collection topics) { 95 | if (topics == null) 96 | throw new IllegalArgumentException("topics cannot be null"); 97 | 98 | Collection partitions = getPartitionsFor(topics); 99 | return consumer.endOffsets(partitions); 100 | } 101 | 102 | /** 103 | * Returns the beginning/earliest offsets for the provided topics 104 | * 105 | * @param topics 106 | * collection of Kafka topics 107 | * @return the beginning/earliest offsets for the provided topics 108 | * @throws org.apache.kafka.common.KafkaException 109 | * if there is an issue fetching the beginning offsets 110 | * @throws IllegalArgumentException 111 | * if topics is null 112 | */ 113 | public Map getBeginningOffsets(Collection topics) { 114 | if (topics == null) 115 | throw new IllegalArgumentException("topics cannot be null"); 116 | 117 | Collection partitions = getPartitionsFor(topics); 118 | return consumer.beginningOffsets(partitions); 119 | } 120 | 121 | /** 122 | * Returns the offsets for the provided topics at the specified {@code time}. If no offset for a topic or partition 123 | * is available at the specified {@code time} then the {@link #getEndOffsets(Collection) latest} offsets 124 | * for that partition are returned. 125 | * 126 | * @param topics 127 | * collection of Kafka topics 128 | * @param time the specific time at which to retrieve offsets 129 | * @return the offsets for the provided topics at the specified time. 130 | * @throws org.apache.kafka.common.KafkaException 131 | * if there is an issue fetching the offsets 132 | * @throws IllegalArgumentException 133 | * if topics is null 134 | */ 135 | public Map getOffsetsForTimes(Collection topics, long time) { 136 | if (topics == null) 137 | throw new IllegalArgumentException("topics cannot be null"); 138 | 139 | Collection partitions = getPartitionsFor(topics); 140 | 141 | //Find all the offsets at a specified time. 142 | Map topicTimes = getPartitionsFor(topics) 143 | .stream().collect(Collectors.toMap(Function.identity(), s -> time)); 144 | Map foundOffsets = consumer.offsetsForTimes(topicTimes); 145 | 146 | 147 | //merge the offsets together into a single collection. 148 | Map offsets = new HashMap<>(); 149 | offsets.putAll(foundOffsets.entrySet() 150 | .stream() 151 | //Filter the null offsets. 152 | .filter(e -> e.getValue() !=null) 153 | .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().offset()))); 154 | 155 | //if some partitions do not have offsets at the specified time, find the latest offsets of the partitions for that time. 156 | List missingPartitions = partitions.stream() 157 | .filter(t -> !offsets.containsKey(t)).collect(Collectors.toList()); 158 | if(!missingPartitions.isEmpty()) { 159 | Map missingOffsets = consumer.endOffsets(missingPartitions); 160 | offsets.putAll(missingOffsets); 161 | } 162 | 163 | return offsets; 164 | } 165 | 166 | /** 167 | * Returns the committed offsets for the consumer group and the provided topics or -1 if no offset is found 168 | * 169 | * @param topics 170 | * collection of Kafka topics 171 | * @return the committed offsets for the consumer group and the provided topics or -1 if no offset is found 172 | * @throws org.apache.kafka.common.KafkaException 173 | * if there is an issue fetching the committed offsets 174 | * @throws IllegalArgumentException 175 | * if topics is null 176 | */ 177 | public Map getCommittedOffsets(Collection topics) { 178 | if (topics == null) 179 | throw new IllegalArgumentException("topics cannot be null"); 180 | 181 | Collection partitions = getPartitionsFor(topics); 182 | Map offsets = new HashMap<>(); 183 | 184 | partitions.forEach(topicPartition -> { 185 | offsets.put(topicPartition, getCommittedOffset(topicPartition)); 186 | }); 187 | 188 | return offsets; 189 | } 190 | 191 | /** 192 | * Commits the {@code offsets}. 193 | * 194 | * @param offsets the offsets to commit. 195 | * @throws IllegalArgumentException if the {@code offsets} are {@code null} or contains a negative value. 196 | * @throws NullPointerException if the {@code offsets} contains a {@code null} value 197 | * @throws org.apache.kafka.common.KafkaException 198 | * if there is an issue committing the offsets 199 | */ 200 | public void commitOffsets(Map offsets){ 201 | if(offsets == null) 202 | throw new IllegalArgumentException("offsets cannot be null"); 203 | 204 | Map offsetsToWrite = offsets.entrySet().stream() 205 | .collect(Collectors.toMap(Map.Entry::getKey, e -> new OffsetAndMetadata(e.getValue()))); 206 | 207 | consumer.commitSync(offsetsToWrite); 208 | } 209 | 210 | /** 211 | * Returns the committed offset or -1 for the consumer group and the given topic partition 212 | * 213 | * @param topicPartition 214 | * a topic partition 215 | * @return the committed offset or -1 for the consumer group and the given topic partition 216 | * @throws org.apache.kafka.common.KafkaException 217 | * if there is an issue fetching the committed offset 218 | * @throws IllegalArgumentException 219 | * if topicPartition is null 220 | */ 221 | public long getCommittedOffset(TopicPartition topicPartition) { 222 | if (topicPartition == null) 223 | throw new IllegalArgumentException("topicPartition cannot be null"); 224 | 225 | OffsetAndMetadata offsetAndMetadata = consumer.committed(Collections.singleton(topicPartition)).get(topicPartition); 226 | return offsetAndMetadata == null ? -1L : offsetAndMetadata.offset(); 227 | } 228 | 229 | /** 230 | * Returns the partitions for the provided topics 231 | * 232 | * @param topics 233 | * collection of Kafka topics 234 | * @return the partitions for the provided topics 235 | * @throws org.apache.kafka.common.KafkaException 236 | * if there is an issue fetching the partitions 237 | * @throws IllegalArgumentException 238 | * if topics is null 239 | */ 240 | public Collection getPartitionsFor(Collection topics) { 241 | if (topics == null) 242 | throw new IllegalArgumentException("topics cannot be null"); 243 | return topics.stream() 244 | .flatMap(topic -> { 245 | List partitionInfos = consumer.partitionsFor(topic); 246 | 247 | // partition infos could be null if the topic does not exist 248 | if (partitionInfos == null) 249 | return Collections.emptyList().stream(); 250 | 251 | return partitionInfos.stream() 252 | .map(partitionInfo -> new TopicPartition(partitionInfo.topic(), partitionInfo.partition())); 253 | }) 254 | .collect(Collectors.toList()); 255 | 256 | } 257 | 258 | @Override 259 | public void close() { 260 | if (!providedByUser) 261 | IOUtils.closeQuietly(consumer); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/consumer/ProcessingConfig.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.consumer; 2 | 3 | import java.util.Locale; 4 | import java.util.Properties; 5 | 6 | import org.apache.kafka.clients.consumer.ConsumerConfig; 7 | import org.apache.kafka.clients.consumer.OffsetResetStrategy; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | /** 12 | * Configuration object used for Kafka 13 | */ 14 | public class ProcessingConfig { 15 | 16 | /** 17 | * Logger 18 | */ 19 | private static final Logger LOGGER = LoggerFactory.getLogger(ProcessingConfig.class); 20 | 21 | /** 22 | * Commit the initial offset for a new consumer 23 | */ 24 | public static final String COMMIT_INITIAL_OFFSET_PROPERTY = "processing.kafka.commit.initial.offset"; 25 | 26 | /** 27 | * The default value for the {@link #COMMIT_INITIAL_OFFSET_PROPERTY}. 28 | */ 29 | public static final String COMMIT_INITIAL_OFFSET_DEFAULT = String.valueOf(true); 30 | 31 | /** 32 | * The amount of time in milliseconds to wait before committing eligible offsets 33 | */ 34 | public static final String COMMIT_TIME_THRESHOLD_PROPERTY = "processing.kafka.commit.time.threshold"; 35 | 36 | /** 37 | * The default amount of time to wait before committing offsets (60s) 38 | */ 39 | public static final String COMMIT_TIME_THRESHOLD_DEFAULT = Long.toString(60000L); 40 | 41 | /** 42 | * The maximum number of messages that can be processed but not committed per partition. In other words this will 43 | * trigger a commit if a partition has the configured value or more messages processed but uncommitted 44 | */ 45 | public static final String COMMIT_SIZE_THRESHOLD_PROPERTY = "processing.kafka.commit.size.threshold"; 46 | 47 | /** 48 | * The default maximum number of messages that can be processed but not committed per partition (500L) 49 | */ 50 | public static final String COMMIT_SIZE_THRESHOLD_DEFAULT = Long.toString(500L); 51 | 52 | /** 53 | * The threshold to pause a partition's progress when this percentage of processing or higher is failures. The value 54 | * should be between [0, 1]. 55 | */ 56 | public static final String FAIL_THRESHOLD_PROPERTY = "processing.kafka.fail.threshold"; 57 | 58 | /** 59 | * The default value for the threshold to pause a partition's progress if enough failures have occurred. Default is 60 | * 0.5 which is equivalent to a 50% failure rate 61 | */ 62 | public static final String FAIL_THRESHOLD_DEFAULT = Double.toString(0.5); 63 | 64 | /** 65 | * The number of samples to consider when calculating the percentage of recently failed records 66 | */ 67 | public static final String FAIL_SAMPLE_SIZE_PROPERTY = "processing.kafka.fail.sample.size"; 68 | 69 | /** 70 | * The default value for the fail sample size (25 results) 71 | */ 72 | public static final String FAIL_SAMPLE_SIZE_DEFAULT = Integer.toString(25); 73 | 74 | /** 75 | * The amount of time in ms to pause a partition when the fail threshold has been met 76 | */ 77 | public static final String FAIL_PAUSE_TIME_PROPERTY = "processing.kafka.fail.pause.time"; 78 | 79 | /** 80 | * The default amount of time to pause a partition when the fail threshold has been met (10s) 81 | */ 82 | public static final String FAIL_PAUSE_TIME_DEFAULT = Long.toString(10000); 83 | 84 | /** 85 | * If initial offsets for a new consumer should be committed 86 | */ 87 | private final boolean commitInitialOffset; 88 | 89 | /** 90 | * The amount of time in milliseconds to wait before committing offsets 91 | */ 92 | protected final long commitTimeThreshold; 93 | 94 | /** 95 | * The number of messages to wait before committing offsets 96 | */ 97 | protected final long commitSizeThreshold; 98 | 99 | /** 100 | * The offset reset strategy for the consumer 101 | */ 102 | private final OffsetResetStrategy offsetResetStrategy; 103 | 104 | /** 105 | * The percentage of failures allowed before pausing a partition 106 | */ 107 | private final double failThreshold; 108 | 109 | /** 110 | * The number of results to consider when calculating the percentage of successful processed records 111 | */ 112 | private final int failSampleSize; 113 | 114 | /** 115 | * The amount of time to pause a partition for 116 | */ 117 | private final long failPauseTime; 118 | 119 | /** 120 | * The consumer's max poll interval 121 | */ 122 | private final long maxPollInterval; 123 | 124 | /** 125 | * The raw properties configured 126 | */ 127 | private final Properties properties; 128 | 129 | /** 130 | * Creates a new processing config object 131 | * 132 | *

133 | * NOTE: You must provide a value for {@link ConsumerConfig#AUTO_OFFSET_RESET_CONFIG} in addition to the requirements 134 | * of {@link org.apache.kafka.clients.consumer.KafkaConsumer} 135 | *

136 | * 137 | * @param properties 138 | * the configuration used by the consumer 139 | * @throws IllegalArgumentException 140 | *
    141 | *
  • properties is {@code null}
  • 142 | *
  • a value for {@link ConsumerConfig#AUTO_OFFSET_RESET_CONFIG} was not provided
  • 143 | *
  • the value for {@link ConsumerConfig#AUTO_OFFSET_RESET_CONFIG} is 'none'
  • 144 | *
  • if the value of {@link ConsumerConfig#AUTO_OFFSET_RESET_CONFIG} was invalid
  • 145 | *
  • if the value of {@link ConsumerConfig#MAX_POLL_INTERVAL_MS_CONFIG} was invalid
  • 146 | *
  • {@link #COMMIT_TIME_THRESHOLD_PROPERTY} is < 0
  • 147 | *
  • {@link #COMMIT_SIZE_THRESHOLD_PROPERTY} is ≤ 0
  • 148 | *
  • {@link #FAIL_THRESHOLD_PROPERTY} is < 0 or > 1
  • 149 | *
  • {@link #FAIL_SAMPLE_SIZE_PROPERTY} is ≤ 0
  • 150 | *
  • {@link #FAIL_PAUSE_TIME_PROPERTY} is < 0
  • 151 | *
  • if any of the numeric properties are not valid numbers
  • 152 | *
153 | */ 154 | public ProcessingConfig(Properties properties) { 155 | if (properties == null) 156 | throw new IllegalArgumentException("properties cannot be null"); 157 | 158 | Properties configProperties = new Properties(); 159 | configProperties.putAll(properties); 160 | 161 | // This cannot be enabled otherwise it will break the guarantees of this processing consumer 162 | configProperties.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); 163 | 164 | this.properties = configProperties; 165 | this.commitInitialOffset = parseBoolean(COMMIT_INITIAL_OFFSET_PROPERTY, 166 | properties.getProperty(COMMIT_INITIAL_OFFSET_PROPERTY, COMMIT_INITIAL_OFFSET_DEFAULT)); 167 | this.commitTimeThreshold = parseLong(COMMIT_TIME_THRESHOLD_PROPERTY, 168 | properties.getProperty(COMMIT_TIME_THRESHOLD_PROPERTY, COMMIT_TIME_THRESHOLD_DEFAULT)); 169 | this.commitSizeThreshold = parseLong(COMMIT_SIZE_THRESHOLD_PROPERTY, 170 | properties.getProperty(COMMIT_SIZE_THRESHOLD_PROPERTY, COMMIT_SIZE_THRESHOLD_DEFAULT)); 171 | this.failThreshold = parseDouble(FAIL_THRESHOLD_PROPERTY, properties.getProperty(FAIL_THRESHOLD_PROPERTY, 172 | FAIL_THRESHOLD_DEFAULT)); 173 | this.failSampleSize = parseInt(FAIL_SAMPLE_SIZE_PROPERTY, properties.getProperty(FAIL_SAMPLE_SIZE_PROPERTY, 174 | FAIL_SAMPLE_SIZE_DEFAULT)); 175 | this.failPauseTime = parseLong(FAIL_PAUSE_TIME_PROPERTY, properties.getProperty(FAIL_PAUSE_TIME_PROPERTY, 176 | FAIL_PAUSE_TIME_DEFAULT)); 177 | this.maxPollInterval = parseLong(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, properties.getProperty( 178 | ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "300000")); 179 | 180 | if (commitTimeThreshold < 0L) 181 | throw new IllegalArgumentException(COMMIT_TIME_THRESHOLD_PROPERTY + " cannot be < 0. Value: " + 182 | commitTimeThreshold); 183 | 184 | if (commitSizeThreshold <= 0L) 185 | throw new IllegalArgumentException(COMMIT_SIZE_THRESHOLD_PROPERTY + " cannot be <= 0. Value: " + 186 | commitSizeThreshold); 187 | 188 | if (failThreshold < 0) 189 | throw new IllegalArgumentException(FAIL_THRESHOLD_PROPERTY + " cannot be < 0. Value: " + failThreshold); 190 | 191 | if (failThreshold > 1) 192 | throw new IllegalArgumentException(FAIL_THRESHOLD_PROPERTY + " cannot be > 1. Value: " + failThreshold); 193 | 194 | if (failSampleSize <= 0) 195 | throw new IllegalArgumentException(FAIL_SAMPLE_SIZE_PROPERTY + " cannot be <= 0. Value: " + failSampleSize); 196 | 197 | if (failPauseTime < 0) 198 | throw new IllegalArgumentException(FAIL_PAUSE_TIME_PROPERTY + " cannot be < 0. Value: " + failPauseTime); 199 | 200 | String offsetResetStrategy = properties.getProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG); 201 | 202 | if (offsetResetStrategy == null) 203 | throw new IllegalArgumentException("A value for " + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG + 204 | " must be provided in the config"); 205 | 206 | this.offsetResetStrategy = OffsetResetStrategy.valueOf(offsetResetStrategy.toUpperCase(Locale.ENGLISH)); 207 | 208 | if (this.offsetResetStrategy.equals(OffsetResetStrategy.NONE)) 209 | throw new IllegalArgumentException("The offset reset strategy 'none' is not valid. Must be either 'earliest'" 210 | + " or 'latest'"); 211 | 212 | LOGGER.debug("Config {}", toString()); 213 | } 214 | 215 | private static double parseDouble(String property, String value) { 216 | try { 217 | return Double.parseDouble(value); 218 | } catch (NumberFormatException e) { 219 | throw new IllegalArgumentException("Unable to parse config [" + property + "] as value [" + value + 220 | "] was not a valid number", e); 221 | } 222 | } 223 | 224 | private static long parseLong(String property, String value) { 225 | try { 226 | return Long.parseLong(value); 227 | } catch (NumberFormatException e) { 228 | throw new IllegalArgumentException("Unable to parse config [" + property + "] as value [" + value + 229 | "] was not a valid number", e); 230 | } 231 | } 232 | 233 | private static int parseInt(String property, String value) { 234 | try { 235 | return Integer.parseInt(value); 236 | } catch (NumberFormatException e) { 237 | throw new IllegalArgumentException("Unable to parse config [" + property + "] as value [" + value + 238 | "] was not a valid number", e); 239 | } 240 | } 241 | 242 | private static boolean parseBoolean(String property, String value) { 243 | if (Boolean.TRUE.toString().equalsIgnoreCase(value) || Boolean.FALSE.toString().equalsIgnoreCase(value)) { 244 | return Boolean.parseBoolean(value); 245 | } 246 | 247 | throw new IllegalArgumentException("Unable to parse config [" + property + "] as value [" + value + "] was not a valid " 248 | + "boolean"); 249 | } 250 | 251 | /** 252 | * If initial offsets for a new consumer should be committed 253 | * 254 | * @return {@code true} if initial offsets for a new consumer should be committed, {@code false} otherwise 255 | */ 256 | public boolean getCommitInitialOffset() { 257 | return commitInitialOffset; 258 | } 259 | 260 | /** 261 | * The threshold for when we should attempt to commit if enough time has passed since our last commit (in ms) 262 | * 263 | * @return threshold for when we should attempt to commit if enough time has passed since our last commit (in ms) 264 | */ 265 | public long getCommitTimeThreshold() { 266 | return commitTimeThreshold; 267 | } 268 | 269 | /** 270 | * The threshold per partition for when we should attempt to commit if we have at least this many commits eligible 271 | * for commit 272 | * 273 | * @return The threshold per partition for when we should attempt to commit if we have at least this many commits 274 | * eligible for commit 275 | */ 276 | public long getCommitSizeThreshold() { 277 | return commitSizeThreshold; 278 | } 279 | 280 | /** 281 | * The offset strategy used by the consumer 282 | * 283 | * @return offset strategy used by the consumer 284 | */ 285 | public OffsetResetStrategy getOffsetResetStrategy() { 286 | return offsetResetStrategy; 287 | } 288 | 289 | /** 290 | * The failure threshold in percent [0, 1] in which if our processing failures go above that percentage we will pause 291 | * processing for that partition for a time 292 | * 293 | * @return the failure threshold in percent [0, 1] 294 | */ 295 | public double getFailThreshold() { 296 | return failThreshold; 297 | } 298 | 299 | /** 300 | * The sample size used in calculating the percentage of processing failures 301 | * 302 | * @return sample size used in calculating the percentage of processing failures 303 | */ 304 | public int getFailSampleSize() { 305 | return failSampleSize; 306 | } 307 | 308 | /** 309 | * The amount of time in ms we should pause a partition for if it has reached the {@link #getFailThreshold()} 310 | * 311 | * @return The amount of time in ms we should pause a partition for 312 | */ 313 | public long getFailPauseTime() { 314 | return failPauseTime; 315 | } 316 | 317 | /** 318 | * The consumer's max poll interval 319 | * 320 | * @return the consumer's max poll interval 321 | */ 322 | public long getMaxPollInterval() { 323 | return maxPollInterval; 324 | } 325 | 326 | /** 327 | * The properties used to configure the consumer 328 | * 329 | * @return properties used to configure the consumer 330 | */ 331 | public Properties getProperties() { 332 | return properties; 333 | } 334 | 335 | /** 336 | * Returns the Kafka consumer group id 337 | * 338 | * @return the Kafka consumer group id 339 | */ 340 | public String getGroupId() { 341 | return properties.getProperty(ConsumerConfig.GROUP_ID_CONFIG); 342 | } 343 | 344 | @Override 345 | public String toString() { 346 | return "ProcessingConfig{" + 347 | "commitTimeThreshold=" + commitTimeThreshold + 348 | ", commitSizeThreshold=" + commitSizeThreshold + 349 | ", offsetResetStrategy=" + offsetResetStrategy + 350 | ", failThreshold=" + failThreshold + 351 | ", failSampleSize=" + failSampleSize + 352 | ", failPauseTime=" + failPauseTime + 353 | ", properties=" + properties + 354 | '}'; 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/consumer/assignors/FairAssignor.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.consumer.assignors; 2 | 3 | import org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignor; 4 | import org.apache.kafka.common.TopicPartition; 5 | import org.apache.kafka.common.utils.Utils; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Comparator; 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | 16 | /** 17 | *

18 | * The fair assignor attempts to balance partitions across consumers such that each consumer is assigned approximately 19 | * the same number of partitions, even if the consumer topic subscriptions are substantially different (if they are 20 | * identical, then the result will be equivalent to that of the roundrobin assignor). The running total of assignments 21 | * per consumer is tracked as the algorithm executes in order to accomplish this. 22 | *

23 | * 24 | *

25 | * The algorithm starts with the topic with the fewest consumer subscriptions, and assigns its partitions in roundrobin 26 | * fashion. In the event of a tie for least subscriptions, the topic with the highest partition count is assigned 27 | * first, as this generally creates a more balanced distribution. The final tiebreaker is the topic name. 28 | *

29 | * 30 | *

31 | * The partitions for subsequent topics are assigned to the subscribing consumer with the fewest number of assignments. 32 | * In the event of a tie for least assignments, the tiebreaker is the consumer id, so that the assignment pattern is 33 | * deterministic and fairly similar to how the roundrobin assignor functions. 34 | *

35 | * 36 | *

37 | * For example, suppose there are two consumers C0 and C1, two topics t0 and t1, and each topic has 3 partitions, 38 | * resulting in partitions t0p0, t0p1, t0p2, t1p0, t1p1, and t1p2. If both C0 and C1 are consuming t0, but only C1 is 39 | * consuming t1 then the assignment will be: 40 | *

41 | * 42 | *
 43 |  * C0 = [t0p0, t0p1, t0p2]
 44 |  * C1 = [t1p0, t1p1, t1p2]
 45 |  * 
46 | * 47 | *

48 | * This implementation is borrowed from https://issues.apache.org/jira/browse/KAFKA-3297. Once this is merged this 49 | * can be removed 50 | *

51 | * 52 | * @author Andrew Olson 53 | */ 54 | public class FairAssignor extends AbstractPartitionAssignor { 55 | 56 | @Override 57 | public Map> assign(Map partitionsPerTopic, 58 | Map subscriptions) { 59 | List consumers = Utils.sorted(subscriptions.keySet()); 60 | 61 | // Invert topics-per-consumer map to consumers-per-topic. 62 | Map> consumersPerTopic = consumersPerTopic(subscriptions); 63 | 64 | // Map for tracking the total number of partitions assigned to each consumer 65 | Map consumerAssignmentCounts = new HashMap<>(); 66 | for (String consumer : consumers) { 67 | consumerAssignmentCounts.put(consumer, 0); 68 | } 69 | 70 | Map> assignment = new HashMap<>(); 71 | for (String memberId : subscriptions.keySet()) { 72 | assignment.put(memberId, new ArrayList<>()); 73 | } 74 | 75 | Comparator consumerComparator = new ConsumerFairness(consumerAssignmentCounts); 76 | for (TopicPartition partition : allPartitionsSorted(partitionsPerTopic, subscriptions, consumersPerTopic)) { 77 | // Find the most appropriate consumer for the partition. 78 | String assignedConsumer = null; 79 | for (String consumer : consumersPerTopic.get(partition.topic())) { 80 | if (assignedConsumer == null || consumerComparator.compare(consumer, assignedConsumer) < 0) { 81 | assignedConsumer = consumer; 82 | } 83 | } 84 | 85 | consumerAssignmentCounts.put(assignedConsumer, consumerAssignmentCounts.get(assignedConsumer) + 1); 86 | assignment.get(assignedConsumer).add(partition); 87 | } 88 | 89 | return assignment; 90 | } 91 | 92 | private static List allPartitionsSorted(Map partitionsPerTopic, 93 | Map topicsPerConsumer, 94 | Map> consumersPerTopic) { 95 | // Collect all topics 96 | Set topics = new HashSet<>(); 97 | topicsPerConsumer.values().forEach(subscription -> topics.addAll(subscription.topics())); 98 | 99 | // Remove any topics that do not have partition information as this means we don't have metadata about them 100 | // or they don't exist 101 | topics.retainAll(partitionsPerTopic.keySet()); 102 | 103 | // Sort topics for optimal fairness, the general idea is to keep the most flexible assignment choices available 104 | // as long as possible by starting with the most constrained assignments. 105 | List sortedTopics = new ArrayList<>(topics); 106 | Collections.sort(sortedTopics, new TopicOrder(partitionsPerTopic, consumersPerTopic)); 107 | 108 | List allPartitions = new ArrayList<>(); 109 | for (String topic : sortedTopics) { 110 | allPartitions.addAll(partitions(topic, partitionsPerTopic.get(topic))); 111 | } 112 | return allPartitions; 113 | } 114 | 115 | @Override 116 | public String name() { 117 | return "fair"; 118 | } 119 | 120 | private static class TopicOrder implements Comparator { 121 | 122 | private final Map topicConsumerCounts; 123 | private final Map partitionsPerTopic; 124 | 125 | TopicOrder(Map partitionsPerTopic, Map> consumersPerTopic) { 126 | this.partitionsPerTopic = partitionsPerTopic; 127 | this.topicConsumerCounts = new HashMap<>(); 128 | for (Map.Entry> consumersPerTopicEntry : consumersPerTopic.entrySet()) { 129 | topicConsumerCounts.put(consumersPerTopicEntry.getKey(), consumersPerTopicEntry.getValue().size()); 130 | } 131 | } 132 | 133 | @Override 134 | public int compare(String t1, String t2) { 135 | // Assign topics with fewer consumers first, tiebreakers are who has more partitions then topic name 136 | int comparison = Integer.compare(topicConsumerCounts.get(t1), topicConsumerCounts.get(t2)); 137 | if (comparison == 0) { 138 | comparison = Integer.compare(partitionsPerTopic.get(t2), partitionsPerTopic.get(t1)); 139 | if (comparison == 0) { 140 | comparison = t1.compareTo(t2); 141 | } 142 | } 143 | 144 | return comparison; 145 | } 146 | } 147 | 148 | protected static Map> consumersPerTopic(Map topicsPerConsumer) { 149 | Map> res = new HashMap<>(); 150 | for (Map.Entry subscriptionEntry : topicsPerConsumer.entrySet()) { 151 | for (String topic : subscriptionEntry.getValue().topics()) 152 | put(res, topic, subscriptionEntry.getKey()); 153 | } 154 | return res; 155 | } 156 | 157 | protected static List partitions(String topic, int numPartitions) { 158 | List partitions = new ArrayList<>(); 159 | for (int i = 0; i < numPartitions; i++) 160 | partitions.add(new TopicPartition(topic, i)); 161 | 162 | return partitions; 163 | } 164 | 165 | private static class ConsumerFairness implements Comparator { 166 | 167 | private final Map consumerAssignmentCounts; 168 | 169 | ConsumerFairness(Map consumerAssignmentCounts) { 170 | this.consumerAssignmentCounts = consumerAssignmentCounts; 171 | } 172 | 173 | @Override 174 | public int compare(String c1, String c2) { 175 | // Prefer consumer with fewer assignments, tiebreaker is consumer id 176 | int comparison = Integer.compare(consumerAssignmentCounts.get(c1), consumerAssignmentCounts.get(c2)); 177 | if (comparison == 0) { 178 | comparison = c1.compareTo(c2); 179 | } 180 | return comparison; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/metrics/MeterPool.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.metrics; 2 | 3 | import com.yammer.metrics.Metrics; 4 | import com.yammer.metrics.core.Meter; 5 | 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.ConcurrentMap; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * A pool of {@link Meter meter} objects. 12 | *

13 | * This class is thread-safe. 14 | *

15 | * 16 | * @author A. Olson 17 | */ 18 | public class MeterPool { 19 | 20 | private final Class clazz; 21 | private final String name; 22 | private final ConcurrentMap meters; 23 | 24 | /** 25 | * Constructs a pool for meters. 26 | * 27 | * @param clazz The class that owns the meter. 28 | * @param name The name of the meter. 29 | * 30 | * @throws IllegalArgumentException 31 | * if any argument is {@code null}. 32 | */ 33 | public MeterPool(Class clazz, String name) { 34 | if (clazz == null) { 35 | throw new IllegalArgumentException("class cannot be null"); 36 | } 37 | if (name == null) { 38 | throw new IllegalArgumentException("name cannot be null"); 39 | } 40 | this.clazz = clazz; 41 | this.name = name; 42 | this.meters = new ConcurrentHashMap(); 43 | } 44 | 45 | /** 46 | * Returns the meter corresponding to the given scope. 47 | * 48 | * @param scope The scope of the meter to return. 49 | * 50 | * @return a {@link Meter} with the given scope. 51 | * 52 | * @throws IllegalArgumentException 53 | * if {@code scope} is {@code null}. 54 | */ 55 | public Meter getMeter(String scope) { 56 | if (scope == null) { 57 | throw new IllegalArgumentException("scope cannot be null"); 58 | } 59 | Meter meter = meters.get(scope); 60 | if (meter == null) { 61 | meter = Metrics.newMeter(clazz, name, scope, name, TimeUnit.SECONDS); 62 | Meter existing = meters.putIfAbsent(scope, meter); 63 | if (existing != null) { 64 | meter = existing; 65 | } 66 | } 67 | return meter; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/producer/KafkaProducerPool.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.producer; 2 | 3 | import org.apache.kafka.clients.producer.KafkaProducer; 4 | import org.apache.kafka.clients.producer.Producer; 5 | import org.apache.kafka.common.record.CompressionType; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.Closeable; 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.Properties; 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | import java.util.concurrent.locks.Lock; 20 | import java.util.concurrent.locks.ReadWriteLock; 21 | import java.util.concurrent.locks.ReentrantReadWriteLock; 22 | import java.util.stream.Stream; 23 | 24 | import static org.apache.kafka.clients.producer.ProducerConfig.ACKS_CONFIG; 25 | import static org.apache.kafka.clients.producer.ProducerConfig.BATCH_SIZE_CONFIG; 26 | import static org.apache.kafka.clients.producer.ProducerConfig.COMPRESSION_TYPE_CONFIG; 27 | import static org.apache.kafka.clients.producer.ProducerConfig.LINGER_MS_CONFIG; 28 | import static org.apache.kafka.clients.producer.ProducerConfig.MAX_REQUEST_SIZE_CONFIG; 29 | import static org.apache.kafka.clients.producer.ProducerConfig.RETRIES_CONFIG; 30 | import static org.apache.kafka.clients.producer.ProducerConfig.RETRY_BACKOFF_MS_CONFIG; 31 | 32 | /** 33 | * Manages a pool of Kafka {@link Producer} instances. 34 | * 35 | *

36 | * This class is thread safe. 37 | *

38 | * 39 | * @param 40 | * Producer key type 41 | * @param 42 | * Producer message type 43 | * 44 | * @author A. Olson 45 | */ 46 | public class KafkaProducerPool implements Closeable { 47 | 48 | private static final Logger LOGGER = LoggerFactory.getLogger(KafkaProducerPool.class); 49 | 50 | /** 51 | * The set of default producer properties 52 | */ 53 | private static final Properties DEFAULT_PRODUCER_PROPERTIES; 54 | 55 | static { 56 | // Setup default producer properties. 57 | DEFAULT_PRODUCER_PROPERTIES = new Properties(); 58 | DEFAULT_PRODUCER_PROPERTIES.setProperty(COMPRESSION_TYPE_CONFIG, CompressionType.LZ4.name); 59 | 60 | // Require acknowledgment from all replicas 61 | DEFAULT_PRODUCER_PROPERTIES.setProperty(ACKS_CONFIG, String.valueOf(-1)); 62 | 63 | // Slightly more conservative retry policy (Kafka default is Integer.MAX_VALUE retries, 100 milliseconds apart, 64 | // up to the delivery.timeout.ms value). 65 | DEFAULT_PRODUCER_PROPERTIES.setProperty(RETRY_BACKOFF_MS_CONFIG, String.valueOf(1000)); 66 | DEFAULT_PRODUCER_PROPERTIES.setProperty(RETRIES_CONFIG, String.valueOf(5)); 67 | 68 | // For better performance increase batch size default to 10MB and linger time to 50 milliseconds 69 | DEFAULT_PRODUCER_PROPERTIES.setProperty(BATCH_SIZE_CONFIG, String.valueOf(10 * 1024 * 1024)); 70 | DEFAULT_PRODUCER_PROPERTIES.setProperty(LINGER_MS_CONFIG, String.valueOf(50)); 71 | 72 | // For better performance, increase the request size default to 100MB. The max message size will be the 73 | // minimum among this and the message.max.bytes set on the brokers. 74 | DEFAULT_PRODUCER_PROPERTIES.setProperty(MAX_REQUEST_SIZE_CONFIG, String.valueOf(100 * 1024 * 1024)); 75 | } 76 | 77 | /** 78 | * KafkaProducerPool concurrency setting. 79 | */ 80 | public static final String KAFKA_PRODUCER_CONCURRENCY = "kafka.pool.concurrency"; 81 | 82 | /** 83 | * The default value for {@link #KAFKA_PRODUCER_CONCURRENCY}. 84 | */ 85 | public static final int DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT = 4; 86 | 87 | /** 88 | * The default value for {@link #KAFKA_PRODUCER_CONCURRENCY}. 89 | */ 90 | public static final String DEFAULT_KAFKA_PRODUCER_CONCURRENCY = String.valueOf(DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT); 91 | 92 | /** 93 | * Pool of producers. A separate group of producers is maintained for each unique configuration. 94 | */ 95 | private final Map>> pool; 96 | 97 | /** 98 | * Read/write lock for the pool's thread safety. 99 | */ 100 | private final ReadWriteLock lock; 101 | 102 | /** 103 | * Read lock for the pool. 104 | */ 105 | private final Lock readLock; 106 | 107 | /** 108 | * Write lock for the pool. 109 | */ 110 | private final Lock writeLock; 111 | 112 | /** 113 | * Round-robin retrieval counter. For the sake of simplicity, a single rotator value is used although there may actually be 114 | * multiple producer arrays managed by the pool. 115 | */ 116 | private final AtomicInteger producerRotation; 117 | 118 | /** 119 | * Pool shutdown indicator. 120 | */ 121 | private boolean shutdown; 122 | 123 | /** 124 | * Creates a new producer pool. 125 | */ 126 | public KafkaProducerPool() { 127 | this.pool = new HashMap<>(); 128 | this.lock = new ReentrantReadWriteLock(true); 129 | this.readLock = lock.readLock(); 130 | this.writeLock = lock.writeLock(); 131 | this.producerRotation = new AtomicInteger(0); 132 | this.shutdown = false; 133 | } 134 | 135 | /** 136 | * Retrieves a {@link Producer} instance corresponding to the given {@link Properties}. 137 | * 138 | *

139 | * The following properties are set by default, and can be overridden by setting a different value in the passed {@code 140 | * properties}: 141 | * 142 | *

143 |      *       key          |            value
144 |      * ================================================================
145 |      * compression.type   | lz4
146 |      * acks               | -1
147 |      * retry.backoff.ms   | 1000 ms
148 |      * retries            | 5
149 |      * batch.size         | 10485760 bytes
150 |      * linger.ms          | 50 ms
151 |      * max.request.size   | 104857600 bytes
152 |      * 
153 | * 154 | *

155 | * The {@link #KAFKA_PRODUCER_CONCURRENCY} property can be set to control the number of producers created 156 | * for each unique configuration. 157 | * 158 | *

159 | * NOTE: the returned producer must not be {@link Producer#close() closed}. Use the {@link #close()} method instead to 160 | * close all producers in the pool. 161 | * 162 | * @param properties 163 | * the properties for the producer 164 | * @return a Kafka message producer 165 | * @throws org.apache.kafka.common.KafkaException 166 | * if an error occurs creating the producer 167 | * @throws IllegalArgumentException 168 | * if properties is {@code null} 169 | * @throws IllegalStateException 170 | * if the pool has already been {@link #close() closed} 171 | * @throws NumberFormatException 172 | * if the value of the {@link #KAFKA_PRODUCER_CONCURRENCY} property cannot be parsed as an 173 | * integer. 174 | */ 175 | public Producer getProducer(Properties properties) { 176 | if (properties == null) { 177 | throw new IllegalArgumentException("properties cannot be null"); 178 | } 179 | 180 | // Include all default producer properties first, then add in the passed properties 181 | Properties producerProperties = new Properties(); 182 | producerProperties.putAll(DEFAULT_PRODUCER_PROPERTIES); 183 | producerProperties.putAll(properties); 184 | 185 | List> producers = null; 186 | readLock.lock(); 187 | try { 188 | if (shutdown) { 189 | throw new IllegalStateException("pool has already been shutdown"); 190 | } 191 | producers = pool.get(producerProperties); 192 | } finally { 193 | readLock.unlock(); 194 | } 195 | 196 | if (producers == null) { 197 | writeLock.lock(); 198 | try { 199 | // Check shutdown status again, in case someone else has already shutdown the pool. 200 | if (shutdown) { 201 | throw new IllegalStateException("pool has already been shutdown"); 202 | } 203 | 204 | // Check for existence again, in case someone else beat us here. 205 | if (pool.containsKey(producerProperties)) { 206 | producers = pool.get(producerProperties); 207 | } else { 208 | int producerConcurrency = getProducerConcurrency(producerProperties); 209 | 210 | // clone the properties in case creating the Producer mutates them (like happens 211 | // when using opentelemtry-agent) which breaks the cache key 212 | Properties originalProperties = (Properties) producerProperties.clone(); 213 | 214 | // Create a new group of producers. 215 | producers = new ArrayList<>(producerConcurrency); 216 | for (int i = 0; i < producerConcurrency; ++i) { 217 | producers.add(createProducer(producerProperties)); 218 | } 219 | 220 | pool.put(originalProperties, producers); 221 | } 222 | } finally { 223 | writeLock.unlock(); 224 | } 225 | } 226 | 227 | // Return the next producer in the rotation. Make sure we correctly handle max int overflow 228 | // if invoked that many times in a long running process. 229 | return producers.get(Math.abs(getProducerRotation()) % producers.size()); 230 | } 231 | 232 | // Visible for testing 233 | Producer createProducer(Properties properties) { 234 | return new KafkaProducer<>(properties); 235 | } 236 | 237 | // Visible for testing 238 | int getProducerRotation() { 239 | return producerRotation.getAndIncrement(); 240 | } 241 | 242 | /** 243 | * Retrieves the {@link #KAFKA_PRODUCER_CONCURRENCY} from the given {@link Properties} 244 | * 245 | * @param props 246 | * the {@link Properties} used to configure the producer 247 | * @return the {@link #KAFKA_PRODUCER_CONCURRENCY} from the given {@link Properties} 248 | */ 249 | private static int getProducerConcurrency(Properties props) { 250 | String producerConcurrencyProperty = props.getProperty(KAFKA_PRODUCER_CONCURRENCY, 251 | DEFAULT_KAFKA_PRODUCER_CONCURRENCY); 252 | 253 | int producerConcurrency = DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT; 254 | 255 | try { 256 | producerConcurrency = Integer.parseInt(producerConcurrencyProperty); 257 | } catch (NumberFormatException e) { 258 | if (LOGGER.isWarnEnabled()) 259 | LOGGER.warn("Unable to parse [{}] config from value [{}]. Using default [{}] instead.", 260 | KAFKA_PRODUCER_CONCURRENCY, producerConcurrencyProperty, 261 | DEFAULT_KAFKA_PRODUCER_CONCURRENCY, e); 262 | } 263 | 264 | // Verify producer concurrency is valid 265 | if (producerConcurrency <= 0) { 266 | if (LOGGER.isWarnEnabled()) 267 | LOGGER.warn("The value for config [{}] was <= 0 [{}]. Using default [{}] instead.", 268 | KAFKA_PRODUCER_CONCURRENCY, producerConcurrency, 269 | DEFAULT_KAFKA_PRODUCER_CONCURRENCY); 270 | 271 | producerConcurrency = DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT; 272 | } 273 | 274 | return producerConcurrency; 275 | } 276 | 277 | /** 278 | * Closes all {@link Producer producers} that have been produced by this pool. 279 | *

280 | * Any subsequent invocation of this method is ignored. 281 | *

282 | * 283 | * @throws IOException 284 | * if an error occurs during producer closure. 285 | * 286 | * {@inheritDoc} 287 | */ 288 | @Override 289 | public void close() throws IOException { 290 | writeLock.lock(); 291 | try { 292 | if (!shutdown) { 293 | shutdown = true; 294 | final Stream exceptions = pool.values().stream() 295 | .flatMap(Collection::stream) 296 | .flatMap(producer -> { 297 | try { 298 | producer.close(); 299 | } catch (Exception e) { 300 | LOGGER.error("Could not close producer", e); 301 | return Stream.of(e); 302 | } 303 | return Stream.empty(); 304 | }); 305 | 306 | // Throw exception if any of the producers in the pool could not be closed. 307 | final Optional exception = exceptions.findFirst(); 308 | if (exception.isPresent()) { 309 | throw new IOException(exception.get()); 310 | } 311 | } 312 | } finally { 313 | writeLock.unlock(); 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/producer/KafkaProducerWrapper.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.producer; 2 | 3 | import com.cerner.common.kafka.KafkaExecutionException; 4 | import com.yammer.metrics.Metrics; 5 | import com.yammer.metrics.core.Histogram; 6 | import com.yammer.metrics.core.Timer; 7 | import com.yammer.metrics.core.TimerContext; 8 | import org.apache.kafka.clients.producer.Producer; 9 | import org.apache.kafka.clients.producer.ProducerRecord; 10 | import org.apache.kafka.clients.producer.RecordMetadata; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.io.Closeable; 15 | import java.io.Flushable; 16 | import java.io.IOException; 17 | import java.util.Collections; 18 | import java.util.LinkedList; 19 | import java.util.List; 20 | import java.util.concurrent.ExecutionException; 21 | import java.util.concurrent.Future; 22 | import java.util.stream.Collectors; 23 | 24 | /** 25 | * Wraps around an instance of a {@link Producer producer}. {@link #close() Closing} the wrapper will 26 | * not affect the wrapped producer instance. The creators of the wrapped producer instances are still 27 | * responsible for closing the producer when appropriate. 28 | * 29 | * Supports synchronously sending {@link ProducerRecord records}. 30 | *

31 | * Example usage: 32 | *

 33 |  *     Properties producerProps = new Properties();
 34 |  *     // required properties to construct a KafkaProducer instance
 35 |  *     // constants for these config keys exist in org.apache.kafka.clients.producer.ProducerConfig
 36 |  *     producerProps.put("bootstrap.servers", "localhost:4242");
 37 |  *     producerProps.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
 38 |  *     producerProps.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
 39 |  *
 40 |  *     // properties that affect batching
 41 |  *
 42 |  *     // linger.ms is used to tell the producer to wait this amount of time
 43 |  *     // for more records to fill the buffer before sending. The default value of
 44 |  *     // 0 will tell the producer to start sending immediately and to not wait.
 45 |  *     // {@link #sendSynchronously(List)} will attempt to send the entire batch at once
 46 |  *     // but could be affected by this value if it is not set, or set too low. This could
 47 |  *     // result in more I/O operations taking place than what is desired. It is recommended
 48 |  *     // to set this value appropriately high as to not affect sending in batches.
 49 |  *     producerProps.put("linger.ms", 10000);
 50 |  *
 51 |  *     // batch.size will tell the producer how many records to hold in memory, in bytes,
 52 |  *     // before it should send its buffer. This value will include both the key and the payload.
 53 |  *     // It is recommended to set this value at the value of 'max.message.bytes' * desired batch (in number of records) size
 54 |  *     // to ensure that all records sent to {@link #sendSynchronously(List)} are batched together.
 55 |  *     // NOTE: the setting of 'linger.ms' will supersede this value.
 56 |  *     producerProps.put("batch.size", 16384);
 57 |  *
 58 |  *     KafkaProducerWrapper<String, String> producerWrapper = new KafkaProducerWrapper<>(
 59 |  *                              new KafkaProducer<>(producerProps);
 60 |  *
 61 |  *     String topic = "myTopic";
 62 |  *     String key = "myKey";
 63 |  *     String value = "myValue";
 64 |  *
 65 |  *     ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, value);
 66 |  *
 67 |  *     // synchronously sends the specified record; the underlying KafkaProducer will be flushed
 68 |  *     // immediately and the record will be sent.
 69 |  *     producerWrapper.sendSynchronously(record);
 70 |  *
 71 |  *     // alternatively, a collection of ProducerRecords can be submitted at a time to
 72 |  *     // the sendSynchronous method and it will behave the same way and block until all messages
 73 |  *     // have been submitted. If a KafkaExecutionException is thrown the context as to which
 74 |  *     // message in the collection caused the failure will not be included. See the
 75 |  *     // javadoc for KafkaExecutionException for more information about the potential errors
 76 |  *
 77 |  *     String topic = "mySecondTopic";
 78 |  *     String key = "mySecondKey";
 79 |  *     String value = "mySecondValue";
 80 |  *
 81 |  *     ProducerRecord<String, String> secondRecord = new ProducerRecord<>(topic, key, value);
 82 |  *     List<ProducerRecord<String, String>> records = new ArrayList<>();
 83 |  *     records.add(record);
 84 |  *     records.add(secondRecord);
 85 |  *
 86 |  *     producerWrapper.sendSynchronously(records);
 87 |  *
 88 |  *     // when the producerWrapper is done being used call #close(), which will close
 89 |  *     // the underlying KafkaProducer. NOTE: be aware of how the wrapper instance is being
 90 |  *     // managed, as closing it may cause issues if the instance is being used
 91 |  *     // across multiple threads.
 92 |  *     producerWrapper.close();
 93 |  * 
94 | * 95 | * The wrapper also supports the idea of sending incrementally instead of as a large collection. The synchronous 96 | * sending behavior can still be supported but feedback about a failure will not be provided until the wrapper has 97 | * been {@link #flush() flushed}. Similar to the description above the "linger.ms" and "batch.size" should be 98 | * configured appropriately for your desired throughput and latency. 99 | * 100 | *
101 |  *      KafkaProducerWrapper<String, String> producerWrapper = new KafkaProducerWrapper<>(
102 |  *                              new KafkaProducer<>(producerProps);
103 |  *
104 |  *      producerWrapper.send(new ProducerRecord<>(topic, key1, value1));
105 |  *      producerWrapper.send(new ProducerRecord<>(topic, key2, value2));
106 |  *      producerWrapper.send(new ProducerRecord<>(topic, key3, value3));
107 |  *
108 |  *      //will block and return only when all sent events have been successfully flushed or an error has occurred.
109 |  *      try{}
110 |  *          producerWrapper.flush();
111 |  *      }catch(IOException ioe){
112 |  *          //an error occurred and all pending sent messages should be expected to be resent or handled
113 |  *          //appropriately.
114 |  *      }finally{
115 |  *          producerWrapper.close();
116 |  *      }
117 |  *
118 |  * 
119 | * 120 | * This class is not thread safe. 121 | * 122 | * @author Stephen Durfey 123 | */ 124 | public class KafkaProducerWrapper implements Closeable, Flushable { 125 | 126 | private static final Logger LOGGER = LoggerFactory.getLogger(KafkaProducerWrapper.class); 127 | 128 | private final Producer kafkaProducer; 129 | 130 | private final List> pendingWrites; 131 | 132 | /** 133 | * A timer for {@link #send(ProducerRecord)} calls 134 | */ 135 | static final Timer SEND_TIMER = Metrics.newTimer(KafkaProducerWrapper.class, "send"); 136 | 137 | /** 138 | * A timer for {@link #sendSynchronously(ProducerRecord)} or {@link #sendSynchronously(List)} calls 139 | */ 140 | static final Timer SYNC_SEND_TIMER = Metrics.newTimer(KafkaProducerWrapper.class, "send-synchronously"); 141 | 142 | /** 143 | * A timer for {@link #flush()} calls 144 | */ 145 | static final Timer FLUSH_TIMER = Metrics.newTimer(KafkaProducerWrapper.class, "flush"); 146 | 147 | /** 148 | * A histogram tracking batch size, updated by {@link #sendSynchronously(List)} or {@link #flush()} 149 | */ 150 | static final Histogram BATCH_SIZE_HISTOGRAM = Metrics.newHistogram(KafkaProducerWrapper.class, "batch-size", true); 151 | 152 | /** 153 | * Creates an instance to manage interacting with the {@code kafkaProducer}. 154 | * 155 | * @param kafkaProducer 156 | * the {@link Producer} to interact with 157 | */ 158 | public KafkaProducerWrapper(Producer kafkaProducer) { 159 | if (kafkaProducer == null) { 160 | throw new IllegalStateException("kafkaProducer cannot be null"); 161 | } 162 | 163 | this.kafkaProducer = kafkaProducer; 164 | pendingWrites = new LinkedList<>(); 165 | } 166 | 167 | /** 168 | * Sends the specified {@code record}. Data is not guaranteed to be written until {@link #flush()} is called. 169 | * 170 | * @param record the record to send to Kafka 171 | * @throws IllegalArgumentException the {@code record} cannot be {@code null}. 172 | * @throws org.apache.kafka.common.KafkaException if there is an issue sending the message to Kafka 173 | */ 174 | public void send(ProducerRecord record){ 175 | if(record == null){ 176 | throw new IllegalArgumentException("The 'record' cannot be 'null'."); 177 | } 178 | 179 | TimerContext context = SEND_TIMER.time(); 180 | try { 181 | pendingWrites.add(kafkaProducer.send(record)); 182 | } finally { 183 | context.stop(); 184 | } 185 | } 186 | 187 | /** 188 | * Synchronously sends the {@code record}. The underlying {@link Producer} is immediately flushed and the call will block until 189 | * the {@code record} is sent. 190 | * 191 | * @param record 192 | * the records to send to Kafka 193 | * @throws IOException 194 | * indicates that there was a Kafka error that led to the message not being sent. 195 | * @throws org.apache.kafka.common.KafkaException 196 | * if there is an issue sending the message to Kafka 197 | */ 198 | public void sendSynchronously(ProducerRecord record) throws IOException { 199 | sendSynchronously(Collections.singletonList(record)); 200 | } 201 | 202 | /** 203 | * Synchronously sends all {@code records}. The underlying {@link Producer} is immediately flushed and the call will block until 204 | * the {@code records} have been sent. 205 | * 206 | * @param records 207 | * the records to send to Kafka 208 | * @throws IOException 209 | * indicates that there was a Kafka error that lead to one of the messages not being sent. 210 | * @throws org.apache.kafka.common.KafkaException 211 | * if there is an issue sending the messages to Kafka 212 | */ 213 | public void sendSynchronously(List> records) throws IOException { 214 | // Disregard empty batches. 215 | if (records.isEmpty()) { 216 | LOGGER.debug("records was empty; nothing to process"); 217 | return; 218 | } 219 | 220 | BATCH_SIZE_HISTOGRAM.update(records.size()); 221 | 222 | TimerContext context = SYNC_SEND_TIMER.time(); 223 | try { 224 | List> futures = records.stream().map(kafkaProducer::send).collect(Collectors.toList()); 225 | 226 | handlePendingWrites(futures); 227 | } finally { 228 | context.stop(); 229 | } 230 | } 231 | 232 | /** 233 | * Flushes the underlying pending writes created by calls to {@link #send(ProducerRecord)} 234 | * to the {@link Producer} ensuring they persisted. 235 | * 236 | * If an exception occurs when flushing, all pending writes should be 237 | * {@link #send(ProducerRecord) sent again}. 238 | * 239 | * @throws IOException If there was an error persisting one of the pending writes 240 | */ 241 | @Override 242 | public void flush() throws IOException{ 243 | if (pendingWrites.isEmpty()) { 244 | LOGGER.debug("nothing to flush"); 245 | return; 246 | } 247 | 248 | BATCH_SIZE_HISTOGRAM.update(pendingWrites.size()); 249 | 250 | TimerContext context = FLUSH_TIMER.time(); 251 | try { 252 | handlePendingWrites(pendingWrites); 253 | } finally{ 254 | pendingWrites.clear(); 255 | context.stop(); 256 | } 257 | } 258 | 259 | private void handlePendingWrites(List> pendingWrites) throws IOException{ 260 | // Future#get will sit and wait until 'linger.ms' has been reached 261 | // or flush is called, so flush here. 262 | try { 263 | kafkaProducer.flush(); 264 | }catch(RuntimeException re){ 265 | //Exception isn't retriable so wrap in known exception which represents fatal. 266 | throw new IOException("Unable to flush producer.",re); 267 | } 268 | for (final Future future : pendingWrites) { 269 | try { 270 | future.get(); 271 | } catch (InterruptedException e) { 272 | throw new IOException(e); 273 | } catch (ExecutionException e) { 274 | throw new KafkaExecutionException(e); 275 | } 276 | } 277 | } 278 | 279 | @Override 280 | public void close() throws IOException { 281 | LOGGER.debug("Closing the producer wrapper and flushing outstanding writes."); 282 | flush(); 283 | } 284 | } -------------------------------------------------------------------------------- /common-kafka/src/main/java/com/cerner/common/kafka/producer/partitioners/FairPartitioner.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.producer.partitioners; 2 | 3 | import org.apache.kafka.clients.producer.Partitioner; 4 | import org.apache.kafka.common.Cluster; 5 | import org.apache.kafka.common.PartitionInfo; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Random; 12 | 13 | /** 14 | * A basic default {@link Partitioner} implementation that uses a combination of {@link #getTimeHash() time-based} and 15 | * {@link #getKeyHash(Object) key-based} hashing to determine the target partition for a produced message. Additionally, 16 | * {@link Cluster#availablePartitionsForTopic(String) available} partitions are always preferred over non-available partitions. 17 | * 18 | * @author A. Olson 19 | */ 20 | public class FairPartitioner implements Partitioner { 21 | 22 | /** 23 | * Logger. 24 | */ 25 | private static final Logger LOG = LoggerFactory.getLogger(FairPartitioner.class); 26 | 27 | /** 28 | * Number of milliseconds between rotations of partition assignments. 29 | */ 30 | public static final int ROTATE_MILLIS = 250; 31 | 32 | /** 33 | * Randomized clock skew to distribute load when there are multiple concurrent writers. 34 | */ 35 | public static final int ROTATE_OFFSET = ROTATE_MILLIS * new Random().nextInt(Short.MAX_VALUE); 36 | 37 | @Override 38 | public void configure(Map configs) { 39 | //no-op because not configuring anything 40 | } 41 | 42 | /** 43 | * Returns a hash value roughly based on the current time. By default the returned value is incremented every 44 | * {@link #ROTATE_MILLIS} milliseconds, so that all messages produced in a single batch are very likely to be written 45 | * to the same partition. 46 | * 47 | * @return the time hash 48 | */ 49 | protected int getTimeHash() { 50 | // Return a temporarily sticky value. 51 | return (int) ((System.currentTimeMillis() + ROTATE_OFFSET) / ROTATE_MILLIS); 52 | } 53 | 54 | /** 55 | * Returns a hash value based on the message key. By default the return value is 0 so that only {@link #getTimeHash()} is 56 | * used to determine which partition the message is written to. 57 | * 58 | * @param key 59 | * the message key 60 | * @return the message key hash 61 | */ 62 | protected int getKeyHash(@SuppressWarnings("unused") Object key) { 63 | return 0; 64 | } 65 | 66 | @Override 67 | public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { 68 | // Combine time and key hashes, then map to a partition. 69 | return getPartition(topic, cluster, getTimeHash() + getKeyHash(key)); 70 | } 71 | 72 | @Override 73 | public void close() { 74 | //no-op because not maintaining resources 75 | } 76 | 77 | private static int getPartition(String topic, Cluster cluster, int hash) { 78 | List partitions = cluster.availablePartitionsForTopic(topic); 79 | if (partitions.isEmpty()) { 80 | LOG.warn("No available partitions for {} therefore using total partition count for calculations.", topic); 81 | partitions = cluster.partitionsForTopic(topic); 82 | } 83 | 84 | int index = Math.abs(hash) % partitions.size(); 85 | return partitions.get(index).partition(); 86 | } 87 | } -------------------------------------------------------------------------------- /common-kafka/src/test/java/com/cerner/common/kafka/KafkaTests.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka; 2 | 3 | import com.cerner.common.kafka.consumer.ProcessingKafkaConsumerITest; 4 | import com.cerner.common.kafka.producer.KafkaProducerPoolTest; 5 | import com.cerner.common.kafka.producer.KafkaProducerWrapperTest; 6 | import com.cerner.common.kafka.testing.AbstractKafkaTests; 7 | import org.junit.platform.suite.api.SelectClasses; 8 | import org.junit.platform.suite.api.Suite; 9 | 10 | /** 11 | * Suite of tests requiring an internal Kafka + Zookeeper cluster to be started up. 12 | * 13 | * IMPORTANT: New tests added to this project requiring Kafka and/or Zookeeper should be added to this suite so they will be 14 | * executed as part of the build. 15 | */ 16 | @Suite 17 | @SelectClasses({ 18 | // com.cerner.common.kafka.consumer 19 | ProcessingKafkaConsumerITest.class, 20 | 21 | // com.cerner.common.kafka.producer 22 | KafkaProducerPoolTest.class, KafkaProducerWrapperTest.class 23 | }) 24 | public class KafkaTests extends AbstractKafkaTests { 25 | } 26 | -------------------------------------------------------------------------------- /common-kafka/src/test/java/com/cerner/common/kafka/StandaloneTests.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka; 2 | 3 | import com.cerner.common.kafka.consumer.ConsumerOffsetClientTest; 4 | import com.cerner.common.kafka.consumer.ProcessingConfigTest; 5 | import com.cerner.common.kafka.consumer.ProcessingKafkaConsumerTest; 6 | import com.cerner.common.kafka.consumer.ProcessingPartitionTest; 7 | import com.cerner.common.kafka.consumer.assignors.FairAssignorTest; 8 | import com.cerner.common.kafka.producer.partitioners.FairPartitionerTest; 9 | import org.junit.platform.suite.api.SelectClasses; 10 | import org.junit.platform.suite.api.Suite; 11 | 12 | /** 13 | * Suite of tests that can be run independently. 14 | * 15 | * IMPORTANT: New tests added to this project not requiring cluster services should be added to this suite so they will be 16 | * executed as part of the build. 17 | */ 18 | @Suite 19 | @SelectClasses({ 20 | // com.cerner.common.kafka.consumer 21 | ConsumerOffsetClientTest.class, ProcessingConfigTest.class, ProcessingKafkaConsumerTest.class, 22 | ProcessingPartitionTest.class, 23 | 24 | // com.cerner.common.kafka.consumer.assignors 25 | FairAssignorTest.class, 26 | 27 | // com.cerner.common.kafka.producer.partitioners 28 | FairPartitionerTest.class 29 | }) 30 | public class StandaloneTests { 31 | } -------------------------------------------------------------------------------- /common-kafka/src/test/java/com/cerner/common/kafka/consumer/ProcessingConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.consumer; 2 | 3 | import static org.hamcrest.core.Is.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | import java.util.Map; 8 | import java.util.Properties; 9 | 10 | import org.apache.kafka.clients.consumer.ConsumerConfig; 11 | import org.apache.kafka.clients.consumer.OffsetResetStrategy; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | 15 | public class ProcessingConfigTest { 16 | 17 | private Properties properties; 18 | private ProcessingConfig config; 19 | 20 | @BeforeEach 21 | public void before() { 22 | properties = new Properties(); 23 | properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, OffsetResetStrategy.EARLIEST.toString().toLowerCase()); 24 | config = new ProcessingConfig(properties); 25 | } 26 | 27 | @Test 28 | public void constructor_nullProperties() { 29 | assertThrows(IllegalArgumentException.class, 30 | () -> new ProcessingConfig(null)); 31 | } 32 | 33 | @Test 34 | public void constructor_offsetResetStrategyNotProvided() { 35 | properties.remove(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG); 36 | assertThrows(IllegalArgumentException.class, 37 | () -> new ProcessingConfig(properties)); 38 | } 39 | 40 | @Test 41 | public void constructor_offsetResetStrategySetToNone() { 42 | properties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "none"); 43 | assertThrows(IllegalArgumentException.class, 44 | () -> new ProcessingConfig(properties)); 45 | } 46 | 47 | @Test 48 | public void constructor_commitInitialOffsetNotBoolean() { 49 | properties.setProperty(ProcessingConfig.COMMIT_INITIAL_OFFSET_PROPERTY, "not_boolean"); 50 | assertThrows(IllegalArgumentException.class, 51 | () -> new ProcessingConfig(properties)); 52 | } 53 | 54 | @Test 55 | public void constructor_commitSizeEqualsZero() { 56 | properties.setProperty(ProcessingConfig.COMMIT_SIZE_THRESHOLD_PROPERTY, "0"); 57 | assertThrows(IllegalArgumentException.class, 58 | () -> new ProcessingConfig(properties)); 59 | } 60 | 61 | @Test 62 | public void constructor_commitSizeLessThanZero() { 63 | properties.setProperty(ProcessingConfig.COMMIT_SIZE_THRESHOLD_PROPERTY, "-1"); 64 | assertThrows(IllegalArgumentException.class, 65 | () -> new ProcessingConfig(properties)); 66 | } 67 | 68 | @Test 69 | public void constructor_commitSizeNotANumber() { 70 | properties.setProperty(ProcessingConfig.COMMIT_SIZE_THRESHOLD_PROPERTY, "notANumber"); 71 | assertThrows(IllegalArgumentException.class, 72 | () -> new ProcessingConfig(properties)); 73 | } 74 | 75 | @Test 76 | public void constructor_commitTimeLessThanZero() { 77 | properties.setProperty(ProcessingConfig.COMMIT_TIME_THRESHOLD_PROPERTY, "-1"); 78 | assertThrows(IllegalArgumentException.class, 79 | () -> new ProcessingConfig(properties)); 80 | } 81 | 82 | @Test 83 | public void constructor_commitTimeNotANumber() { 84 | properties.setProperty(ProcessingConfig.COMMIT_TIME_THRESHOLD_PROPERTY, "notANumber"); 85 | assertThrows(IllegalArgumentException.class, 86 | () -> new ProcessingConfig(properties)); 87 | } 88 | 89 | @Test 90 | public void constructor_pauseThresholdLessThanZero() { 91 | properties.setProperty(ProcessingConfig.FAIL_THRESHOLD_PROPERTY, "-1"); 92 | assertThrows(IllegalArgumentException.class, 93 | () -> new ProcessingConfig(properties)); 94 | } 95 | 96 | @Test 97 | public void constructor_pauseThresholdGreaterThanOne() { 98 | properties.setProperty(ProcessingConfig.FAIL_THRESHOLD_PROPERTY, "2"); 99 | assertThrows(IllegalArgumentException.class, 100 | () -> new ProcessingConfig(properties)); 101 | } 102 | 103 | @Test 104 | public void constructor_pauseThresholdEqualsZero() { 105 | properties.setProperty(ProcessingConfig.FAIL_THRESHOLD_PROPERTY, "0"); 106 | config = new ProcessingConfig(properties); 107 | assertThat(config.getFailThreshold(), is(0.0)); 108 | } 109 | 110 | @Test 111 | public void constructor_pauseThresholdEqualsOne() { 112 | properties.setProperty(ProcessingConfig.FAIL_THRESHOLD_PROPERTY, "1"); 113 | config = new ProcessingConfig(properties); 114 | assertThat(config.getFailThreshold(), is(1.0)); 115 | } 116 | 117 | @Test 118 | public void constructor_pauseThresholdNotANumber() { 119 | properties.setProperty(ProcessingConfig.FAIL_THRESHOLD_PROPERTY, "notANumber"); 120 | assertThrows(IllegalArgumentException.class, 121 | () -> new ProcessingConfig(properties)); 122 | } 123 | 124 | @Test 125 | public void constructor_pauseSampleSizeEqualsZero() { 126 | properties.setProperty(ProcessingConfig.FAIL_SAMPLE_SIZE_PROPERTY, "0"); 127 | assertThrows(IllegalArgumentException.class, 128 | () -> new ProcessingConfig(properties)); 129 | } 130 | 131 | @Test 132 | public void constructor_pauseSampleSizeLessThanZero() { 133 | properties.setProperty(ProcessingConfig.FAIL_SAMPLE_SIZE_PROPERTY, "-1"); 134 | assertThrows(IllegalArgumentException.class, 135 | () -> new ProcessingConfig(properties)); 136 | } 137 | 138 | @Test 139 | public void constructor_pauseSampleSizeNotANumber() { 140 | properties.setProperty(ProcessingConfig.FAIL_SAMPLE_SIZE_PROPERTY, "notANumber"); 141 | assertThrows(IllegalArgumentException.class, 142 | () -> new ProcessingConfig(properties)); 143 | } 144 | 145 | @Test 146 | public void constructor_pauseTimeLessThanZero() { 147 | properties.setProperty(ProcessingConfig.FAIL_PAUSE_TIME_PROPERTY, "-1"); 148 | assertThrows(IllegalArgumentException.class, 149 | () -> new ProcessingConfig(properties)); 150 | } 151 | 152 | @Test 153 | public void constructor_pauseTimeNotANumber() { 154 | properties.setProperty(ProcessingConfig.FAIL_PAUSE_TIME_PROPERTY, "notANumber"); 155 | assertThrows(IllegalArgumentException.class, 156 | () -> new ProcessingConfig(properties)); 157 | } 158 | 159 | @Test 160 | public void constructor_maxPollIntervalNotANumber() { 161 | properties.setProperty(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "notANumber"); 162 | assertThrows(IllegalArgumentException.class, 163 | () -> new ProcessingConfig(properties)); 164 | } 165 | 166 | @Test 167 | public void constructor_defaults() { 168 | assertThat(config.getCommitInitialOffset(), is(true)); 169 | assertThat(config.getCommitSizeThreshold(), is(Long.parseLong(ProcessingConfig.COMMIT_SIZE_THRESHOLD_DEFAULT))); 170 | assertThat(config.getCommitTimeThreshold(), is(Long.parseLong(ProcessingConfig.COMMIT_TIME_THRESHOLD_DEFAULT))); 171 | assertThat(config.getFailPauseTime(), is(Long.parseLong(ProcessingConfig.FAIL_PAUSE_TIME_DEFAULT))); 172 | assertThat(config.getFailSampleSize(), is(Integer.parseInt(ProcessingConfig.FAIL_SAMPLE_SIZE_DEFAULT))); 173 | assertThat(config.getFailThreshold(), is(Double.parseDouble(ProcessingConfig.FAIL_THRESHOLD_DEFAULT))); 174 | assertThat(config.getOffsetResetStrategy(), is(OffsetResetStrategy.EARLIEST)); 175 | assertThat(config.getMaxPollInterval(), is(300000L)); 176 | 177 | // config properties should at least contain everything from properties 178 | for(Map.Entry entry : properties.entrySet()) { 179 | assertThat(config.getProperties().get(entry.getKey()), is(entry.getValue())); 180 | } 181 | 182 | assertThat(config.getProperties().getProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG), is(Boolean.FALSE.toString())); 183 | } 184 | 185 | @Test 186 | public void constructor_customProperties() { 187 | properties.setProperty(ProcessingConfig.COMMIT_INITIAL_OFFSET_PROPERTY, String.valueOf(false)); 188 | properties.setProperty(ProcessingConfig.COMMIT_SIZE_THRESHOLD_PROPERTY, "123"); 189 | properties.setProperty(ProcessingConfig.COMMIT_TIME_THRESHOLD_PROPERTY, "234"); 190 | properties.setProperty(ProcessingConfig.FAIL_PAUSE_TIME_PROPERTY, "456"); 191 | properties.setProperty(ProcessingConfig.FAIL_SAMPLE_SIZE_PROPERTY, "567"); 192 | properties.setProperty(ProcessingConfig.FAIL_THRESHOLD_PROPERTY, "0.1"); 193 | properties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, OffsetResetStrategy.LATEST.toString().toLowerCase()); 194 | properties.setProperty(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "10000"); 195 | 196 | config = new ProcessingConfig(properties); 197 | 198 | assertThat(config.getCommitInitialOffset(), is(false)); 199 | assertThat(config.getCommitSizeThreshold(), is(123L)); 200 | assertThat(config.getCommitTimeThreshold(), is(234L)); 201 | assertThat(config.getFailPauseTime(), is(456L)); 202 | assertThat(config.getFailSampleSize(), is(567)); 203 | assertThat(config.getFailThreshold(), is(0.1)); 204 | assertThat(config.getOffsetResetStrategy(), is(OffsetResetStrategy.LATEST)); 205 | assertThat(config.getMaxPollInterval(), is(10000L)); 206 | 207 | // config properties should at least contain everything from properties 208 | for(Map.Entry entry : properties.entrySet()) { 209 | assertThat(config.getProperties().get(entry.getKey()), is(entry.getValue())); 210 | } 211 | 212 | assertThat(config.getProperties().getProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG), is(Boolean.FALSE.toString())); 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /common-kafka/src/test/java/com/cerner/common/kafka/consumer/assignors/FairAssignorTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.consumer.assignors; 2 | 3 | import org.apache.kafka.clients.consumer.ConsumerPartitionAssignor; 4 | import org.apache.kafka.common.TopicPartition; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.core.Is.is; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | 17 | 18 | public class FairAssignorTest { 19 | 20 | private FairAssignor assignor = new FairAssignor(); 21 | 22 | @Test 23 | public void testOneConsumerNoTopic() { 24 | String consumerId = "consumer"; 25 | 26 | Map partitionsPerTopic = new HashMap<>(); 27 | 28 | Map> assignment = assignor.assign(partitionsPerTopic, 29 | Collections.singletonMap(consumerId, new ConsumerPartitionAssignor.Subscription(Collections.emptyList()))); 30 | assertEquals(Collections.singleton(consumerId), assignment.keySet()); 31 | assertThat(assignment.get(consumerId).isEmpty(), is(true)); 32 | } 33 | 34 | @Test 35 | public void testOneConsumerNonexistentTopic() { 36 | String topic = "topic"; 37 | String consumerId = "consumer"; 38 | 39 | Map partitionsPerTopic = new HashMap<>(); 40 | partitionsPerTopic.put(topic, 0); 41 | 42 | Map> assignment = assignor.assign(partitionsPerTopic, 43 | Collections.singletonMap(consumerId, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic)))); 44 | 45 | assertEquals(Collections.singleton(consumerId), assignment.keySet()); 46 | assertThat(assignment.get(consumerId).isEmpty(), is(true)); 47 | } 48 | 49 | @Test 50 | public void testOneConsumerOneTopic() { 51 | String topic = "topic"; 52 | String consumerId = "consumer"; 53 | 54 | Map partitionsPerTopic = new HashMap<>(); 55 | partitionsPerTopic.put(topic, 3); 56 | 57 | Map> assignment = assignor.assign(partitionsPerTopic, 58 | Collections.singletonMap(consumerId, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic)))); 59 | assertEquals(Arrays.asList( 60 | new TopicPartition(topic, 0), 61 | new TopicPartition(topic, 1), 62 | new TopicPartition(topic, 2)), assignment.get(consumerId)); 63 | } 64 | 65 | @Test 66 | public void testOnlyAssignsPartitionsFromSubscribedTopics() { 67 | String topic = "topic"; 68 | String otherTopic = "other"; 69 | String consumerId = "consumer"; 70 | 71 | Map partitionsPerTopic = new HashMap<>(); 72 | partitionsPerTopic.put(topic, 3); 73 | partitionsPerTopic.put(otherTopic, 3); 74 | 75 | Map> assignment = assignor.assign(partitionsPerTopic, 76 | Collections.singletonMap(consumerId, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic)))); 77 | assertEquals(Arrays.asList( 78 | new TopicPartition(topic, 0), 79 | new TopicPartition(topic, 1), 80 | new TopicPartition(topic, 2)), assignment.get(consumerId)); 81 | } 82 | 83 | @Test 84 | public void testSubscriptionIncludesTopicThatDoesNotExist() { 85 | String topic = "topic"; 86 | String topicDoesNotExist = "doesNotExist"; 87 | String consumerId = "consumer"; 88 | 89 | Map partitionsPerTopic = new HashMap<>(); 90 | partitionsPerTopic.put(topic, 1); 91 | 92 | Map> assignment = assignor.assign(partitionsPerTopic, 93 | Collections.singletonMap(consumerId, new ConsumerPartitionAssignor.Subscription(Arrays.asList(topic, topicDoesNotExist)))); 94 | assertEquals(Arrays.asList(new TopicPartition(topic, 0)), assignment.get(consumerId)); 95 | } 96 | 97 | @Test 98 | public void testOneConsumerMultipleTopics() { 99 | String topic1 = "topic1"; 100 | String topic2 = "topic2"; 101 | String consumerId = "consumer"; 102 | 103 | Map partitionsPerTopic = new HashMap<>(); 104 | partitionsPerTopic.put(topic1, 1); 105 | partitionsPerTopic.put(topic2, 2); 106 | 107 | Map> assignment = assignor.assign(partitionsPerTopic, 108 | Collections.singletonMap(consumerId, new ConsumerPartitionAssignor.Subscription(Arrays.asList(topic1, topic2)))); 109 | assertEquals(Arrays.asList( 110 | new TopicPartition(topic2, 0), 111 | new TopicPartition(topic2, 1), 112 | new TopicPartition(topic1, 0)), assignment.get(consumerId)); 113 | } 114 | 115 | @Test 116 | public void testTwoConsumersOneTopicOnePartition() { 117 | String topic = "topic"; 118 | String consumer1 = "consumer1"; 119 | String consumer2 = "consumer2"; 120 | 121 | Map partitionsPerTopic = new HashMap<>(); 122 | partitionsPerTopic.put(topic, 1); 123 | 124 | Map consumers = new HashMap<>(); 125 | consumers.put(consumer1, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic))); 126 | consumers.put(consumer2, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic))); 127 | 128 | Map> assignment = assignor.assign(partitionsPerTopic, consumers); 129 | assertEquals(Arrays.asList(new TopicPartition(topic, 0)), assignment.get(consumer1)); 130 | assertEquals(Collections.emptyList(), assignment.get(consumer2)); 131 | } 132 | 133 | @Test 134 | public void testTwoConsumersOneTopicTwoPartitions() { 135 | String topic = "topic"; 136 | String consumer1 = "consumer1"; 137 | String consumer2 = "consumer2"; 138 | 139 | Map partitionsPerTopic = new HashMap<>(); 140 | partitionsPerTopic.put(topic, 2); 141 | 142 | Map consumers = new HashMap<>(); 143 | consumers.put(consumer1, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic))); 144 | consumers.put(consumer2, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic))); 145 | 146 | Map> assignment = assignor.assign(partitionsPerTopic, consumers); 147 | assertEquals(Arrays.asList(new TopicPartition(topic, 0)), assignment.get(consumer1)); 148 | assertEquals(Arrays.asList(new TopicPartition(topic, 1)), assignment.get(consumer2)); 149 | } 150 | 151 | @Test 152 | public void testMultipleConsumersMixedTopics() { 153 | String topic1 = "topic1"; 154 | String topic2 = "topic2"; 155 | String consumer1 = "consumer1"; 156 | String consumer2 = "consumer2"; 157 | String consumer3 = "consumer3"; 158 | 159 | Map partitionsPerTopic = new HashMap<>(); 160 | partitionsPerTopic.put(topic1, 3); 161 | partitionsPerTopic.put(topic2, 2); 162 | 163 | Map consumers = new HashMap<>(); 164 | consumers.put(consumer1, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic1))); 165 | consumers.put(consumer2, new ConsumerPartitionAssignor.Subscription(Arrays.asList(topic1, topic2))); 166 | consumers.put(consumer3, new ConsumerPartitionAssignor.Subscription(Collections.singletonList(topic1))); 167 | 168 | Map> assignment = assignor.assign(partitionsPerTopic, consumers); 169 | assertEquals(Arrays.asList( 170 | new TopicPartition(topic1, 0), 171 | new TopicPartition(topic1, 2)), assignment.get(consumer1)); 172 | assertEquals(Arrays.asList( 173 | new TopicPartition(topic2, 0), 174 | new TopicPartition(topic2, 1)), assignment.get(consumer2)); 175 | assertEquals(Arrays.asList( 176 | new TopicPartition(topic1, 1)), assignment.get(consumer3)); 177 | } 178 | 179 | @Test 180 | public void testTwoConsumersTwoTopicsSixPartitions() { 181 | String topic1 = "topic1"; 182 | String topic2 = "topic2"; 183 | String consumer1 = "consumer1"; 184 | String consumer2 = "consumer2"; 185 | 186 | Map partitionsPerTopic = new HashMap<>(); 187 | partitionsPerTopic.put(topic1, 3); 188 | partitionsPerTopic.put(topic2, 3); 189 | 190 | Map consumers = new HashMap<>(); 191 | consumers.put(consumer1, new ConsumerPartitionAssignor.Subscription(Arrays.asList(topic1, topic2))); 192 | consumers.put(consumer2, new ConsumerPartitionAssignor.Subscription(Arrays.asList(topic1, topic2))); 193 | 194 | Map> assignment = assignor.assign(partitionsPerTopic, consumers); 195 | assertEquals(Arrays.asList( 196 | new TopicPartition(topic1, 0), 197 | new TopicPartition(topic1, 2), 198 | new TopicPartition(topic2, 1)), assignment.get(consumer1)); 199 | assertEquals(Arrays.asList( 200 | new TopicPartition(topic1, 1), 201 | new TopicPartition(topic2, 0), 202 | new TopicPartition(topic2, 2)), assignment.get(consumer2)); 203 | } 204 | 205 | @Test 206 | public void testMultipleConsumersUnbalancedSubscriptions() { 207 | String topic1 = "topic1"; 208 | String topic2 = "topic2"; 209 | String topic3 = "topic3"; 210 | String topic4 = "topic4"; 211 | String topic5 = "topic5"; 212 | String consumer1 = "consumer1"; 213 | String consumer2 = "consumer2"; 214 | String consumer3 = "consumer3"; 215 | String consumer4 = "consumer4"; 216 | int oddTopicPartitions = 2; 217 | int evenTopicPartitions = 1; 218 | 219 | Map partitionsPerTopic = new HashMap<>(); 220 | partitionsPerTopic.put(topic1, oddTopicPartitions); 221 | partitionsPerTopic.put(topic2, evenTopicPartitions); 222 | partitionsPerTopic.put(topic3, oddTopicPartitions); 223 | partitionsPerTopic.put(topic4, evenTopicPartitions); 224 | partitionsPerTopic.put(topic5, oddTopicPartitions); 225 | 226 | List oddTopics = Arrays.asList(topic1, topic3, topic5); 227 | List allTopics = Arrays.asList(topic1, topic2, topic3, topic4, topic5); 228 | 229 | Map consumers = new HashMap<>(); 230 | consumers.put(consumer1, new ConsumerPartitionAssignor.Subscription(allTopics)); 231 | consumers.put(consumer2, new ConsumerPartitionAssignor.Subscription(oddTopics)); 232 | consumers.put(consumer3, new ConsumerPartitionAssignor.Subscription(oddTopics)); 233 | consumers.put(consumer4, new ConsumerPartitionAssignor.Subscription(allTopics)); 234 | 235 | Map> assignment = assignor.assign(partitionsPerTopic, consumers); 236 | assertEquals(Arrays.asList( 237 | new TopicPartition(topic2, 0), 238 | new TopicPartition(topic3, 0)), assignment.get(consumer1)); 239 | assertEquals(Arrays.asList( 240 | new TopicPartition(topic1, 0), 241 | new TopicPartition(topic3, 1)), assignment.get(consumer2)); 242 | assertEquals(Arrays.asList( 243 | new TopicPartition(topic1, 1), 244 | new TopicPartition(topic5, 0)), assignment.get(consumer3)); 245 | assertEquals(Arrays.asList( 246 | new TopicPartition(topic4, 0), 247 | new TopicPartition(topic5, 1)), assignment.get(consumer4)); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /common-kafka/src/test/java/com/cerner/common/kafka/metrics/MeterPoolTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.metrics; 2 | 3 | import com.yammer.metrics.Metrics; 4 | import com.yammer.metrics.core.Meter; 5 | import com.yammer.metrics.core.MetricName; 6 | import com.yammer.metrics.core.MetricsRegistry; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.BeforeEach; 9 | 10 | import static org.hamcrest.CoreMatchers.equalTo; 11 | import static org.hamcrest.CoreMatchers.is; 12 | import static org.hamcrest.CoreMatchers.not; 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.junit.jupiter.api.Assertions.assertSame; 15 | import static org.junit.jupiter.api.Assertions.assertThrows; 16 | 17 | /** 18 | * Tests for {@link MeterPool}. 19 | */ 20 | public class MeterPoolTest { 21 | 22 | @BeforeEach 23 | public void clearMetrics() { 24 | MetricsRegistry metricsRegistry = Metrics.defaultRegistry(); 25 | for (MetricName metric : metricsRegistry.allMetrics().keySet()) { 26 | metricsRegistry.removeMetric(metric); 27 | } 28 | } 29 | 30 | @Test 31 | public void nullClass() { 32 | assertThrows(IllegalArgumentException.class, 33 | () -> new MeterPool(null, "name")); 34 | } 35 | 36 | @Test 37 | public void nullName() { 38 | assertThrows(IllegalArgumentException.class, 39 | () -> new MeterPool(Object.class, null)); 40 | } 41 | 42 | @Test 43 | public void getMeterNullScope() { 44 | assertThrows(IllegalArgumentException.class, 45 | () -> new MeterPool(Object.class, "name").getMeter(null)); 46 | } 47 | 48 | @Test 49 | public void getMeterSameScope() { 50 | MeterPool pool = new MeterPool(Object.class, "name"); 51 | 52 | Meter meter1 = pool.getMeter("scope"); 53 | Meter meter2 = pool.getMeter("scope"); 54 | 55 | assertSame(meter1, meter2); 56 | } 57 | 58 | @Test 59 | public void getMeterDifferentScope() { 60 | MeterPool pool = new MeterPool(Object.class, "name"); 61 | 62 | Meter meter1 = pool.getMeter("scope1"); 63 | Meter meter2 = pool.getMeter("scope2"); 64 | 65 | assertThat(meter1, is(not(equalTo(meter2)))); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /common-kafka/src/test/java/com/cerner/common/kafka/producer/KafkaProducerPoolTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.producer; 2 | 3 | import com.cerner.common.kafka.consumer.ConsumerOffsetClient; 4 | 5 | import org.apache.kafka.clients.admin.AdminClient; 6 | import org.apache.kafka.clients.admin.NewTopic; 7 | import org.apache.kafka.clients.producer.Producer; 8 | import org.apache.kafka.clients.producer.ProducerRecord; 9 | import org.apache.kafka.common.serialization.StringSerializer; 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.AfterAll; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.BeforeAll; 14 | import org.junit.jupiter.api.Test; 15 | 16 | import com.cerner.common.kafka.KafkaExecutionException; 17 | import com.cerner.common.kafka.KafkaTests; 18 | import com.cerner.common.kafka.testing.KafkaTestUtils; 19 | import org.junit.jupiter.api.Timeout; 20 | 21 | import java.io.IOException; 22 | import java.util.Arrays; 23 | import java.util.Collections; 24 | import java.util.HashSet; 25 | import java.util.IdentityHashMap; 26 | import java.util.LinkedList; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Properties; 30 | import java.util.Set; 31 | import java.util.UUID; 32 | import java.util.concurrent.Callable; 33 | import java.util.concurrent.CompletionService; 34 | import java.util.concurrent.ExecutionException; 35 | import java.util.concurrent.ExecutorCompletionService; 36 | import java.util.concurrent.ExecutorService; 37 | import java.util.concurrent.Executors; 38 | import java.util.concurrent.Future; 39 | 40 | import static com.cerner.common.kafka.producer.KafkaProducerPool.DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT; 41 | import static com.cerner.common.kafka.producer.KafkaProducerPool.KAFKA_PRODUCER_CONCURRENCY; 42 | import static org.apache.kafka.clients.producer.ProducerConfig.LINGER_MS_CONFIG; 43 | import static org.hamcrest.CoreMatchers.is; 44 | import static org.hamcrest.CoreMatchers.not; 45 | import static org.hamcrest.MatcherAssert.assertThat; 46 | import static org.junit.jupiter.api.Assertions.assertNotNull; 47 | import static org.junit.jupiter.api.Assertions.assertThrows; 48 | import static org.mockito.Mockito.mock; 49 | import static org.mockito.Mockito.times; 50 | import static org.mockito.Mockito.verify; 51 | import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG; 52 | import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG; 53 | import static org.apache.kafka.clients.producer.ProducerConfig.ACKS_CONFIG; 54 | 55 | /** 56 | * @author Stephen Durfey 57 | */ 58 | public class KafkaProducerPoolTest { 59 | 60 | private KafkaProducerPool pool; 61 | private static AdminClient kafkaAdminClient; 62 | 63 | private static final String FORCE_PRODUCER_ROTATION_OVERFLOW = "producer.rotation.overflow"; 64 | 65 | @BeforeAll 66 | public static void startup() throws Exception { 67 | KafkaTests.startTest(); 68 | kafkaAdminClient = AdminClient.create(KafkaTests.getProps()); 69 | } 70 | 71 | @AfterAll 72 | public static void shutdown() throws Exception { 73 | kafkaAdminClient.close(); 74 | KafkaTests.endTest(); 75 | } 76 | 77 | @BeforeEach 78 | public void initializePool() { 79 | pool = new KafkaProducerPool<>(); 80 | } 81 | 82 | @AfterEach 83 | public void closePool() throws IOException { 84 | pool.close(); 85 | } 86 | 87 | @Test 88 | public void nullProperties() { 89 | assertThrows(IllegalArgumentException.class, () -> pool.getProducer(null), 90 | "Expected pool.getProdcuer to throw illegal argument exception, but it wasn't thrown."); 91 | } 92 | 93 | @Test 94 | public void sameConfiguration() { 95 | Properties props = KafkaTests.getProps(); 96 | props.setProperty(KAFKA_PRODUCER_CONCURRENCY, "1"); 97 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 98 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 99 | 100 | Producer p1 = pool.getProducer(props); 101 | Producer p2 = pool.getProducer(props); 102 | assertThat(p1, is(p2)); 103 | } 104 | 105 | @Test 106 | public void differentConfiguration() { 107 | Properties props1 = KafkaTests.getProps(); 108 | props1.setProperty(KAFKA_PRODUCER_CONCURRENCY, "1"); 109 | props1.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 110 | props1.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 111 | 112 | Properties props2 = new Properties(); 113 | props2.putAll(props1); 114 | props2.setProperty("some.property", "property.value"); 115 | 116 | Producer p1 = pool.getProducer(props1); 117 | Producer p2 = pool.getProducer(props2); 118 | assertThat(p1, is(not(p2))); 119 | } 120 | 121 | @Test 122 | public void sameConfigurationDefaultConcurrency() throws IOException { 123 | Properties props = KafkaTests.getProps(); 124 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 125 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 126 | assertPoolConcurrency(props, DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT); 127 | } 128 | 129 | @Test 130 | public void sameConfigurationCustomConcurrency() throws IOException { 131 | int producerConcurrency = 25; 132 | Properties props = KafkaTests.getProps(); 133 | props.setProperty(KAFKA_PRODUCER_CONCURRENCY, String.valueOf(producerConcurrency)); 134 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 135 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 136 | assertPoolConcurrency(props, producerConcurrency); 137 | } 138 | 139 | @Test 140 | public void concurrencyConfigIsString() throws IOException { 141 | Properties props = KafkaTests.getProps(); 142 | props.setProperty(KAFKA_PRODUCER_CONCURRENCY, "notANumber"); 143 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 144 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 145 | assertPoolConcurrency(props, DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT); 146 | } 147 | 148 | @Test 149 | public void concurrencyConfigIsZero() throws IOException { 150 | Properties props = KafkaTests.getProps(); 151 | props.setProperty(KAFKA_PRODUCER_CONCURRENCY, String.valueOf(0)); 152 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 153 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 154 | assertPoolConcurrency(props, DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT); 155 | } 156 | 157 | @Test 158 | public void concurrencyConfigIsNegative() throws IOException { 159 | Properties props = KafkaTests.getProps(); 160 | props.setProperty(KAFKA_PRODUCER_CONCURRENCY, String.valueOf(-12)); 161 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 162 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 163 | assertPoolConcurrency(props, DEFAULT_KAFKA_PRODUCER_CONCURRENCY_INT); 164 | } 165 | 166 | @Test 167 | public void defaultUsed() throws IOException { 168 | Properties props = KafkaTests.getProps(); 169 | 170 | MockProducerPool mockPool = new MockProducerPool(); 171 | mockPool.getProducer(props); 172 | 173 | assertThat(mockPool.getProducerProperties().getProperty(ACKS_CONFIG), is(String.valueOf(-1))); 174 | } 175 | 176 | @Test 177 | public void defaultOverridden() throws IOException { 178 | Properties props = KafkaTests.getProps(); 179 | props.setProperty(ACKS_CONFIG, String.valueOf(1)); 180 | 181 | MockProducerPool mockPool = new MockProducerPool(); 182 | mockPool.getProducer(props); 183 | 184 | assertThat(mockPool.getProducerProperties().getProperty(ACKS_CONFIG), is(String.valueOf(1))); 185 | } 186 | 187 | @Test 188 | public void closeEmptyPool() throws IOException { 189 | pool.close(); 190 | } 191 | 192 | @Test 193 | public void closeClosesProducers() throws IOException, ExecutionException, InterruptedException { 194 | KafkaProducerPool mockPool = new MockProducerPool(); 195 | Properties props = KafkaTests.getProps(); 196 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 197 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 198 | Producer producer = mockPool.getProducer(props); 199 | mockPool.close(); 200 | verify(producer).close(); 201 | } 202 | 203 | @Test 204 | public void multipleCloses() throws IOException { 205 | KafkaProducerPool mockPool = new MockProducerPool(); 206 | Properties props = KafkaTests.getProps(); 207 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 208 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 209 | List> producers = new LinkedList<>(); 210 | for (int i = 0; i < 10; ++i) { 211 | producers.add(mockPool.getProducer(props)); 212 | } 213 | mockPool.close(); 214 | mockPool.close(); 215 | producers.forEach(producer -> verify(producer, times(1)).close()); 216 | } 217 | 218 | @Test 219 | @Timeout(10) 220 | public void messageProductionWithProducerConfig() throws InterruptedException, KafkaExecutionException, ExecutionException { 221 | Properties props = KafkaTests.getProps(); 222 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 223 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 224 | props.setProperty(LINGER_MS_CONFIG, String.valueOf(100)); 225 | 226 | messageProduction(props); 227 | } 228 | 229 | @Test 230 | @Timeout(10) 231 | public void messageProductionWithProperties() throws InterruptedException, KafkaExecutionException, ExecutionException { 232 | Properties props = KafkaTests.getProps(); 233 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 234 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 235 | props.setProperty(LINGER_MS_CONFIG, String.valueOf(100)); 236 | 237 | messageProduction(props); 238 | } 239 | 240 | @Test 241 | public void producerRotationOverflow() throws IOException { 242 | KafkaProducerPool mockPool = new MockProducerPool(); 243 | Properties props = KafkaTests.getProps(); 244 | props.setProperty(FORCE_PRODUCER_ROTATION_OVERFLOW, String.valueOf(true)); 245 | 246 | mockPool.getProducer(props); 247 | mockPool.close(); 248 | } 249 | 250 | private void messageProduction(Properties config) throws InterruptedException, KafkaExecutionException, ExecutionException { 251 | String topicName = "topic_" + UUID.randomUUID().toString(); 252 | NewTopic topic = new NewTopic(topicName, 4, (short) 1); 253 | Set topics = new HashSet<>(); 254 | topics.add(topic); 255 | 256 | kafkaAdminClient.createTopics(topics); 257 | 258 | Producer producer = pool.getProducer(config); 259 | 260 | long messages = 10; 261 | for (long i = 0; i < messages; ++i) { 262 | producer.send(new ProducerRecord<>(topicName, String.valueOf(i), UUID.randomUUID().toString())).get(); 263 | } 264 | 265 | // We loop here since the producer doesn't necessarily write to ZK immediately after receiving a write 266 | ConsumerOffsetClient consumerOffsetClient = new ConsumerOffsetClient(config); 267 | while(KafkaTestUtils.getTopicAndPartitionOffsetSum(topicName, consumerOffsetClient.getEndOffsets(Arrays.asList(topicName))) != messages) 268 | Thread.sleep(100); 269 | } 270 | 271 | @Test 272 | public void concurrencyTest() throws Exception { 273 | // Run 10 threads in parallel for approximately 3 seconds. 274 | int threads = 10; 275 | long stopTime = System.currentTimeMillis() + 3000; 276 | 277 | int producerConcurrency = 25; 278 | final Properties props = KafkaTests.getProps(); 279 | props.setProperty(KAFKA_PRODUCER_CONCURRENCY, String.valueOf(producerConcurrency)); 280 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 281 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 282 | 283 | final Set> producers = Collections.synchronizedSet(new HashSet<>()); 284 | 285 | ExecutorService executor = Executors.newFixedThreadPool(threads); 286 | CompletionService service = new ExecutorCompletionService(executor); 287 | Map, Callable> futures = new IdentityHashMap, Callable>(threads); 288 | 289 | while (System.currentTimeMillis() < stopTime) { 290 | final Future future = service.submit(() -> { 291 | producers.add(pool.getProducer(props)); 292 | return null; 293 | }); 294 | 295 | // Keep track of submitted jobs. 296 | futures.put(future, () -> { 297 | Void nothing = future.get(); 298 | return nothing; 299 | }); 300 | 301 | // Collect any completed jobs. 302 | Future done; 303 | while ((done = service.poll()) != null) { 304 | futures.remove(done).call(); 305 | } 306 | 307 | // Respect the concurrency limit. 308 | while (futures.size() >= threads) { 309 | done = service.take(); 310 | futures.remove(done).call(); 311 | } 312 | } 313 | 314 | // Allow pending jobs to complete. 315 | while (futures.size() > 0) { 316 | Future done = service.take(); 317 | futures.remove(done).call(); 318 | } 319 | 320 | executor.shutdown(); 321 | assertThat(producers.size(), is(producerConcurrency)); 322 | } 323 | 324 | private void assertPoolConcurrency(Properties props, int concurrency) { 325 | Set> producers = new HashSet<>(); 326 | for (int i = 0; i < concurrency * 10; ++i) { 327 | producers.add(pool.getProducer(props)); 328 | } 329 | assertThat(producers.size(), is(concurrency)); 330 | } 331 | 332 | @SuppressWarnings("unchecked") 333 | private class MockProducerPool extends KafkaProducerPool { 334 | private Properties producerProperties; 335 | 336 | @Override 337 | Producer createProducer(Properties properties) { 338 | producerProperties = properties; 339 | return mock(Producer.class); 340 | } 341 | 342 | @Override 343 | public int getProducerRotation() { 344 | if (producerProperties.getProperty(FORCE_PRODUCER_ROTATION_OVERFLOW) != null) { 345 | return Integer.MAX_VALUE + 2; 346 | } else { 347 | return super.getProducerRotation(); 348 | } 349 | } 350 | 351 | public Properties getProducerProperties() { 352 | assertNotNull(producerProperties); 353 | return producerProperties; 354 | } 355 | } 356 | 357 | } 358 | -------------------------------------------------------------------------------- /common-kafka/src/test/java/com/cerner/common/kafka/producer/KafkaProducerWrapperTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.producer; 2 | 3 | import com.cerner.common.kafka.KafkaExecutionException; 4 | import com.cerner.common.kafka.KafkaTests; 5 | import org.apache.kafka.clients.admin.AdminClient; 6 | import org.apache.kafka.clients.admin.NewTopic; 7 | import org.apache.kafka.clients.producer.KafkaProducer; 8 | import org.apache.kafka.clients.producer.ProducerConfig; 9 | import org.apache.kafka.clients.producer.ProducerRecord; 10 | import org.apache.kafka.clients.producer.RecordMetadata; 11 | import org.apache.kafka.common.errors.RecordTooLargeException; 12 | import org.apache.kafka.common.errors.TimeoutException; 13 | import org.apache.kafka.common.serialization.StringSerializer; 14 | import org.junit.jupiter.api.AfterAll; 15 | import org.junit.jupiter.api.BeforeAll; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.junit.jupiter.api.TestInfo; 19 | import org.junit.jupiter.api.extension.ExtendWith; 20 | import org.mockito.ArgumentMatchers; 21 | import org.mockito.Mock; 22 | import org.mockito.junit.jupiter.MockitoExtension; 23 | 24 | import java.io.IOException; 25 | import java.util.ArrayList; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.Properties; 29 | import java.util.Set; 30 | import java.util.UUID; 31 | import java.util.concurrent.ExecutionException; 32 | import java.util.concurrent.Future; 33 | import java.util.stream.IntStream; 34 | 35 | import static com.cerner.common.kafka.testing.AbstractKafkaTests.getProps; 36 | import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG; 37 | import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG; 38 | import static org.hamcrest.CoreMatchers.instanceOf; 39 | import static org.hamcrest.CoreMatchers.is; 40 | import static org.hamcrest.MatcherAssert.assertThat; 41 | import static org.junit.jupiter.api.Assertions.assertThrows; 42 | import static org.mockito.Mockito.doThrow; 43 | import static org.mockito.Mockito.never; 44 | import static org.mockito.Mockito.times; 45 | import static org.mockito.Mockito.verify; 46 | import static org.mockito.Mockito.when; 47 | 48 | /** 49 | * @author Stephen Durfey 50 | */ 51 | @ExtendWith(MockitoExtension.class) 52 | public class KafkaProducerWrapperTest { 53 | 54 | private static AdminClient kafkaAdminClient; 55 | 56 | private static Properties customProps; 57 | 58 | private String testName; 59 | 60 | @Mock 61 | private KafkaProducer mockedProducer; 62 | 63 | @Mock 64 | private Future mockedFuture; 65 | 66 | private String topicName; 67 | 68 | @BeforeAll 69 | public static void startup() throws Exception { 70 | customProps = new Properties(); 71 | customProps.setProperty("message.max.bytes", "1000"); 72 | customProps.setProperty("group.max.session.timeout.ms", "2000"); 73 | customProps.setProperty("group.min.session.timeout.ms", "500"); 74 | 75 | // start Kafka with custom properties. 76 | KafkaTests.startKafka(customProps); 77 | 78 | kafkaAdminClient = AdminClient.create(KafkaTests.getProps()); 79 | } 80 | 81 | @AfterAll 82 | public static void shutdown() throws Exception { 83 | kafkaAdminClient.close(); 84 | 85 | // Restart Kafka with default properties. 86 | KafkaTests.stopKafka(); 87 | KafkaTests.startKafka(); 88 | } 89 | 90 | @BeforeEach 91 | public void setup(TestInfo testInfo){ 92 | testName = testInfo.getDisplayName().replaceAll("[^a-zA-Z0-9]", "-").trim(); 93 | topicName = "topic_" + testName; 94 | } 95 | 96 | @Test 97 | public void test_messageSentSynchronouslySuccessfully() throws IOException { 98 | long previousSendCount = KafkaProducerWrapper.SEND_TIMER.count(); 99 | long previousSyncSendCount = KafkaProducerWrapper.SYNC_SEND_TIMER.count(); 100 | long previousFlushCount = KafkaProducerWrapper.FLUSH_TIMER.count(); 101 | long previousBatchSizeCount = KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.count(); 102 | double previousBatchSizeSum = KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.sum(); 103 | 104 | Set topics = new HashSet<>(); 105 | topics.add(new NewTopic(topicName, 4, (short) 1)); 106 | kafkaAdminClient.createTopics(topics); 107 | 108 | Properties props = KafkaTests.getProps(); 109 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 110 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 111 | props.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "10000"); 112 | props.setProperty(ProducerConfig.LINGER_MS_CONFIG, "60000"); 113 | 114 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(new KafkaProducer<>(props)); 115 | 116 | producer.sendSynchronously( 117 | new ProducerRecord<>(topicName, "key"+testName, "value"+ UUID.randomUUID())); 118 | producer.close(); 119 | 120 | assertThat(KafkaProducerWrapper.SEND_TIMER.count(), is(previousSendCount)); 121 | assertThat(KafkaProducerWrapper.SYNC_SEND_TIMER.count(), is(previousSyncSendCount + 1)); 122 | assertThat(KafkaProducerWrapper.FLUSH_TIMER.count(), is(previousFlushCount)); 123 | assertThat(KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.count(), is(previousBatchSizeCount + 1)); 124 | assertThat(KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.sum(), is(previousBatchSizeSum + 1)); 125 | } 126 | 127 | @Test 128 | public void test_multipleMessagesSentSynchronouslySuccessfully() throws IOException { 129 | long previousSendCount = KafkaProducerWrapper.SEND_TIMER.count(); 130 | long previousSyncSendCount = KafkaProducerWrapper.SYNC_SEND_TIMER.count(); 131 | long previousFlushCount = KafkaProducerWrapper.FLUSH_TIMER.count(); 132 | long previousBatchSizeCount = KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.count(); 133 | double previousBatchSizeSum = KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.sum(); 134 | 135 | Set topics = new HashSet<>(); 136 | topics.add(new NewTopic(topicName, 4, (short) 1)); 137 | kafkaAdminClient.createTopics(topics); 138 | 139 | Properties props = KafkaTests.getProps(); 140 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 141 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 142 | props.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "10000"); 143 | props.setProperty(ProducerConfig.LINGER_MS_CONFIG, "60000"); 144 | 145 | int batchSize = 10; 146 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(new KafkaProducer<>(props)); 147 | 148 | List> records = new ArrayList<>(); 149 | 150 | IntStream.range(0, batchSize).forEach(i -> 151 | records.add(new ProducerRecord<>(topicName, "key"+testName+i, "value"+i))); 152 | 153 | producer.sendSynchronously(records); 154 | producer.close(); 155 | 156 | assertThat(KafkaProducerWrapper.SEND_TIMER.count(), is(previousSendCount)); 157 | assertThat(KafkaProducerWrapper.SYNC_SEND_TIMER.count(), is(previousSyncSendCount + 1)); 158 | assertThat(KafkaProducerWrapper.FLUSH_TIMER.count(), is(previousFlushCount)); 159 | assertThat(KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.count(), is(previousBatchSizeCount + 1)); 160 | assertThat(KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.sum(), is(previousBatchSizeSum + batchSize)); 161 | } 162 | 163 | @Test 164 | public void test_flushRetriable() throws IOException { 165 | doThrow(new TimeoutException("boom")).when(mockedProducer).flush(); 166 | 167 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(mockedProducer); 168 | 169 | producer.send(new ProducerRecord<>(topicName, "key"+testName, 170 | "value"+UUID.randomUUID())); 171 | assertThrows(IOException.class, producer::flush, 172 | "Expected producer.flush() to throw IO exception, but it wasn't thrown."); 173 | } 174 | 175 | @Test 176 | public void test_flushNonRetriable() throws IOException { 177 | doThrow(new RuntimeException("boom")).when(mockedProducer).flush(); 178 | 179 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(mockedProducer); 180 | 181 | producer.send(new ProducerRecord<>(topicName, "key"+testName, 182 | "value"+UUID.randomUUID())); 183 | assertThrows(IOException.class, producer::flush, 184 | "Expected producer.flush() to throw IO exception, but it wasn't thrown."); 185 | } 186 | 187 | @Test 188 | public void test_flushFutureExecutionException() throws IOException, ExecutionException, InterruptedException { 189 | when(mockedProducer.send(ArgumentMatchers.any())).thenReturn(mockedFuture); 190 | when(mockedFuture.get()).thenThrow(new ExecutionException("boom", new IllegalStateException())); 191 | 192 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(mockedProducer); 193 | 194 | producer.send(new ProducerRecord<>(topicName, "key"+testName, 195 | "value"+UUID.randomUUID())); 196 | assertThrows(IOException.class, producer::flush, 197 | "Expected producer.flush() to throw IO exception, but it wasn't thrown."); 198 | } 199 | 200 | @Test 201 | public void test_messageSentSuccessfully() throws IOException { 202 | long previousSendCount = KafkaProducerWrapper.SEND_TIMER.count(); 203 | long previousSyncSendCount = KafkaProducerWrapper.SYNC_SEND_TIMER.count(); 204 | long previousFlushCount = KafkaProducerWrapper.FLUSH_TIMER.count(); 205 | long previousBatchSizeCount = KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.count(); 206 | double previousBatchSizeSum = KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.sum(); 207 | 208 | Set topics = new HashSet<>(); 209 | topics.add(new NewTopic(topicName, 4, (short) 1)); 210 | kafkaAdminClient.createTopics(topics); 211 | 212 | Properties props = KafkaTests.getProps(); 213 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 214 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 215 | props.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "10000"); 216 | props.setProperty(ProducerConfig.LINGER_MS_CONFIG, "60000"); 217 | 218 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(new KafkaProducer<>(props)); 219 | 220 | producer.send(new ProducerRecord<>(topicName, "key"+testName, "value"+UUID.randomUUID())); 221 | producer.flush(); 222 | producer.close(); 223 | 224 | assertThat(KafkaProducerWrapper.SEND_TIMER.count(), is(previousSendCount + 1)); 225 | assertThat(KafkaProducerWrapper.SYNC_SEND_TIMER.count(), is(previousSyncSendCount)); 226 | assertThat(KafkaProducerWrapper.FLUSH_TIMER.count(), is(previousFlushCount + 1)); 227 | assertThat(KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.count(), is(previousBatchSizeCount + 1)); 228 | assertThat(KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.sum(), is(previousBatchSizeSum + 1)); 229 | } 230 | 231 | @Test 232 | public void test_messageSentMultipleSuccessfully() throws IOException, ExecutionException, InterruptedException { 233 | long previousSendCount = KafkaProducerWrapper.SEND_TIMER.count(); 234 | long previousSyncSendCount = KafkaProducerWrapper.SYNC_SEND_TIMER.count(); 235 | long previousFlushCount = KafkaProducerWrapper.FLUSH_TIMER.count(); 236 | long previousBatchSizeCount = KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.count(); 237 | double previousBatchSizeSum = KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.sum(); 238 | 239 | when(mockedProducer.send(ArgumentMatchers.any())).thenReturn(mockedFuture); 240 | int batchSize = 10; 241 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(mockedProducer); 242 | 243 | IntStream.range(0, batchSize).forEach(i -> 244 | producer.send(new ProducerRecord<>(topicName, "key"+testName+i, "value"+i))); 245 | producer.flush(); 246 | 247 | verify(mockedFuture, times(batchSize)).get(); 248 | 249 | producer.close(); 250 | 251 | assertThat(KafkaProducerWrapper.SEND_TIMER.count(), is(previousSendCount + batchSize)); 252 | assertThat(KafkaProducerWrapper.SYNC_SEND_TIMER.count(), is(previousSyncSendCount)); 253 | assertThat(KafkaProducerWrapper.FLUSH_TIMER.count(), is(previousFlushCount + 1)); 254 | assertThat(KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.count(), is(previousBatchSizeCount + 1)); 255 | assertThat(KafkaProducerWrapper.BATCH_SIZE_HISTOGRAM.sum(), is(previousBatchSizeSum + batchSize)); 256 | } 257 | 258 | @Test 259 | public void test_sendNullRecord() throws IOException, ExecutionException, InterruptedException { 260 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(mockedProducer); 261 | assertThrows(IllegalArgumentException.class, () -> producer.send(null), 262 | "Expected producer.send() to throw Illegal Argument exception, but it wasn't thrown."); 263 | } 264 | 265 | @Test 266 | public void test_WrapperNotCloseProducer() throws IOException, ExecutionException, InterruptedException { 267 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(mockedProducer); 268 | 269 | producer.close(); 270 | 271 | verify(mockedProducer, never()).close(); 272 | } 273 | 274 | @Test 275 | public void testSynchronous_messageTooLarge() throws IOException { 276 | Set topics = new HashSet<>(); 277 | topics.add(new NewTopic(topicName, 4, (short) 1)); 278 | kafkaAdminClient.createTopics(topics); 279 | 280 | Properties props = KafkaTests.getProps(); 281 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 282 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 283 | props.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "10000"); 284 | props.setProperty(ProducerConfig.LINGER_MS_CONFIG, "60000"); 285 | 286 | // create a payload that is too large 287 | StringBuilder bldr = new StringBuilder(); 288 | for(int i = 0; i < 100000; i++) { 289 | bldr.append(i); 290 | } 291 | 292 | List> records = new ArrayList<>(2); 293 | records.add(new ProducerRecord<>(topicName, "key", bldr.toString())); 294 | records.add(new ProducerRecord<>(topicName, "key2", "small")); 295 | boolean caughtRecordTooLargeException = false; 296 | try (KafkaProducerWrapper producer = 297 | new KafkaProducerWrapper<>(new KafkaProducer<>(props))) { 298 | producer.sendSynchronously(records); 299 | } catch (KafkaExecutionException e) { 300 | Throwable cause = e.getCause(); 301 | 302 | assertThat(cause, instanceOf(ExecutionException.class)); 303 | 304 | Throwable cause2 = cause.getCause(); 305 | 306 | assertThat(cause2, instanceOf(RecordTooLargeException.class)); 307 | 308 | caughtRecordTooLargeException = true; 309 | } 310 | 311 | assertThat(caughtRecordTooLargeException, is(true)); 312 | } 313 | 314 | @Test 315 | public void test_messageSentSuccessfullyEvenWithFailure() throws IOException { 316 | 317 | Set topics = new HashSet<>(); 318 | topics.add(new NewTopic(topicName, 4, (short) 1)); 319 | kafkaAdminClient.createTopics(topics); 320 | 321 | Properties props = getProps(); 322 | props.setProperty(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 323 | props.setProperty(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); 324 | props.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "10000"); 325 | props.setProperty(ProducerConfig.LINGER_MS_CONFIG, "60000"); 326 | 327 | KafkaProducerWrapper producer = new KafkaProducerWrapper<>(new KafkaProducer<>(props)); 328 | 329 | producer.send(new ProducerRecord<>(topicName, "key1", "value1")); 330 | producer.flush(); 331 | 332 | // Just stopping Kafka brokers not the Zookeepers (as every time Zookeepers are stopped and brought up they do not have 333 | // same config so for this test we need same config) 334 | KafkaTests.stopOnlyKafkaBrokers(); 335 | 336 | // to simulate after some transient error Kafka brokers come back up and resumes 337 | Thread kafkaThread = new StartKafkaThread(); 338 | 339 | kafkaThread.start(); 340 | 341 | try { 342 | producer.send(new ProducerRecord<>(topicName, "key2", "value2")); 343 | producer.flush(); 344 | producer.close(); 345 | 346 | } finally { 347 | try { 348 | kafkaThread.join(); 349 | } catch (InterruptedException e) { 350 | e.printStackTrace(); 351 | } 352 | } 353 | } 354 | 355 | // Separate thread class just to start Kafka cluster independent of test thread 356 | public class StartKafkaThread extends Thread { 357 | 358 | public void run() { 359 | try { 360 | try { 361 | // to have some delay between stopping and starting of the brokers 362 | Thread.sleep(3000L); 363 | } catch (InterruptedException e) { 364 | e.printStackTrace(); 365 | } 366 | 367 | // Just start the Kafka brokers, Zookeepers should already be running 368 | KafkaTests.startOnlyKafkaBrokers(); 369 | 370 | } catch (Exception e) { 371 | e.printStackTrace(); 372 | } 373 | } 374 | } 375 | 376 | } -------------------------------------------------------------------------------- /common-kafka/src/test/java/com/cerner/common/kafka/producer/partitioners/FairPartitionerTest.java: -------------------------------------------------------------------------------- 1 | package com.cerner.common.kafka.producer.partitioners; 2 | 3 | import org.apache.kafka.common.Cluster; 4 | import org.apache.kafka.common.Node; 5 | import org.apache.kafka.common.PartitionInfo; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.TestInfo; 9 | 10 | import java.util.Collections; 11 | import java.util.HashSet; 12 | import java.util.List; 13 | import java.util.Set; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.IntStream; 16 | 17 | import static org.hamcrest.core.Is.is; 18 | import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo; 19 | import static org.hamcrest.number.OrderingComparison.lessThan; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | 22 | public class FairPartitionerTest { 23 | 24 | private FairPartitioner partitioner; 25 | private String topic; 26 | private Object key; 27 | private byte[] keyBytes; 28 | private Object value; 29 | private byte[] valueBytes; 30 | private Cluster cluster; 31 | private List allPartitions; 32 | private List notAvailablePartitions; 33 | private Node node; 34 | 35 | @BeforeEach 36 | public void setup(TestInfo testInfo) throws InterruptedException { 37 | partitioner = new FairPartitioner(); 38 | topic = testInfo.getDisplayName().replaceAll("[^a-zA-Z0-9]", "-").trim(); 39 | key = new Object(); 40 | keyBytes = new byte[0]; 41 | value = new Object(); 42 | valueBytes = new byte[0]; 43 | 44 | node = new Node(1, "example.com", 6667); 45 | 46 | allPartitions = 47 | IntStream.range(0, 8).mapToObj(i -> { 48 | //null leader means not available 49 | Node leader = null; 50 | if(i % 2 == 0){ 51 | //a non-null leader means it is available 52 | leader = node; 53 | } 54 | return new PartitionInfo(topic, i, leader, null, null); 55 | }).collect(Collectors.toList()); 56 | notAvailablePartitions = allPartitions.stream().filter(p -> p.leader() == null).collect(Collectors.toList()); 57 | 58 | cluster = new Cluster("clusterId", Collections.singleton(node), allPartitions, 59 | Collections.emptySet(), Collections.emptySet()); 60 | 61 | // Wait until next clock window tick. 62 | long millis = System.currentTimeMillis() / FairPartitioner.ROTATE_MILLIS; 63 | while (System.currentTimeMillis() / FairPartitioner.ROTATE_MILLIS == millis) { 64 | Thread.sleep(1); 65 | } 66 | } 67 | 68 | @Test 69 | public void partitionAvailable() { 70 | int partition = partitioner.partition(topic, key, keyBytes, value, valueBytes, cluster); 71 | assertThat(partition, is(lessThan(allPartitions.size()))); 72 | assertThat(partition, is(greaterThanOrEqualTo(0))); 73 | assertThat(partition % 2, is(0)); 74 | } 75 | 76 | @Test 77 | public void partitionNotAvailable() { 78 | cluster = new Cluster("clusterId", Collections.singleton(node), notAvailablePartitions, 79 | Collections.emptySet(), Collections.emptySet()); 80 | int partition = partitioner.partition(topic, key, keyBytes, value, valueBytes, cluster); 81 | assertThat(partition, is(lessThan(allPartitions.size()))); 82 | assertThat(partition, is(greaterThanOrEqualTo(0))); 83 | assertThat(partition % 2, is(1)); 84 | } 85 | 86 | @Test 87 | public void partitionSameTimeWindow() { 88 | int messages = allPartitions.size(); 89 | Set partitions = new HashSet<>(); 90 | 91 | for (int i = 0; i < messages; ++i) { 92 | int partition = partitioner.partition(topic, key, keyBytes, value, valueBytes, cluster); 93 | assertThat(partition, is(greaterThanOrEqualTo(0))); 94 | assertThat(partition, is(lessThan(allPartitions.size()))); 95 | partitions.add(partition); 96 | } 97 | 98 | // Since all messages were produced in the same time window, they should be assigned to the same partition. 99 | assertThat(partitions.size(), is(1)); 100 | } 101 | 102 | @Test 103 | public void partitionDifferentTimeWindows() throws InterruptedException { 104 | int messages = allPartitions.size(); 105 | Set partitions = new HashSet<>(); 106 | 107 | for (int i = 0; i < messages; ++i) { 108 | int partition = partitioner.partition(topic, key, keyBytes, value, valueBytes, cluster); 109 | assertThat(partition, is(greaterThanOrEqualTo(0))); 110 | assertThat(partition, is(lessThan(allPartitions.size()))); 111 | partitions.add(partition); 112 | Thread.sleep(FairPartitioner.ROTATE_MILLIS); 113 | } 114 | 115 | // Verify that partition is periodically rotated across all available partitions as expected. 116 | assertThat(partitions.size(), is(allPartitions.size() - notAvailablePartitions.size())); 117 | } 118 | } -------------------------------------------------------------------------------- /common-kafka/src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, A1 2 | log4j.appender.A1=org.apache.log4j.ConsoleAppender 3 | log4j.appender.A1.layout=org.apache.log4j.PatternLayout 4 | 5 | # Print the date in ISO 8601 format 6 | log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n 7 | 8 | # Limit Kafka logging 9 | log4j.logger.kafka=WARN 10 | log4j.logger.org.apache.kafka=WARN 11 | log4j.logger.org.apache.kafka.clients.consumer.ConsumerConfig=ERROR 12 | log4j.logger.org.apache.kafka.clients.producer.ProducerConfig=ERROR 13 | log4j.logger.org.apache.kafka.clients.NetworkClient=ERROR 14 | 15 | # Limit Zookeeper logging 16 | log4j.logger.org.apache.zookeeper=WARN 17 | log4j.logger.org.I0Itec.zkclient=WARN --------------------------------------------------------------------------------