├── .github ├── dependabot.yml ├── pull_request_template.md ├── renovate.json └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── connector-java ├── pom.xml └── src │ ├── main │ └── java │ │ └── io │ │ └── zeebe │ │ └── hazelcast │ │ └── connect │ │ └── java │ │ └── ZeebeHazelcast.java │ └── test │ ├── java │ └── io │ │ └── zeebe │ │ └── hazelcast │ │ ├── ExampleApplication.java │ │ ├── ExporterDmnRecordTest.java │ │ ├── ExporterRecordTest.java │ │ ├── ExporterTest.java │ │ ├── ZeebeHazelcastClientTest.java │ │ └── testcontainers │ │ └── ZeebeTestContainer.java │ └── resources │ ├── log4j2-test.xml │ └── rating.dmn ├── docker ├── application.yaml └── docker-compose.yml ├── exporter ├── pom.xml └── src │ ├── main │ └── java │ │ └── io │ │ └── zeebe │ │ └── hazelcast │ │ └── exporter │ │ ├── ExporterConfiguration.java │ │ ├── HazelcastExporter.java │ │ └── HazelcastRecordFilter.java │ └── test │ └── java │ └── io │ └── zeebe │ └── hazelcast │ ├── ExporterClusterNameTest.java │ ├── ExporterJsonTest.java │ ├── ExporterTest.java │ ├── RemoteExporterClusterNameTest.java │ ├── RemoteExporterTest.java │ └── testcontainers │ ├── HazelcastContainer.java │ └── ZeebeTestContainer.java ├── how-it-works.png └── pom.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 0 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | * 6 | 7 | ## Related issues 8 | 9 | 13 | 14 | closes # 15 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { "enabled": false } 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build project with Maven 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | schedule: 6 | - cron: '2 2 * * 1-5' # run nightly master builds on weekdays 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@f1d3225b5376a0791fdee5a0e8eac5289355e43a # pin@v2 14 | - name: Java setup 15 | uses: actions/setup-java@e54a62b3df9364d4b4c1c29c7225e57fe605d7dd # pin@v1 16 | with: 17 | java-version: 21 18 | - name: Cache 19 | uses: actions/cache@99d99cd262b87f5f8671407a1e5c1ddfa36ad5ba # pin@v1 20 | with: 21 | path: ~/.m2/repository 22 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 23 | restore-keys: | 24 | ${{ runner.os }}-maven- 25 | - name: Run Maven 26 | run: mvn -B clean verify com.mycila:license-maven-plugin:check 27 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # If this workflow is triggered by a push to master, it 2 | # deploys a SNAPSHOT 3 | # If this workflow is triggered by publishing a Release, it 4 | # deploys a RELEASE with the selected version 5 | # updates the project version by incrementing the patch version 6 | # commits the version update change to the repository's default branch. 7 | name: Deploy artifacts with Maven 8 | on: 9 | push: 10 | branches: [ main ] 11 | release: 12 | types: [ published ] 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-20.04 16 | steps: 17 | - uses: actions/checkout@f1d3225b5376a0791fdee5a0e8eac5289355e43a # pin@v2 18 | - name: Cache 19 | uses: actions/cache@0781355a23dac32fd3bac414512f4b903437991a # pin@v2 20 | with: 21 | path: ~/.m2/repository 22 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 23 | restore-keys: | 24 | ${{ runner.os }}-maven- 25 | - name: Set up Java environment 26 | uses: actions/setup-java@e54a62b3df9364d4b4c1c29c7225e57fe605d7dd # pin@v1 27 | with: 28 | java-version: 21 29 | gpg-private-key: ${{ secrets.MAVEN_CENTRAL_GPG_SIGNING_KEY_SEC }} 30 | gpg-passphrase: MAVEN_CENTRAL_GPG_PASSPHRASE 31 | - name: Login to Docker 32 | run: | 33 | # new login with new container registry url and PAT 34 | echo ${{ secrets.CR_PAT }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin 35 | - name: Deploy SNAPSHOT / Release 36 | uses: camunda-community-hub/community-action-maven-release@v1 # pin@v1 37 | with: 38 | release-version: ${{ github.event.release.tag_name }} 39 | release-profile: community-action-maven-release 40 | nexus-usr: ${{ secrets.NEXUS_USR }} 41 | nexus-psw: ${{ secrets.NEXUS_PSW }} 42 | maven-usr: ${{ secrets.MAVEN_CENTRAL_DEPLOYMENT_USR }} 43 | maven-psw: ${{ secrets.MAVEN_CENTRAL_DEPLOYMENT_PSW }} 44 | maven-gpg-passphrase: ${{ secrets.MAVEN_CENTRAL_GPG_SIGNING_KEY_PASSPHRASE }} 45 | github-token: ${{ secrets.GITHUB_TOKEN }} 46 | id: release 47 | - if: github.event.release 48 | name: Attach artifacts to GitHub Release (Release only) 49 | uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # pin@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | upload_url: ${{ github.event.release.upload_url }} 54 | asset_path: ${{ steps.release.outputs.artifacts_archive_path }} 55 | asset_name: ${{ steps.release.outputs.artifacts_archive_path }} 56 | asset_content_type: application/zip 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | **/.classpath 25 | **/.project 26 | **/.settings 27 | **/target 28 | 29 | # intellij / vs 30 | .idea 31 | *.iml 32 | bin/ 33 | packages/ 34 | *.userprefs 35 | obj/ 36 | .vs/ 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - language: csharp 4 | solution: connector-csharp/Connector.sln 5 | 6 | - language: java 7 | before_script: 8 | - cd exporter/ 9 | 10 | 11 | - language: java 12 | before_script: 13 | - cd connector-java/ 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at code-of-conduct@zeebe.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Zeebe-Hazelcast-Exporter 2 | 3 | :tada: First off, thanks for taking the time to contribute! :+1: 4 | 5 | ## How Can I Contribute? 6 | 7 | ### Reporting Bugs 8 | 9 | If you found a bug or an unexpected behevior then please create 10 | a [new issue](https://github.com/camunda-community-hub/zeebe-hazelcast-exporter/issues). Before creating an issue, make sure 11 | that there is no issue yet. Any information you provide in the issue, helps to solve it. 12 | 13 | ### Suggesting Enhancements 14 | 15 | If you have an idea how to improve the project then please create 16 | a [new issue](https://github.com/camunda-community-hub/zeebe-hazelcast-exporter/issues). Describe your idea and the 17 | motivation behind it. 18 | 19 | Please note that this is a community-driven project. The maintainers may have not much time to implement new features if 20 | they don't benefit directly from it. So, think about providing a pull request. 21 | 22 | ### Providing Pull Requests 23 | 24 | You want to provide a bug fix or an improvement? Great! :tada: 25 | 26 | Before opening a pull request, make sure that there is a related issue. The issue helps to confirm that the behavior is 27 | unexpected, or the idea of the improvement is valid. (Following the rule "Talk, then code") 28 | 29 | In order to verify that you don't break anything, you should build the whole project and run all tests. This also apply 30 | the code formatting. 31 | 32 | Please note that this is a community-driven project. The maintainers may have no time to review your pull request 33 | immediately. Stay patient! 34 | 35 | ## Building the Project from Source 36 | 37 | You can build the project with [Maven](http://maven.apache.org). 38 | 39 | In the root directory: 40 | 41 | Run the tests with 42 | 43 | ``` 44 | mvn test 45 | ``` 46 | 47 | Build the JAR files with 48 | 49 | ``` 50 | mvn clean install 51 | ``` 52 | 53 | ## Styleguides 54 | 55 | ### Source Code 56 | 57 | The Java code should be formatted using [Google's Java Format](https://github.com/google/google-java-format). 58 | 59 | ### Git Commit Messages 60 | 61 | Commit messages should follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format. 62 | 63 | For example: 64 | 65 | ``` 66 | feat(connector): support JSON format 67 | 68 | * the Java connector deserialize JSON records 69 | * ... 70 | ``` 71 | 72 | Available commit types: 73 | 74 | * `feat` - enhancements, new features 75 | * `fix` - bug fixes 76 | * `refactor` - non-behavior changes 77 | * `test` - only changes in tests 78 | * `docs` - changes in the documentation, readme, etc. 79 | * `style` - apply code styles 80 | * `build` - changes to the build (e.g. to Maven's `pom.xml`) 81 | * `ci` - changes to the CI (e.g. to GitHub related configs) 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # zeebe-hazelcast-exporter 3 | 4 | [![](https://img.shields.io/badge/Community%20Extension-An%20open%20source%20community%20maintained%20project-FF4700)](https://github.com/camunda-community-hub/community) 5 | [![](https://img.shields.io/badge/Lifecycle-Unmaintained-lightgrey)](https://github.com/Camunda-Community-Hub/community/blob/main/extension-lifecycle.md#Unmaintained-) 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 7 | 8 | [![Compatible with: Camunda Platform 8](https://img.shields.io/badge/Compatible%20with-Camunda%20Platform%208-0072Ce)](https://github.com/camunda-community-hub/community/blob/main/extension-lifecycle.md#compatiblilty) 9 | 10 | > [!IMPORTANT] 11 | > This community extension is **unmaintained** and outdated. It may not support new Camunda versions. 12 | > 13 | > Thank you to all [contributors](https://github.com/camunda-community-hub/zeebe-hazelcast-exporter/graphs/contributors) for making it a great extension. :tada: 14 | 15 | Export records from [Zeebe](https://github.com/camunda-cloud/zeebe) to [Hazelcast](https://github.com/hazelcast/hazelcast/). Hazelcast is an in-memory data grid which is used as a transport layer. 16 | 17 | ![How it works](how-it-works.png) 18 | 19 | The records are transformed into [Protobuf](https://github.com/camunda-community-hub/zeebe-exporter-protobuf) and added to one [ringbuffer](https://hazelcast.com/blog/ringbuffer-data-structure/). The ringbuffer has a fixed capacity and will override the oldest entries when the capacity is reached. 20 | 21 | Multiple applications can read from the ringbuffer. The application itself controls where to read from by proving a sequence number. Every application can read from a different sequence. 22 | 23 | The Java module provide a convenient way to read the records from the ringbuffer. 24 | 25 | ## Usage 26 | 27 | ### Java Application 28 | 29 | Add the Maven dependency to your `pom.xml` 30 | 31 | ``` 32 | 33 | io.zeebe.hazelcast 34 | zeebe-hazelcast-connector 35 | 1.0.1 36 | 37 | ``` 38 | 39 | Connect to Hazelcast and register a listener 40 | 41 | ```java 42 | ClientConfig clientConfig = new ClientConfig(); 43 | clientConfig.getNetworkConfig().addAddress("127.0.0.1:5701"); 44 | HazelcastInstance hz = HazelcastClient.newHazelcastClient(clientConfig); 45 | 46 | final ZeebeHazelcast zeebeHazelcast = ZeebeHazelcast.newBuilder(hz) 47 | .addProcessInstanceListener(processInstance -> { ... }) 48 | .readFrom(sequence) / .readFromHead() / .readFromTail() 49 | .build(); 50 | 51 | // ... 52 | 53 | zeebeHazelcast.close(); 54 | ``` 55 | ## Install 56 | 57 | ### Docker 58 | 59 | A docker image is published to [GitHub Packages](https://github.com/orgs/camunda-community-hub/packages/container/package/zeebe-with-hazelcast-exporter) that is based on the Zeebe image and includes the Hazelcast exporter (the exporter is enabled by default). 60 | 61 | ``` 62 | docker pull ghcr.io/camunda-community-hub/zeebe-with-hazelcast-exporter:1.1.2-1.0.1 63 | ``` 64 | 65 | For a local setup, the repository contains a [docker-compose file](docker/docker-compose.yml). It starts a Zeebe broker with the Hazelcast exporter. The version of the exporter is defined in the `.env` file. 66 | 67 | ``` 68 | mvn clean install -DskipTests 69 | cd docker 70 | docker-compose up 71 | ``` 72 | 73 | ### Manual 74 | 75 | 1. Download the latest [Zeebe distribution](https://github.com/camunda-cloud/zeebe/releases) _(zeebe-distribution-%{VERSION}.tar.gz 76 | )_ 77 | 78 | 1. Download the latest [exporter JAR](https://github.com/camunda-community-hub/zeebe-hazelcast-exporter/releases) (_zeebe-hazelcast-exporter-1.0.1-jar-with-dependencies.jar_) 79 | 80 | 1. Copy the exporter JAR into the broker folder `~/zeebe-broker-%{VERSION}/exporters`. 81 | 82 | ``` 83 | cp exporter/target/zeebe-hazelcast-exporter-1.0.1-jar-with-dependencies.jar ~/zeebe-broker-%{VERSION}/exporters/ 84 | ``` 85 | 86 | 1. Add the exporter to the broker configuration `~/zeebe-broker-%{VERSION}/config/application.yaml`: 87 | 88 | ``` 89 | zeebe: 90 | broker: 91 | exporters: 92 | hazelcast: 93 | className: io.zeebe.hazelcast.exporter.HazelcastExporter 94 | jarPath: exporters/zeebe-hazelcast-exporter-1.0.1-jar-with-dependencies.jar 95 | ``` 96 | 97 | 1. Start the broker 98 | `~/zeebe-broker-%{VERSION}/bin/broker` 99 | 100 | ### Configuration 101 | 102 | In the Zeebe configuration file, you can change 103 | 104 | * the Hazelcast port 105 | * the Hazelcast cluster name 106 | * the value and record types which are exported 107 | * the ringbuffer's name 108 | * the ringbuffer's capacity 109 | * the ringbuffer's time-to-live 110 | * the record serialization format 111 | 112 | Default values: 113 | 114 | ``` 115 | zeebe: 116 | broker: 117 | exporters: 118 | hazelcast: 119 | className: io.zeebe.hazelcast.exporter.HazelcastExporter 120 | jarPath: exporters/zeebe-hazelcast-exporter.jar 121 | args: 122 | # Hazelcast port 123 | port = 5701 124 | 125 | # Hazelcast cluster name 126 | clusterName = "dev" 127 | 128 | # comma separated list of io.zeebe.protocol.record.ValueType to export or empty to export all types 129 | enabledValueTypes = "" 130 | 131 | # comma separated list of io.zeebe.protocol.record.RecordType to export or empty to export all types 132 | enabledRecordTypes = "" 133 | 134 | # Hazelcast ringbuffer's name 135 | name = "zeebe" 136 | 137 | # Hazelcast ringbuffer's capacity 138 | capacity = 10000 139 | 140 | # Hazelcast ringbuffer's time-to-live in seconds. Don't remove the records until reaching the capacity by setting it to 0. 141 | timeToLiveInSeconds = 0 142 | 143 | # record serialization format: [protobuf|json] 144 | format = "protobuf" 145 | ``` 146 | 147 | The values can be overridden by environment variables with the same name and a `ZEEBE_HAZELCAST_` prefix (e.g. `ZEEBE_HAZELCAST_PORT`). 148 | 149 | #### Connect to an External/Remote Hazelcast Cluster 150 | 151 | By default, the exporter creates an in-memory Hazelcast instance and publishes the records to it. But it can also be configured to export the records to a remote/external Hazecast instance by setting the argument `remoteAddress` or the environment variable `ZEEBE_HAZELCAST_REMOTE_ADDRESS` to the address of the remote Hazelcast instance. 152 | 153 | * the remote Hazelcast address 154 | * the remote Hazelcast cluster name 155 | * the connection timeout 156 | 157 | Default values: 158 | 159 | ``` 160 | zeebe: 161 | broker: 162 | exporters: 163 | hazelcast: 164 | className: io.zeebe.hazelcast.exporter.HazelcastExporter 165 | jarPath: exporters/zeebe-hazelcast-exporter.jar 166 | args: 167 | # remote Hazelcast address 168 | remoteAddress = 127.0.0.1:5702 169 | 170 | # Hazelcast cluster name 171 | clusterName = "dev" 172 | 173 | # connection timeout 174 | remoteConnectionTimeout = "PT30S" 175 | ``` 176 | 177 |
178 | Full docker-compose.yml with external Hazelcast 179 |

180 | 181 | ``` 182 | version: "2" 183 | 184 | networks: 185 | zeebe_network: 186 | driver: bridge 187 | 188 | services: 189 | zeebe: 190 | container_name: zeebe_broker 191 | image: camunda/zeebe:1.1.2 192 | environment: 193 | - ZEEBE_LOG_LEVEL=debug 194 | - ZEEBE_HAZELCAST_REMOTE_ADDRESS=hazelcast:5701 195 | ports: 196 | - "26500:26500" 197 | - "9600:9600" 198 | volumes: 199 | - ../exporter/target/zeebe-hazelcast-exporter-1.0.1-jar-with-dependencies.jar:/usr/local/zeebe/exporters/zeebe-hazelcast-exporter.jar 200 | - ./application.yaml:/usr/local/zeebe/config/application.yaml 201 | networks: 202 | - zeebe_network 203 | depends_on: 204 | - hazelcast 205 | 206 | hazelcast: 207 | container_name: hazelcast 208 | image: hazelcast/hazelcast:4.2 209 | ports: 210 | - "5701:5701" 211 | environment: 212 | - JAVA_OPTS="-Dhazelcast.local.publicAddress=hazelcast:5701" 213 | networks: 214 | - zeebe_network 215 | 216 | hazelcast-management: 217 | container_name: hazelcast-management 218 | image: hazelcast/management-center:4.2 219 | ports: 220 | - "8083:8080" 221 | networks: 222 | - zeebe_network 223 | depends_on: 224 | - hazelcast 225 | ``` 226 | 227 |

228 |
229 | 230 | Check out the Hazelcast documentation on how to [deploy](https://docs.hazelcast.com/hazelcast/5.0/deploy/deploying-in-kubernetes) and [configure](https://docs.hazelcast.com/hazelcast/5.0/deploy/configuring-kubernetes) Hazelcast on Kubernetes. 231 | 232 | ## Build it from Source 233 | 234 | The exporter and the Java connector can be built with Maven 235 | 236 | `mvn clean install` 237 | 238 | ## Code of Conduct 239 | 240 | This project adheres to the Contributor Covenant [Code of 241 | Conduct](/CODE_OF_CONDUCT.md). By participating, you are expected to uphold 242 | this code. Please report unacceptable behavior to 243 | code-of-conduct@zeebe.io. 244 | -------------------------------------------------------------------------------- /connector-java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | Zeebe Hazelcast Connector 5 | zeebe-hazelcast-connector 6 | jar 7 | 8 | 9 | io.zeebe.hazelcast 10 | root 11 | 1.4.1-SNAPSHOT 12 | 13 | 14 | 15 | 16 | com.hazelcast 17 | hazelcast 18 | 19 | 20 | 21 | 22 | io.camunda 23 | zeebe-client-java 24 | test 25 | 26 | 27 | io.zeebe 28 | zeebe-test-container 29 | test 30 | 31 | 32 | org.testcontainers 33 | junit-jupiter 34 | test 35 | 36 | 37 | org.junit.jupiter 38 | junit-jupiter-api 39 | test 40 | 41 | 42 | 43 | org.assertj 44 | assertj-core 45 | test 46 | 47 | 48 | 49 | org.apache.logging.log4j 50 | log4j-slf4j-impl 51 | test 52 | 53 | 54 | 55 | org.apache.logging.log4j 56 | log4j-core 57 | test 58 | 59 | 60 | 61 | io.zeebe.hazelcast 62 | zeebe-hazelcast-exporter 63 | test 64 | 65 | 66 | 67 | io.zeebe 68 | zeebe-exporter-protobuf 69 | 70 | 71 | 72 | org.awaitility 73 | awaitility 74 | test 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /connector-java/src/main/java/io/zeebe/hazelcast/connect/java/ZeebeHazelcast.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast.connect.java; 2 | 3 | import com.google.protobuf.InvalidProtocolBufferException; 4 | import com.hazelcast.client.HazelcastClientNotActiveException; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import com.hazelcast.ringbuffer.Ringbuffer; 7 | import com.hazelcast.ringbuffer.StaleSequenceException; 8 | import io.zeebe.exporter.proto.Schema; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | import java.util.concurrent.Future; 19 | import java.util.function.Consumer; 20 | 21 | public class ZeebeHazelcast implements AutoCloseable { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(ZeebeHazelcast.class); 24 | 25 | private static final List> RECORD_MESSAGE_TYPES; 26 | 27 | static { 28 | RECORD_MESSAGE_TYPES = new ArrayList<>(); 29 | RECORD_MESSAGE_TYPES.add(Schema.DeploymentRecord.class); 30 | RECORD_MESSAGE_TYPES.add(Schema.DeploymentDistributionRecord.class); 31 | RECORD_MESSAGE_TYPES.add(Schema.ErrorRecord.class); 32 | RECORD_MESSAGE_TYPES.add(Schema.IncidentRecord.class); 33 | RECORD_MESSAGE_TYPES.add(Schema.JobRecord.class); 34 | RECORD_MESSAGE_TYPES.add(Schema.JobBatchRecord.class); 35 | RECORD_MESSAGE_TYPES.add(Schema.MessageStartEventSubscriptionRecord.class); 36 | RECORD_MESSAGE_TYPES.add(Schema.MessageSubscriptionRecord.class); 37 | RECORD_MESSAGE_TYPES.add(Schema.MessageRecord.class); 38 | RECORD_MESSAGE_TYPES.add(Schema.ProcessRecord.class); 39 | RECORD_MESSAGE_TYPES.add(Schema.ProcessEventRecord.class); 40 | RECORD_MESSAGE_TYPES.add(Schema.ProcessInstanceRecord.class); 41 | RECORD_MESSAGE_TYPES.add(Schema.ProcessInstanceCreationRecord.class); 42 | RECORD_MESSAGE_TYPES.add(Schema.ProcessMessageSubscriptionRecord.class); 43 | RECORD_MESSAGE_TYPES.add(Schema.TimerRecord.class); 44 | RECORD_MESSAGE_TYPES.add(Schema.VariableRecord.class); 45 | RECORD_MESSAGE_TYPES.add(Schema.VariableDocumentRecord.class); 46 | RECORD_MESSAGE_TYPES.add(Schema.DecisionRecord.class); 47 | RECORD_MESSAGE_TYPES.add(Schema.DecisionRequirementsRecord.class); 48 | RECORD_MESSAGE_TYPES.add(Schema.DecisionEvaluationRecord.class); 49 | RECORD_MESSAGE_TYPES.add(Schema.SignalRecord.class); 50 | RECORD_MESSAGE_TYPES.add(Schema.SignalSubscriptionRecord.class); 51 | } 52 | 53 | private final Ringbuffer ringbuffer; 54 | private final Map, List>> listeners; 55 | private final Consumer postProcessListener; 56 | 57 | private long sequence; 58 | 59 | private Future future; 60 | private ExecutorService executorService; 61 | 62 | private volatile boolean isClosed = false; 63 | 64 | private ZeebeHazelcast( 65 | Ringbuffer ringbuffer, 66 | long sequence, 67 | Map, List>> listeners, 68 | Consumer postProcessListener) { 69 | this.ringbuffer = ringbuffer; 70 | this.sequence = sequence; 71 | this.listeners = listeners; 72 | this.postProcessListener = postProcessListener; 73 | } 74 | 75 | /** Returns a new builder to read from the ringbuffer. */ 76 | public static Builder newBuilder(HazelcastInstance hazelcastInstance) { 77 | return new ZeebeHazelcast.Builder(hazelcastInstance); 78 | } 79 | 80 | private void start() { 81 | executorService = Executors.newSingleThreadExecutor(); 82 | future = executorService.submit(this::readFromBuffer); 83 | } 84 | 85 | public boolean isClosed() { 86 | return isClosed; 87 | } 88 | 89 | /** Stop reading from the ringbuffer. */ 90 | @Override 91 | public void close() throws Exception { 92 | LOGGER.info("Closing. Stop reading from ringbuffer. Current sequence: '{}'", getSequence()); 93 | 94 | isClosed = true; 95 | 96 | if (future != null) { 97 | future.cancel(true); 98 | } 99 | if (executorService != null) { 100 | executorService.shutdown(); 101 | } 102 | } 103 | 104 | /** Returns the current sequence. */ 105 | public long getSequence() { 106 | return sequence; 107 | } 108 | 109 | private void readFromBuffer() { 110 | while (!isClosed) { 111 | readNext(); 112 | } 113 | } 114 | 115 | private void readNext() { 116 | LOGGER.trace("Read from ring-buffer with sequence '{}'", sequence); 117 | 118 | try { 119 | final byte[] item = ringbuffer.readOne(sequence); 120 | 121 | final var genericRecord = Schema.Record.parseFrom(item); 122 | handleRecord(genericRecord); 123 | 124 | sequence += 1; 125 | 126 | postProcessListener.accept(sequence); 127 | 128 | } catch (InvalidProtocolBufferException e) { 129 | LOGGER.error("Failed to deserialize Protobuf message at sequence '{}'", sequence, e); 130 | 131 | sequence += 1; 132 | 133 | } catch (StaleSequenceException e) { 134 | // if the sequence is smaller than headSequence(). Because a Ringbuffer won't store all event 135 | // indefinitely, it can be that the data for the given sequence doesn't exist anymore and the 136 | // StaleSequenceException is thrown. It is up to the caller to deal with this particular 137 | // situation, e.g. throw an Exception or restart from the last known head. That is why the 138 | // StaleSequenceException contains the last known head. 139 | final var headSequence = e.getHeadSeq(); 140 | LOGGER.warn( 141 | "Fail to read from ring-buffer at sequence '{}'. The sequence is reported as stale. Continue with new head sequence at '{}'", 142 | sequence, 143 | headSequence, 144 | e); 145 | 146 | sequence = headSequence; 147 | 148 | } catch (IllegalArgumentException e) { 149 | // if sequence is smaller than 0 or larger than tailSequence()+1 150 | final var headSequence = ringbuffer.headSequence(); 151 | LOGGER.warn( 152 | "Fail to read from ring-buffer at sequence '{}'. Continue with head sequence at '{}'", 153 | sequence, 154 | headSequence, 155 | e); 156 | 157 | sequence = headSequence; 158 | 159 | } catch (HazelcastClientNotActiveException e) { 160 | LOGGER.warn("Lost connection to the Hazelcast server", e); 161 | 162 | try { 163 | close(); 164 | } catch (Exception closingFailure) { 165 | LOGGER.debug("Failure while closing the client", closingFailure); 166 | } 167 | 168 | } catch (InterruptedException e) { 169 | LOGGER.debug("Interrupted while reading from ring-buffer with sequence '{}'", sequence); 170 | throw new RuntimeException("Interrupted while reading from ring-buffer", e); 171 | 172 | } catch (Exception e) { 173 | if (!isClosed) { 174 | LOGGER.error( 175 | "Fail to read from ring-buffer at sequence '{}'. Will try again.", sequence, e); 176 | } 177 | } 178 | } 179 | 180 | private void handleRecord(Schema.Record genericRecord) throws InvalidProtocolBufferException { 181 | for (Class type : RECORD_MESSAGE_TYPES) { 182 | final var handled = handleRecord(genericRecord, type); 183 | if (handled) { 184 | return; 185 | } 186 | } 187 | } 188 | 189 | private boolean handleRecord( 190 | Schema.Record genericRecord, Class t) throws InvalidProtocolBufferException { 191 | 192 | if (genericRecord.getRecord().is(t)) { 193 | final var record = genericRecord.getRecord().unpack(t); 194 | 195 | listeners 196 | .getOrDefault(t, List.of()) 197 | .forEach(listener -> ((Consumer) listener).accept(record)); 198 | 199 | return true; 200 | } else { 201 | return false; 202 | } 203 | } 204 | 205 | public static class Builder { 206 | 207 | private final HazelcastInstance hazelcastInstance; 208 | 209 | private final Map, List>> listeners = new HashMap<>(); 210 | 211 | private String name = "zeebe"; 212 | 213 | private long readFromSequence = -1; 214 | private boolean readFromHead = false; 215 | 216 | private Consumer postProcessListener = sequence -> {}; 217 | 218 | private Builder(HazelcastInstance hazelcastInstance) { 219 | this.hazelcastInstance = hazelcastInstance; 220 | } 221 | 222 | /** Set the name of the ringbuffer to read from. */ 223 | public Builder name(String name) { 224 | this.name = name; 225 | return this; 226 | } 227 | 228 | /** Start reading from the given sequence. */ 229 | public Builder readFrom(long sequence) { 230 | this.readFromSequence = sequence; 231 | readFromHead = false; 232 | return this; 233 | } 234 | 235 | /** Start reading from the oldest item of the ringbuffer. */ 236 | public Builder readFromHead() { 237 | readFromSequence = -1; 238 | readFromHead = true; 239 | return this; 240 | } 241 | 242 | /** Start reading from the newest item of the ringbuffer. */ 243 | public Builder readFromTail() { 244 | readFromSequence = -1; 245 | readFromHead = false; 246 | return this; 247 | } 248 | 249 | /** 250 | * Register a listener that is called when an item is read from the ringbuffer and consumed by 251 | * the registered listeners. The listener is called with the next sequence number of the 252 | * ringbuffer. It can be used to store the sequence number externally. 253 | */ 254 | public Builder postProcessListener(Consumer listener) { 255 | postProcessListener = listener; 256 | return this; 257 | } 258 | 259 | private void addListener( 260 | Class recordType, Consumer listener) { 261 | final var recordListeners = listeners.getOrDefault(recordType, new ArrayList<>()); 262 | recordListeners.add(listener); 263 | listeners.put(recordType, recordListeners); 264 | } 265 | 266 | public Builder addDeploymentListener(Consumer listener) { 267 | addListener(Schema.DeploymentRecord.class, listener); 268 | return this; 269 | } 270 | 271 | public Builder addDeploymentDistributionListener( 272 | Consumer listener) { 273 | addListener(Schema.DeploymentDistributionRecord.class, listener); 274 | return this; 275 | } 276 | 277 | public Builder addProcessListener(Consumer listener) { 278 | addListener(Schema.ProcessRecord.class, listener); 279 | return this; 280 | } 281 | 282 | public Builder addProcessInstanceListener(Consumer listener) { 283 | addListener(Schema.ProcessInstanceRecord.class, listener); 284 | return this; 285 | } 286 | 287 | public Builder addProcessEventListener(Consumer listener) { 288 | addListener(Schema.ProcessEventRecord.class, listener); 289 | return this; 290 | } 291 | 292 | public Builder addVariableListener(Consumer listener) { 293 | addListener(Schema.VariableRecord.class, listener); 294 | return this; 295 | } 296 | 297 | public Builder addVariableDocumentListener(Consumer listener) { 298 | addListener(Schema.VariableDocumentRecord.class, listener); 299 | return this; 300 | } 301 | 302 | public Builder addJobListener(Consumer listener) { 303 | addListener(Schema.JobRecord.class, listener); 304 | return this; 305 | } 306 | 307 | public Builder addJobBatchListener(Consumer listener) { 308 | addListener(Schema.JobBatchRecord.class, listener); 309 | return this; 310 | } 311 | 312 | public Builder addIncidentListener(Consumer listener) { 313 | addListener(Schema.IncidentRecord.class, listener); 314 | return this; 315 | } 316 | 317 | public Builder addTimerListener(Consumer listener) { 318 | addListener(Schema.TimerRecord.class, listener); 319 | return this; 320 | } 321 | 322 | public Builder addMessageListener(Consumer listener) { 323 | addListener(Schema.MessageRecord.class, listener); 324 | return this; 325 | } 326 | 327 | public Builder addMessageSubscriptionListener( 328 | Consumer listener) { 329 | addListener(Schema.MessageSubscriptionRecord.class, listener); 330 | return this; 331 | } 332 | 333 | public Builder addMessageStartEventSubscriptionListener( 334 | Consumer listener) { 335 | addListener(Schema.MessageStartEventSubscriptionRecord.class, listener); 336 | return this; 337 | } 338 | 339 | public Builder addProcessMessageSubscriptionListener( 340 | Consumer listener) { 341 | addListener(Schema.ProcessMessageSubscriptionRecord.class, listener); 342 | return this; 343 | } 344 | 345 | public Builder addProcessInstanceCreationListener( 346 | Consumer listener) { 347 | addListener(Schema.ProcessInstanceCreationRecord.class, listener); 348 | return this; 349 | } 350 | 351 | public Builder addErrorListener(Consumer listener) { 352 | addListener(Schema.ErrorRecord.class, listener); 353 | return this; 354 | } 355 | 356 | public Builder addDecisionListener(Consumer listener) { 357 | addListener(Schema.DecisionRecord.class, listener); 358 | return this; 359 | } 360 | 361 | public Builder addDecisionRequirementsListener( 362 | Consumer listener) { 363 | addListener(Schema.DecisionRequirementsRecord.class, listener); 364 | return this; 365 | } 366 | 367 | public Builder addDecisionEvaluationListener( 368 | Consumer listener) { 369 | addListener(Schema.DecisionEvaluationRecord.class, listener); 370 | return this; 371 | } 372 | 373 | public Builder addSignalListener(Consumer listener) { 374 | addListener(Schema.SignalRecord.class, listener); 375 | return this; 376 | } 377 | 378 | public Builder addSignalSubscriptionListener( 379 | Consumer listener) { 380 | addListener(Schema.SignalSubscriptionRecord.class, listener); 381 | return this; 382 | } 383 | 384 | private long getSequence(Ringbuffer ringbuffer) { 385 | 386 | final var headSequence = ringbuffer.headSequence(); 387 | final var tailSequence = ringbuffer.tailSequence(); 388 | 389 | if (readFromSequence > 0) { 390 | if (readFromSequence > (tailSequence + 1)) { 391 | LOGGER.info( 392 | "The given sequence '{}' is greater than the current tail-sequence '{}' of the ringbuffer. Using the head-sequence instead.", 393 | readFromSequence, 394 | tailSequence); 395 | return headSequence; 396 | } else { 397 | return readFromSequence; 398 | } 399 | 400 | } else if (readFromHead) { 401 | return headSequence; 402 | 403 | } else { 404 | return Math.max(headSequence, tailSequence); 405 | } 406 | } 407 | 408 | /** 409 | * Start a background task that reads from the ringbuffer and invokes the listeners. After an 410 | * item is read and the listeners are invoked, the sequence is incremented (at-least-once 411 | * semantic).
412 | * The current sequence is returned by {@link #getSequence()}.
413 | * Call {@link #close()} to stop reading. 414 | */ 415 | public ZeebeHazelcast build() { 416 | 417 | LOGGER.debug("Read from ringbuffer with name '{}'", name); 418 | final Ringbuffer ringbuffer = hazelcastInstance.getRingbuffer(name); 419 | 420 | if (ringbuffer == null) { 421 | throw new IllegalArgumentException( 422 | String.format("No ring buffer found with name '%s'", name)); 423 | } 424 | 425 | LOGGER.debug( 426 | "Ringbuffer status: [head: {}, tail: {}, size: {}, capacity: {}]", 427 | ringbuffer.headSequence(), 428 | ringbuffer.tailSequence(), 429 | ringbuffer.size(), 430 | ringbuffer.capacity()); 431 | 432 | final long sequence = getSequence(ringbuffer); 433 | LOGGER.info("Read from ringbuffer '{}' starting from sequence '{}'", name, sequence); 434 | 435 | final var zeebeHazelcast = 436 | new ZeebeHazelcast(ringbuffer, sequence, listeners, postProcessListener); 437 | zeebeHazelcast.start(); 438 | 439 | return zeebeHazelcast; 440 | } 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /connector-java/src/test/java/io/zeebe/hazelcast/ExampleApplication.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import io.zeebe.hazelcast.connect.java.ZeebeHazelcast; 7 | 8 | import java.util.concurrent.CountDownLatch; 9 | 10 | public class ExampleApplication { 11 | 12 | public static void main(String[] args) throws Exception { 13 | 14 | ClientConfig clientConfig = new ClientConfig(); 15 | clientConfig.getNetworkConfig().addAddress("127.0.0.1:5701"); 16 | HazelcastInstance hz = HazelcastClient.newHazelcastClient(clientConfig); 17 | 18 | ZeebeHazelcast zeebeHazelcast = 19 | ZeebeHazelcast.newBuilder(hz) 20 | .readFromTail() 21 | .addDeploymentListener(deployment -> System.out.println("> deployment: " + deployment)) 22 | .addProcessInstanceListener( 23 | workflowInstance -> System.out.println("> workflow instance: " + workflowInstance)) 24 | .addJobListener(job -> System.out.println("> job: " + job)) 25 | .addIncidentListener(incident -> System.out.println("> incident: " + incident)) 26 | .build(); 27 | 28 | try { 29 | new CountDownLatch(1).await(); 30 | 31 | } catch (InterruptedException e) { 32 | zeebeHazelcast.close(); 33 | hz.shutdown(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /connector-java/src/test/java/io/zeebe/hazelcast/ExporterDmnRecordTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under 3 | * one or more contributor license agreements. See the NOTICE file distributed 4 | * with this work for additional information regarding copyright ownership. 5 | * Licensed under the Zeebe Community License 1.1. You may not use this file 6 | * except in compliance with the Zeebe Community License 1.1. 7 | */ 8 | package io.zeebe.hazelcast; 9 | 10 | import com.hazelcast.client.HazelcastClient; 11 | import com.hazelcast.client.config.ClientConfig; 12 | import com.hazelcast.core.HazelcastInstance; 13 | import io.camunda.zeebe.client.ZeebeClient; 14 | import io.camunda.zeebe.model.bpmn.Bpmn; 15 | import io.zeebe.exporter.proto.Schema; 16 | import io.zeebe.hazelcast.connect.java.ZeebeHazelcast; 17 | import io.zeebe.hazelcast.testcontainers.ZeebeTestContainer; 18 | import org.junit.jupiter.api.AfterAll; 19 | import org.junit.jupiter.api.BeforeAll; 20 | import org.junit.jupiter.api.Test; 21 | import org.testcontainers.junit.jupiter.Container; 22 | import org.testcontainers.junit.jupiter.Testcontainers; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Map; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | import static org.awaitility.Awaitility.await; 29 | 30 | @Testcontainers 31 | public final class ExporterDmnRecordTest { 32 | 33 | private static ZeebeClient ZEEBE_CLIENT; 34 | private static HazelcastInstance HAZELCAST_INSTANCE; 35 | 36 | @Container public static ZeebeTestContainer ZEEBE_TESTCONTAINER = new ZeebeTestContainer(); 37 | 38 | @BeforeAll 39 | public static void init() { 40 | ZEEBE_CLIENT = ZEEBE_TESTCONTAINER.getClient(); 41 | 42 | final ClientConfig clientConfig = new ClientConfig(); 43 | clientConfig.getNetworkConfig().addAddress(ZEEBE_TESTCONTAINER.getHazelcastAddress()); 44 | HAZELCAST_INSTANCE = HazelcastClient.newHazelcastClient(clientConfig); 45 | } 46 | 47 | @AfterAll 48 | public static void cleanUp() { 49 | HAZELCAST_INSTANCE.shutdown(); 50 | } 51 | 52 | private static ZeebeHazelcast.Builder createZeebeHazelcastClient() { 53 | return ZeebeHazelcast.newBuilder(HAZELCAST_INSTANCE).readFromHead(); 54 | } 55 | 56 | @Test 57 | void shouldExportDecisionRecords() throws Exception { 58 | final var decisions = new ArrayList(); 59 | final var zeebeHazelcast = 60 | createZeebeHazelcastClient().addDecisionListener(decisions::add).build(); 61 | 62 | try (zeebeHazelcast) { 63 | ZEEBE_CLIENT.newDeployResourceCommand().addResourceFromClasspath("rating.dmn").send().join(); 64 | 65 | await() 66 | .untilAsserted( 67 | () -> { 68 | assertThat(decisions) 69 | .hasSize(2) 70 | .extracting(Schema.DecisionRecord::getDecisionId) 71 | .contains("decision_a", "decision_b"); 72 | }); 73 | } 74 | } 75 | 76 | @Test 77 | void shouldExportDecisionRequirementsRecords() throws Exception { 78 | final var decisionRequirements = new ArrayList(); 79 | final var zeebeHazelcast = 80 | createZeebeHazelcastClient() 81 | .addDecisionRequirementsListener(decisionRequirements::add) 82 | .build(); 83 | 84 | try (zeebeHazelcast) { 85 | ZEEBE_CLIENT.newDeployResourceCommand().addResourceFromClasspath("rating.dmn").send().join(); 86 | 87 | await() 88 | .untilAsserted( 89 | () -> { 90 | assertThat(decisionRequirements) 91 | .hasSize(1) 92 | .extracting( 93 | record -> 94 | record.getDecisionRequirementsMetadata().getDecisionRequirementsId()) 95 | .contains("Ratings"); 96 | }); 97 | } 98 | } 99 | 100 | @Test 101 | void shouldExportDecisionEvaluationRecords() throws Exception { 102 | final var decisionEvaluations = new ArrayList(); 103 | final var zeebeHazelcast = 104 | createZeebeHazelcastClient() 105 | .addDecisionEvaluationListener(decisionEvaluations::add) 106 | .build(); 107 | 108 | try (zeebeHazelcast) { 109 | ZEEBE_CLIENT 110 | .newDeployResourceCommand() 111 | .addResourceFromClasspath("rating.dmn") 112 | .addProcessModel( 113 | Bpmn.createExecutableProcess("process") 114 | .startEvent() 115 | .businessRuleTask( 116 | "task", 117 | t -> t.zeebeCalledDecisionId("decision_a").zeebeResultVariable("result")) 118 | .done(), 119 | "process.bpmn") 120 | .send() 121 | .join(); 122 | 123 | ZEEBE_CLIENT 124 | .newCreateInstanceCommand() 125 | .bpmnProcessId("process") 126 | .latestVersion() 127 | .variables(Map.of("x", 7)) 128 | .send() 129 | .join(); 130 | 131 | await() 132 | .untilAsserted( 133 | () -> { 134 | assertThat(decisionEvaluations) 135 | .hasSize(1) 136 | .extracting(Schema.DecisionEvaluationRecord::getDecisionOutput) 137 | .contains("\"A+\""); 138 | }); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /connector-java/src/test/java/io/zeebe/hazelcast/ExporterRecordTest.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import io.camunda.zeebe.client.ZeebeClient; 7 | import io.camunda.zeebe.model.bpmn.Bpmn; 8 | import io.camunda.zeebe.model.bpmn.BpmnModelInstance; 9 | import io.zeebe.exporter.proto.Schema; 10 | import io.zeebe.hazelcast.connect.java.ZeebeHazelcast; 11 | import io.zeebe.hazelcast.testcontainers.ZeebeTestContainer; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.testcontainers.junit.jupiter.Container; 16 | import org.testcontainers.junit.jupiter.Testcontainers; 17 | 18 | import java.time.Duration; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.awaitility.Awaitility.await; 25 | 26 | @Testcontainers 27 | public class ExporterRecordTest { 28 | 29 | private static final BpmnModelInstance PROCESS = 30 | Bpmn.createExecutableProcess("process") 31 | .startEvent("start") 32 | .parallelGateway("fork") 33 | .serviceTask("task", s -> s.zeebeJobType("test").zeebeInputExpression("key", "x")) 34 | .exclusiveGateway() 35 | .condition("=foo") 36 | .endEvent("end") 37 | .moveToNode("fork") 38 | .receiveTask("receive-task") 39 | .message(m -> m.name("message").zeebeCorrelationKeyExpression("key")) 40 | .boundaryEvent("timer", b -> b.timerWithDuration("PT1M")) 41 | .endEvent() 42 | .done(); 43 | 44 | 45 | private static final BpmnModelInstance MESSAGE_PROCESS = 46 | Bpmn.createExecutableProcess("message-process") 47 | .startEvent() 48 | .message("start") 49 | .zeebeOutputExpression("x", "x") 50 | .endEvent() 51 | .done(); 52 | 53 | private final List deploymentRecords = new ArrayList<>(); 54 | private final List incidentRecords = new ArrayList<>(); 55 | private final List jobBatchRecords = new ArrayList<>(); 56 | private final List jobRecords = new ArrayList<>(); 57 | private final List messageRecords = new ArrayList<>(); 58 | private final List 59 | messageStartEventSubscriptionRecords = new ArrayList<>(); 60 | private final List messageSubscriptionRecords = 61 | new ArrayList<>(); 62 | private final List processEventRecords = new ArrayList<>(); 63 | private final List processInstanceCreationRecords = 64 | new ArrayList<>(); 65 | private final List processInstanceRecords = new ArrayList<>(); 66 | private final List processMessageSubscriptionRecords = 67 | new ArrayList<>(); 68 | private final List processRecords = new ArrayList<>(); 69 | private final List timerRecords = new ArrayList<>(); 70 | private final List variableDocumentRecords = new ArrayList<>(); 71 | private final List variableRecords = new ArrayList<>(); 72 | 73 | private ZeebeClient client; 74 | private HazelcastInstance hz; 75 | private ZeebeHazelcast zeebeHazelcast; 76 | 77 | @Container 78 | public ZeebeTestContainer zeebeContainer = new ZeebeTestContainer(); 79 | 80 | @BeforeEach 81 | public void init() { 82 | client = zeebeContainer.getClient(); 83 | 84 | final ClientConfig clientConfig = new ClientConfig(); 85 | clientConfig.getNetworkConfig().addAddress(zeebeContainer.getHazelcastAddress()); 86 | hz = HazelcastClient.newHazelcastClient(clientConfig); 87 | 88 | zeebeHazelcast = 89 | ZeebeHazelcast.newBuilder(hz) 90 | .addDeploymentListener(deploymentRecords::add) 91 | .addIncidentListener(incidentRecords::add) 92 | .addJobBatchListener(jobBatchRecords::add) 93 | .addJobListener(jobRecords::add) 94 | .addMessageListener(messageRecords::add) 95 | .addMessageStartEventSubscriptionListener(messageStartEventSubscriptionRecords::add) 96 | .addMessageSubscriptionListener(messageSubscriptionRecords::add) 97 | .addProcessEventListener(processEventRecords::add) 98 | .addProcessInstanceCreationListener(processInstanceCreationRecords::add) 99 | .addProcessInstanceListener(processInstanceRecords::add) 100 | .addProcessMessageSubscriptionListener(processMessageSubscriptionRecords::add) 101 | .addProcessListener(processRecords::add) 102 | .addTimerListener(timerRecords::add) 103 | .addVariableDocumentListener(variableDocumentRecords::add) 104 | .addVariableListener(variableRecords::add) 105 | .build(); 106 | } 107 | 108 | @AfterEach 109 | public void cleanUp() throws Exception { 110 | zeebeHazelcast.close(); 111 | hz.shutdown(); 112 | } 113 | 114 | @Test 115 | public void shouldExportRecords() { 116 | // given 117 | client 118 | .newDeployResourceCommand() 119 | .addProcessModel(PROCESS, "process.bpmn") 120 | .addProcessModel(MESSAGE_PROCESS, "message-process.bpmn") 121 | .send() 122 | .join(); 123 | 124 | // when 125 | final var processInstance = 126 | client 127 | .newCreateInstanceCommand() 128 | .bpmnProcessId("process") 129 | .latestVersion() 130 | .variables(Map.of("key", "key-1")) 131 | .send() 132 | .join(); 133 | 134 | client 135 | .newSetVariablesCommand(processInstance.getProcessInstanceKey()) 136 | .variables(Map.of("y", 2)) 137 | .send() 138 | .join(); 139 | 140 | client.newPublishMessageCommand().messageName("start").correlationKey("key-2").send().join(); 141 | client 142 | .newPublishMessageCommand() 143 | .messageName("message") 144 | .correlationKey("key-1") 145 | .timeToLive(Duration.ofMinutes(1)) 146 | .send() 147 | .join(); 148 | 149 | final var jobsResponse = 150 | client.newActivateJobsCommand().jobType("test").maxJobsToActivate(1).send().join(); 151 | jobsResponse.getJobs().forEach(job -> client.newCompleteCommand(job.getKey()).send().join()); 152 | 153 | // then 154 | await() 155 | .untilAsserted( 156 | () -> { 157 | assertThat(deploymentRecords) 158 | .hasSizeGreaterThanOrEqualTo(2) 159 | .allSatisfy( 160 | r -> 161 | assertThat(r.getMetadata().getValueType()) 162 | .isEqualTo(Schema.RecordMetadata.ValueType.DEPLOYMENT)) 163 | .extracting(r -> r.getMetadata().getIntent()) 164 | .contains("CREATE", "CREATED"); 165 | 166 | assertThat(incidentRecords) 167 | .hasSizeGreaterThanOrEqualTo(1) 168 | .allSatisfy( 169 | r -> 170 | assertThat(r.getMetadata().getValueType()) 171 | .isEqualTo(Schema.RecordMetadata.ValueType.INCIDENT)) 172 | .extracting(r -> r.getMetadata().getIntent()) 173 | .contains("CREATED"); 174 | 175 | assertThat(jobBatchRecords) 176 | .hasSizeGreaterThanOrEqualTo(2) 177 | .allSatisfy( 178 | r -> 179 | assertThat(r.getMetadata().getValueType()) 180 | .isEqualTo(Schema.RecordMetadata.ValueType.JOB_BATCH)) 181 | .extracting(r -> r.getMetadata().getIntent()) 182 | .contains("ACTIVATE", "ACTIVATED"); 183 | 184 | assertThat(jobRecords) 185 | .hasSizeGreaterThanOrEqualTo(2) 186 | .allSatisfy( 187 | r -> 188 | assertThat(r.getMetadata().getValueType()) 189 | .isEqualTo(Schema.RecordMetadata.ValueType.JOB)) 190 | .extracting(r -> r.getMetadata().getIntent()) 191 | .contains("CREATED", "COMPLETED"); 192 | 193 | assertThat(messageRecords) 194 | .hasSizeGreaterThanOrEqualTo(2) 195 | .allSatisfy( 196 | r -> 197 | assertThat(r.getMetadata().getValueType()) 198 | .isEqualTo(Schema.RecordMetadata.ValueType.MESSAGE)) 199 | .extracting(r -> r.getMetadata().getIntent()) 200 | .contains("PUBLISH", "PUBLISHED"); 201 | 202 | assertThat(messageStartEventSubscriptionRecords) 203 | .hasSizeGreaterThanOrEqualTo(1) 204 | .allSatisfy( 205 | r -> 206 | assertThat(r.getMetadata().getValueType()) 207 | .isEqualTo( 208 | Schema.RecordMetadata.ValueType.MESSAGE_START_EVENT_SUBSCRIPTION)) 209 | .extracting(r -> r.getMetadata().getIntent()) 210 | .contains("CREATED"); 211 | 212 | assertThat(messageSubscriptionRecords) 213 | .hasSizeGreaterThanOrEqualTo(3) 214 | .allSatisfy( 215 | r -> 216 | assertThat(r.getMetadata().getValueType()) 217 | .isEqualTo(Schema.RecordMetadata.ValueType.MESSAGE_SUBSCRIPTION)) 218 | .extracting(r -> r.getMetadata().getIntent()) 219 | .contains("CREATED", "CORRELATING", "CORRELATED"); 220 | 221 | assertThat(processEventRecords) 222 | .hasSizeGreaterThanOrEqualTo(2) 223 | .allSatisfy( 224 | r -> 225 | assertThat(r.getMetadata().getValueType()) 226 | .isEqualTo(Schema.RecordMetadata.ValueType.PROCESS_EVENT)) 227 | .extracting(r -> r.getMetadata().getIntent()) 228 | .contains("TRIGGERING", "TRIGGERED"); 229 | 230 | assertThat(processInstanceCreationRecords) 231 | .hasSizeGreaterThanOrEqualTo(2) 232 | .allSatisfy( 233 | r -> 234 | assertThat(r.getMetadata().getValueType()) 235 | .isEqualTo(Schema.RecordMetadata.ValueType.PROCESS_INSTANCE_CREATION)) 236 | .extracting(r -> r.getMetadata().getIntent()) 237 | .contains("CREATE", "CREATED"); 238 | 239 | assertThat(processInstanceRecords) 240 | .hasSizeGreaterThanOrEqualTo(3) 241 | .allSatisfy( 242 | r -> 243 | assertThat(r.getMetadata().getValueType()) 244 | .isEqualTo(Schema.RecordMetadata.ValueType.PROCESS_INSTANCE)) 245 | .extracting(r -> r.getMetadata().getIntent()) 246 | .contains("ACTIVATE_ELEMENT", "ELEMENT_ACTIVATING", "ELEMENT_ACTIVATED"); 247 | 248 | assertThat(processMessageSubscriptionRecords) 249 | .hasSizeGreaterThanOrEqualTo(2) 250 | .allSatisfy( 251 | r -> 252 | assertThat(r.getMetadata().getValueType()) 253 | .isEqualTo( 254 | Schema.RecordMetadata.ValueType.PROCESS_MESSAGE_SUBSCRIPTION)) 255 | .extracting(r -> r.getMetadata().getIntent()) 256 | .contains("CREATING", "CORRELATED"); 257 | 258 | assertThat(processRecords) 259 | .hasSizeGreaterThanOrEqualTo(2) 260 | .allSatisfy( 261 | r -> 262 | assertThat(r.getMetadata().getValueType()) 263 | .isEqualTo(Schema.RecordMetadata.ValueType.PROCESS)) 264 | .extracting(r -> r.getMetadata().getIntent()) 265 | .contains("CREATED"); 266 | 267 | assertThat(timerRecords) 268 | .hasSizeGreaterThanOrEqualTo(1) 269 | .allSatisfy( 270 | r -> 271 | assertThat(r.getMetadata().getValueType()) 272 | .isEqualTo(Schema.RecordMetadata.ValueType.TIMER)) 273 | .extracting(r -> r.getMetadata().getIntent()) 274 | .contains("CREATED"); 275 | 276 | assertThat(variableDocumentRecords) 277 | .hasSizeGreaterThanOrEqualTo(1) 278 | .allSatisfy( 279 | r -> 280 | assertThat(r.getMetadata().getValueType()) 281 | .isEqualTo(Schema.RecordMetadata.ValueType.VARIABLE_DOCUMENT)) 282 | .extracting(r -> r.getMetadata().getIntent()) 283 | .contains("UPDATE", "UPDATED"); 284 | 285 | assertThat(variableRecords) 286 | .hasSizeGreaterThanOrEqualTo(2) 287 | .allSatisfy( 288 | r -> 289 | assertThat(r.getMetadata().getValueType()) 290 | .isEqualTo(Schema.RecordMetadata.ValueType.VARIABLE)) 291 | .extracting(r -> r.getMetadata().getIntent()) 292 | .contains("CREATED"); 293 | }); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /connector-java/src/test/java/io/zeebe/hazelcast/ExporterTest.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import io.camunda.zeebe.client.ZeebeClient; 7 | import io.camunda.zeebe.model.bpmn.Bpmn; 8 | import io.camunda.zeebe.model.bpmn.BpmnModelInstance; 9 | import io.camunda.zeebe.protocol.record.intent.DeploymentIntent; 10 | import io.camunda.zeebe.protocol.record.value.BpmnElementType; 11 | import io.zeebe.exporter.proto.Schema; 12 | import io.zeebe.hazelcast.connect.java.ZeebeHazelcast; 13 | import io.zeebe.hazelcast.testcontainers.ZeebeTestContainer; 14 | import org.awaitility.Awaitility; 15 | import org.junit.jupiter.api.AfterEach; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.testcontainers.junit.jupiter.Container; 19 | import org.testcontainers.junit.jupiter.Testcontainers; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.stream.Collectors; 25 | import java.util.stream.LongStream; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | 29 | @Testcontainers 30 | public class ExporterTest { 31 | 32 | private static final BpmnModelInstance PROCESS = 33 | Bpmn.createExecutableProcess("process") 34 | .startEvent("start") 35 | .sequenceFlowId("to-task") 36 | .serviceTask("task", s -> s.zeebeJobType("test").zeebeInputExpression("foo", "bar")) 37 | .sequenceFlowId("to-end") 38 | .endEvent("end") 39 | .done(); 40 | 41 | private ZeebeClient client; 42 | private HazelcastInstance hz; 43 | private ZeebeHazelcast zeebeHazelcast; 44 | 45 | @Container 46 | public ZeebeTestContainer zeebeContainer = new ZeebeTestContainer(); 47 | 48 | @BeforeEach 49 | public void init() { 50 | client = zeebeContainer.getClient(); 51 | 52 | final ClientConfig clientConfig = new ClientConfig(); 53 | clientConfig.getNetworkConfig().addAddress(zeebeContainer.getHazelcastAddress()); 54 | hz = HazelcastClient.newHazelcastClient(clientConfig); 55 | } 56 | 57 | @AfterEach 58 | public void cleanUp() throws Exception { 59 | zeebeHazelcast.close(); 60 | hz.shutdown(); 61 | } 62 | 63 | @Test 64 | public void shouldIncrementSequence() { 65 | // given 66 | final List deploymentRecords = new ArrayList<>(); 67 | final List processInstanceRecords = new ArrayList<>(); 68 | 69 | zeebeHazelcast = 70 | ZeebeHazelcast.newBuilder(hz) 71 | .addDeploymentListener(deploymentRecords::add) 72 | .addProcessInstanceListener(processInstanceRecords::add) 73 | .build(); 74 | 75 | final var sequence1 = zeebeHazelcast.getSequence(); 76 | 77 | // when 78 | client.newDeployResourceCommand().addProcessModel(PROCESS, "process.bpmn").send().join(); 79 | 80 | Awaitility.await("await until the deployment is created") 81 | .untilAsserted( 82 | () -> 83 | assertThat(deploymentRecords) 84 | .extracting(r -> r.getMetadata().getIntent()) 85 | .contains(DeploymentIntent.CREATED.name())); 86 | 87 | final var sequence2 = zeebeHazelcast.getSequence(); 88 | 89 | client.newCreateInstanceCommand().bpmnProcessId("process").latestVersion().send().join(); 90 | 91 | Awaitility.await("await until the service task is activated") 92 | .untilAsserted( 93 | () -> 94 | assertThat(processInstanceRecords) 95 | .extracting(Schema.ProcessInstanceRecord::getBpmnElementType) 96 | .contains(BpmnElementType.SERVICE_TASK.name())); 97 | 98 | final var sequence3 = zeebeHazelcast.getSequence(); 99 | 100 | // then 101 | assertThat(sequence2).isGreaterThan(sequence1); 102 | assertThat(sequence3).isGreaterThan(sequence2); 103 | } 104 | 105 | @Test 106 | public void shouldInvokePostProcessListener() { 107 | // given 108 | final List invocations = new ArrayList<>(); 109 | 110 | final List deploymentRecords = new ArrayList<>(); 111 | final List processInstanceRecords = new ArrayList<>(); 112 | final List jobRecords = new ArrayList<>(); 113 | 114 | zeebeHazelcast = 115 | ZeebeHazelcast.newBuilder(hz) 116 | .addDeploymentListener(deploymentRecords::add) 117 | .addProcessInstanceListener(processInstanceRecords::add) 118 | .addJobListener(jobRecords::add) 119 | .postProcessListener(invocations::add) 120 | .build(); 121 | 122 | final var initialSequence = zeebeHazelcast.getSequence(); 123 | 124 | // when 125 | client.newDeployResourceCommand().addProcessModel(PROCESS, "process.bpmn").send().join(); 126 | client 127 | .newCreateInstanceCommand() 128 | .bpmnProcessId("process") 129 | .latestVersion() 130 | .variables(Map.of("foo", "bar")) 131 | .send() 132 | .join(); 133 | 134 | Awaitility.await() 135 | .untilAsserted( 136 | () -> { 137 | assertThat(jobRecords).hasSizeGreaterThanOrEqualTo(1); 138 | assertThat(invocations) 139 | .hasSizeGreaterThanOrEqualTo( 140 | deploymentRecords.size() + processInstanceRecords.size() + jobRecords.size()); 141 | }); 142 | 143 | final var lastSequence = zeebeHazelcast.getSequence(); 144 | 145 | // then 146 | final var expectedSequence = 147 | LongStream.rangeClosed(initialSequence + 1, lastSequence) 148 | .boxed() 149 | .collect(Collectors.toList()); 150 | 151 | assertThat(invocations).isEqualTo(expectedSequence); 152 | } 153 | 154 | @Test 155 | public void shouldReadFromHead() throws Exception { 156 | // given 157 | final List deploymentRecords = new ArrayList<>(); 158 | final List processInstanceRecords = new ArrayList<>(); 159 | 160 | zeebeHazelcast = 161 | ZeebeHazelcast.newBuilder(hz) 162 | .addDeploymentListener(deploymentRecords::add) 163 | .readFromHead() 164 | .build(); 165 | 166 | client.newDeployResourceCommand().addProcessModel(PROCESS, "process.bpmn").send().join(); 167 | 168 | Awaitility.await("await until the deployment is created") 169 | .untilAsserted( 170 | () -> 171 | assertThat(deploymentRecords) 172 | .hasSize(2) 173 | .extracting(r -> r.getMetadata().getIntent()) 174 | .contains("CREATE", "CREATED")); 175 | 176 | zeebeHazelcast.close(); 177 | deploymentRecords.clear(); 178 | 179 | // when 180 | zeebeHazelcast = 181 | ZeebeHazelcast.newBuilder(hz) 182 | .addDeploymentListener(deploymentRecords::add) 183 | .addProcessInstanceListener(processInstanceRecords::add) 184 | .readFromHead() 185 | .build(); 186 | 187 | client.newCreateInstanceCommand().bpmnProcessId("process").latestVersion().send().join(); 188 | 189 | Awaitility.await("await until the service task is activated") 190 | .untilAsserted( 191 | () -> 192 | assertThat(processInstanceRecords) 193 | .extracting(Schema.ProcessInstanceRecord::getBpmnElementType) 194 | .contains(BpmnElementType.SERVICE_TASK.name())); 195 | 196 | // then 197 | assertThat(deploymentRecords) 198 | .hasSize(2) 199 | .extracting(r -> r.getMetadata().getIntent()) 200 | .contains("CREATE", "CREATED"); 201 | } 202 | 203 | @Test 204 | public void shouldReadFromTail() throws Exception { 205 | // given 206 | final List deploymentRecords = new ArrayList<>(); 207 | final List processInstanceRecords = new ArrayList<>(); 208 | 209 | zeebeHazelcast = 210 | ZeebeHazelcast.newBuilder(hz).addDeploymentListener(deploymentRecords::add).build(); 211 | 212 | client.newDeployResourceCommand().addProcessModel(PROCESS, "process.bpmn").send().join(); 213 | Awaitility.await("await until the deployment is created") 214 | .untilAsserted( 215 | () -> 216 | assertThat(deploymentRecords) 217 | .extracting(r -> r.getMetadata().getIntent()) 218 | .contains(DeploymentIntent.CREATED.name())); 219 | 220 | zeebeHazelcast.close(); 221 | deploymentRecords.clear(); 222 | 223 | // when 224 | zeebeHazelcast = 225 | ZeebeHazelcast.newBuilder(hz) 226 | .addDeploymentListener(deploymentRecords::add) 227 | .addProcessInstanceListener(processInstanceRecords::add) 228 | .readFromTail() 229 | .build(); 230 | 231 | client.newCreateInstanceCommand().bpmnProcessId("process").latestVersion().send().join(); 232 | 233 | Awaitility.await("await until the service task is activated") 234 | .untilAsserted( 235 | () -> 236 | assertThat(processInstanceRecords) 237 | .extracting(Schema.ProcessInstanceRecord::getBpmnElementType) 238 | .contains(BpmnElementType.SERVICE_TASK.name())); 239 | 240 | // then 241 | assertThat(deploymentRecords).hasSize(1); 242 | } 243 | 244 | @Test 245 | public void shouldReadFromSequence() throws Exception { 246 | // given 247 | final List deploymentRecords = new ArrayList<>(); 248 | final List processInstanceRecords = new ArrayList<>(); 249 | 250 | zeebeHazelcast = 251 | ZeebeHazelcast.newBuilder(hz).addDeploymentListener(deploymentRecords::add).build(); 252 | 253 | client.newDeployResourceCommand().addProcessModel(PROCESS, "process.bpmn").send().join(); 254 | 255 | Awaitility.await("await until the deployment is created") 256 | .untilAsserted( 257 | () -> 258 | assertThat(deploymentRecords) 259 | .extracting(r -> r.getMetadata().getIntent()) 260 | .contains(DeploymentIntent.CREATED.name())); 261 | 262 | final var sequence = zeebeHazelcast.getSequence(); 263 | 264 | zeebeHazelcast.close(); 265 | deploymentRecords.clear(); 266 | 267 | // when 268 | zeebeHazelcast = 269 | ZeebeHazelcast.newBuilder(hz) 270 | .addDeploymentListener(deploymentRecords::add) 271 | .addProcessInstanceListener(processInstanceRecords::add) 272 | .readFrom(sequence) 273 | .build(); 274 | 275 | client.newCreateInstanceCommand().bpmnProcessId("process").latestVersion().send().join(); 276 | 277 | Awaitility.await("await until the service task is activated") 278 | .untilAsserted( 279 | () -> 280 | assertThat(processInstanceRecords) 281 | .extracting(Schema.ProcessInstanceRecord::getBpmnElementType) 282 | .contains(BpmnElementType.SERVICE_TASK.name())); 283 | 284 | // then 285 | assertThat(deploymentRecords).isEmpty(); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /connector-java/src/test/java/io/zeebe/hazelcast/ZeebeHazelcastClientTest.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.config.Config; 6 | import com.hazelcast.core.Hazelcast; 7 | import com.hazelcast.core.HazelcastInstance; 8 | import io.zeebe.hazelcast.connect.java.ZeebeHazelcast; 9 | import io.zeebe.hazelcast.exporter.ExporterConfiguration; 10 | import org.awaitility.Awaitility; 11 | import org.junit.jupiter.api.AfterEach; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | 15 | import java.time.Duration; 16 | 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | public class ZeebeHazelcastClientTest { 20 | 21 | private static final ExporterConfiguration CONFIGURATION = new ExporterConfiguration(); 22 | private static final Duration CONNECTION_TIMEOUT = Duration.ofSeconds(5); 23 | 24 | private ZeebeHazelcast zeebeHazelcast; 25 | 26 | private HazelcastInstance hzInstance; 27 | private HazelcastInstance hzClient; 28 | 29 | @BeforeEach 30 | public void init() { 31 | final Config config = new Config(); 32 | config.getNetworkConfig().setPort(5702); 33 | hzInstance = Hazelcast.newHazelcastInstance(config); 34 | 35 | final ClientConfig clientConfig = new ClientConfig(); 36 | 37 | final var connectionRetryConfig = 38 | clientConfig.getConnectionStrategyConfig().getConnectionRetryConfig(); 39 | connectionRetryConfig.setClusterConnectTimeoutMillis(CONNECTION_TIMEOUT.toMillis()); 40 | 41 | clientConfig.getNetworkConfig().addAddress("127.0.0.1:5702"); 42 | hzClient = HazelcastClient.newHazelcastClient(clientConfig); 43 | } 44 | 45 | @AfterEach 46 | public void cleanUp() throws Exception { 47 | zeebeHazelcast.close(); 48 | hzClient.shutdown(); 49 | hzInstance.shutdown(); 50 | } 51 | 52 | @Test 53 | public void shouldCloseIfHazelcastIsUnavailable() { 54 | // given 55 | zeebeHazelcast = ZeebeHazelcast.newBuilder(hzClient).build(); 56 | 57 | // when 58 | hzInstance.shutdown(); 59 | 60 | // then 61 | Awaitility.await() 62 | .atMost(CONNECTION_TIMEOUT.multipliedBy(2)) 63 | .untilAsserted(() -> assertThat(zeebeHazelcast.isClosed()).isTrue()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /connector-java/src/test/java/io/zeebe/hazelcast/testcontainers/ZeebeTestContainer.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast.testcontainers; 2 | 3 | import io.camunda.zeebe.client.ZeebeClient; 4 | import io.zeebe.containers.ZeebeContainer; 5 | import org.testcontainers.utility.DockerImageName; 6 | 7 | import java.util.Map; 8 | 9 | public class ZeebeTestContainer extends ZeebeContainer { 10 | 11 | public ZeebeTestContainer() { 12 | this(null); 13 | } 14 | 15 | public ZeebeTestContainer(Map env) { 16 | super(DockerImageName.parse("ghcr.io/camunda-community-hub/zeebe-with-hazelcast-exporter")); 17 | withExposedPorts(26500,9600,5701); 18 | if (env != null && !env.isEmpty()) { 19 | withEnv(env); 20 | } 21 | } 22 | 23 | public ZeebeClient getClient() { 24 | return ZeebeClient.newClientBuilder() 25 | .gatewayAddress(getExternalGatewayAddress()) 26 | .usePlaintext() 27 | .build(); 28 | } 29 | 30 | public String getHazelcastAddress() { 31 | return getExternalAddress(5701); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /connector-java/src/test/resources/log4j2-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /connector-java/src/test/resources/rating.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | decision_b 11 | 12 | 13 | 14 | 15 | 16 | "high" 17 | 18 | 19 | "A++" 20 | 21 | 22 | 23 | 24 | "mid" 25 | 26 | 27 | "A+" 28 | 29 | 30 | 31 | 32 | "low" 33 | 34 | 35 | "A" 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | x 45 | 46 | 47 | 48 | 49 | 50 | > 10 51 | 52 | 53 | "high" 54 | 55 | 56 | 57 | 58 | > 5 59 | 60 | 61 | "mid" 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | "low" 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /docker/application.yaml: -------------------------------------------------------------------------------- 1 | zeebe: 2 | broker: 3 | exporters: 4 | hazelcast: 5 | className: io.zeebe.hazelcast.exporter.HazelcastExporter 6 | jarPath: exporters/zeebe-hazelcast-exporter-jar-with-dependencies.jar 7 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | networks: 4 | zeebe_network: 5 | driver: bridge 6 | 7 | services: 8 | zeebe: 9 | container_name: zeebe_broker 10 | image: camunda/zeebe:1.0.0 11 | environment: 12 | - ZEEBE_LOG_LEVEL=debug 13 | ports: 14 | - "26500:26500" 15 | - "9600:9600" 16 | - "5701:5701" 17 | volumes: 18 | - ../exporter/target/jib/zeebe-hazelcast-exporter-jar-with-dependencies.jar:/usr/local/zeebe/exporters/zeebe-hazelcast-exporter-jar-with-dependencies.jar 19 | - ./application.yaml:/usr/local/zeebe/config/application.yaml 20 | networks: 21 | - zeebe_network 22 | -------------------------------------------------------------------------------- /exporter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | Zeebe Hazelcast Exporter 5 | zeebe-hazelcast-exporter 6 | jar 7 | 8 | 9 | io.zeebe.hazelcast 10 | root 11 | 1.4.1-SNAPSHOT 12 | 13 | 14 | 15 | 16 | 17 | io.camunda 18 | zeebe-exporter-api 19 | provided 20 | 21 | 22 | 23 | io.zeebe 24 | zeebe-exporter-protobuf 25 | 26 | 27 | 28 | com.hazelcast 29 | hazelcast 30 | 31 | 32 | 33 | 34 | io.camunda 35 | zeebe-client-java 36 | test 37 | 38 | 39 | io.zeebe 40 | zeebe-test-container 41 | test 42 | 43 | 44 | org.junit.jupiter 45 | junit-jupiter-api 46 | test 47 | 48 | 49 | org.testcontainers 50 | junit-jupiter 51 | test 52 | 53 | 54 | 55 | org.assertj 56 | assertj-core 57 | test 58 | 59 | 60 | 61 | org.apache.logging.log4j 62 | log4j-slf4j-impl 63 | test 64 | 65 | 66 | 67 | org.apache.logging.log4j 68 | log4j-core 69 | test 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-assembly-plugin 79 | 80 | 3.7.1 81 | 82 | 83 | jar-with-dependencies 84 | 85 | ${project.build.directory} 86 | 87 | 88 | 89 | assemble-all 90 | package 91 | 92 | single 93 | 94 | 95 | 96 | assemble-for-jib 97 | compile 98 | 99 | single 100 | 101 | 102 | ${project.build.directory}/jib 103 | ${project.artifactId} 104 | 105 | 106 | 107 | 108 | 109 | 110 | com.google.cloud.tools 111 | jib-maven-plugin 112 | 3.4.4 113 | 114 | 115 | docker-local 116 | compile 117 | 118 | dockerBuild 119 | 120 | 121 | 122 | deploy 123 | 124 | build 125 | 126 | 127 | 128 | 129 | 130 | docker.io/camunda/zeebe:${version.zeebe} 131 | 132 | 133 | ghcr.io/camunda-community-hub/zeebe-with-hazelcast-exporter 134 | 135 | ${version.zeebe}-${project.version} 136 | ${version.zeebe} 137 | 138 | 139 | 140 | INHERIT 141 | 142 | 26500 143 | 9600 144 | 5701 145 | 146 | 147 | io.zeebe.hazelcast.exporter.HazelcastExporter 148 | 149 | 150 | exporters/zeebe-hazelcast-exporter-jar-with-dependencies.jar 151 | 152 | 153 | 154 | 155 | 156 | 157 | ${project.build.directory}/jib 158 | /usr/local/zeebe/exporters 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /exporter/src/main/java/io/zeebe/hazelcast/exporter/ExporterConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast.exporter; 2 | 3 | import java.util.Optional; 4 | 5 | public class ExporterConfiguration { 6 | 7 | private static final String ENV_PREFIX = "ZEEBE_HAZELCAST_"; 8 | 9 | private int port = 5701; 10 | 11 | private String clusterName = "dev"; 12 | 13 | private String name = "zeebe"; 14 | 15 | private int capacity = -1; 16 | private int timeToLiveInSeconds = 0; 17 | 18 | private String format = "protobuf"; 19 | 20 | private String enabledValueTypes = ""; 21 | private String enabledRecordTypes = ""; 22 | 23 | private String remoteAddress; 24 | private String remoteConnectionTimeout = "PT30S"; 25 | 26 | public int getPort() { 27 | return getEnv("PORT").map(Integer::parseInt).orElse(port); 28 | } 29 | 30 | public String getName() { 31 | return getEnv("NAME").orElse(name); 32 | } 33 | 34 | public int getCapacity() { 35 | return getEnv("CAPACITY").map(Integer::parseInt).orElse(capacity); 36 | } 37 | 38 | public int getTimeToLiveInSeconds() { 39 | return getEnv("TIME_TO_LIVE_IN_SECONDS").map(Integer::parseInt).orElse(timeToLiveInSeconds); 40 | } 41 | 42 | public String getFormat() { 43 | return getEnv("FORMAT").orElse(format); 44 | } 45 | 46 | public String getEnabledValueTypes() { 47 | return getEnv("ENABLED_VALUE_TYPES").orElse(enabledValueTypes); 48 | } 49 | 50 | public String getEnabledRecordTypes() { 51 | return getEnv("ENABLED_RECORD_TYPES").orElse(enabledRecordTypes); 52 | } 53 | 54 | public Optional getRemoteAddress() { 55 | return getEnv("REMOTE_ADDRESS") 56 | .or(() -> Optional.ofNullable(remoteAddress)) 57 | .filter(remoteAddress -> !remoteAddress.isEmpty()); 58 | } 59 | 60 | public String getClusterName() { 61 | return getEnv("CLUSTER_NAME").orElse(clusterName); 62 | } 63 | 64 | public String getRemoteConnectionTimeout() { 65 | return getEnv("REMOTE_CONNECTION_TIMEOUT").orElse(remoteConnectionTimeout); 66 | } 67 | 68 | private Optional getEnv(String name) { 69 | return Optional.ofNullable(System.getenv(ENV_PREFIX + name)); 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | return "[port=" 75 | + port 76 | + ", remoteAddress=" 77 | + remoteAddress 78 | + ", name=" 79 | + name 80 | + ", clusterName=" 81 | + clusterName 82 | + ", enabledValueTypes=" 83 | + enabledValueTypes 84 | + ", enabledRecordTypes=" 85 | + enabledRecordTypes 86 | + ", capacity=" 87 | + capacity 88 | + ", timeToLiveInSeconds=" 89 | + timeToLiveInSeconds 90 | + ", format=" 91 | + format 92 | + ", remoteConnectionTimeout=" 93 | + remoteConnectionTimeout 94 | + "]"; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /exporter/src/main/java/io/zeebe/hazelcast/exporter/HazelcastExporter.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast.exporter; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.config.Config; 6 | import com.hazelcast.config.RingbufferConfig; 7 | import com.hazelcast.core.Hazelcast; 8 | import com.hazelcast.core.HazelcastInstance; 9 | import com.hazelcast.ringbuffer.Ringbuffer; 10 | import io.camunda.zeebe.exporter.api.Exporter; 11 | import io.camunda.zeebe.exporter.api.context.Context; 12 | import io.camunda.zeebe.exporter.api.context.Controller; 13 | import io.camunda.zeebe.protocol.record.Record; 14 | import io.zeebe.exporter.proto.RecordTransformer; 15 | import io.zeebe.exporter.proto.Schema; 16 | import org.slf4j.Logger; 17 | 18 | import java.time.Duration; 19 | import java.util.function.Function; 20 | 21 | public class HazelcastExporter implements Exporter { 22 | 23 | private ExporterConfiguration config; 24 | private Logger logger; 25 | private Controller controller; 26 | 27 | private HazelcastInstance hazelcast; 28 | private Ringbuffer ringbuffer; 29 | 30 | private Function recordTransformer; 31 | 32 | @Override 33 | public void configure(Context context) { 34 | logger = context.getLogger(); 35 | config = context.getConfiguration().instantiate(ExporterConfiguration.class); 36 | 37 | logger.debug("Starting exporter with configuration: {}", config); 38 | 39 | final var filter = new HazelcastRecordFilter(config); 40 | context.setFilter(filter); 41 | 42 | configureFormat(); 43 | } 44 | 45 | private void configureFormat() { 46 | final var format = config.getFormat(); 47 | if (format.equalsIgnoreCase("protobuf")) { 48 | recordTransformer = this::recordToProtobuf; 49 | 50 | } else if (format.equalsIgnoreCase("json")) { 51 | recordTransformer = this::recordToJson; 52 | 53 | } else { 54 | throw new IllegalArgumentException( 55 | String.format( 56 | "Expected the parameter 'format' to be one fo 'protobuf' or 'json' but was '%s'", 57 | format)); 58 | } 59 | } 60 | 61 | @Override 62 | public void open(Controller controller) { 63 | this.controller = controller; 64 | 65 | hazelcast = 66 | config 67 | .getRemoteAddress() 68 | .map(this::connectToHazelcast) 69 | .orElseGet(this::createHazelcastInstance); 70 | 71 | ringbuffer = hazelcast.getRingbuffer(config.getName()); 72 | if (ringbuffer == null) { 73 | throw new IllegalStateException( 74 | String.format("Failed to open ring-buffer with name '%s'", config.getName())); 75 | } 76 | 77 | logger.info( 78 | "Export records to ring-buffer with name '{}' [head: {}, tail: {}, size: {}, capacity: {}]", 79 | ringbuffer.getName(), 80 | ringbuffer.headSequence(), 81 | ringbuffer.tailSequence(), 82 | ringbuffer.size(), 83 | ringbuffer.capacity()); 84 | } 85 | 86 | private HazelcastInstance createHazelcastInstance() { 87 | final var port = this.config.getPort(); 88 | final var clusterName = this.config.getClusterName(); 89 | 90 | final var hzConfig = new Config(); 91 | hzConfig.getNetworkConfig().setPort(port); 92 | hzConfig.setProperty("hazelcast.logging.type", "slf4j"); 93 | 94 | hzConfig.setClusterName(clusterName); 95 | 96 | final var ringbufferConfig = new RingbufferConfig(this.config.getName()); 97 | 98 | if (this.config.getCapacity() > 0) { 99 | ringbufferConfig.setCapacity(this.config.getCapacity()); 100 | } 101 | if (this.config.getTimeToLiveInSeconds() > 0) { 102 | ringbufferConfig.setTimeToLiveSeconds(this.config.getTimeToLiveInSeconds()); 103 | } 104 | 105 | hzConfig.addRingBufferConfig(ringbufferConfig); 106 | 107 | logger.info( 108 | "Creating new in-memory Hazelcast instance [port: {}, cluster-name: {}]", 109 | port, 110 | clusterName); 111 | 112 | return Hazelcast.newHazelcastInstance(hzConfig); 113 | } 114 | 115 | private HazelcastInstance connectToHazelcast(String remoteAddress) { 116 | final var clusterName = this.config.getClusterName(); 117 | 118 | final var clientConfig = new ClientConfig(); 119 | clientConfig.setProperty("hazelcast.logging.type", "slf4j"); 120 | clientConfig.setClusterName(clusterName); 121 | 122 | final var networkConfig = clientConfig.getNetworkConfig(); 123 | networkConfig.addAddress(remoteAddress); 124 | 125 | final var connectionRetryConfig = 126 | clientConfig.getConnectionStrategyConfig().getConnectionRetryConfig(); 127 | final var connectionTimeout = Duration.parse(this.config.getRemoteConnectionTimeout()); 128 | connectionRetryConfig.setClusterConnectTimeoutMillis(connectionTimeout.toMillis()); 129 | 130 | logger.info( 131 | "Connecting to remote Hazelcast instance [address: {}, cluster-name: {}]", 132 | remoteAddress, 133 | clusterName); 134 | 135 | return HazelcastClient.newHazelcastClient(clientConfig); 136 | } 137 | 138 | @Override 139 | public void close() { 140 | hazelcast.shutdown(); 141 | } 142 | 143 | @Override 144 | public void export(Record record) { 145 | 146 | if (ringbuffer != null) { 147 | final byte[] transformedRecord = recordTransformer.apply(record); 148 | 149 | final var sequenceNumber = ringbuffer.add(transformedRecord); 150 | logger.trace( 151 | "Added a record to the ring-buffer [record-position: {}, ring-buffer sequence-number: {}]", 152 | record.getPosition(), 153 | sequenceNumber); 154 | } 155 | 156 | controller.updateLastExportedRecordPosition(record.getPosition()); 157 | } 158 | 159 | private byte[] recordToProtobuf(Record record) { 160 | final Schema.Record dto = RecordTransformer.toGenericRecord(record); 161 | return dto.toByteArray(); 162 | } 163 | 164 | private byte[] recordToJson(Record record) { 165 | final var json = record.toJson(); 166 | return json.getBytes(); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /exporter/src/main/java/io/zeebe/hazelcast/exporter/HazelcastRecordFilter.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast.exporter; 2 | 3 | import io.camunda.zeebe.exporter.api.context.Context; 4 | import io.camunda.zeebe.protocol.record.RecordType; 5 | import io.camunda.zeebe.protocol.record.ValueType; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | public final class HazelcastRecordFilter implements Context.RecordFilter { 12 | 13 | private final List enabledRecordTypes; 14 | private final List enabledValueTypes; 15 | 16 | public HazelcastRecordFilter(ExporterConfiguration config) { 17 | 18 | final var enabledRecordTypeList = parseAsList(config.getEnabledRecordTypes()); 19 | 20 | enabledRecordTypes = 21 | Arrays.stream(RecordType.values()) 22 | .filter( 23 | recordType -> 24 | enabledRecordTypeList.isEmpty() 25 | || enabledRecordTypeList.contains(recordType.name())) 26 | .collect(Collectors.toList()); 27 | 28 | final var enabledValueTypeList = parseAsList(config.getEnabledValueTypes()); 29 | 30 | enabledValueTypes = 31 | Arrays.stream(ValueType.values()) 32 | .filter( 33 | valueType -> 34 | enabledValueTypeList.isEmpty() 35 | || enabledValueTypeList.contains(valueType.name())) 36 | .collect(Collectors.toList()); 37 | } 38 | 39 | private List parseAsList(String list) { 40 | return Arrays.stream(list.split(",")) 41 | .map(String::trim) 42 | .filter(item -> !item.isEmpty()) 43 | .map(String::toUpperCase) 44 | .collect(Collectors.toList()); 45 | } 46 | 47 | @Override 48 | public boolean acceptType(RecordType recordType) { 49 | return enabledRecordTypes.contains(recordType); 50 | } 51 | 52 | @Override 53 | public boolean acceptValue(ValueType valueType) { 54 | return enabledValueTypes.contains(valueType); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /exporter/src/test/java/io/zeebe/hazelcast/ExporterClusterNameTest.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import com.hazelcast.ringbuffer.Ringbuffer; 7 | import io.camunda.zeebe.model.bpmn.Bpmn; 8 | import io.camunda.zeebe.model.bpmn.BpmnModelInstance; 9 | import io.zeebe.exporter.proto.Schema; 10 | import io.zeebe.hazelcast.exporter.ExporterConfiguration; 11 | import io.zeebe.hazelcast.testcontainers.ZeebeTestContainer; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.testcontainers.junit.jupiter.Container; 16 | import org.testcontainers.junit.jupiter.Testcontainers; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | @Testcontainers 21 | public class ExporterClusterNameTest { 22 | 23 | private static final BpmnModelInstance WORKFLOW = 24 | Bpmn.createExecutableProcess("process") 25 | .startEvent("start") 26 | .sequenceFlowId("to-task") 27 | .serviceTask("task", s -> s.zeebeJobType("test")) 28 | .sequenceFlowId("to-end") 29 | .endEvent("end") 30 | .done(); 31 | 32 | private static final ExporterConfiguration CONFIGURATION = new ExporterConfiguration(); 33 | 34 | private static final String CLUSTER_NAME = "test"; 35 | 36 | @Container 37 | public ZeebeTestContainer zeebeContainer = ZeebeTestContainer.withDefaultConfig(CLUSTER_NAME); 38 | 39 | private HazelcastInstance hz; 40 | 41 | @BeforeEach 42 | public void init() { 43 | 44 | final ClientConfig clientConfig = new ClientConfig(); 45 | clientConfig.getNetworkConfig().addAddress(zeebeContainer.getHazelcastAddress()); 46 | clientConfig.setClusterName(CLUSTER_NAME); 47 | hz = HazelcastClient.newHazelcastClient(clientConfig); 48 | } 49 | 50 | @AfterEach 51 | public void cleanUp() { 52 | hz.shutdown(); 53 | } 54 | 55 | @Test 56 | public void shouldExportEventsAsProtobuf() throws Exception { 57 | // given 58 | final Ringbuffer buffer = hz.getRingbuffer(CONFIGURATION.getName()); 59 | 60 | var sequence = buffer.headSequence(); 61 | 62 | // when 63 | zeebeContainer.getClient().newDeployResourceCommand().addProcessModel(WORKFLOW, "process.bpmn").send().join(); 64 | 65 | // then 66 | final var message = buffer.readOne(sequence); 67 | assertThat(message).isNotNull(); 68 | 69 | final var record = Schema.Record.parseFrom(message); 70 | assertThat(record.getRecord().is(Schema.DeploymentRecord.class)).isTrue(); 71 | 72 | final var deploymentRecord = record.getRecord().unpack(Schema.DeploymentRecord.class); 73 | final Schema.DeploymentRecord.Resource resource = deploymentRecord.getResources(0); 74 | assertThat(resource.getResourceName()).isEqualTo("process.bpmn"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /exporter/src/test/java/io/zeebe/hazelcast/ExporterJsonTest.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import com.hazelcast.ringbuffer.Ringbuffer; 7 | import io.camunda.zeebe.model.bpmn.Bpmn; 8 | import io.camunda.zeebe.model.bpmn.BpmnModelInstance; 9 | import io.zeebe.hazelcast.exporter.ExporterConfiguration; 10 | import io.zeebe.hazelcast.testcontainers.ZeebeTestContainer; 11 | import org.junit.jupiter.api.AfterEach; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.testcontainers.junit.jupiter.Container; 15 | import org.testcontainers.junit.jupiter.Testcontainers; 16 | 17 | import java.util.Map; 18 | 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | @Testcontainers 22 | public class ExporterJsonTest { 23 | 24 | private static final BpmnModelInstance WORKFLOW = 25 | Bpmn.createExecutableProcess("process") 26 | .startEvent("start") 27 | .sequenceFlowId("to-task") 28 | .serviceTask("task", s -> s.zeebeJobType("test")) 29 | .sequenceFlowId("to-end") 30 | .endEvent("end") 31 | .done(); 32 | 33 | private static final ExporterConfiguration CONFIGURATION = new ExporterConfiguration(); 34 | 35 | @Container 36 | public ZeebeTestContainer zeebeContainer = ZeebeTestContainer.withJsonFormat(); 37 | 38 | private HazelcastInstance hz; 39 | 40 | @BeforeEach 41 | public void init() { 42 | final ClientConfig clientConfig = new ClientConfig(); 43 | clientConfig.getNetworkConfig().addAddress(zeebeContainer.getHazelcastAddress()); 44 | hz = HazelcastClient.newHazelcastClient(clientConfig); 45 | } 46 | 47 | @AfterEach 48 | public void cleanUp() { 49 | hz.shutdown(); 50 | } 51 | 52 | @Test 53 | public void shouldExportEventsAsProtobuf() throws Exception { 54 | // given 55 | final Ringbuffer buffer = hz.getRingbuffer(CONFIGURATION.getName()); 56 | 57 | var sequence = buffer.headSequence(); 58 | 59 | // when 60 | zeebeContainer.getClient().newDeployResourceCommand().addProcessModel(WORKFLOW, "process.bpmn").send().join(); 61 | 62 | // then 63 | final var message = buffer.readOne(sequence); 64 | assertThat(message).isNotNull(); 65 | 66 | final var jsonRecord = new String(message); 67 | 68 | assertThat(jsonRecord) 69 | .startsWith("{") 70 | .endsWith("}") 71 | .contains("\"valueType\":\"DEPLOYMENT\"") 72 | .contains("\"recordType\":\"COMMAND\"") 73 | .contains("\"intent\":\"CREATE\""); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /exporter/src/test/java/io/zeebe/hazelcast/ExporterTest.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import com.hazelcast.ringbuffer.Ringbuffer; 7 | import io.camunda.zeebe.model.bpmn.Bpmn; 8 | import io.camunda.zeebe.model.bpmn.BpmnModelInstance; 9 | import io.zeebe.exporter.proto.Schema; 10 | import io.zeebe.hazelcast.exporter.ExporterConfiguration; 11 | import io.zeebe.hazelcast.testcontainers.ZeebeTestContainer; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.testcontainers.junit.jupiter.Container; 16 | import org.testcontainers.junit.jupiter.Testcontainers; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | @Testcontainers 21 | public class ExporterTest { 22 | 23 | private static final BpmnModelInstance WORKFLOW = 24 | Bpmn.createExecutableProcess("process") 25 | .startEvent("start") 26 | .sequenceFlowId("to-task") 27 | .serviceTask("task", s -> s.zeebeJobType("test")) 28 | .sequenceFlowId("to-end") 29 | .endEvent("end") 30 | .done(); 31 | 32 | private static final ExporterConfiguration CONFIGURATION = new ExporterConfiguration(); 33 | 34 | @Container 35 | public ZeebeTestContainer zeebeContainer = ZeebeTestContainer.withDefaultConfig(); 36 | 37 | private HazelcastInstance hz; 38 | 39 | @BeforeEach 40 | public void init() { 41 | final ClientConfig clientConfig = new ClientConfig(); 42 | clientConfig.getNetworkConfig().addAddress(zeebeContainer.getHazelcastAddress()); 43 | hz = HazelcastClient.newHazelcastClient(clientConfig); 44 | } 45 | 46 | @AfterEach 47 | public void cleanUp() { 48 | hz.shutdown(); 49 | } 50 | 51 | @Test 52 | public void shouldExportEventsAsProtobuf() throws Exception { 53 | // given 54 | final Ringbuffer buffer = hz.getRingbuffer(CONFIGURATION.getName()); 55 | 56 | var sequence = buffer.headSequence(); 57 | 58 | // when 59 | zeebeContainer.getClient().newDeployResourceCommand().addProcessModel(WORKFLOW, "process.bpmn").send().join(); 60 | 61 | // then 62 | final var message = buffer.readOne(sequence); 63 | assertThat(message).isNotNull(); 64 | 65 | final var record = Schema.Record.parseFrom(message); 66 | assertThat(record.getRecord().is(Schema.DeploymentRecord.class)).isTrue(); 67 | 68 | final var deploymentRecord = record.getRecord().unpack(Schema.DeploymentRecord.class); 69 | final Schema.DeploymentRecord.Resource resource = deploymentRecord.getResources(0); 70 | assertThat(resource.getResourceName()).isEqualTo("process.bpmn"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /exporter/src/test/java/io/zeebe/hazelcast/RemoteExporterClusterNameTest.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import com.hazelcast.ringbuffer.Ringbuffer; 7 | import io.camunda.zeebe.model.bpmn.Bpmn; 8 | import io.camunda.zeebe.model.bpmn.BpmnModelInstance; 9 | import io.zeebe.exporter.proto.Schema; 10 | import io.zeebe.hazelcast.exporter.ExporterConfiguration; 11 | import io.zeebe.hazelcast.testcontainers.ZeebeTestContainer; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.testcontainers.junit.jupiter.Container; 16 | import org.testcontainers.junit.jupiter.Testcontainers; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | @Testcontainers 21 | public class RemoteExporterClusterNameTest { 22 | 23 | private static final BpmnModelInstance WORKFLOW = 24 | Bpmn.createExecutableProcess("process") 25 | .startEvent("start") 26 | .sequenceFlowId("to-task") 27 | .serviceTask("task", s -> s.zeebeJobType("test")) 28 | .sequenceFlowId("to-end") 29 | .endEvent("end") 30 | .done(); 31 | 32 | private static final ExporterConfiguration CONFIGURATION = new ExporterConfiguration(); 33 | 34 | private static final String CLUSTER_NAME = "test"; 35 | 36 | @Container 37 | public ZeebeTestContainer zeebeContainer = ZeebeTestContainer.withHazelcastContainer(CLUSTER_NAME); 38 | 39 | private HazelcastInstance hzClient; 40 | 41 | @BeforeEach 42 | public void init() { 43 | final ClientConfig clientConfig = new ClientConfig(); 44 | clientConfig.getNetworkConfig().addAddress(zeebeContainer.getHazelcastContainer().getHazelcastServerExternalAddress()); 45 | clientConfig.setClusterName(CLUSTER_NAME); 46 | hzClient = HazelcastClient.newHazelcastClient(clientConfig); 47 | } 48 | 49 | @AfterEach 50 | public void cleanUp() { 51 | hzClient.shutdown(); 52 | } 53 | 54 | @Test 55 | public void shouldExportEventsAsProtobuf() throws Exception { 56 | // given 57 | final Ringbuffer buffer = hzClient.getRingbuffer(CONFIGURATION.getName()); 58 | 59 | var sequence = buffer.headSequence(); 60 | 61 | // when 62 | zeebeContainer.getClient().newDeployResourceCommand().addProcessModel(WORKFLOW, "process.bpmn").send().join(); 63 | 64 | // then 65 | final var message = buffer.readOne(sequence); 66 | assertThat(message).isNotNull(); 67 | 68 | final var record = Schema.Record.parseFrom(message); 69 | assertThat(record.getRecord().is(Schema.DeploymentRecord.class)).isTrue(); 70 | 71 | final var deploymentRecord = record.getRecord().unpack(Schema.DeploymentRecord.class); 72 | final Schema.DeploymentRecord.Resource resource = deploymentRecord.getResources(0); 73 | assertThat(resource.getResourceName()).isEqualTo("process.bpmn"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /exporter/src/test/java/io/zeebe/hazelcast/RemoteExporterTest.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast; 2 | 3 | import com.hazelcast.client.HazelcastClient; 4 | import com.hazelcast.client.config.ClientConfig; 5 | import com.hazelcast.core.HazelcastInstance; 6 | import com.hazelcast.ringbuffer.Ringbuffer; 7 | import io.camunda.zeebe.model.bpmn.Bpmn; 8 | import io.camunda.zeebe.model.bpmn.BpmnModelInstance; 9 | import io.zeebe.exporter.proto.Schema; 10 | import io.zeebe.hazelcast.exporter.ExporterConfiguration; 11 | import io.zeebe.hazelcast.testcontainers.ZeebeTestContainer; 12 | import org.junit.jupiter.api.AfterEach; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.testcontainers.junit.jupiter.Container; 16 | import org.testcontainers.junit.jupiter.Testcontainers; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | @Testcontainers 21 | public class RemoteExporterTest { 22 | 23 | private static final BpmnModelInstance WORKFLOW = 24 | Bpmn.createExecutableProcess("process") 25 | .startEvent("start") 26 | .sequenceFlowId("to-task") 27 | .serviceTask("task", s -> s.zeebeJobType("test")) 28 | .sequenceFlowId("to-end") 29 | .endEvent("end") 30 | .done(); 31 | 32 | private static final ExporterConfiguration CONFIGURATION = new ExporterConfiguration(); 33 | 34 | @Container 35 | public ZeebeTestContainer zeebeContainer = ZeebeTestContainer.withHazelcastContainer(); 36 | 37 | private HazelcastInstance hzClient; 38 | 39 | @BeforeEach 40 | public void init() { 41 | final ClientConfig clientConfig = new ClientConfig(); 42 | clientConfig.getNetworkConfig().addAddress(zeebeContainer.getHazelcastContainer().getHazelcastServerExternalAddress()); 43 | hzClient = HazelcastClient.newHazelcastClient(clientConfig); 44 | } 45 | 46 | @AfterEach 47 | public void cleanUp() { 48 | hzClient.shutdown(); 49 | } 50 | 51 | @Test 52 | public void shouldExportEventsAsProtobuf() throws Exception { 53 | // given 54 | final Ringbuffer buffer = hzClient.getRingbuffer(CONFIGURATION.getName()); 55 | 56 | var sequence = buffer.headSequence(); 57 | 58 | // when 59 | zeebeContainer.getClient().newDeployResourceCommand().addProcessModel(WORKFLOW, "process.bpmn").send().join(); 60 | 61 | // then 62 | final var message = buffer.readOne(sequence); 63 | assertThat(message).isNotNull(); 64 | 65 | final var record = Schema.Record.parseFrom(message); 66 | assertThat(record.getRecord().is(Schema.DeploymentRecord.class)).isTrue(); 67 | 68 | final var deploymentRecord = record.getRecord().unpack(Schema.DeploymentRecord.class); 69 | final Schema.DeploymentRecord.Resource resource = deploymentRecord.getResources(0); 70 | assertThat(resource.getResourceName()).isEqualTo("process.bpmn"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /exporter/src/test/java/io/zeebe/hazelcast/testcontainers/HazelcastContainer.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast.testcontainers; 2 | 3 | import org.testcontainers.containers.GenericContainer; 4 | import org.testcontainers.containers.Network; 5 | 6 | import static java.lang.String.format; 7 | 8 | public class HazelcastContainer extends GenericContainer { 9 | 10 | private static final String IMAGE_AND_VERSION_FORMAT = "%s:%s"; 11 | public static String HAZELCAST_DOCKER_IMAGE_NAME = "hazelcast/hazelcast"; 12 | public static Integer PORT = 5701; 13 | public static String ALIAS = "hazelcast"; 14 | public static String VERSION = "5.1.1"; 15 | 16 | protected HazelcastContainer() { 17 | super(format(IMAGE_AND_VERSION_FORMAT, HAZELCAST_DOCKER_IMAGE_NAME, VERSION)); 18 | } 19 | 20 | @Override 21 | protected void configure() { 22 | withNetwork(Network.SHARED); 23 | withNetworkAliases(ALIAS); 24 | withExposedPorts(PORT); 25 | } 26 | 27 | public String getHazelcastServerExternalAddress() { 28 | return this.getHost() + ":" + this.getMappedPort(PORT); 29 | } 30 | 31 | public String getHazelcastServerAddress() { 32 | return ALIAS + ":" + PORT; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /exporter/src/test/java/io/zeebe/hazelcast/testcontainers/ZeebeTestContainer.java: -------------------------------------------------------------------------------- 1 | package io.zeebe.hazelcast.testcontainers; 2 | 3 | import io.camunda.zeebe.client.ZeebeClient; 4 | import io.zeebe.containers.ZeebeContainer; 5 | import org.testcontainers.utility.DockerImageName; 6 | 7 | import java.util.Map; 8 | 9 | public class ZeebeTestContainer extends ZeebeContainer { 10 | 11 | private HazelcastContainer hazelcastContainer; 12 | 13 | protected ZeebeTestContainer(HazelcastContainer hazelcastContainer) { 14 | super(DockerImageName.parse("ghcr.io/camunda-community-hub/zeebe-with-hazelcast-exporter")); 15 | withExposedPorts(26500,9600,5701); 16 | this.hazelcastContainer = hazelcastContainer; 17 | } 18 | 19 | public static ZeebeTestContainer withDefaultConfig() { 20 | return new ZeebeTestContainer(null); 21 | } 22 | 23 | public static ZeebeTestContainer withDefaultConfig(String clusterName) { 24 | ZeebeTestContainer container = withDefaultConfig(); 25 | container.withEnv("ZEEBE_HAZELCAST_CLUSTER_NAME", clusterName); 26 | return container; 27 | } 28 | 29 | public static ZeebeTestContainer withJsonFormat() { 30 | ZeebeTestContainer container = withDefaultConfig(); 31 | container.withEnv("ZEEBE_HAZELCAST_FORMAT", "json"); 32 | return container; 33 | } 34 | 35 | public static ZeebeTestContainer withHazelcastContainer() { 36 | return new ZeebeTestContainer(new HazelcastContainer()); 37 | } 38 | 39 | public static ZeebeTestContainer withHazelcastContainer(String clusterName) { 40 | HazelcastContainer hazelcast = new HazelcastContainer(); 41 | hazelcast.withEnv("HZ_CLUSTERNAME", clusterName); 42 | ZeebeTestContainer container = new ZeebeTestContainer(hazelcast); 43 | container.withEnv("ZEEBE_HAZELCAST_CLUSTER_NAME", clusterName); 44 | return container; 45 | } 46 | 47 | public ZeebeClient getClient() { 48 | return ZeebeClient.newClientBuilder() 49 | .gatewayAddress(getExternalGatewayAddress()) 50 | .usePlaintext() 51 | .build(); 52 | } 53 | 54 | public String getHazelcastAddress() { 55 | return getExternalAddress(5701); 56 | } 57 | 58 | @Override 59 | public void start() { 60 | if (hazelcastContainer != null) { 61 | hazelcastContainer.start(); 62 | withNetwork(hazelcastContainer.getNetwork()); 63 | withEnv("ZEEBE_HAZELCAST_REMOTE_ADDRESS", hazelcastContainer.getHazelcastServerAddress()); 64 | } 65 | super.start(); 66 | } 67 | 68 | @Override 69 | public void stop() { 70 | if (hazelcastContainer != null) { 71 | hazelcastContainer.stop(); 72 | } 73 | super.stop(); 74 | } 75 | 76 | public HazelcastContainer getHazelcastContainer() { 77 | return hazelcastContainer; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /how-it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camunda-community-hub/zeebe-hazelcast-exporter/0ad04a12fc4531bebabf77d6011d459f553d355d/how-it-works.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | Zeebe Hazelcast Root 5 | io.zeebe.hazelcast 6 | root 7 | 1.4.1-SNAPSHOT 8 | pom 9 | 10 | 11 | org.camunda.community 12 | community-hub-release-parent 13 | 1.4.4 14 | 15 | 16 | 17 | 18 | 8.5.4 19 | 1.6.1 20 | 5.4.0 21 | 2.24.1 22 | 3.6.3 23 | 5.11.3 24 | 1.20.3 25 | 26 | 3.4.2 27 | 28 | 29 | 17 30 | https://artifacts.camunda.com/artifactory/zeebe-io-snapshots/ 31 | 32 | https://artifacts.camunda.com/artifactory/zeebe-io/ 33 | 34 | 35 | 36 | -Xdoclint:none 37 | 38 | 39 | 40 | exporter 41 | connector-java 42 | 43 | 44 | 45 | 46 | 47 | io.zeebe.hazelcast 48 | zeebe-hazelcast-exporter 49 | ${project.version} 50 | 51 | 52 | 53 | io.zeebe 54 | zeebe-exporter-protobuf 55 | ${version.exporter.protobuf} 56 | 57 | 58 | 59 | io.camunda 60 | zeebe-bom 61 | ${version.zeebe} 62 | import 63 | pom 64 | 65 | 66 | 67 | com.hazelcast 68 | hazelcast 69 | ${version.hazelcast} 70 | 71 | 72 | io.zeebe 73 | zeebe-test-container 74 | ${version.zeebe.testcontainers} 75 | test 76 | 77 | 78 | org.junit 79 | junit-bom 80 | ${version.junit} 81 | pom 82 | import 83 | 84 | 85 | org.testcontainers 86 | testcontainers-bom 87 | ${version.testcontainers} 88 | pom 89 | import 90 | 91 | 92 | 93 | 94 | org.assertj 95 | assertj-core 96 | 3.26.3 97 | test 98 | 99 | 100 | 101 | org.apache.logging.log4j 102 | log4j-slf4j-impl 103 | ${version.log4j} 104 | 105 | 106 | 107 | org.apache.logging.log4j 108 | log4j-core 109 | ${version.log4j} 110 | 111 | 112 | 113 | org.awaitility 114 | awaitility 115 | 4.2.2 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-surefire-plugin 126 | 3.5.1 127 | 128 | 129 | org.apache.maven.plugins 130 | maven-jar-plugin 131 | ${plugin.version.maven-jar} 132 | 133 | false 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-compiler-plugin 139 | 3.13.0 140 | 141 | 11 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-javadoc-plugin 148 | 3.10.1 149 | 150 | ${version.java} 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | org.sonatype.plugins 159 | nexus-staging-maven-plugin 160 | 1.7.0 161 | 162 | 163 | org.apache.maven.plugins 164 | maven-site-plugin 165 | 3.21.0 166 | 167 | 168 | org.apache.maven.plugins 169 | maven-project-info-reports-plugin 170 | 3.8.0 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | community-action-maven-release 179 | 180 | 181 | 182 | org.apache.maven.plugins 183 | maven-gpg-plugin 184 | 3.2.7 185 | 186 | 187 | sign-artifacts 188 | verify 189 | 190 | sign 191 | 192 | 193 | 194 | 195 | 196 | 197 | --pinentry-mode 198 | loopback 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | zeebe 210 | Zeebe Repository 211 | https://artifacts.camunda.com/artifactory/zeebe-io/ 212 | 213 | true 214 | 215 | 216 | false 217 | 218 | 219 | 220 | 221 | zeebe-snapshots 222 | Zeebe Snapshot Repository 223 | https://artifacts.camunda.com/artifactory/zeebe-io-snapshots/ 224 | 225 | false 226 | 227 | 228 | true 229 | 230 | 231 | 232 | 233 | 234 | https://github.com/camunda-community-hub/zeebe-hazelcast-exporter 235 | scm:git:git@github.com:camunda-community-hub/zeebe-hazelcast-exporter.git 236 | scm:git:git@github.com:camunda-community-hub/zeebe-hazelcast-exporter.git 237 | 238 | HEAD 239 | 240 | 241 | 242 | --------------------------------------------------------------------------------